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:
jgrusewski
2026-04-16 19:05:43 +02:00
parent c0327ab319
commit ae5743ef4a

View File

@@ -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.