diff --git a/crates/ml-alpha/cuda/actions_to_market_targets.cu b/crates/ml-alpha/cuda/actions_to_market_targets.cu index e82b174b4..f118cf44e 100644 --- a/crates/ml-alpha/cuda/actions_to_market_targets.cu +++ b/crates/ml-alpha/cuda/actions_to_market_targets.cu @@ -126,15 +126,36 @@ extern "C" __global__ void actions_to_market_targets( // ──────────────────────────────────────────────────────────────────── // Layer 1 (CMDP hard constraints — spec 2026-05-30-adaptive-risk-management). - // Session-level overrides force Hold regardless of agent's choice. - // Per-batch (one independent backtest session per b); a tripped DD - // or active cooldown on session `b` does NOT lock out the others. + // Session-level overrides force the account FLAT regardless of agent's + // choice. Per-batch (one independent backtest session per b); a tripped + // DD or active cooldown on session `b` does NOT lock out the others. + // + // Fix E (2026-05-30): emit a *closing* market order when the account + // is non-flat — otherwise an existing losing position bleeds m2m for + // the entire 500-step cooldown with no exit (trail stops are also + // suppressed by this branch). Cluster alpha-rl-rjsjq step 371 showed + // worst session_pnl growing monotonically -$3.7k → -$9.3k from + // exactly this mechanism. + // + // Once the position is flat, subsequent cooldown steps emit a true + // no-op (side=2, size=0) and the account waits for its recovery clock. // ──────────────────────────────────────────────────────────────────── const bool dd_triggered = session_dd_triggered_per_batch[b] >= 0.5f; const bool in_cooldown = cooldown_remaining_per_batch[b] > 0.0f; if (dd_triggered || in_cooldown) { - market_targets[b * 2 + 0] = 2; // no-op side - market_targets[b * 2 + 1] = 0; // size = 0 + if (position_lots > 0) { + // long → close via sell + market_targets[b * 2 + 0] = 1; + market_targets[b * 2 + 1] = position_lots; + } else if (position_lots < 0) { + // short → close via buy + market_targets[b * 2 + 0] = 0; + market_targets[b * 2 + 1] = -position_lots; + } else { + // already flat → true no-op + market_targets[b * 2 + 0] = 2; + market_targets[b * 2 + 1] = 0; + } return; }