Per spec §4.0 + plan C1.3. Without this gate, every fractional change in target_lots seeds an order via target-delta semantics; combined with the continuous controller invocation (every event after A1) this generates hyperactivity even with the multi-horizon §4.4 conviction formula (C1.2). v2 Gate-1 failures confirmed: scalar EMA + amplification clamp + per-event controller = 155k trades on a 2M-event smoke. The band absorbs fractional target adjustments so most events produce no order. When the signal genuinely shifts and target moves enough to cross the band, the kernel seeds. Continuous evaluation, sparse action. Greenfields atomic refactor — single config knob threaded through every layer in one commit: SweepBase.delta_floor: f32 (YAML, default 1.0 = 1 lot) SimVariant.delta_floor: Option<f32> (per-variant override) ResolvedSimVariant.delta_floor: f32 UniformSimParams.delta_floor: f32 BatchedSimConfig.delta_floor: Vec<f32> LobSimCuda.delta_floor_d: CudaSlice<f32> seed_inflight_limits_batched kernel param + skip-if-below-floor logic New accessor: LobSimCuda::read_inflight_count(b) — counts active != 0 limit slots for backtest b. Not cfg(test); follows existing read_limit_slot pattern. Test: no_trade_band_blocks_micro_delta verifies that a second decision with the same strong-bullish alpha (same target, effective = in-flight lots) produces delta=0 and does not re-seed. max_lots=1 keeps the arithmetic unambiguous. Existing tests: all UniformSimParams struct literals updated with delta_floor=0.0 (band disabled) so existing behaviour is preserved. harness.rs from_uniform path uses delta_floor=1.0 (production default). Hot-path discipline: the delta_floor_d upload happens once per run in the existing config-upload block (alongside threshold, cost, max_hold_ns); the kernel reads the device slot per-event without any host roundtrip. No memcpy_htod / dtoh / dtov / synchronize introduced on the per-event path. Per pearl_controller_anchors_isv_driven, this floor is currently a config constant; CRT.2 (Phase 2) makes it ISV-derived from rolling spread cost / signal volatility. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
150 lines
7.0 KiB
Rust
150 lines
7.0 KiB
Rust
//! Per-backtest sim parameter arrays. Replaces the scalar-broadcast
|
||
//! parameters previously passed to `step_decision_with_latency`.
|
||
//! `from_uniform` rebuilds the legacy uniform behaviour for smoke /
|
||
//! fixture tests where every backtest shares the same config.
|
||
//! `from_grid` packs a sweep cell-grid (n_parallel=140 typical) into
|
||
//! one BatchedSimConfig.
|
||
|
||
use anyhow::{anyhow, Result};
|
||
|
||
/// Single source of truth for per-backtest sim params at run time.
|
||
/// Lengths MUST all equal `n_backtests`.
|
||
#[derive(Clone, Debug)]
|
||
pub struct BatchedSimConfig {
|
||
pub target_annual_vol_units: Vec<f32>,
|
||
pub annualisation_factor: Vec<f32>,
|
||
pub max_lots: Vec<u16>,
|
||
pub latency_ns: Vec<u32>,
|
||
pub kelly_frac_floor: Vec<f32>,
|
||
pub sharpe_weight_floor: Vec<f32>,
|
||
// P4: threshold gate + per-fill cost. threshold = absolute conviction
|
||
// cutoff (max|p_h - 0.5| * 2 below threshold → noop). cost is deducted
|
||
// per fill in apply_fill_to_pos.
|
||
pub threshold: Vec<f32>,
|
||
pub cost_per_lot_per_side: Vec<f32>,
|
||
// Follow-up gp74n: max-hold force-close. 0 = disabled; > 0 fires
|
||
// force-flat when (current_ts - entry_ts) >= max_hold_ns.
|
||
pub max_hold_ns: Vec<u64>,
|
||
// Parameterized price-range sanitization. Snapshots whose top-of-book
|
||
// bid or ask falls outside [min_reasonable_px, max_reasonable_px] are
|
||
// skipped entirely. Defaults: 0.0 / f32::INFINITY = no check (backward
|
||
// compatible). Set per-instrument: ES smoke uses 1000.0 / 20000.0.
|
||
pub min_reasonable_px: Vec<f32>,
|
||
pub max_reasonable_px: Vec<f32>,
|
||
// CRT.1 C1.3: no-trade band. seed_inflight_limits_batched skips seeding
|
||
// when |target_signed − effective| < delta_floor. Absorbs fractional
|
||
// target changes so most per-event controller invocations produce no
|
||
// order. Default 1.0 = 1 lot (ES single-contract sizing).
|
||
pub delta_floor: Vec<f32>,
|
||
}
|
||
|
||
/// P6: one fully-resolved sim variant for the sweep runner's grid-pack
|
||
/// flow. Built from sweep-YAML `SimVariant` entries (per-cell threshold +
|
||
/// cost + per-variant overrides) layered over the SweepBase scalars.
|
||
#[derive(Clone, Debug)]
|
||
pub struct ResolvedSimVariant {
|
||
pub name: String,
|
||
pub target_annual_vol_units: f32,
|
||
pub annualisation_factor: f32,
|
||
pub max_lots: u16,
|
||
pub latency_ns: u32,
|
||
pub kelly_frac_floor: f32,
|
||
pub sharpe_weight_floor: f32,
|
||
pub threshold: f32,
|
||
pub cost_per_lot_per_side: f32,
|
||
pub max_hold_ns: u64,
|
||
pub min_reasonable_px: f32,
|
||
pub max_reasonable_px: f32,
|
||
// CRT.1 C1.3: no-trade band in lots.
|
||
pub delta_floor: f32,
|
||
}
|
||
|
||
/// Convenience scalar input for `from_uniform`. Mirror of the historical
|
||
/// BacktestHarnessConfig sim fields.
|
||
#[derive(Clone, Debug)]
|
||
pub struct UniformSimParams {
|
||
pub target_annual_vol_units: f32,
|
||
pub annualisation_factor: f32,
|
||
pub max_lots: u16,
|
||
pub latency_ns: u32,
|
||
pub kelly_frac_floor: f32,
|
||
pub sharpe_weight_floor: f32,
|
||
pub threshold: f32,
|
||
pub cost_per_lot_per_side: f32,
|
||
pub max_hold_ns: u64,
|
||
/// Minimum plausible price for top-of-book bid/ask. 0.0 = disabled (no check).
|
||
pub min_reasonable_px: f32,
|
||
/// Maximum plausible price for top-of-book bid/ask. f32::INFINITY = disabled (no check).
|
||
pub max_reasonable_px: f32,
|
||
/// CRT.1 C1.3: no-trade band in lots. 1.0 = skip seeding when |delta| < 1 lot.
|
||
pub delta_floor: f32,
|
||
}
|
||
|
||
impl BatchedSimConfig {
|
||
pub fn n_backtests(&self) -> usize {
|
||
self.target_annual_vol_units.len()
|
||
}
|
||
|
||
pub fn from_uniform(n: usize, p: &UniformSimParams) -> Self {
|
||
Self {
|
||
target_annual_vol_units: vec![p.target_annual_vol_units; n],
|
||
annualisation_factor: vec![p.annualisation_factor; n],
|
||
max_lots: vec![p.max_lots; n],
|
||
latency_ns: vec![p.latency_ns; n],
|
||
kelly_frac_floor: vec![p.kelly_frac_floor; n],
|
||
sharpe_weight_floor: vec![p.sharpe_weight_floor; n],
|
||
threshold: vec![p.threshold; n],
|
||
cost_per_lot_per_side: vec![p.cost_per_lot_per_side; n],
|
||
max_hold_ns: vec![p.max_hold_ns; n],
|
||
min_reasonable_px: vec![p.min_reasonable_px; n],
|
||
max_reasonable_px: vec![p.max_reasonable_px; n],
|
||
delta_floor: vec![p.delta_floor; n],
|
||
}
|
||
}
|
||
|
||
/// P6: pack a list of resolved sim variants into one BatchedSimConfig.
|
||
/// Used by the sweep runner when a cell carries N variants (140 typical)
|
||
/// to run n_parallel=N backtests in one pod with shared forward.
|
||
pub fn from_grid(variants: &[ResolvedSimVariant]) -> Self {
|
||
Self {
|
||
target_annual_vol_units: variants.iter().map(|v| v.target_annual_vol_units).collect(),
|
||
annualisation_factor: variants.iter().map(|v| v.annualisation_factor).collect(),
|
||
max_lots: variants.iter().map(|v| v.max_lots).collect(),
|
||
latency_ns: variants.iter().map(|v| v.latency_ns).collect(),
|
||
kelly_frac_floor: variants.iter().map(|v| v.kelly_frac_floor).collect(),
|
||
sharpe_weight_floor: variants.iter().map(|v| v.sharpe_weight_floor).collect(),
|
||
threshold: variants.iter().map(|v| v.threshold).collect(),
|
||
cost_per_lot_per_side: variants.iter().map(|v| v.cost_per_lot_per_side).collect(),
|
||
max_hold_ns: variants.iter().map(|v| v.max_hold_ns).collect(),
|
||
min_reasonable_px: variants.iter().map(|v| v.min_reasonable_px).collect(),
|
||
max_reasonable_px: variants.iter().map(|v| v.max_reasonable_px).collect(),
|
||
delta_floor: variants.iter().map(|v| v.delta_floor).collect(),
|
||
}
|
||
}
|
||
|
||
pub fn validate(&self) -> Result<()> {
|
||
let n = self.target_annual_vol_units.len();
|
||
let lens = [
|
||
("annualisation_factor", self.annualisation_factor.len()),
|
||
("max_lots", self.max_lots.len()),
|
||
("latency_ns", self.latency_ns.len()),
|
||
("kelly_frac_floor", self.kelly_frac_floor.len()),
|
||
("sharpe_weight_floor", self.sharpe_weight_floor.len()),
|
||
("threshold", self.threshold.len()),
|
||
("cost_per_lot_per_side", self.cost_per_lot_per_side.len()),
|
||
("max_hold_ns", self.max_hold_ns.len()),
|
||
("min_reasonable_px", self.min_reasonable_px.len()),
|
||
("max_reasonable_px", self.max_reasonable_px.len()),
|
||
("delta_floor", self.delta_floor.len()),
|
||
];
|
||
for (name, l) in lens {
|
||
if l != n {
|
||
return Err(anyhow!(
|
||
"BatchedSimConfig length mismatch: {name} has {l} entries, expected {n}"
|
||
));
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|