feat: configurable contract spec — eliminate hardcoded ES constants

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) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-27 09:41:53 +01:00
parent 4e37a16b77
commit e5ec2fc834
7 changed files with 60 additions and 9 deletions

View File

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

View File

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

View File

@@ -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}"))

View File

@@ -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}"

View File

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

View File

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

View File

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