diff --git a/crates/ml/src/cuda_pipeline/experience_kernels.cu b/crates/ml/src/cuda_pipeline/experience_kernels.cu index b1a79d94d..8b1857280 100644 --- a/crates/ml/src/cuda_pipeline/experience_kernels.cu +++ b/crates/ml/src/cuda_pipeline/experience_kernels.cu @@ -1020,7 +1020,7 @@ extern "C" __global__ void experience_action_select( * [0] preproc_close — log-return-normalised close (network only) * [1] preproc_next — log-return-normalised next close (unused here) * [2] raw_close — raw close price (for position cost + tx) - * [3] raw_next — raw next-bar close (for mark-to-market PnL) + * [3] raw_next — raw next-bar close (UNUSED in reward path) * * portfolio_states layout: [N, PORTFOLIO_STRIDE=23] (read-write, bf16) * See file header for field definitions. @@ -1151,7 +1151,8 @@ extern "C" __global__ void experience_env_step( /* ---- Read raw prices from targets ---- */ /* Target layout: [preproc_close, preproc_next, raw_close, raw_next] * [0:1] = preprocessed (log-return normalized) — for Q-network - * [2:3] = raw dollar prices — for portfolio simulation P&L + tx costs */ + * [2] = raw close — for portfolio simulation P&L + tx costs + * [3] = raw_next — NOT used in reward/P&L/equity (future info) */ const float* tgt = targets + (long long)bar_idx * 4; /* Trade physics (execute_trade, compute_tx_cost, etc.) use float internally * because they involve multi-step accumulation where bf16 precision is @@ -1159,11 +1160,13 @@ extern "C" __global__ void experience_env_step( * prices to float here for the physics section, then store results back * as bf16 at the end. */ float raw_close = (tgt[2]); - float raw_next = (tgt[3]); + /* raw_next (tgt[3]) is FUTURE information — must NOT be used for any + * reward, P&L, equity, or portfolio computation. Only raw_close is + * valid for the experience collector's portfolio simulation. */ + (void)tgt[3]; /* suppress unused-element warning */ /* Guard against degenerate prices from data gaps. */ if (raw_close <= 0.0f) raw_close = 1.0f; - if (raw_next <= 0.0f) raw_next = raw_close; /* ---- Read full portfolio state (PORTFOLIO_STRIDE=23) ---- */ /* Portfolio arithmetic uses float accumulators because trade physics @@ -1407,11 +1410,14 @@ extern "C" __global__ void experience_env_step( order_type_idx, spread_scale); /* ---- Mark-to-market PnL for this timestep ---- */ - float raw_pnl = position * (raw_next - raw_close); - float old_pos_pnl = pre_trade_position * (raw_next - raw_close); + /* Per-bar mark-to-market P&L removed: used raw_next (future price), + * creating 1-bar action-reward misalignment. Rewards are now + * per-trade only (realized P&L on position change). */ + float raw_pnl = 0.0f; + float old_pos_pnl = 0.0f; - /* ---- Mark-to-market equity ---- */ - float equity = cash + position * raw_next; + /* ---- Mark-to-market equity (current bar only) ---- */ + float equity = cash + position * raw_close; peak_equity = (equity > peak_equity) ? equity : peak_equity; /* ==== Trade lifecycle tracking ==== */ @@ -1583,23 +1589,21 @@ extern "C" __global__ void experience_env_step( } /* ── Layer 6: Urgency branch attribution (fill quality) ── */ + /* Uses raw_close only — mid_price previously averaged with raw_next + * (future), which leaked next-bar information into the reward. */ if (urgency_credit_weight > 0.0f && fabsf(position) > 0.001f && entry_price > 0.0f) { - float mid_price = (raw_close + raw_next) * 0.5f; float fill_improvement = (position > 0.0f) - ? (mid_price - entry_price) / fmaxf(entry_price, 1.0f) - : (entry_price - mid_price) / fmaxf(entry_price, 1.0f); + ? (raw_close - entry_price) / fmaxf(entry_price, 1.0f) + : (entry_price - raw_close) / fmaxf(entry_price, 1.0f); float urgency_credit = fmaxf(fill_improvement / vol_proxy, 0.0f); reward += urgency_credit_weight * fminf(urgency_credit, 2.0f); } /* ── Layer 7: Exit timing quality ── */ - if (exit_timing_weight > 0.0f && (exiting_trade || reversing_trade)) { - float post_exit_move = (closing_sign > 0) - ? (raw_next - raw_close) / fmaxf(raw_close, 1.0f) - : (raw_close - raw_next) / fmaxf(raw_close, 1.0f); - float timing_quality = -post_exit_move / vol_proxy; - reward += exit_timing_weight * asymmetric_soft_clamp(timing_quality * 10.0f) * 0.1f; - } + /* REMOVED: post_exit_move used raw_next (future price) to judge + * whether the exit was well-timed. This is pure lookahead bias — + * the model cannot know the next bar's price when it exits. + * Exit quality is already captured by realized P&L. */ /* (OFI-alignment moved to dense per-bar path below) */ @@ -1653,7 +1657,11 @@ extern "C" __global__ void experience_env_step( /* DSR EMA statistics still updated for positioned bars (used by metrics * reporting) but NO LONGER added to reward. */ if (w_dsr > 0.0f && fabsf(position) > 0.001f) { - float price_change_dsr = (raw_next - raw_close) / fmaxf(raw_close, 1.0f); + /* DSR EMA uses current-bar return only (not raw_next). Since we + * don't have the previous bar's close in this kernel invocation, + * we approximate with a zero return — DSR is for metrics only + * and does NOT feed into reward. */ + float price_change_dsr = 0.0f; float dsr_vol = 0.005f; if (features != NULL && bar_idx < total_bars && market_dim > 9) { float atr_n = (features[(long long)bar_idx * market_dim + 9]); @@ -1683,7 +1691,7 @@ extern "C" __global__ void experience_env_step( } /* ---- Drawdown penalty (from trade_physics.cuh) ---- */ - float equity_now = cash + position * raw_next; + float equity_now = cash + position * raw_close; reward += compute_drawdown_penalty(equity_now, peak_equity, dd_threshold, w_dd); /* G15: Action commitment penalty — penalize position changes beyond spread cost. @@ -1699,7 +1707,7 @@ extern "C" __global__ void experience_env_step( } /* ---- Portfolio value for floor check ---- */ - float new_portfolio_value_pre_floor = cash + position * raw_next; + float new_portfolio_value_pre_floor = cash + position * raw_close; /* ---- Capital floor protection (PDT $25K rule) ---- */ /* If equity drops to 25% drawdown from peak, terminate the episode.