EnergyDecision-DT-V2-Forecast: Forecast-Conditioned Decision Transformer for AEMO FCAS Battery Trading
Model Description
EnergyDecision-DT-V2-Forecast is a ForecastDecisionTransformer β an extension of the modern v2 Decision Transformer that conditions on 48-step TTM price forecasts alongside the historical observation/action sequence. It predicts optimal battery dispatch and FCAS bids using both past market data and predicted future prices (RRP, demand, 4 FCAS services).
This is the first model in the benchmark to use explicit price forecasts as conditioning tokens, removing the information bottleneck of pure history conditioning.
Key Features
- Forecast token prefix: 48 TTM-generated price forecasts prepended as a learned prefix. History tokens attend to forecast tokens via the existing causal mask.
- RoPE enabled: Rotary Position Embeddings for better position-aware sequence modeling.
- Modern architecture: Grouped-Query Attention (6 KV heads, 12 Q heads), QK-Norm, SwiGLU FFN, RMSNorm pre-norm, weight tying.
- Action Space (9-dim): Energy dispatch + 8 FCAS contingency bids.
- State Space (18-dim): Normalized market observations.
- Context Length: 210 history + 48 forecast = 258 timesteps total.
Intended Use
This model is intended for:
- Research into forecast-conditioned offline RL for energy markets
- Simulation of battery trading strategies using both historical data and price forecasts
- Baseline for comparing forecast-enhanced transformers against standard Decision Transformers and RL baselines
It is not intended for live trading without further validation, risk management, and regulatory compliance.
Training Data
- Source: AEMO simulated trade dataset
- Datasets used:
aemo_fcas_dataset.parquet(FCAS v2, 76.9M rows) +aemo_sdp_trajectories.parquet(SDP, 2.1M rows) +aemo_fcas_grpo_dataset.parquet(GRPO, 11.9M rows) - Forecast conditioning:
ttm_forecasts.npzβ TTM (Granite TTM-R3) 48-step normalize price forecasts for 6 channels (RRP, demand, 4 FCAS services), aligned viaepisode_startcolumn - Source policies: SB3(PPO, A2C, etc), GRPO-DT, SDP energy-only
Model Architecture
ForecastDecisionTransformer(
(embed_return): Linear(1 -> 768)
(embed_state): Linear(18 -> 768)
(embed_action): Linear(9 -> 768)
(embed_timestep): Embedding(100000 -> 768)
(embed_forecast_type): Embedding(2 -> 768) # NEW: history(0) vs forecast(1)
(embed_ln): RMSNorm(768)
(transformer): 8x ModernBlock(
(norm1): RMSNorm(768)
(attn): CausalSelfAttention(
q_proj: Linear(768 -> 768) # 12 Q heads Γ 64 head_dim
k_proj: Linear(768 -> 384) # 6 KV heads Γ 64 head_dim
v_proj: Linear(768 -> 384) # 6 KV heads Γ 64 head_dim
out_proj: Linear(768 -> 768)
qk_norm: RMSNorm(64) per head
n_rep: 2 (GQA)
)
(norm2): RMSNorm(768)
(ffn): SwiGLU(768 -> 3072 -> 768, dropout=0.15)
)
(ln_f): RMSNorm(768)
(pred_act): Linear(768 -> 9) -> Tanh [tied with embed_act]
(pred_state): Linear(768 -> 18) [tied with embed_state]
(pred_return): Linear(768 -> 1) [tied with embed_return]
)
Forecast token processing: At each forward pass, the model receives forecast_states (48 timesteps Γ 18-dim) and forecast_rtgs. These are embedded with type=1 (forecast) and prepended before the history tokens. The model's causal attention allows history tokens to attend to all forecast tokens + prior history.
Hyperparameters
| Parameter | Value |
|---|---|
| Blocks | 8 |
| Hidden dim | 768 |
| Attention heads (Q) | 12 |
| KV heads (GQA) | 6 |
| History context length | 210 |
| Forecast length | 48 |
| Total sequence length | 774 (210+48 Γ 3 interleaved) |
| Dropout | 0.15 |
| QK-Norm | β Enabled |
| Weight tying | β Enabled |
| RoPE | β Enabled |
| State dim | 18 |
| Action dim | 9 |
| Discount factor | 0.95 |
| Return scale | 2.0 |
| Loss weights | action=0.999, state=0.002, return=0.0001 |
Training Results
Loss Curves
Epoch,Train Loss,Val Loss 1,0.041705,0.007797 2,0.006725,0.005105 3,0.005220,0.004579 4,0.004722,0.004228
| Epoch | Train Loss | Val Loss |
|---|---|---|
| 1 | 0.041705 | 0.007797 |
| 2 | 0.006725 | 0.005105 |
| 3 | 0.005220 | 0.004579 |
| 4 | 0.004722 | 0.004228 |
Best validation loss: 0.004228 (epoch 4). Global steps completed: 43,648.
Training Configuration
| Parameter | Value |
|---|---|
| Optimizer | AdamW (lr=3e-5, weight_decay=1e-4) |
| Batch size | 64 |
| Epochs | 4 |
| Scheduler | StepLR (gamma=0.9, step=1 epoch) |
| Gradient clip norm | 1.0 |
| Mixed precision | AMP (torch.cuda.amp) |
RTG Calibration
The forecast DT uses forecast_states as conditioning, which changes the optimal RTG prompt.
For the best dispatch-matched profit, calibrate RTG on your evaluation surface. Note that the
forecast DT's RTG response differs from the standard DT β preliminary testing suggests
rtg_value=0.0 remains effective, but full calibration sweeps are ongoing.
Usage
Installation
The model is loaded with the ForecastDecisionTransformer class from the
energydecision repository:
git clone https://github.com/mrvictoru/energydecision.git
cd energydecision
Loading the Model
import torch
from forecast_decision_transformer import ForecastDecisionTransformer
model = ForecastDecisionTransformer(
state_dim=18,
act_dim=9,
n_block=8,
h_dim=768,
context_len=210,
forecast_len=48,
n_heads=12,
drop_p=0.15,
max_timestep=100000,
rope_enabled=True,
n_kv_heads=6,
qk_norm=True,
tie_weights=True,
)
state_dict = torch.hub.load_state_dict_from_url(
"https://huggingface.co/mrvictoru/energydecision-dt-v2-forecast/resolve/main/forecast_dt_model.pt",
map_location="cpu"
)
if "model_state_dict" in state_dict:
model.load_state_dict(state_dict["model_state_dict"])
else:
model.load_state_dict(state_dict)
model.eval()
Inference with Forecast Tokens
import torch
# History inputs (batch, context_len, dim)
states = torch.zeros(1, 210, 18)
actions = torch.zeros(1, 210, 9)
rtgs = torch.full((1, 210, 1), 0.0) # RTG prompt β use 0.0
timesteps = torch.arange(210).unsqueeze(0)
mask = torch.ones(1, 210, dtype=torch.bool)
# Forecast inputs (batch, forecast_len, dim) β from TTM model
forecast_states = torch.randn(1, 48, 18) # predicted market states
forecast_rtgs = torch.randn(1, 48, 1) # forecast RTGs
forecast_timesteps = torch.arange(210, 210 + 48).unsqueeze(0)
with torch.no_grad():
ret, state, action = model(
states, actions, rtgs, timesteps, mask,
forecast_states=forecast_states,
forecast_rtgs=forecast_rtgs,
forecast_timesteps=forecast_timesteps,
)
# action[0, -1] is the next-step action (9-dim)
Using with TTM Forecasts at Inference
In a real deployment, the TTM forecasts would be generated on-the-fly from the AEMO context window:
# Forward: TTM takes last 512 steps of prices, predicts next 48
prices_context = aemo_data.iloc[current_step - 512:current_step]
ttm_forecast = ttm_model(past_values=prices_context) # predicted prices
forecast_states = assemble_observation(ttm_forecast) # build 18-dim from predictions
Training Infrastructure
| Resource | Details |
|---|---|
| Hardware | CUDA-capable GPU |
| Framework | PyTorch, transformers, granite-tsfm |
| Precision | Mixed precision (AMP) |
| Checkpointing | Every 500 batches + best model on validation loss |
| Source Code | github.com/mrvictoru/energydecision |
Remarks
- Forecast quality: The TTM forecasts are generated by a fine-tuned
ibm-granite/granite-timeseries-ttm-r3model (512-48-dec-512-r3 revision). See the dataset page for details on the forecast data. - No perfect foresight: The model trains on the same imperfect TTM forecasts it receives at inference β no train-inference gap.
- Architecture gap: Whether the forecast conditioning provides meaningful improvement over the standard DT is an open research question. The forecast DT is a step towards incorporating forward-looking information that the standard DT (210-step context window) cannot access.
Standard Tier Leaderboard (Jul 2026)
Oct 2024 Β· 5 NEM regions Β· 144h Β· medium_1c Β· full_fcas Β· best RTG per model
| Model | Profit/ep | FCAS/ep | Deg/ep | Best RTG |
|---|---|---|---|---|
| Modern v2 | $4,991 | $4,836 | $229 | 10.0 |
| Dispatch Dalrymple North | $4,660 | $2,287 | $1,020 | β |
| Forecast DT | $4,564 | $3,663 | $270 | 50.0 |
| Phase C GRPO (mod v2) | $4,322 | $2,508 | $1,058 | 10.0 |
| Phase 1 GRPO (legacy) | $2,678 | $2,914 | $384 | 50.0 |
| PPO reference | $2,353 | $2,192 | $236 | β |
Key findings:
- RTG calibration (0β100) revealed every DT variant gains 5β75%. The original 0.0β2.0 range was far too narrow.
- Modern v2 peaks at RTG=10 ($4,991/ep). Forecast DT peaks at RTG=50 ($4,564/ep).
- The forecast DT is a well-implemented negative result β explicit TTM price forecasts do not beat the implicit 210-step context window.
- FCAS TTM forecasts have near-zero correlation (~0.01β0.07), limiting any forecast-conditioned approach.