fix: CRITICAL — remove raw_next from ALL reward/P&L/equity computations
The experience collector used raw_next (NEXT bar's close) for P&L and equity, creating a 1-bar misalignment between actions and rewards. The model couldn't learn which actions produce which outcomes. val_Sharpe=24 (backtest uses raw_close correctly) but training Sharpe=0 (experience collector used raw_next, shifting reward by 1 bar). Fix: ALL portfolio computations use raw_close only. raw_next removed from reward path entirely. Rewards are per-trade only (position change = realized P&L, holding = tiny cost, flat = zero). This is the root cause of training Sharpe being capped at ~0 across ALL runs regardless of model complexity or component changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user