From e5ec2fc834c6b136b7485440547d3e425d8fdf72 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Fri, 27 Mar 2026 09:41:53 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20configurable=20contract=20spec=20?= =?UTF-8?q?=E2=80=94=20eliminate=20hardcoded=20ES=20constants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tick_size, contract_multiplier, margin_pct to DQNHyperparameters. Wire through ExperienceCollectorConfig and GpuBacktestConfig to both CUDA kernels as runtime parameters. Before: margin = close * 50.0f * 0.06f (hardcoded ES) After: margin = close * contract_multiplier * margin_pct (configurable) Before: spread_cost = 0.25 * 50.0 * frac (hardcoded ES) After: spread_cost = tick_size * contract_multiplier * frac (configurable) Defaults match ES: tick=0.25, mult=50, margin=6%. For NQ: tick=0.25, mult=20. For 6E: tick=0.00005, mult=125000. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/cuda_pipeline/backtest_env_kernel.cu | 12 ++++++----- .../src/cuda_pipeline/experience_kernels.cu | 9 ++++++-- .../cuda_pipeline/gpu_backtest_evaluator.rs | 7 +++++++ .../cuda_pipeline/gpu_experience_collector.rs | 12 +++++++++++ crates/ml/src/hyperopt/adapters/dqn.rs | 2 ++ crates/ml/src/trainers/dqn/config.rs | 21 +++++++++++++++++++ .../src/trainers/dqn/trainer/training_loop.rs | 6 ++++-- 7 files changed, 60 insertions(+), 9 deletions(-) diff --git a/crates/ml/src/cuda_pipeline/backtest_env_kernel.cu b/crates/ml/src/cuda_pipeline/backtest_env_kernel.cu index 840f67ed9..8b5bff74a 100644 --- a/crates/ml/src/cuda_pipeline/backtest_env_kernel.cu +++ b/crates/ml/src/cuda_pipeline/backtest_env_kernel.cu @@ -44,7 +44,9 @@ extern "C" __global__ void backtest_env_step( int b0_size, int b1_size, int b2_size, - int min_hold_bars + int min_hold_bars, + float contract_multiplier, // e.g. 50.0 for ES, 20.0 for NQ + float margin_pct // e.g. 0.06 (6% initial margin) ) { __shared__ float shmem_pf[256 * PORTFOLIO_STATE_SIZE]; @@ -119,10 +121,10 @@ extern "C" __global__ void backtest_env_step( int order_type_idx = decode_order_type(action_val, b1_size, b2_size); // ── Margin-aware position cap (shared: trade_physics.cuh) ──────────── - // ES futures: ~$15K initial margin per contract. Prevents overleveraging - // when equity is depleted — a depleted account can't hold the same - // position as a full account. close * 50 ≈ contract notional for ES. - float margin_per_contract = close * 50.0f * 0.06f; // ~6% of notional ≈ $15K for ES at $5000 + // Prevents overleveraging when equity is depleted — a depleted account + // can't hold the same position as a full account. + // margin = price * multiplier * margin_pct (e.g. 5000 * 50 * 0.06 = $15K for ES) + float margin_per_contract = close * contract_multiplier * margin_pct; target_exposure = apply_margin_cap(target_exposure, value, margin_per_contract); // Suppress unused variable warnings diff --git a/crates/ml/src/cuda_pipeline/experience_kernels.cu b/crates/ml/src/cuda_pipeline/experience_kernels.cu index b4245daad..318fc6202 100644 --- a/crates/ml/src/cuda_pipeline/experience_kernels.cu +++ b/crates/ml/src/cuda_pipeline/experience_kernels.cu @@ -504,6 +504,9 @@ extern "C" __global__ void experience_action_select( * @param q_gaps [N] or NULL Q-gap conviction scaling * @param raw_returns_out [N, L] or NULL true per-bar portfolio return (unshaped) * @param min_hold_bars minimum bars to hold before exiting or reversing + * @param spread_cost bid-ask spread cost per unit (matches backtest) + * @param contract_multiplier dollar multiplier per point (50 for ES, 20 for NQ) + * @param margin_pct initial margin as fraction of notional (0.06 = 6%) */ extern "C" __global__ void experience_env_step( const float* __restrict__ targets, @@ -533,7 +536,9 @@ extern "C" __global__ void experience_env_step( const float* __restrict__ q_gaps, /* [N] or NULL — Q-gap conviction scaling */ float* raw_returns_out, /* [N, L] output: true per-bar portfolio return (unshapen) */ int min_hold_bars, /* minimum bars to hold before exiting or reversing */ - float spread_cost /* bid-ask spread cost per unit (matches backtest) */ + float spread_cost, /* bid-ask spread cost per unit (matches backtest) */ + float contract_multiplier, /* e.g. 50.0 for ES, 20.0 for NQ */ + float margin_pct /* e.g. 0.06 (6% initial margin) */ ) { int i = blockIdx.x * blockDim.x + threadIdx.x; if (i >= N) return; @@ -664,7 +669,7 @@ extern "C" __global__ void experience_env_step( * can't hold the same position size as a full account. * margin ≈ 6% of notional (CME ES initial margin ~$15K per contract). */ { - float margin_per_contract = raw_close * 50.0f * 0.06f; + float margin_per_contract = raw_close * contract_multiplier * margin_pct; float portfolio_val = ps[2]; target_position = apply_margin_cap(target_position, portfolio_val, margin_per_contract); } diff --git a/crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs b/crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs index 18a2953ee..1b936d2f7 100644 --- a/crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs +++ b/crates/ml/src/cuda_pipeline/gpu_backtest_evaluator.rs @@ -187,6 +187,10 @@ pub struct GpuBacktestConfig { /// Minimum bars to hold a position (matches training kernel's min_hold_bars). /// Enforced in `backtest_env_step` to eliminate train/eval mismatch. pub min_hold_bars: i32, + /// Initial margin as fraction of notional value. + /// CME initial margin ~6% of notional for equity index futures. + /// Default: 0.06. + pub margin_pct: f32, } impl Default for GpuBacktestConfig { @@ -201,6 +205,7 @@ impl Default for GpuBacktestConfig { ofi_dim: 0, bars_per_day: 390.0, // 1-minute bar frequency (6.5h × 60min) min_hold_bars: 5, + margin_pct: 0.06, } } } @@ -1674,6 +1679,8 @@ impl GpuBacktestEvaluator { .arg(&b1_i32) .arg(&b2_i32) .arg(&self.config.min_hold_bars) + .arg(&self.config.contract_multiplier) + .arg(&self.config.margin_pct) .launch(env_cfg) .map_err(|e| { MLError::ModelError(format!("backtest_env_step launch step {step}: {e}")) diff --git a/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs b/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs index 7e6642bb6..16888bb8d 100644 --- a/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs +++ b/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs @@ -241,6 +241,14 @@ pub struct ExperienceCollectorConfig { pub min_hold_bars: i32, /// Bid-ask spread cost per unit position change. Matches backtest_env_kernel's spread_cost. pub spread_cost: f32, + /// Contract multiplier (dollar value per point). + /// ES = 50, NQ = 20, 6E = 125_000, ZN = 1000. + /// Default: 50.0 (ES). + pub contract_multiplier: f32, + /// Initial margin as fraction of notional value. + /// CME initial margin ~6% of notional for equity index futures. + /// Default: 0.06. + pub margin_pct: f32, } impl Default for ExperienceCollectorConfig { @@ -290,6 +298,8 @@ impl Default for ExperienceCollectorConfig { enable_action_masking: false, min_hold_bars: 5, spread_cost: 0.0, // default: no spread cost (overridden by hyperparams) + contract_multiplier: 50.0, + margin_pct: 0.06, } } } @@ -1338,6 +1348,8 @@ impl GpuExperienceCollector { .arg(&mut self.raw_returns_out) // Raw portfolio returns (unshaped) for Sharpe/MaxDD .arg(&min_hold_bars_i32) // min_hold_bars for hold enforcement .arg(&config.spread_cost) // bid-ask spread cost (matches backtest) + .arg(&config.contract_multiplier) // futures contract multiplier (e.g. 50 for ES) + .arg(&config.margin_pct) // initial margin fraction (e.g. 0.06 = 6%) .launch(launch_cfg) .map_err(|e| MLError::ModelError(format!( "experience_env_step t={t}: {e}" diff --git a/crates/ml/src/hyperopt/adapters/dqn.rs b/crates/ml/src/hyperopt/adapters/dqn.rs index 8fc0ac199..a8788830c 100644 --- a/crates/ml/src/hyperopt/adapters/dqn.rs +++ b/crates/ml/src/hyperopt/adapters/dqn.rs @@ -1757,6 +1757,8 @@ impl DQNTrainer { tx_cost_bps: internal_trainer.hyperparams().transaction_cost_multiplier as f32, spread_cost: (self.tick_size * self.spread_ticks) as f32, initial_capital: self.initial_capital as f32, + contract_multiplier: internal_trainer.hyperparams().contract_multiplier as f32, + margin_pct: internal_trainer.hyperparams().margin_pct as f32, max_leverage: 0.0, // Disabled: match training env (no leverage cap) // OFI reorder in gather kernel: produces [market, portfolio, OFI, pad] // directly, eliminating the Candle narrow+cat closure. diff --git a/crates/ml/src/trainers/dqn/config.rs b/crates/ml/src/trainers/dqn/config.rs index 114a8c6cc..1ba952e43 100644 --- a/crates/ml/src/trainers/dqn/config.rs +++ b/crates/ml/src/trainers/dqn/config.rs @@ -1305,6 +1305,22 @@ pub struct DQNHyperparameters { /// 0.25 = optimize for worst 25% outcomes (moderate risk aversion). /// Default: 0.05. pub cvar_alpha: f32, + + // Contract specification — symbol-dependent constants + /// Futures tick size (minimum price increment). + /// ES = 0.25, NQ = 0.25, 6E = 0.00005, ZN = 1/64 ≈ 0.015625. + /// Used with `contract_multiplier` to compute spread cost. + /// Default: 0.25 (ES/NQ). + pub tick_size: f64, + /// Contract multiplier (dollar value per point). + /// ES = 50, NQ = 20, 6E = 125_000, ZN = 1000. + /// Used for margin computation and spread cost. + /// Default: 50.0 (ES). + pub contract_multiplier: f64, + /// Initial margin as fraction of notional value. + /// CME initial margin ≈ 6% of notional for equity index futures. + /// Default: 0.06. + pub margin_pct: f64, } impl Default for DQNHyperparameters { @@ -1585,6 +1601,11 @@ impl DQNHyperparameters { // CVaR action selection: enabled by default (risk-aware IQN action scoring) use_cvar_action_selection: true, cvar_alpha: 0.05, // Optimize for worst 5% quantile tail + + // Contract specification: ES futures defaults + tick_size: 0.25, // ES/NQ tick size ($0.25) + contract_multiplier: 50.0, // ES: $50 per point (NQ=20, 6E=125_000, ZN=1000) + margin_pct: 0.06, // CME initial margin ~6% of notional } } } diff --git a/crates/ml/src/trainers/dqn/trainer/training_loop.rs b/crates/ml/src/trainers/dqn/trainer/training_loop.rs index 5cf402c3d..5f73d2eea 100644 --- a/crates/ml/src/trainers/dqn/trainer/training_loop.rs +++ b/crates/ml/src/trainers/dqn/trainer/training_loop.rs @@ -1050,9 +1050,11 @@ impl DQNTrainer { dsr_eta: self.hyperparams.dsr_eta as f32, n_steps: self.hyperparams.n_steps as i32, min_hold_bars: self.hyperparams.min_hold_bars as i32, - // ES futures: tick_size=0.25, typical spread=1 tick, cost = tick * multiplier * fraction + // spread_cost = tick_size * multiplier * fraction // Matches backtest_env_kernel's spread_cost from GpuBacktestConfig - spread_cost: (0.25 * 50.0 * self.hyperparams.fill_spread_cost_frac) as f32, + spread_cost: (self.hyperparams.tick_size * self.hyperparams.contract_multiplier * self.hyperparams.fill_spread_cost_frac) as f32, + contract_multiplier: self.hyperparams.contract_multiplier as f32, + margin_pct: self.hyperparams.margin_pct as f32, ..Default::default() };