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:
@@ -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 =
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user