spec: OOS Performance Enhancement — 8 components for closing val/OOS gap

1. CVaR objective (risk-sensitive Bellman target, adaptive α)
2. Adaptive DSR (training Sharpe EMA drives w_dsr weight)
3. Ensemble heads (3 heads, epistemic uncertainty for exploration)
4. Mamba2 temporal scan (K=8 bar context, O(K) selective scan)
5. Multi-horizon prediction (1/5/20 bar value heads, regime-blended)
6. Adaptive atom positions (learned C51 spacing via softmax — NOVEL)
7. Regime-conditioned branch gating (ADX/CUSUM → branch importance — NOVEL)

Designed from live H100 analysis: val_Sharpe=45 but training_Sharpe=-1..+1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-04-15 18:44:18 +02:00
parent e1d548bdf3
commit d0b70b6c8a

View File

@@ -0,0 +1,245 @@
# OOS Performance Enhancement — Design Spec
**Goal:** Close the val_Sharpe/OOS gap (val=45 but training Sharpe oscillates -1 to +1) by adding risk-sensitive objectives, temporal context, ensemble diversity, multi-horizon prediction, adaptive reward shaping, and two novel techniques (adaptive atom positions + regime-conditioned branch gating).
**Problem:** The model achieves val_Sharpe=45+ on a fixed validation window but training Sharpe (OOS proxy) oscillates -1.2 to +0.9. The model finds good strategies but can't consistently profit on fresh data. Root causes: (1) optimizes E[Q] not worst-case, (2) no temporal context — sees one bar at a time, (3) single Q-estimate — can't distinguish epistemic from aleatoric uncertainty, (4) fixed reward shaping — can't adapt to market regime, (5) fixed atom spacing — wastes resolution on unlikely returns.
**Architecture:** 8 independent components composing through the existing pipeline.
---
## Component 1: CVaR Objective for Bellman Target
### Problem
Expected SARSA target optimizes E[Q] — tolerates high-variance strategies if the mean is good. Training Sharpe oscillates because the model takes risky bets.
### Solution
Replace E[Q] with CVaR_α[Q] in the Bellman target action sampling:
```
CVaR_α[Q_a] = mean of atoms in the bottom α-percentile of action a's distribution
target_action ~ softmax(CVaR_α[Q_target] / tau)
```
α is adaptive: starts at 0.5 (balanced), decreases toward 0.1 (risk-averse) as iqn_readiness increases.
### Implementation
- Modify `c51_loss_kernel.cu`: the Expected SARSA target sampling computes softmax weights from Q-values. Change to compute CVaR from the CDF (same infrastructure as quantile_q_select) and use CVaR for the softmax weights.
- `α = 0.5 - 0.4 * iqn_readiness` — adaptive schedule.
- Zero extra parameters.
---
## Component 2: Adaptive DSR Reward Shaping
### Problem
`w_dsr = 1.0` (fixed). Can't adapt to market regime — trending markets need low DSR, ranging needs high.
### Solution
```
sharpe_ema = EMA(training_sharpe, adaptive_alpha)
w_dsr_adaptive = w_dsr_base * clamp(1.0 - sharpe_ema, 0.5, 3.0)
```
Negative Sharpe → high DSR (force risk reduction). Positive Sharpe → low DSR (exploit).
### Implementation
- `training_sharpe_ema: f32` field, updated at epoch end from experience collection Sharpe.
- `w_dsr_adaptive` written to pinned device-mapped scalar, read by env_step kernel.
- Zero kernel changes — replaces the fixed `w_dsr` config value.
---
## Component 3: Ensemble Heads (3 heads)
### Problem
`ensemble_heads=1`. Single Q-estimate. Uncertainty from C51 is aleatoric (market randomness), not epistemic (model ignorance). Can't distinguish "I don't know" from "the market is random."
### Solution
Increase to `ensemble_heads=3`. Each shares the trunk but has independent branch weights.
```
Q_mean = mean(Q_1, Q_2, Q_3)
epistemic_var = var(Q_1, Q_2, Q_3) // model disagreement
exploration_bonus = sqrt(epistemic_var)
Q_select = Q_mean + exploration_bonus // OFU on epistemic uncertainty
```
### Implementation
- Config change: `ensemble_heads: 3`. Infrastructure already exists (`ensemble_kernels.cu`).
- ~2× branch parameter increase (~174K extra params).
- Ensemble variance replaces quantile Q-select for exploration (more principled).
---
## Component 4: Mamba2 Selective Scan for Temporal Context
### Problem
Model processes one bar at a time. No memory of recent market behavior within an episode.
### Solution
Rolling buffer of last K=8 trunk activations. Mamba2 selective scan compresses into temporal context:
```
A_t = sigmoid(W_A @ h_t) // forget gate (input-dependent)
B_t = W_B @ h_t // input projection
x_t = A_t * x_{t-1} + B_t * h_t // recurrent scan (O(K) time)
temporal_context = (W_C @ h_t) * x_t
h_s2_enriched = h_s2 + temporal_context // residual
```
### Implementation
- `h_history[B, K, SH2]` rolling buffer (~128MB for K=8, acceptable on H100).
- State_dim=16. Parameters: 3 × 256 × 16 = 12,288.
- New CUDA kernel `mamba2_temporal_scan`. Runs outside CUDA graph.
- From `crates/ml-supervised/src/mamba/scan_algorithms.rs`.
---
## Component 5: Multi-Horizon Prediction
### Problem
Single-step return prediction. Urgency branch has no temporal signal — can't distinguish scalping from swing opportunities.
### Solution
3 value heads (shared trunk, shared branches): 1-bar, 5-bar, 20-bar horizons.
```
Q_1bar = compute_expected_q(v_logits_1, adv_logits)
Q_5bar = compute_expected_q(v_logits_5, adv_logits)
Q_20bar = compute_expected_q(v_logits_20, adv_logits)
// Regime-weighted blend:
trend_weight = sigmoid((ADX - 25) / 5)
Q_blended = (1-trend_weight) * Q_1bar + trend_weight * Q_20bar
```
### Implementation
- 2 extra value heads: W_v1_5[VH, SH2], b_v1_5, W_v2_5[NA, VH], b_v2_5 (× 2 horizons).
- ~144K extra params.
- 3 passes of n-step kernel with step counts {1, 5, 20}.
- Urgency branch learns: Q_1bar >> Q_20bar → act now. Q_20bar >> Q_1bar → wait.
---
## Component 6: Adaptive Atom Positions (Novel)
### Problem
C51 atoms at fixed uniform positions z[j] = v_min + j * delta_z. 90% of atoms may cover returns that never happen, while the critical region near zero has too few atoms.
### Solution
Learnable atom positions via softmax-normalized spacing:
```
spacing_raw[51] // learnable parameter vector
spacing = softmax(spacing_raw) // sum-to-1 normalized
positions[j] = v_min + (v_max - v_min) * cumsum(spacing)[j]
```
The model concentrates atoms where returns have mass. Near-zero returns get higher resolution.
### Implementation
- 51 floats per branch × 4 branches = 204 extra params (in params_buf).
- New CUDA kernel `adaptive_atom_positions`: computes positions from spacing_raw.
- Replaces fixed `v_min + j * delta_z` in compute_expected_q, c51_loss, c51_grad.
- Gradient flows through atom positions via the C51 loss.
---
## Component 7: Regime-Conditioned Branch Gating (Novel)
### Problem
All 4 branches contribute equally to action selection. But branch importance is regime-dependent: direction matters in trends, magnitude matters in ranges.
### Solution
Learned regime gate produces per-branch importance weights:
```
regime_input = [ADX, CUSUM, Q_gap, atom_utilization] // 4 features
branch_importance[4] = softmax(W_regime @ regime_input + b_regime)
// Weight Q-values by branch importance:
Q_select[a] = Σ_d branch_importance[d] * Q_d[a_d]
```
### Implementation
- `W_regime[4, 4] + b_regime[4]` = 20 params.
- New CUDA kernel `regime_branch_gate`: computes softmax importance, scales Q-values.
- Runs after Q-mean centering, before Q-attention.
- Features already available in the state vector (ADX=40, CUSUM=41) and Q-stats (Q-gap, utilization).
---
## Full Pipeline (after all 8 components)
```
── Trunk ──
h_s2 = trunk_forward(states)
h_s2 = attention(h_s2)
h_s2_enriched = h_s2 + mamba2_temporal_scan(h_history) [Component 4]
── Branches (3 ensemble heads × 4 branches) ──
For each head e, branch d:
h_bd_e = KAN_gate(VSN(h_s2_enriched)) @ W_bdf_e_d
── Value Heads (3 horizons) ──
v_logits_1/5/20 = value_head_1/5/20(h_s2_enriched) [Component 5]
── Q-Values ──
Q_raw = compute_expected_q(v_logits_blended, adv_logits) [Component 5]
Q_raw = adaptive_atom_positions(Q_raw) [Component 6]
Q_centered = Q_raw - mean(Q_raw) [existing]
Q_regime = regime_branch_gate(Q_centered, ADX, CUSUM) [Component 7]
Q_coord = cross_branch_attention(Q_regime) [existing]
Q_graph = graph_message_pass(Q_coord) [existing]
── Action Selection ──
Q_ensemble_mean = mean(Q across 3 heads) [Component 3]
exploration = sqrt(var(Q across 3 heads)) [Component 3]
Q_select = Q_ensemble_mean + exploration
── Training ──
CVaR target: softmax(CVaR_α[Q_target] / tau) [Component 1]
Adaptive DSR: w_dsr *= clamp(1 - sharpe_ema, 0.5, 3.0) [Component 2]
```
---
## Implementation Order
1. Component 2 (Adaptive DSR) — zero kernel changes, CPU-side, immediate OOS impact
2. Component 1 (CVaR objective) — modify existing c51_loss kernel, zero params
3. Component 7 (Regime branch gating) — 20 params, one kernel
4. Component 6 (Adaptive atom positions) — 204 params, modifies existing kernels
5. Component 3 (Ensemble heads) — config change, existing infrastructure
6. Component 4 (Mamba2 temporal scan) — new trunk layer, 12K params
7. Component 5 (Multi-horizon prediction) — 2 new value heads, 144K params
---
## Success Criteria
| Metric | Current (train-w2z55 ep 21) | Target | Source |
|--------|---------------------------|--------|--------|
| Training Sharpe | -1.2 to +0.9 oscillating | >0.5 sustained | Components 1, 2 |
| val_Sharpe | 45.81 (climbing) | >50 sustained | Components 4, 5, 6 |
| val/OOS gap | val=45, training=0 | val≈training | Components 1, 2, 3 |
| Exploration quality | Aleatoric-based (C51 variance) | Epistemic-based (ensemble disagreement) | Component 3 |
| Temporal awareness | None (single bar) | 8-bar Mamba2 context | Component 4 |
| Branch importance | Uniform (25% each) | Regime-adaptive | Component 7 |
| Atom resolution | Uniform (wasted on unlikely returns) | Concentrated where returns have mass | Component 6 |
---
## Risks and Mitigations
| Risk | Mitigation |
|------|-----------|
| CVaR too conservative → stops trading | α starts at 0.5 (balanced), not 0.1 (extreme) |
| Adaptive DSR oscillates | EMA smoothing, clamped to [0.5, 3.0] |
| 3 ensemble heads → 2× params → overfitting | Shared trunk, only branches triplicated |
| Mamba2 history buffer 128MB | K=4 for 64MB if needed; H100 has 80GB |
| Multi-horizon 3× C51 loss compute | Value heads are small (VH=256); branch GEMMs shared |
| Adaptive atoms destabilize C51 loss | Softmax spacing → always valid positions; small learning rate for spacing params |
| Regime gate collapses to uniform | Softmax with temperature; initialized to uniform |