feat(ml-backtesting): wire --latency-ns end-to-end + fixture (C12)
The in-flight machinery from C11 is now reachable from the harness +
CLI. New step_decision_with_latency on LobSimCuda:
latency_ns == 0 → existing immediate-match path (submit_market_immediate
kernel fills against current book).
latency_ns > 0 → host reads market_targets, converts each non-noop
into seed_limit_order(active=2, price=very-aggressive,
arrival_ts_ns = current + latency_ns). The
resting_orders kernel promotes + fills at arrival
time, so the order sees whatever book exists then —
not the book at decision time.
Aggressive-price heuristic: buy at 1e9 / sell at 0 — the marketability
check (price ≥ best ask for buy, ≤ best bid for sell) unconditionally
crosses at arrival; the kernel then walks book levels for the actual
fill price. This models "market order with latency" correctly because
slippage emerges from the book-walking at arrival, not from a limit
price boundary.
BacktestHarness gains a latency_ns field; harness::run() always calls
step_decision_with_latency now. Also calls sim.step_resting_orders
per event (with trade_signed_vol=0.0 — the Mbp10RawInput trade-flow
hookup is deferred to a trades-feed integration commit) so in-flight
orders get a chance to promote on every snapshot.
bin/fxt-backtest no longer ignores --latency-ns; the flag value
propagates through BacktestHarnessConfig.latency_ns into the kernel
path. Default stays at 100_000_000 (IBKR + Scaleway baseline per
spec §4). Setting --latency-ns 0 selects the legacy immediate path.
Adds latency_in_flight_miss GPU fixture: limit at 5510 submitted
active=2 at T=1s with arrival_ts=T+100ms. step_resting at T+50ms
sees no promotion (still in-flight). Book moves +5 against buyer
during the 100ms window. step_resting at T+150ms promotes the
limit and fills it at the WORSE post-move book (vwap=5505 vs the
original 5500 it would have hit without latency). Slot ends active=0.
11/11 GPU fixtures green on RTX 3050. 33 lib tests still green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -55,9 +55,10 @@ struct RunArgs {
|
||||
/// Decide every Sth event (matches training stride; default 4).
|
||||
#[arg(long, default_value_t = 4)]
|
||||
decision_stride: usize,
|
||||
/// Total latency budget in nanoseconds (IBKR+Scaleway baseline = 100ms).
|
||||
/// Not currently consumed by the v1 immediate-match path; reserved for
|
||||
/// follow-up commits that add resting-order in-flight promotion.
|
||||
/// Total submission→fill latency in nanoseconds (IBKR + Scaleway
|
||||
/// baseline = 100_000_000 = 100ms). Propagated to the resting-order
|
||||
/// engine so in-flight orders wait for arrival_ts_ns before crossing.
|
||||
/// 0 = immediate-match path (legacy).
|
||||
#[arg(long, default_value_t = 100_000_000)]
|
||||
latency_ns: u32,
|
||||
/// Target annual volatility in price-unit lot-equivalents
|
||||
@@ -147,9 +148,9 @@ fn run(args: RunArgs) -> Result<()> {
|
||||
annualisation_factor: args.annualisation_factor,
|
||||
max_lots: args.max_lots,
|
||||
max_events: args.max_events,
|
||||
latency_ns: args.latency_ns,
|
||||
};
|
||||
|
||||
let _ = args.latency_ns; // reserved for follow-up resting-limit work
|
||||
std::fs::create_dir_all(&args.out)
|
||||
.with_context(|| format!("create out dir {}", args.out.display()))?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user