fix(ml): wire minimum_profit_factor, fix eval capital, HFT score, Sortino

4 bugs found by deep investigation agents:

1. HIGH: minimum_profit_factor (search dim 30) was never forwarded from
   DQNHyperparameters to DQNConfig — trainer hardcoded 1.5, making the
   entire dimension wasted. Added field to DQNHyperparameters, wired
   through trainer.rs.

2. HIGH: Backtest EvaluationEngine used hardcoded $10K initial capital
   while training used $35K (self.initial_capital). Returns/Sharpe were
   3.5x distorted. Now uses self.initial_capital.

3. MEDIUM: calculate_hft_activity_score_wave10 multiplied already-100x
   buy_pct/sell_pct by 100 again, making the diversity penalty threshold
   (15%) unreachable (values were ~2700). Removed double multiplication.

4. MEDIUM: Sortino ratio returned 0.0 for all-positive returns (no
   downside deviation), penalizing perfect strategies in the 40%-weighted
   composite score. Now returns 100.0 (capped) when mean return > 0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-05 21:07:07 +01:00
parent 34b881fa14
commit c033a34fec
4 changed files with 25 additions and 10 deletions

View File

@@ -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 =

View File

@@ -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();

View File

@@ -775,6 +775,11 @@ pub struct DQNHyperparameters {
/// Mixed precision for GPU training (auto-detected from HardwareBudget)
pub mixed_precision: Option<crate::dqn::mixed_precision::MixedPrecisionConfig>,
/// 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
}
}
}

View File

@@ -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),