fix: 4 critical root causes killing profitability
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user