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