fix: hold enforcement moved before execute_trade — eliminates undo pattern

Training kernel now has same order as backtest:
  decode → trailing_stop → hold_enforcement → execute_trade → mark_to_market

Previously hold ran AFTER execute_trade and undid it by resetting
position/cash from ps[]. Now hold modifies target_position BEFORE
execute_trade, so the trade sees the correct target and no undo is needed.

This eliminates the last train/eval order-of-operations mismatch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-27 09:07:48 +01:00
parent 809ee287d8
commit ea57ed3d86

View File

@@ -722,59 +722,22 @@ extern "C" __global__ void experience_env_step(
}
}
/* ---- Execute trade (shared: trade_physics.cuh) ---- */
int order_type_idx = decode_order_type(action_idx, b1_size, b2_size);
float tx_cost = execute_trade(&position, &cash, target_position, raw_close,
tx_cost_multiplier, 0.0f, max_position,
order_type_idx, spread_scale);
/* ---- Mark-to-market PnL for this timestep ---- */
float raw_pnl = position * (raw_next - raw_close);
/* Old position's PnL — needed for reversal trade tracking. */
float old_pos_pnl = pre_trade_position * (raw_next - raw_close);
/* ---- Mark-to-market equity (needed for peak_equity and reward) ---- */
float equity = cash + position * raw_next;
peak_equity = (equity > peak_equity) ? equity : peak_equity;
/* ==== Trade lifecycle tracking (v4: segment-based, reversal-aware) ==== */
float is_flat = (fabsf(position) < 0.001f) ? 1.0f : 0.0f;
int curr_sign = (position > 0.001f) ? 1 : ((position < -0.001f) ? -1 : 0);
int entering_trade = (prev_sign == 0 && curr_sign != 0);
int exiting_trade = trail_triggered ? 1 : 0;
int reversing_trade = (prev_sign != 0 && curr_sign != 0 && prev_sign != curr_sign);
/* NOTE: reversal P&L booking moved AFTER hold enforcement (below). */
float reversal_return = 0.0f;
/* On entry from flat */
if (entering_trade) {
entry_price = raw_close;
trade_start_pnl = ps[11];
}
/* ════════════════════════════════════════════════════════════════════
* HOLD ENFORCEMENT: block ALL position changes during minimum hold.
* Applies to BOTH flat exits AND reversals.
* HOLD ENFORCEMENT: block exits and reversals during minimum hold.
*
* Runs BEFORE execute_trade (same order as backtest kernel).
* If hold is violated, override target_position to keep old position.
* execute_trade then sees delta≈0 and does nothing.
*
* Layer 2 safety net — Layer 1 (action masking) prevents most hold
* violations, but epsilon exploration can still slip through.
* Uses enforce_hold() from trade_physics.cuh for the decision.
* ════════════════════════════════════════════════════════════════════ */
int is_last_bar = (bar_idx >= total_bars - 1) ? 1 : 0;
float enforced_position = enforce_hold(ps[0], position, hold_time, min_hold_bars, is_last_bar);
int hold_violation = (fabsf(enforced_position - position) > 0.001f);
/* wants_exit / wants_reversal needed downstream for trade lifecycle */
int wants_exit = (prev_sign != 0 && curr_sign == 0);
int wants_reversal = (prev_sign != 0 && curr_sign != 0 && prev_sign != curr_sign);
float enforced_position = enforce_hold(ps[0], target_position, hold_time, min_hold_bars, is_last_bar);
int hold_violation = (fabsf(enforced_position - target_position) > 0.001f);
if (hold_violation) {
/* Override: keep previous position, cancel the action */
position = ps[0];
cash = ps[1];
is_flat = (fabsf(position) < 0.001f) ? 1.0f : 0.0f;
target_position = enforced_position; /* keep old position */
/* Fix the stored action to match held exposure (prevent action aliasing) */
float prev_exposure_frac = ps[0] / (max_position > 0.0f ? max_position : 1.0f);
@@ -785,18 +748,40 @@ extern "C" __global__ void experience_env_step(
int original_urgency = (b2_size > 0) ? action_idx % b2_size : 0;
action_idx = held_exposure * b1_size * b2_size + original_order * b2_size + original_urgency;
out_actions[out_off] = action_idx;
/* Recompute signs from overridden position */
curr_sign = prev_sign;
wants_exit = 0;
wants_reversal = 0;
}
/* Compute final trade lifecycle from (possibly overridden) state */
exiting_trade = wants_exit;
reversing_trade = wants_reversal;
/* ---- Execute trade (shared: trade_physics.cuh) ---- */
int order_type_idx = decode_order_type(action_idx, b1_size, b2_size);
float tx_cost = execute_trade(&position, &cash, target_position, raw_close,
tx_cost_multiplier, 0.0f, max_position,
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);
/* ---- Mark-to-market equity ---- */
float equity = cash + position * raw_next;
peak_equity = (equity > peak_equity) ? equity : peak_equity;
/* ==== Trade lifecycle tracking ==== */
float is_flat = (fabsf(position) < 0.001f) ? 1.0f : 0.0f;
int curr_sign = (position > 0.001f) ? 1 : ((position < -0.001f) ? -1 : 0);
int entering_trade = (prev_sign == 0 && curr_sign != 0);
int wants_exit = hold_violation ? 0 : (prev_sign != 0 && curr_sign == 0);
int wants_reversal = hold_violation ? 0 : (prev_sign != 0 && curr_sign != 0 && prev_sign != curr_sign);
int exiting_trade = trail_triggered ? 1 : wants_exit;
int reversing_trade = wants_reversal;
int segment_complete = exiting_trade || reversing_trade;
float reversal_return = 0.0f;
if (entering_trade) {
entry_price = raw_close;
trade_start_pnl = ps[11];
}
/* On reversal: close old segment, open new one.
* MUST run AFTER hold enforcement — if hold guard cancelled the reversal,
* reversing_trade is 0 and this block correctly does nothing. */