The test proves that global filter_front_month on merged trades drops
entire quarters (Q1 lost when Q2 has higher volume). Per-file filtering
keeps both quarters. This is the regression test for the bug that
produced "Need at least 18 months of data" on H100.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Global filter selected only ONE quarter's instrument_id, producing 3 months
of data instead of 2+ years. Now filters within each file so quarterly
rolls are handled correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three fixes:
1. spread_cost removed contract_multiplier ($50) — was computing in dollars
but PnL is in points, making spread 12.5× actual. The model saw every
trade as guaranteed loss → preferred Flat.
2. Mirror universe now negates ALL 17 directional features (returns, MACD,
Bollinger, SMA ratios, regression slope, CUSUM) + flips RSI. Was only
negating 4, teaching wrong direction on 50% of epochs.
3. tx_cost_multiplier updated to 0.18 bps (IBKR ES RT=$4.50/contract).
min_hold_bars=10 for volume bars (~26s at 23 bars/min).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace extract_ohlcv_bars_from_dbn() with trades-based volume bar pipeline
(load_trades_sync -> filter_front_month -> build_volume_bars) in both the
precompute binary and the runtime fallback path, eliminating fake price jumps
from interleaved contract months in OHLCV DBN files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The mirror universe feature negated only features[0..3] (4 return-like
features) but inverted the direction action (Short↔Long). The remaining
38 directional features (momentum, trend, patterns) were NOT negated.
This taught the model the WRONG direction on 50% of epochs, cancelling
out the directional signal and producing worse-than-random performance.
Disabled until proper full-feature mirroring is implemented.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Delete 3 unused bf16 scratch buffers (exp_h_b0/b1/b2) from
GpuExperienceCollector — allocated VRAM but never read.
The f32 versions (exp_h_b0_f32 etc.) remain in use.
- Fix stale doc comments: 45 actions -> 81 actions, index range 0-44 -> 0-80
- Extend round-trip tests to cover all 81 action indices
- Add TODO for future 4-branch struct refactor (direction + magnitude fields)
— 64 callers make it too invasive for this commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The single-step backtest_env_step kernel still called decode_exposure_index()
and compute_target_position() (old linear 3-branch mapping). Updated to use
decode_direction_4b/decode_magnitude_4b/compute_target_position_4branch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CQL grad kernel had identical bug to expected_q: global mean across
ALL atoms instead of per-atom dueling centering. This disabled CQL's
conservative penalty by destroying per-atom C51 structure.
Also: backtest_env_step single-step kernel was missing b3_size param
(only had b0,b1,b2). Added b3_size and fixed capital floor breach
calls that passed b2_size twice instead of b2_size,b3_size.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Q-diagnostics was reading 9 values from a 12-element per-branch
buffer, mislabeling branch outputs as dir×mag combos. The Q-gap was
computed across ALL branches instead of within direction only.
Fixed: gap measures direction conviction (best_dir - 2nd_dir), and
per-action Q display combines dir+mag Q-values into 9 dir×mag combos.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The expected_q kernel computed mean_adv as a SINGLE scalar averaged
across ALL atoms AND actions, then subtracted from every logit.
This destroyed per-atom advantage structure, making all actions
produce nearly identical expected Q-values.
Fixed to compute per-atom mean: mean_a(A[a,j]) separately for each
atom j, matching the C51/MSE loss kernels' correct implementation.
This bug affected: backtest evaluation action selection, Q-gap
conviction filter (always 0 → floor 0.25), and Q-value monitoring.
Training loss kernels were NOT affected (they had correct per-atom mean).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CVaR, Q-gap conviction, and Kelly were scaling target_position AFTER
compute_target_position_4branch, which collapsed all magnitudes to the
same effective size (conviction floor 0.25 made Full ≈ Small). Now they
scale effective_max_pos BEFORE the mapping, so the magnitude branch
controls relative sizing while risk management controls absolute scale.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
total_branch_atoms and total_actions were summing only b0+b1+b2,
missing b3. This under-allocated logit buffers by 25%, causing
out-of-bounds writes during the forward pass on H100.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The constructor hardcoded num_actions: 9 (old 9-exposure scheme).
IQN head computed total_branch_actions = b0+b1+b2 = 15, missing b3.
Both caused SIGSEGV on H100 from buffer overrun.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add magnitude head (branch 3) to BranchingWeightSet, GpuBranchPtrs, and all
BF16 mirror structs. Update flatten/unflatten from 20 to 24 individual tensors
(indices 24-25 = bottleneck remain flat-buffer-only). Fix bottleneck index
in experience collector (was 20, now 24). Update all construction sites:
fused_training clone, gradient_budget smoke tests, backtest evaluator.
Fix monitoring comments/labels for 4-branch encoding (dir*mag 3x3), delete
dead `if false` block, delete DELETED marker comments.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove dead code from the 3-branch DQN era:
- CUDA kernels: exposure_aux_grad_kernel, exposure_pretrain_step
- GpuDqnTrainer: 16 struct fields (aux Adam optimizer, scratch buffers, kernel handles)
- Methods: set_exposure_aux_weight, launch_exposure_aux_grad, run_pretrain_step
- Constructor: aux allocations, kernel loading, struct literal entries
- compile_training_kernels: reduced from 35 to 33 return values
Note: compute_target_position() in trade_physics.cuh retained — still used
by backtest_env_step kernel (single-step evaluator).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire 4th magnitude branch (size=3) into BranchingDuelingQNetwork construction,
DQNAgentType::branch_sizes() return type, training loop diversity metrics,
backtest config in metrics.rs and hyperopt adapter, and DT pre-training config.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove exposure_aux_weight, exposure_targets, best_exposure_out fields and
associated methods (run_pretrain_step, replay_adam_and_readback_pretrain) from
fused_training.rs and gpu_experience_collector.rs. Delete corresponding
callers in training_loop.rs: pretrain loop, exposure copy block, aux weight
decay. All were exposure-branch artifacts incompatible with the 4-branch
(direction/magnitude/order/urgency) factorization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces broken 9-output exposure branch with two 3-output branches.
Each 3-output branch has -1/2 gradient (33%) — strong enough for dueling.
Eliminates i%3 Q-value degeneracy permanently.
3 novel gems: direction-specific dense reward, per-branch epsilon,
Flat detection shortcut with gradient zeroing.
~480 lines of dead code removed (aux optimizer, CEA, pre-training).
20+ files changed. Full CUDA, no CPU path, no stubs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
With separate optimizer, no gradient competition — safe to increase.
Smoke test shows within-group differentiation starting (S100≠S25≠L50).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The dueling A'[i] = A[i] - mean(A) gives identical -1/9 gradient to all
8 non-selected exposure outputs, causing i%3 Q-value degeneracy.
For d==0 (exposure, 9 outputs): skip mean subtraction entirely.
Non-selected outputs get ZERO C51 gradient — only the separate aux
optimizer updates them with unique per-output directional signal.
Branches d=1,2 (order=3, urgency=3) keep mean subtraction — the -1/2
gradient (33%) is strong enough to differentiate 3 outputs.
4 locations patched in the C51 loss kernel.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The exposure aux loss now has its OWN Adam state (m, v, t) and updates
the exposure output weights INDEPENDENTLY from the main C51 optimizer.
No shared gradient buffer, no gradient clipping competition.
This is multi-task learning done right — each objective has its own
optimizer that doesn't interfere with the other.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The exposure aux backward GEMM amplifies gradients through the
hidden activation matrix (d_logits × h_b0), producing grad_norm=5000+.
With max_grad_norm=10, this throttles ALL gradients by 500x (lr/500).
Fix: reduce aux_weight 0.5→0.01 (50x) + clip_grad_buf_inplace after
aux GEMM to cap combined gradient at max_grad_norm.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix 7: Merge spectral+forward+aux into graph_mega — 2 graph launches per step
instead of 3-4. Batch upload, stochastic depth mask, HER donors, and adam step
counter run outside the graph. Exposure aux GEMM moved into submit_aux_ops so
it's captured inside graph_mega. Falls back to individual graphs if mega capture
fails.
Fix 3: Validation runs on dedicated CUDA stream forked in constructor. Uses
one-epoch-delayed cached result to avoid blocking the training stream. Epoch 0
runs synchronously to bootstrap the async pipeline.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Changed exposure_aux_grad_kernel parameter from `float aux_weight` to
`const float* aux_weight_ptr`. GPU-resident scalar updated via async
HtoD before each launch. Enables capturing aux GEMM in CUDA graph.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix 2: Eliminate validation graph re-capture (DtoD into same buffers)
Fix 3: Async validation on separate CUDA stream (overlap with experience)
Fix 4: Aux GEMM in graph_aux (GPU scalar + kernel sig change)
Fix 7: Mega-graph fusion (spectral+forward+aux → 2 launches)
Combined with 5 implemented fixes: 47s → ~16s/epoch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TD(λ): self-bootstrap with rewards as Q(s') approximation, overwrites n-step
Hindsight: relabel fraction of experience rewards with optimal exit PnL
Curriculum: sort bars by difficulty, restrict to easy bars early, expand over training
No stubs, no debug placeholders — all fully wired.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Pre-training: 50 batch supervised direction init at epoch 0
- Exposure aux targets: DtoD copy from collector to fused context
- PopArt: wired behind config flag (disabled by default)
- TD(λ)/hindsight/curriculum: stubs with config guards
- Loss threshold 500→100K for v8 reward distribution
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>