fix(eval): use hyperopt max_position_absolute in walk-forward evaluator

The GpuBacktestConfig hardcoded max_position=1.0 with a 2× leverage cap,
reducing effective position to 0.2026 contracts on ES at $35K capital.
Training uses max_position_absolute from PSO params (1.0-4.0 contracts)
with no leverage cap — a 5.4× mismatch that makes transaction costs
overwhelm any alpha in walk-forward evaluation (0% win rate, -688% return).

Fix: Pass max_position_absolute through evaluate_gpu() and disable
leverage cap (max_leverage=0) to match training conditions. Same fix
applied to evaluate_baseline.rs via --max-position CLI arg.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-12 18:11:17 +01:00
parent 923ec3b9cc
commit 08eaef4a4f
2 changed files with 21 additions and 4 deletions

View File

@@ -174,6 +174,11 @@ struct Args {
#[arg(long, default_value_t = 100_000.0)]
initial_capital: f64,
/// Maximum absolute position size in contracts (must match training config).
/// Disable leverage cap to match training env (no leverage constraint during training).
#[arg(long, default_value_t = 2.0)]
max_position: f64,
/// Use CUDA Graph capture for the DQN evaluation step loop.
///
/// When enabled, the entire step loop (gather + forward + env_step for
@@ -1075,10 +1080,11 @@ fn evaluate_dqn_fold_gpu(
}
let gpu_config = GpuBacktestConfig {
max_position: 1.0,
max_position: args.max_position as f32,
tx_cost_bps: args.tx_cost_bps as f32,
spread_cost: (args.tick_size * args.spread_ticks) as f32,
initial_capital: args.initial_capital as f32,
max_leverage: 0.0, // Disabled: match training env (no leverage cap)
..Default::default()
};
@@ -1361,10 +1367,11 @@ fn evaluate_ppo_fold_gpu(
}
let gpu_config = GpuBacktestConfig {
max_position: 1.0,
max_position: args.max_position as f32,
tx_cost_bps: args.tx_cost_bps as f32,
spread_cost: (args.tick_size * args.spread_ticks) as f32,
initial_capital: args.initial_capital as f32,
max_leverage: 0.0, // Disabled: match training env (no leverage cap)
..Default::default()
};
@@ -1478,10 +1485,11 @@ fn evaluate_supervised_fold_gpu(
}
let gpu_config = GpuBacktestConfig {
max_position: 1.0,
max_position: args.max_position as f32,
tx_cost_bps: args.tx_cost_bps as f32,
spread_cost: (args.tick_size * args.spread_ticks) as f32,
initial_capital: args.initial_capital as f32,
max_leverage: 0.0, // Disabled: match training env (no leverage cap)
..Default::default()
};

View File

@@ -1650,6 +1650,7 @@ impl DQNTrainer {
window_size: usize,
stride: usize,
device: &candle_core::Device,
max_position_absolute: f64,
) -> Result<Option<BacktestMetrics>, MLError> {
use crate::cuda_pipeline::gpu_backtest_evaluator::{
GpuBacktestConfig, GpuBacktestEvaluator,
@@ -1721,11 +1722,18 @@ impl DQNTrainer {
window_prices.push(prices);
window_features.push(features);
}
// Use the hyperopt param's position limit — NOT hardcoded 1.0.
// Training env uses max_position_absolute directly, so the walk-forward
// evaluator must match. Leverage cap disabled (max_leverage=0) because
// the training env doesn't have one; applying it here creates a
// train/eval mismatch that makes transaction costs dominate any alpha.
#[allow(clippy::cast_possible_truncation)]
let config = GpuBacktestConfig {
max_position: 1.0,
max_position: max_position_absolute as f32,
tx_cost_bps: self.tx_cost_bps as f32,
spread_cost: (self.tick_size * self.spread_ticks) as f32,
initial_capital: self.initial_capital as f32,
max_leverage: 0.0, // Disabled: match training env (no leverage cap)
..Default::default()
};
@@ -3165,6 +3173,7 @@ impl HyperparameterOptimizable for DQNTrainer {
window_size,
stride,
&device,
params.max_position_absolute,
) {
Ok(m) => {
if m.is_some() {