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:
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user