From 35a5b4f1dc9542402eaf3d517b9dba2ef265e896 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Mon, 23 Mar 2026 12:44:03 +0100 Subject: [PATCH] fix: 4 critical root causes killing profitability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ROOT CAUSE 1: Stop-loss 0.3%→1%, take-profit 0.5%→2% (2:1 R:R). Old 0.3% = 8.3 ticks on ES. Normal 1-min noise is 4-6 ticks. 99.88% of trades were stopped out by NOISE, not by bad entries. ROOT CAUSE 2: Dense shaping 0.1x→0.01x. Over 50 bars, old dense signal = 5.0 vs completion ±2.0 — dense dominated. Now dense = 0.5 vs completion ±2.0 — trade completion is the primary signal. ROOT CAUSE 3: Action aliasing in 5-bar hold override. When model chose Flat but was forced to Hold, replay stored (state, Flat, Hold's reward) — corrupting Q-values for Flat. Now overwrites out_actions with the ACTUAL held exposure action. ROOT CAUSE 4: Hyperopt HFT activity weight 25%→5%. Old objective penalized selective trading. MIN_VIABLE_TRADES 100→20. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/cuda_pipeline/experience_kernels.cu | 34 +++++++++++++++---- crates/ml/src/hyperopt/adapters/dqn.rs | 4 +-- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/crates/ml/src/cuda_pipeline/experience_kernels.cu b/crates/ml/src/cuda_pipeline/experience_kernels.cu index 5ac94aeb4..1f8ff5b18 100644 --- a/crates/ml/src/cuda_pipeline/experience_kernels.cu +++ b/crates/ml/src/cuda_pipeline/experience_kernels.cu @@ -786,9 +786,13 @@ extern "C" __global__ void experience_env_step( float q_gap_val = (q_gaps != NULL) ? q_gaps[i] : 1.0f; float conv_mult = (q_gap_val < 0.5f) ? 0.5f : ((q_gap_val > 2.0f) ? 2.0f : q_gap_val); - float sl_level = -0.003f * vol_mult * conv_mult; /* conviction-scaled SL */ - float tp_level = 0.005f * vol_mult * trend_mult * conv_mult; /* conviction-scaled TP */ - float time_stop_bars = 100.0f * vol_mult * conv_mult; /* conviction-scaled time */ + /* Stop levels: 1% SL / 2% TP = 2:1 reward-to-risk ratio. + * Old 0.3% SL = 8.3 ticks on ES — normal 1-min noise (4-6 ticks) triggers it. + * New 1% SL = ~28 ticks (7 ES points) — survives normal market noise. + * New 2% TP = ~56 ticks (14 ES points) — captures meaningful moves. */ + float sl_level = -0.01f * vol_mult * conv_mult; /* 1% base SL */ + float tp_level = 0.02f * vol_mult * trend_mult * conv_mult; /* 2% base TP, 2:1 R:R */ + float time_stop_bars = 100.0f * vol_mult * conv_mult; /* Trailing stop: once trade is profitable, stop TRAILS the peak. * If peak unrealized = +0.4%, trailing stop = +0.4% - 0.3% = +0.1%. @@ -818,11 +822,26 @@ extern "C" __global__ void experience_env_step( is_flat = 1.0f; exiting_trade = 1; } else if (!was_flat && is_flat > 0.5f && hold_time < 5.0f && hold_time > 0.0f) { - /* Model tries to exit too early — override to hold */ + /* Model tries to exit too early — override to hold. + * CRITICAL: also fix the stored action to match what actually executed. + * Without this, the replay buffer stores (state, action=Flat, reward_from_Hold) + * which corrupts Q-values for Flat — classic action aliasing bug. */ position = ps[0]; cash = ps[1]; is_flat = 0.0f; exiting_trade = 0; + /* Overwrite the action output to the PREVIOUS action (hold = keep position). + * The previous action's exposure_idx maps back to a factored action. */ + float prev_exposure_frac = ps[0] / (max_position > 0.0f ? max_position : 1.0f); + int held_exposure = (int)((prev_exposure_frac + 1.0f) / 0.25f + 0.5f); + if (held_exposure < 0) held_exposure = 0; + if (held_exposure >= b0_size) held_exposure = b0_size - 1; + /* Reconstruct factored action with held exposure + original order/urgency */ + int original_order = (action_idx / b2_size) % b1_size; + int original_urgency = action_idx % b2_size; + action_idx = held_exposure * b1_size * b2_size + original_order * b2_size + original_urgency; + /* Overwrite the ALREADY-WRITTEN action in the output buffer */ + out_actions[out_off] = action_idx; } /* Idle counter (flat bars) — NO penalty. DSR already penalizes inaction @@ -903,8 +922,11 @@ extern "C" __global__ void experience_env_step( reward = choppy_bonus; } else { /* WEAK: 0.1x dense shaping while holding */ - reward = 0.1f * (w_dsr * dsr_t + w_pnl * asym_pnl) - - 0.1f * w_dd * drawdown_penalty_t + /* Dense shaping at 0.01x — trade completion reward must be 100x stronger. + * Old 0.1x: over 50 bars, dense=5.0 vs completion=±2.0 → dense dominated. + * New 0.01x: over 50 bars, dense=0.5 vs completion=±2.0 → completion dominates. */ + reward = 0.01f * (w_dsr * dsr_t + w_pnl * asym_pnl) + - 0.01f * w_dd * drawdown_penalty_t - hold_time_penalty; } diff --git a/crates/ml/src/hyperopt/adapters/dqn.rs b/crates/ml/src/hyperopt/adapters/dqn.rs index 4f37499df..b0f31cf8b 100644 --- a/crates/ml/src/hyperopt/adapters/dqn.rs +++ b/crates/ml/src/hyperopt/adapters/dqn.rs @@ -2402,7 +2402,7 @@ fn calculate_diversity_penalty(action_distribution: &[f64; 3]) -> f64 { /// /// Returns penalty in [0.0, 10.0] (additive on objective, higher = worse). fn calculate_trade_insufficiency_penalty(total_trades: usize) -> f64 { - const MIN_VIABLE_TRADES: usize = 100; + const MIN_VIABLE_TRADES: usize = 20; // 20 quality trades >> 100 noise trades if total_trades == 0 { 10.0 // Model does nothing at all @@ -3654,7 +3654,7 @@ impl HyperparameterOptimizable for DQNTrainer { // Combine: base objective + trade insufficiency + diversity penalty let base_objective = - -0.60 * composite_score + cvar_penalty + -0.25 * hft_activity + 0.15 * stability_penalty_raw; + -0.60 * composite_score + cvar_penalty + -0.05 * hft_activity + 0.15 * stability_penalty_raw; let objective = base_objective + trade_penalty + mid_diversity_penalty;