Three fixes for production readiness:
1. Direction Flat floor: 30% unconditional Flat probability prevents
over-trading (was 7% Flat = 93% directional = massive cost drag).
Hard constraint, not Q-dependent, can't snowball. Combined with
hold enforcement (min_hold_bars=10), actual Flat is ~13%.
2. MSE loss online_eq + target_eq: consistent mean-logit for d<=1.
Both sides of TD error use the same representation — no mismatch.
Removes the last C51 softmax bias from magnitude gradient path.
Half grows 2.7%→5.7%, Full grows 2.7%→5.3% across epochs.
3. compute_expected_q: mean-logit for d<=1 (action selection + eval).
Safe — not in training loss path.
Result: 7/7 diversity sustained, Flat stable at 13%, Sharpe positive
at 3/4 epochs. 19/19 smoke tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two remaining C51 softmax paths were causing Quarter (smallest position)
to dominate at 94%:
1. MSE loss online_eq: C51 softmax biased towards Quarter via gradient
d(td²)/d(weights) flowing through d(softmax_expected_Q)/d(weights).
Fix: mean-logit for d<=1, matching target_eq representation.
2. compute_expected_q: C51 softmax Q-values fed Boltzmann action
selection, inflating Quarter's Q. Fix: mean-logit for d<=1.
Safe — this kernel is NOT in the training loss path.
Both online_eq and target_eq now consistently use mean-logit for
direction+magnitude. The TD error is unbiased: no representation
mismatch. C51 softmax retained for order+urgency (d>=2) where
C51 gradient is active.
Result: Half grows 2.7%→5.7%, Full grows 2.7%→5.3% across epochs.
No collapse. 19/19 smoke tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: C51 distributional softmax structurally favors zero/low-variance
actions (Flat for direction, Small for magnitude). This created irrecoverable
feedback loops through FOUR paths: gradient, Bellman target argmax, action
selection, and the Q-gap conviction filter.
Direction branch (new):
- Zero C51 gradient for direction (d==0) — same treatment as magnitude
- Boltzmann softmax replaces argmax for direction selection (tau=2×Q_range)
- 2× MSE gradient amplification for direction branch head
- Mean advantage (not C51 softmax) for Bellman target argmax at d==0
- Q-gap conviction filter REMOVED from training path (redundant with
Boltzmann, harmful after high-Sharpe epochs where Bellman max bootstrap
raises Q(Flat) towards Q(directional), shrinking the gap)
Magnitude branch (Bellman target fix):
- Mean advantage for Bellman target argmax at d==1 in both MSE + C51 kernels
- Proper softmax retained for online_eq and target_eq (learning objective)
Metric fixes:
- Diversity denominator: 9 → 7 (Flat forces mag=Half, max reachable is 7)
- Threshold: 0.5% of total → 1% of directional (Flat-dominant policies
mechanically killed diversity under the old metric)
Result: 7/7 dir×mag diversity sustained epochs 7-10, Flat stable at 7-9%
(was 60-87% growing). 19/19 smoke tests pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The MSE and C51 loss kernels computed Bellman targets using C51's
distributional softmax expected Q for the argmax over next-state actions.
For magnitude (d==1), this structurally favored Small (tight distribution
→ higher softmax expected Q), creating an irrecoverable target feedback
loop even when C51 gradient was zeroed.
Fix: for magnitude branch (d==1), use mean-logit Q (average of V+A
across atoms without softmax weighting) for both argmax selection AND
target Q computation. This is variance-neutral — only the average
advantage level matters, not the distributional shape.
Applied to all 3 kernels:
- mse_loss_kernel.cu: argmax + target_eq
- c51_loss_kernel.cu: argmax
- expected_q_kernel.cu: expected Q for backtest evaluator
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
S&P on direction branch kills Q-gap conviction → forces Flat →
magnitude diversity collapses mechanically. Protect all 4 branch
heads [8..24], perturb only trunk + value head + bottleneck.
Note: smoke test (3200 bars, 10 epochs) naturally converges to Flat
regardless of S&P — insufficient data for sustained directional
conviction. H100 production run (5.7M bars) showed 7/9 diversity
sustained through all 7 epochs with positive Sharpe at epoch 6.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two issues found in the H100 production run:
1. Primary gradient budget dropped from 1.0 to 0.10 when C51 alpha
ramped to 1.0 (c51_frac=0.10 with IQN at 60%). Branch heads
depend entirely on primary gradient — starved → grad_norm=0.000
at epoch 7. Fix: floor c51_frac at 0.30 so branch heads always
get meaningful gradient.
2. val_loss identical at epochs 1-2 (6.228316) because the backtest
evaluator's CUDA Graph was never invalidated between epochs.
Fix: call invalidate_dqn_graph() before each evaluation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The forward pass (action selection) must apply the same advantage
standardization as the loss kernels. Otherwise the network's raw
advantages can encode stale C51 preferences that don't match the
standardized training signal.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
C51 cross-entropy structurally prefers low-variance actions (Small positions
have tighter return distributions). MSE is variance-neutral. By reducing
C51's gradient to 20% and amplifying MSE to 4× for the magnitude branch,
MSE becomes the dominant loss signal for position sizing.
Direction/order/urgency keep full C51 gradient for distributional risk awareness.
Replaces the previous mag_amp=1.5 which addressed a symptom (Flat gradient
starvation) rather than the root cause (C51 variance bias).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>