diff --git a/crates/ml/src/evaluation/metrics.rs b/crates/ml/src/evaluation/metrics.rs index 215908a25..732c01254 100644 --- a/crates/ml/src/evaluation/metrics.rs +++ b/crates/ml/src/evaluation/metrics.rs @@ -225,7 +225,9 @@ fn calculate_sortino_ratio(returns: &[f64]) -> f64 { .collect(); if downside_returns.is_empty() { - return 0.0; + // All returns are non-negative: infinite Sortino, capped at 100.0 + // to avoid distorting composite scores. + return if mean_return > 0.0 { 100.0 } else { 0.0 }; } let downside_deviation = diff --git a/crates/ml/src/hyperopt/adapters/dqn.rs b/crates/ml/src/hyperopt/adapters/dqn.rs index 92e06b774..812ac75aa 100644 --- a/crates/ml/src/hyperopt/adapters/dqn.rs +++ b/crates/ml/src/hyperopt/adapters/dqn.rs @@ -2110,16 +2110,14 @@ fn calculate_trade_insufficiency_penalty(total_trades: usize) -> f64 { } fn calculate_hft_activity_score_wave10(buy_pct: f64, sell_pct: f64, hold_pct: f64) -> f64 { - let buy_pct_100 = buy_pct * 100.0; - let sell_pct_100 = sell_pct * 100.0; - let hold_pct_100 = hold_pct * 100.0; - - let buy_sell_ratio = (buy_pct_100 + sell_pct_100) / (hold_pct_100 + 1e-6); + // NOTE: buy_pct/sell_pct/hold_pct arrive in 0-100 range (e.g. 27.0 = 27%). + // No additional scaling needed — thresholds are calibrated for 0-100 values. + let buy_sell_ratio = (buy_pct + sell_pct) / (hold_pct + 1e-6); let min_action_threshold = 15.0; - if buy_pct_100 < min_action_threshold || sell_pct_100 < min_action_threshold { + if buy_pct < min_action_threshold || sell_pct < min_action_threshold { // Penalize models that don't use all actions - -5.0 * (min_action_threshold - buy_pct_100.min(sell_pct_100)) / min_action_threshold + -5.0 * (min_action_threshold - buy_pct.min(sell_pct)) / min_action_threshold } else { // Reward high BUY/SELL activity (trend-following needs decisive actions) 2.0 * (buy_sell_ratio / 3.0).min(1.0) // Cap reward at 3:1 ratio @@ -2559,6 +2557,10 @@ impl HyperparameterOptimizable for DQNTrainer { use_cql: true, cql_alpha: params.cql_alpha, + // BUG #7: minimum_profit_factor — now tunable via hyperopt (idx 30, [1.1, 2.0]) + // Previously hardcoded to 1.5, meaning dimension 30 of the search space was wasted. + minimum_profit_factor: params.minimum_profit_factor, + // Phase 3: GPU experience collection (defaults, not in search space) gpu_n_episodes: 128, gpu_timesteps_per_episode: 500, @@ -2828,7 +2830,9 @@ impl HyperparameterOptimizable for DQNTrainer { const EVAL_CHUNK_SIZE: usize = 1024; let kelly_fraction = internal_trainer.get_kelly_fraction(); - let mut engine = EvaluationEngine::new_with_kelly(10000.0, kelly_fraction); + #[allow(clippy::cast_possible_truncation)] + let eval_capital = self.initial_capital as f32; + let mut engine = EvaluationEngine::new_with_kelly(eval_capital, kelly_fraction); let agent_arc = internal_trainer.get_agent().clone(); let device = internal_trainer.device().clone(); diff --git a/crates/ml/src/trainers/dqn/config.rs b/crates/ml/src/trainers/dqn/config.rs index 357f16d95..dbf9eeea1 100644 --- a/crates/ml/src/trainers/dqn/config.rs +++ b/crates/ml/src/trainers/dqn/config.rs @@ -775,6 +775,11 @@ pub struct DQNHyperparameters { /// Mixed precision for GPU training (auto-detected from HardwareBudget) pub mixed_precision: Option, + + /// Minimum profit factor for trade execution (BUG #7 fix) + /// Trades must have profit > cost × this factor to be executed. + /// Range [1.1, 2.0]: 1.1 = 10% margin above costs, 2.0 = 100% margin. + pub minimum_profit_factor: f64, } impl Default for DQNHyperparameters { @@ -966,6 +971,9 @@ impl DQNHyperparameters { // Mixed precision: None = auto-detect at trainer initialization mixed_precision: None, + + // BUG #7: Minimum profit factor for trade execution + minimum_profit_factor: 1.5, // Default: 50% margin above breakeven } } } diff --git a/crates/ml/src/trainers/dqn/trainer.rs b/crates/ml/src/trainers/dqn/trainer.rs index 450826b5e..b68066cba 100644 --- a/crates/ml/src/trainers/dqn/trainer.rs +++ b/crates/ml/src/trainers/dqn/trainer.rs @@ -449,7 +449,8 @@ impl DQNTrainer { use_cvar_action_selection: false, cvar_alpha: 0.05, - minimum_profit_factor: 1.5, + #[allow(clippy::cast_possible_truncation)] + minimum_profit_factor: hyperparams.minimum_profit_factor as f32, weight_decay: hyperparams.weight_decay, mixed_precision: hyperparams.mixed_precision.clone().or(mixed_precision_detected), entropy_coefficient: hyperparams.entropy_coefficient.unwrap_or(0.01),