Skip to content

Backtest vs live — execution parity

Where qkt backtest and live MT5 trading agree, where they don't, and what you can claim from a backtest report.

This is the execution-side companion to the data-side parity reports in this directory. Those compare TradingView vs MT5 as market-data sources. This one compares the execution pipelines that consume those ticks.

The proven contract — strategy pipeline is shared

Both Backtest (src/main/kotlin/com/qkt/backtest/Backtest.kt) and LiveSession (src/main/kotlin/com/qkt/app/LiveSession.kt) construct the same TradingPipeline. The strategy compilation, indicator math, candle aggregation, rule firing, signal-to-OrderRequest translation, and risk engine are byte-identical between modes.

BacktestLiveParityTest at src/test/kotlin/com/qkt/parity/BacktestLiveParityTest.kt enforces this contract: same ticks + same strategy must produce identical trade lists in both paths. CI runs it on every push.

If the trade lists ever drift in that test, the pipeline contract is broken and the test fails.

Broker-layer proof boundary

BacktestLiveParityTest uses LiveSession with its default PaperBroker. That test proves:

Backtest + PaperBroker  ===  LiveSession + PaperBroker

That test alone does not prove Backtest === LiveSession + MT5Broker. Separate MT5BrokerSimulatorTest coverage pins deterministic venue rules. The authentic MT5GoldenVerifierTest additionally replays one retained Exness demo XAUUSD market order against its raw bid/ask ticks and matches the venue deal at zero price and volume tolerance.

The authentic row is deliberately narrow. It proves one demo market-fill shape, instrument metadata normalization, and source provenance. It does not prove OCO cancellation races, partial fills, rejection retcodes, latency distributions, pending-order recovery, or a second broker.

Catalog of broker-layer divergences

Each row lists the symptom, the source file the live behavior lives in, and whether the backtest models it.

# Concern Backtest (PaperBroker) Live (MT5Broker) Status
1 Volume quantization fills exactly the requested quantity (PaperBroker.publishFill) rounds DOWN to volume_step from /symbol_info (MT5Broker.quantizeForPlacement, v0.26.3) closed in MT5_SIM
2 Price rounding uses the raw 8-decimal BigDecimal from the engine rounds price/sl/tp/stopLimit to digits (HALF_EVEN) before sending (MT5Broker.quantizeForPlacement, v0.26.4) closed in MT5_SIM
3 Below-volume_min orders fills regardless of how small rejected pre-flight with OrderRejected("quantized volume below venue volumeMin …") closed in MT5_SIM
4 Bracket entry fills fills at tickPrice the moment the trigger is crossed (PaperBroker.fillFromTrigger) venue fills at actual ask (for BUY_STOP) or bid (for SELL_STOP) when the trigger prints closed in MT5_SIM
5 Spread / slippage uses tick.price (the mid set by Mt5TickFeedSource when last=0) live pays the venue spread; volatile bars also slip closed in MT5_SIM
6 Market-order fill price priceProvider.lastPrice(symbol) — the last tracked tick (PaperBroker.fillMarket) MT5 fills at venue ask/bid at submit time, with deviation slack closed in MT5_SIM
7 Contract size reads contractSize from InstrumentRegistry; both backtest and live multiply through it MT5 sizes positions as lot × contract_size (XAUUSD = 100 oz/lot) closed in Phase 30
8 tradeStopsLevel mt5-sim (opt-in enforceStopsLevel) validates pending trigger/limit distance from current price. A bracket's exits reach it as separate orders after the entry fills, so a stop inside the stops level is refused as a leg (and then held engine-side), not with the entry rejected pre-placement in MT5Broker: entry distance from current price, SL/TP distance from entry, freeze-level on modifies (#638); a market entry whose attached stop is too close is refused by the venue (10016) and never fills pending distances aligned in #658; a bracket's too-close stop diverges (backtest holds a protected position, live none); freeze-level live-only (see residuals)
9 OCO atomicity both legs always coupled in memory client-emulated independent tickets; cancel-on-fill has a poll/network window, and a detected second fill is closed immediately by its owned position ticket with a critical alert divergent edge case with compensation
10 Pending-order persistence always in memory of the running backtest persists to the broker's order book; daemon restart re-reads via MT5StateRecovery divergent edge case
11 Latency market entries: execution.latency delays placement (fill on the first tick at/after release); protective stops: execution.stop_latency delays execution after the trigger (fill at the first quote at/after trigger + delay); take-profits: execution.tp_fill picks crossing print or level. Defaults 0 / print reproduce the historical on-trigger fill gateway HTTP round-trip + venue execution latency. Measured on the Exness demo (2026-09-13/14, 33 matched round trips): entries ~0 ms, stops +264 ms median and $0.19/stop worse than the crossing print, TPs at the level modelled per order kind (#1135); calibrate stop_latency from a golden capture of the venue you trade. Re-replaying the same captures after #1134: on the 7 round trips whose replay entry equals the venue entry, stop exits differ from live by $0.014 per trip, so most of the earlier $0.19/stop gap was entry timing, not execution delay; --stop-latency 260ms --tp-fill level moves the 20 matched stops from $0.19 to $0.15 optimistic per trip and stop exit times from 260 ms early to 217 ms late. With the #1138 heartbeat-aligned close (default settings) the 35 matched trips replay at −$7.98 against live −$9.63
12 Retcode handling no concept MT5-specific retcodes (10009, 10015, 10015 price, etc.) translated to OrderRejected reasons divergent
13 Trading calendar / sessions runs through every tick the feed produces respects venue session hours (gaps in /tick during weekends, holidays) aligned in qkt by the TradingCalendar injection; divergent if backtest data covers a window live wouldn't trade
14 Above-volume_max orders MT5_SIM rejects from InstrumentMeta.volumeMax rejects pre-flight from /symbol_info.volume_max or a profile override closed in MT5_SIM (MT5BrokerSimulatorTest, MT5BrokerIntegrationTest)

Rows 1, 2, 3, 4-6 — closed in MT5_SIM

MT5BrokerSimulator (added 2026-05-25, issue #43) is an opt-in backtest broker that mirrors the live MT5 venue's quantization, rounding, volume-min validation, and ask/bid fill rules. Closes the five "high-impact, deterministic" divergences that previously made backtest fill prices and sizes diverge from what live MT5 would have produced.

Opt in:

qkt backtest <file> --broker mt5-sim ...

Or programmatically:

Backtest(strategies = ..., ticks = ..., brokerKind = BrokerKind.MT5_SIM, instruments = registry)

What it requires: InstrumentMeta for every symbol the strategy trades (volumeStep, volumeMin, digits, pointSize). Provided via YamlInstrumentRegistry loaded from data/instruments.yaml, or any other InstrumentRegistry implementation. A missing entry fails the order with OrderRejected, consistent with the Phase 30 hard-error stance.

What remains empirical or live-only: venue OCO cancellation races, exact rejection retcodes, and latency calibration. The simulator enforces configured stop-distance rules and supports deterministic latency/rejection stress models, but those configured distributions are not measurements of a particular live session.

PaperBroker remains the default. Existing backtests are unaffected unless they opt in explicitly.

Contract size (#7) — closed in Phase 30

Phase 30 added an InstrumentMeta primitive resolved at strategy load via InstrumentRegistry. Both PaperBroker and live MT5 paths multiply through contractSize, so a backtest trade and a live trade for the same symbol now use the same dollar-per-unit-of-price math. The hedge-straddle's /100 workaround was removed as part of the migration.

Historical note kept for context: before Phase 30, backtest PnL was off by a factor of contractSize (~100× for XAUUSD), so it could be used for ranking and drawdown comparison but not as a dollar figure. That caveat no longer applies.

How to use the backtest safely today

  • Use the backtest to compare strategies and parameters against each other. Rule firing, signal counts, win rate, drawdown ordering, sharpe ranking all transfer.
  • PnL is now in real dollars as of Phase 30 — but still don't expect bit-identical live numbers. Spread, slippage, latency, and bid/ask fill prices (rows 4–6, 11) still differ. Treat backtest PnL as a defensible estimate, not a tick-perfect prediction.
  • Don't backtest a brand-new strategy and immediately wire to live without a paper-mode run. Plain PaperBroker remains permissive, while MT5 can still reject for live-only retcodes and session state.
  • Use --broker mt5-sim for venue-shaped fill tests. The default paper tier is still a fast research model and should not be cited as MT5 fill-price evidence.

Remaining MT5 gaps

MT5BrokerSimulator now models deterministic volume and price quantization, bid/ask fills, contract-size PnL, stop-distance rejection, and configurable latency/rejection stress. The authentic golden replay covers one market order. qkt golden capture now turns retained demo sessions into checksummed tick/fill/order/gateway bundles, and qkt golden materialize verifies and converts their structured ticks and candles into the normal replay stores. A captured bundle is evidence, not automatically a new verifier assertion. The retained live-validation scenarios can be checked offline with scripts/live-validation/compare-golden-replay.sh; it compares full-tick, plain-bar, and tick-resolved report bundles with the linked live request and fill. Promote representative captures into regression tests for pending/OCO orders, partial fills, rejected requests, and volatile-period latency. Those residuals must not be inferred from the single exact fill. qkt backtest --chaos applies the seeded stress preset; it does not claim to reproduce every gateway HTTP or venue-retcode sequence. Operational proof for a second MT5 profile remains tracked by #44.

Strict read-only captures use the same comparator in a separate mode. Their engine journal records source-timeframe warmup ticks and exact DSL stream candles, allowing the materializer to retain M1 and M5 bar stores without mixing synthetic warmup streams. The comparator requires exact live/full-tick/plain-bar warmup and indicator traces plus flat accounting; it makes no order/fill claim.

2026-06-10 audit addendum — divergences this catalog was missing

Rows surfaced by the full engine audit (#142, issues #356-#401). Items marked FIXED now behave identically in both modes; the rest are inherent differences to keep in mind when reading a backtest.

# Divergence Status
A1 Halt rules: backtest used to wire ZERO halt rules while live halts FIXED (#362) — backtests build the same config-driven halt set and report halts
A2 Warmup: live waited a full live window post-deploy; CLI backtests consumed the first N in-window bars FIXED (#383, #947) — one shared coordinator seeds closed pre-window history in live and backtest before DSL binding (BacktestFromStoreTest). Seeding also closes the stream's aggregator through the last seeded bar, so a tick stamped inside that history (a gateway re-sending its last quote on subscribe, stamped when it last changed) is dropped as late instead of opening a past bar whose close fires the rules (CandleHubSeedTest; seen on the Deribit testnet, where entries were decided on bars 11 minutes and 8 hours old)
A3 GTD expiry: venue ignores expiration; engine sweep was disabled FIXED (#368) — engine sweep owns GTD in live; backtest sweep identical
A4 Trigger side: everything triggered on mid; venue triggers on bid/ask FIXED (#382) — side-aware in PaperBroker, MT5_SIM, and engine-held triggers; bar-sourced backtests have no quote depth, so they still effectively trigger on the synthesized price
A5 Costs: live PnL/halts were commission/swap-blind FIXED (#392, #644) — venue costs net out of realized in live; backtest models per-lot commission and deterministic long/short swap points at configured UTC rollovers. Live uses venue-reported swap, while replay uses the point-in-time rates in instruments.yaml; rate-history drift remains an input-data divergence
A6 Bar synthesis order: BarTickFeed emits each bar's extremes adverse-first for the net position side (net LONG → Low first, net SHORT → High first, flat → Low first), decided after the bar's open tick so an entry filled on the open steers its own bar. Residual approximation: opposing exposure nets to one sign, and the true intra-bar path is unknowable from OHLC — plain-bars sweeps therefore run per-combo (BacktestSweep), since one shared tick stream cannot be adverse-first for every combo's positions MITIGATED for the plain --bars research tier (pessimistic for both sides; was optimistic for shorts). RESOLVED by --bars --tick-fills, which resolves fills on real ticks for every fill-possible bar and is byte-identical to a full-tick replay (TickResolvedParityTest)
A7 Tick sampling: backtest replays every stored tick; live MT5 used to poll /symbol_info_tick, which returns only the quote current at the instant of the call, so every tick arriving between two polls was lost permanently. Measured against Exness XAUUSD M1 bars over 240 bars / 74,331 ticks: live captured 52-214 of 310 ticks per bar depending on cadence, understating each bar's high by about 0.04 and overstating its low by about 0.06, compressing bar range 2.0-6.5% and ATR(1m) 1.9-5.4% below the venue's own bars. Engine-held trails, latches and stacks therefore walked a coarser price path than replay FIXED — the live source now polls /copy_ticks_range for the window (watermark, now], so the delivered stream no longer depends on poll cadence: a tick arriving between rounds falls inside the next window by construction. Aggregation itself was never the defect — fed every tick, CandleAggregator reproduces MT5's own bars exactly (239/240 bars bit-identical on OHLC; the one miss was a still-forming bar). Ticks are deduped against the watermark and merged across symbols into one timestamp-ordered stream (Mt5TickRangePollingTest). Verified live against a local Exness gateway with the real daemon: across 13 complete M1 bars, every one matched the venue's own bar EXACTLY on both tick_volume and range (293/293, 203/203, 171/171, 160/160, 159/159, 120/120, 109/109, 91/91, 135/135, 121/121, 135/135, 125/125, 203/203), the only residual being a constant +0.130 level offset — exactly half the 0.26 spread, since qkt builds from the bid/ask mid and MT5 builds from the bid. The same daemon on the predecessor endpoint under identical conditions under-counted every bar, by 20% to 59% (mean -37%), with a -12.8% range bias. Residual: bounded-queue shedding under load
A8 SCHEDULE timing: backtest fires on the next replayed tick after the trigger time; live fires from a 1Hz wall-clock heartbeat even with no ticks INHERENT — sub-second placement differences
A9 Calendars: the backtest CLI uses fixed per-symbol calendar rules (crypto for BTC*/*USDT, FX default otherwise); live and portfolio book-risk annualization use the broker profile calendar. The FX weekend boundary is a FIXED UTC hour year-round and does not track New York DST (up to 1h off near the close/open in winter) INHERENT — pinned by FxCalendarTest, PortfolioRiskAggregatorTest
A10 x.bid / x.ask / x.spread evaluate Undefined on bar-sourced backtest data — spread-aware rules silently never fire in bar backtests (tick-sourced backtests carry real quotes) OPEN (#389) — prefer tick data for spread-aware strategies
A12 Quiet-symbol candle close: live closes an ended bar from the 1Hz heartbeat even with no next tick; backtest used to close it only on that symbol's next replayed tick, so a SYNCHRONIZE group with a sparse member (Exness AUDUSD quotes a median 3.9 s apart; 41.5% of gaps exceed a 5s window) decided seconds late at a different price — measured 2026-09-13 on lag_aud_xau: live entered ~2 s after the bar end, replay on the next AUDUSD tick 6–10 s later FIXED (#1134) — replay closes every ended window on the first tick stamped strictly past its end, whichever symbol quoted, through the same flushClosed the heartbeat uses; sync groups release as soon as the tape moves past the last member's window end (ticks sharing a timestamp are all ingested first, so a symbol's own boundary tick still closes its bar and prices its fill). Pinned by ReplayEventTimeCandleCloseTest. Re-replaying the 2026-09-13 captures: all 35 live round trips now pair with a replay trip (33 before), and the three late lag_aud_xau entries moved from 10.5 s / 6.2 s / 2.8 s after the venue entry to within 1.6 s / 0.3 s / 1.4 s. #1138 then aligned the close moment itself: replay closes a quiet bar on the first tick at or past the 1 Hz heartbeat step that is at least runtime.candle_close_grace_ms (default 2000, one key shared with the daemon) after the window end, so both modes decide on the same heartbeat step. Re-replaying the same captures: median live-venue-entry minus replay-entry went from 745 ms (grace-less close) to 208 ms, median absolute gap 225 ms and at most 251 ms per session, identical entry prices from 8.6% to 45.7% (57.9% and 66.7% on the two dense-leader sessions, 22–25% on the sparse XAU-leader ones, where the quote moves inside the ~200 ms MT5 accept round trip). Adding --execution-latency 200ms on top overshoots (median −476 ms), so the default stays 0. Residual: the last bar before the tape ends still closes at the replay boundary
A11 Live-only operational effects: restart reconcile, OCO restore, poller-synthesized closes, gateway-outage suspensions, and the market-data gate including its broker-clock-skew check (#395/#396/#810 are live-only by design) PARTIAL — replay now evaluates the runaway breaker with the configured live thresholds, reports every would-be trip, and can enforce them with --enforce-live-breakers. The remaining operational effects have no replay equivalent. The expired-before-submit GTD reject (#811) is wired in both modes but cannot fire under event time, where a fresh deadline is always in the future
A13 Week-close entries: an entry signalled on the final pre-weekend bar closes via the live heartbeat (A12) when the feed is already stale and the venue shut, so the market-data gate rejects it; backtest closes the same bar on the venue's reopen tick and fills at the reopen price MITIGATED (#888/#890) — the rejected fire re-arms and, if the condition still holds on the first post-reopen bar close, enters one bar later than backtest. The one-bar entry lag is INHERENT
A14 Warmup seed grid: MT5 aggregates multi-hour history on the broker's day boundary, which put seeded H4/D1 bars on a shifted grid vs the epoch-aligned UTC bars live aggregation and backtest use FIXED (#887) — multi-hour warmup history is fetched as H1 and rebuilt on the UTC grid; CandleHub.seed fail-closes on off-grid bars
A15 Margin floor is a live pre-trade rule because replay has no venue margin-level feed INHERENT — repeated missing reads fail closed for new exposure; risk-reducing exits remain allowed (MarginFloorTest)
A16 Standalone live sessions default to fresh venue equity for drawdown and percent-of-equity sizing; replay uses model equity DECLARED/CONFIGURABLE (#939) — risk.live_equity_basis: modeled pins live to starting_balance + qkt realized + qkt unrealized; venue remains the compatibility default (LiveSessionBrokerEquityTest)
A17 Burst entries versus the runaway breaker: the breaker counts closing fills per strategy (default 10 in 600s), runs only in the live assembly, and halts the strategy PERSISTENTLY. A strategy that opens and closes more than that in ten minutes — any TIMES N, deep STACK, or N-action rule with N above the threshold — halts live while backtest completes the run DECLARED/CONFIGURABLE — raise max_round_trips_10m to cover the strategy's busiest ten minutes, or 0 to disable. Pinned by TimesEntryParityTest; replay can enforce it with --enforce-live-breakers
A18 Cross-stream entry before the target stream has closed a bar: a bracketed or pending order on a stream other than the one whose bar fired the rule prices itself from that stream's last CLOSED candle, so on the target's first bar there is none and the order is dropped with only a warn line — no rejection, no suppressed signal, and it is invisible in the trade record. Plain unbracketed orders are unaffected (they need no price to construct) OPEN — declare WARMUP on every stream a rule can trade. Behaviour pinned by TimesEntryParityTest; the silent drop is an observability gap
A19 Ladder rungs anchor to the SEED FILL, so ordinary market-seed entry drift shifts every rung with it. A ten-level 2-point ladder filled 4 legs live and 5 in replay purely because the market seed filled 2 points apart (…247 live, …249 replay), moving all ten rungs and bringing one more into range of the same low. The fill model itself is correct and parity-clean: a tick gapping through several rungs fills each at the gap price (a resting limit fills at its level or better), a rung the tape never reaches never fills, and tick, bar and live-paper agree all the way down a ten-rung ladder (LimitLadderMechanicsTest) INHERENT — a ladder backtest predicts leg COUNT only as well as it predicts the seed fill. Size a ladder to survive one rung either way, or seed with a LIMIT instead of a market order to pin the anchor
A20 Burst entry price dispersion: a backtest fills every leg of a burst at one price, while live places them serially (about four a second) and each leg fills at the market it arrives to. Measured on a 5-leg gold burst: replay filled all five at 4402.490, live filled 4402.490, 4402.490, 4402.301, 4402.174, 4402.174 — 316 points of dispersion in 1.4 seconds. A 30-leg EURUSD burst over 7 seconds drifted only 2 points, because the instrument barely moved. The dispersion scales with the instrument's volatility over the placement window, not with the leg count alone MODELLED (opt-in) — execution.order_spacing puts every order on one send lane, so a burst's legs are released a spacing apart and each fills at the sided quote prevailing at its release (MT5BrokerSimulatorOrderSpacingTest). Checked against the venue: of 46 Exness market-entry deals on 2026-09-23 (runs 002 and 004), 45 filled exactly at the quote prevailing at the deal time and none only at the next quote, which arrived a median 350 ms (max 1.1 s) later. The same rule now applies to any execution.latency, so latency-delayed market orders no longer take a future quote. Measured on the Exness demo on 2026-09-23: consecutive STACK_AT legs filled about 140-160 ms apart (runs 002 and 004). In run 004 a single-price replay turned the last two legs, which filled live 0.70 higher and timed out at -11.84 each, into take-profit winners. Still unmodelled: the 1-3 s gateway stalls seen mid-burst (run 004: 2.25 s between legs 5 and 6), spacing jitter, and one lane per simulated broker rather than per physical gateway. A fill carries the engine time at which the release is observed (the next tick), as a live ack arrives after the venue's deal time. Protective-stop delay (execution.stop_latency, #1135) still fills at the first quote at or after trigger + delay; it has not been re-measured against this rule. With spacing 0 a burst backtest's average entry stays optimistic by roughly half the instrument's movement over the placement window; compare-stack-replay.sh measures it per run
A21 max_open_positions counted only FILLED positions, so a burst outran it live: every order is risk-checked before any of its fills return, so each saw an empty book and passed. A strategy capped at one symbol opened two live (5 gold + 10 EURUSD, zero rejections) while the backtest — where fills land between submissions — enforced the cap and rejected the second symbol. Same strategy, same config, opposite answers, with live being the unsafe side FIXED — the rule now counts symbols held OR with a live entry order (MaxStrategyOpenPositions, PositionProvider.pendingEntrySymbols). Re-run live: the same scenario now fills 5 gold and rejects all 10 EURUSD with the backtest's reason. Single-symbol bursts are unaffected (30-leg EURUSD burst still fills 30/30)
A22 max_trades_per_day counted entry FILLS, so a burst outran it live for the same reason A21 did: the whole burst is risk-checked before any of it fills, and every order reads the same pre-burst total. Measured live: a strategy capped at 60 executed 100 in one TIMES 100 burst with zero rejections, while the backtest stopped it at 60 FIXED — the cap now adds this strategy's live entry orders on the request's side. The side filter is load-bearing: an open position's protective stop and target rest on the OPPOSITE side and stay live until it closes, so counting both sides reported a phantom pending entry per filled position and halved the effective cap. Re-run live: 60 entries and 40 rejections, matching the backtest exactly, with replay parity passing (PacerRulesTest)
A23 book_risk.max_gross_exposure is inert for a standalone strategy: BookRiskController is constructed only by PortfolioDeployer, so a single-strategy deployment ignores the block. Measured: a 100-leg EURUSD burst reached roughly 1.16x account capital in notional against a declared max_gross_exposure: 0.60, with no rejection. The standalone backtest used to enforce the same block, so it rejected orders the live deploy sent: a 10-leg gold STACK_AT burst replayed on its own live ticks lost 6 of 10 legs to a book concentration > 1.0x cap rejection live never raised MITIGATED, modes AGREE — both the daemon and a standalone qkt backtest warn at start when book_risk limits are configured and neither enforces them; a PORTFOLIO backtest and deployment still do (BacktestStandaloneBookRiskTest). Enforcement for standalone deployments remains a design decision: max_position_size is per-symbol and max_open_positions caps distinct symbols, so neither bounds total notional. Deploy as a portfolio for book risk, or bound exposure inside the strategy
A24 Live tick catch-up after an outage: the live source resumes from the newest broker timestamp it has emitted, and that watermark survives the out-of-session skip. An unclamped resume would request the entire gap in one round — the whole weekend on the Monday open — and replay ticks that are minutes or days stale into rules that would act on them at current prices. A backtest has no such gap DECLARED — maxCatchupMs (60s default) bounds one round's window; ticks older than that after an outage are skipped, not replayed. The engine resumes at the current market rather than firing rules against prices that are gone (Mt5TickRangePollingTest)
A25 Heartbeat-driven bar close versus tick arrival lag: live closes a quiet symbol's bar on a 1Hz wall-clock heartbeat, lagged by candle_close_grace_ms, while a backtest closes purely on event time. When a tick's arrival lag exceeds the grace, its bar has already closed and the tick is rejected as late — the bar then silently under-reports the venue. Measured live: the lag between a tick's broker stamp and its arrival ran a 100ms median and 192ms p99 unloaded, but a competing poller against the same single-threaded MT5 terminal pushed the p90 to 1458ms, and one bar recorded 18 of the venue's 111 ticks while every neighbouring bar matched exactly (93 dropped ticks, all in that bar) FIXED — the grace default rose from 500ms to 2000ms, sized from that measured distribution, and a dropped late tick now logs a throttled warning instead of only incrementing a counter. Re-run under contention: 0 dropped ticks and 6/6 bars exact on volume and range. An active symbol closes tick-driven and never reaches the heartbeat path, so the grace costs close latency on quiet symbols only. Contention severity differed between the two runs, so this is not a controlled A/B: the grace is sized from the lag distribution, not from the drop count. A venue-side stall is the residual case no grace can cover: observed live, the MT5 terminal delivered no ticks for 63s while the venue recorded 418 and 334 ticks in those minutes, then backfilled the whole burst at once — 322 ticks arrived after their bars had closed and were dropped, every one of them inside a single millisecond. A closed bar cannot accept backfill without breaking determinism, so those bars under-report on any polling scheme; the predecessor endpoint never even fetched them. maxCatchupMs bounds the wasted work. Counting was itself wrong here: droppedLateTicks read only the default window aggregator and reported zero while a multi-stream strategy's hub slots were dropping, so the hub's slots are now summed in too (CandleHubLateDropTest)
A26 Warmup history silently fell back to tick aggregation whenever the bar store missed ANY day of the requested range. Every range longer than a few days misses the days the venue did not trade, so in practice any multi-day (and therefore any higher-timeframe) warmup took the fallback. Against an ordinary tick store that is merely slow; against a golden-replay store it returns different numbers, because the materialized tick file also carries warmup ticks rehydrated from EVERY stream's timeframe and BarTickFeed puts a bar's whole volume on its close tick — so aggregating that file into one timeframe sums volume across all of them. Measured on a 4-stream gold capture: a 120-bar 1h warmup read 52,737 on a bar the venue recorded as 9,828 (= 9,828 twice plus the 4h bar's 33,081), while the same warmup shortened to 10 bars — short enough to stay inside days the store fully covered — read it correctly. Live was never affected: its warmup comes from Mt5BarFetcher bars per stream, and its sma(volume,10) matched the venue's own last ten hours exactly FIXED — the CSV bar tier now skips days it does not have instead of disqualifying the range, matching the binary --bars tier which already tolerated gaps; only a range the store cannot serve at all still falls back (LocalMarketSourceBarStoreTest). Re-measured on the same capture: an 11-rule strategy across 1m/5m/1h/4h spanning a live 4h close now replays IDENTICALLY to live in both full-tick paper and full-tick mt5-sim. Bars-paper still differs on the live-window bar it rebuilds from 1m bars, which is the documented bar-synthesis tier, not this defect
A27 STACK_AT leg protection was anchored on the tick mark (the mid on MT5 metals and FX) and never re-anchored on the leg's fill, so a leg's target sat half a spread closer than declared. Worse, the attach path shipped each leg's pre-fill target placeholder with its market entry, and the MT5 gateway validates it against the live ask at execution. Measured live on Exness XAUUSD (0.26 spread): with mid anchoring a TAKE PROFIT BY 0.10 tier lost all 10 legs (For BUY orders, TP must be above entry price); with ask anchoring it still lost 8 of 10 in a rising market, because the gateway serializes sends and the ask moved 4292.039 to 4292.222 within 6 ms — past the 0.10 distance. The mt5-sim replay opened the mid-anchored legs with the target below the fill FIXED — a firing tier anchors its bracket at the ask (BUY) or bid (SELL) and carries its BY distances, so the leg re-anchors on its actual fill in both modes; on an attach venue a fill-anchored target is no longer sent with the entry but attached by position modify at fill + distance, and a refused modify arms an engine-held target (market-if-touched, closing the ticket) paired one-cancels-other with the engine-held stop; both are children of the leg's bracket, so whichever closes the ticket completes the bracket and releases its exposure instead of leaving it pending until a restart (live run 004, OrderManagerAttachedFallbackCompletionTest). The stop placeholder still ships, so the leg is protected during the fill round-trip. Excursion stays measured on the mark (StackEngineFillAnchorTest, TradingPipelineStackFillAnchorTest, MT5OrderTranslatorBracketTargetTest)
A28 Submit-time protection was validated against the mid and only for absolute AT targets, while the MT5 gateway (validate_sl_tp) validates the SL and TP it receives against the entry's execution price (ask for a market BUY, bid for a market SELL). So a backtest filled brackets live refused, and refused a BUY stop between the mid and the ask that live accepted. A primary BY target smaller than half the spread was also refused live, because its placeholder shipped FIXED — VenueSubmission checks what the venue receives against the execution quote (or the pending entry's own price) with the gateway's strict inequalities and message text, in both modes: the stop, and an absolute target. A BY/PCT/RR target is never sent with the entry on an attach venue (it attaches at fill), so it is not judged at submit and a sub-half-spread relative target is accepted on both paths (OrderManagerBracketCrossedProtectionTest, OrderManagerBracketExecQuoteValidationTest, OrderManagerMt5SimSubmittedProtectionTest)
A29 Elapsed-time exits: a WHEN POSITION.x.holding_duration >= N THEN CLOSE x rule is evaluated only at the rule stream's bar close, in both modes, so the close lands on the first bar close at or after the horizon — up to one bar late, and a whole bar late when the entry itself filled at a bar close. Measured live on a 5m gold stream: a 90s hold closed at 300s, 0.4s after the bar boundary, and the golden replay of that window closed at the same instant. Backtest and live agree; both disagree with the horizon the rule reads as DECLARED — the quantization is symmetric, not a parity break, and is now documented on conditions.md. EXIT AFTER <duration> is the per-tick, fill-anchored exit for when the time itself matters; DslExitAfterParityTest runs bracket-less, bracketed, early-take-profit and STACK_AT cases through backtest and live and asserts identical trades including the close timestamps. Armed timers persist through a daemon restart: a pending deadline fires at its original time, a deadline missed during downtime closes on the first tick, and a leg gone at the venue drops its timer (OrderManagerTimeExitRestartTest)
A30 Unanswered placements of identical orders could not be resolved live. The venue keeps only a prefix of each order comment, so a burst of look-alike orders (ten STACK_AT legs, same size, stop and target) lands as positions that all read dsl-gold_scale_burst_fixe. Measured live on Exness XAUUSD: three legs of one burst timed out at the gateway; each resolution saw the same three candidates, stayed UNRESOLVED after four venue reads, and the three positions ran with no engine attribution, no EXIT AFTER timer and no fill re-anchor until closed by hand. A backtest never has an unknown send outcome, so it never saw this FIXED (live only) — when exactly as many unanswered orders of one shape are in flight as there are interchangeable look-alike positions, none opened before the first send, they are paired in send order and each ticket is claimed atomically; one order facing two look-alikes, or look-alikes that differ in size, side, stop, target or stored comment, still stays unresolved (MT5InterchangeableOutcomesTest, MT5BrokerInterchangeableResolutionTest, MT5BrokerIntegrationTest "multiple legacy comment candidates leave the send unresolved")
A31 Starting balance: a single-strategy backtest always started at the CLI default 10000 unless --starting-balance was passed, even when --config set starting_balance — the balance the live daemon sizes max_drawdown_pct halts and percent sizing on. The same config therefore halted and sized on two different balances. Measured: a strategy that loses $10k halted at 100% drawdown on the default while the config said 100000 FIXED — the backtest resolves --starting-balance, else a positive config starting_balance, else 10000, and prints the source (BacktestStartingBalanceTest). Portfolio backtests are unchanged: their CAPITAL already governs
A17 Measured-usage ramp caps live order quantity during a configured post-deploy window; replay does not model deployment age INHERENT — pinned by MeasuredUsageTest
A18 Equity-curve window: replay used to sample warmup ticks and seeded pre-window bars onto the equity curve, so a replay's sample count (and Sharpe) depended on how much warmup history preceded --from; live never had those samples FIXED — EquityCurveCollector floors samples at the replay window start (windowStartMs); trades, PnL, and drawdown are unaffected. Measured on the same fills: a 2023–2024 daily replay whose warmup reached back to 2021 carried 600 flat pre-window samples, so Sharpe read 0.66 instead of 0.92 and Sortino 1.06 instead of 1.49; golden replays and forge gate metrics (sharpe is qkt's sharpeRatio) recorded before this change are diluted in proportion to their warmup length (EquityCurveCollectorTest)
A32 Futures and option instruments (futures: and options: roots, their catalogs, contract fees, the uncatalogued-contract guard) resolve the same way in backtest and live: both build the registry with InstrumentFiles FIXED (phase 45) — InstrumentFilesTest; the daemon and the backtest call the same builder, and LiveGatewayStructureTest resolves a structure's legs live from it
A33 Continuous futures streams (VENUE:ROOT@front) route to ContinuousContractBroker in every mode: it trades the contract the roll schedule names and carries positions and resting orders across each roll. Backtest replays the stored roll history; live trades a stream on a gateway account through one lane per stream (A53–A57), and paper and live refuse a stream whose futures root is undeclared or has no roll history on disk DECLARED (phase 42.5, live in phase 46) — ContinuousFuturesExecutionTest (backtest), LiveContinuousGatewayTest (live, end to end); the start-up refusal by requireLiveTradable (LiveSymbolChecksTest)
A34 Futures (a root's dated contracts and its perpetual) fill on the exchange simulator whatever --broker says: market orders at the side's executable price, then the run's slippage model (root slippageTicks under instrument slippage), and triggered stops slip the same way; limits are not slipped; limit and stop levels off the tick grid are snapped in the direction that never fills early; each fill carries the root's fees as a venue cost, reported inside commissionPaid. Latency, venue-rejection and partial-fill settings do not apply to futures (a warning says so) DECLARED (phase 42.5) — ExchangeSimulatorTest, ReplayBrokerFuturesTest; a live venue reports its own fills and fees the same way. Live, a type: gateway account snaps every limit and stop level to the tick its gateway lists, in the same direction (GatewayTickGridTest): Deribit refuses an off-grid price (-32602 must conform to tick size, seen on testnet), so before this a computed bracket level was refused at the venue
A35 Roll legs trade at the roll history's reference prices (each contract's last 1m close at the roll instant) plus slippage, so the booked roll cost is slippage and fees only; a live roll trades the market at that moment. Continuous-space P&L plus the booked roll costs equals the P&L of the contract legs to the cent DECLARED (phase 42.5) — identity pinned on real data by ContinuousFuturesExecutionTest; roll mechanics by RollExecutorTest, failures by RollFailureTest
A36 A listed contract held into expiry is settled by the exchange simulator at the catalog's delivery price (a settlement print closes the contract's data at its expiry); a live venue settles it itself DECLARED (phase 42.5) — ExpirySettlementTest, DatedContractDataTest
A37 Expiry guard: within a root's expiryGuardHours (default 24) before expiry the exchange simulator refuses orders that open, add to or flip a contract position, judging each with the strategy's other working orders that could fill on the same tick (same side, set off the same way), so a bracket's take-profit and stop both stand and a close always passes; exits left over once the position is flat are cancelled. A live venue does not enforce it DECLARED (phase 42.6) — ExpiryGuardTest; phase 44 applies the same rule in the contract venue wrapper
A38 cme_globex models the weekly hours and the daily 16:00–17:00 Chicago halt but no exchange holidays or early closes; a backtest treats a CME holiday as a normal session DECLARED (phase 42.7) — CmeGlobexCalendarTest; live sessions take the venue's own trading hours
A39 Options (options: roots, catalogs, chain series, the option venue) resolve and trade in backtest only; the live daemon's registry does not load them DECLARED (phase 43.3) — phase 44's gateway connector supplies the live option venue
A40 Option fills: a market order fills at the first quote of its contract strictly after it was decided, at the ask (buy) or bid (sell). A book series supplies real sides. A trade series has no book, so a mark at most maxQuoteAgeMinutes old gets mark ∓ max(tick, markSpread × mark) on the tick grid. Any size fills at the top of the book. A quote without the needed side cancels the order, as does no quote within the age (checked before a late quote can fill it). A limit marketable at its first quote fills at that quote. A resting limit fills at its limit once the side reaches it, seen only at snapshot instants. Live trades the venue's book continuously. The trade fee is charged on the index each quote recorded, as the venue charges: a trade series stores Deribit's index_price, a fetched book its estimated_delivery_price, and a book the gateway account records the quote's index. A series without one (written before the index column, or from a gateway that does not report it) falls back to the expiry's forward, which runs high by the forward premium (about 0.4% of the fee at 30 days on the Deribit testnet, 2026-10-02: 0.2599 vs the venue's 0.2589 per 0.01 contract) DECLARED (phase 43.3) — OptionExchangeTest, OptionQuotesTest, OptionBacktestTest
A41 Option expiry: the chain source ends a contract's data with a settlement print at its intrinsic value from the catalog's delivery price (zero is a legal option price). The venue cash-settles every holder at that value less the capped delivery fee, exit reason EXPIRY. Deribit delivers in-the-money linear options into a future that cash-settles at the same delivery price, which is the same economics. An expiry with no recorded delivery price fails the run DECLARED (phase 43.3) — OptionExpiryTest, OptionChainMarketSourceTest, OptionBacktestTest (P&L identity on real data)
A42 Option margin is the exact worst-case expiry loss per root and expiry: the group's mark value less its minimum payoff over every settlement price. A long option needs its premium, a credit spread its width less its credit, a short put its strike less its mark (cash-secured). A naked short call (unbounded loss) is refused. Deribit's standard margin lends against short options, so a live account can carry more short exposure than the backtest allows. No offset across expiries or against futures or spot DECLARED (phase 43.5) — OptionMarginRequirementTest, OptionPayoffTest, ShortOptionBacktestTest (a real put credit spread reconciles; a naked short call is refused)
A43 Option positions mark at the tracker's side price (long at the bid). When a quote has no bid (cheap or stale), the mark falls back to the stored mark. On a trade series, unrealized P&L can therefore step between the synthetic bid and the mark as a mark ages past maxQuoteAgeMinutes, which moves drawdown and Sharpe. Spec §6.5's clamped mid and model-price fallback are not modelled DECLARED (phase 43.3) — MarketPriceTracker semantics shared with every symbol
A44 Chain analytics streams (CHAIN:<VENUE>.<ROOT>.<metric>.<tenor>) are computed from the stored chain snapshots in both modes. Live, the snapshots are the ones the gateway account records (A47) and the store is polled every 5 s, so a value arrives up to 5 s after its snapshot; backtest delivers it at the snapshot instant. Live also requires the root fed (OPTIONS:), refused at start otherwise DECLARED (phase 45) — ChainAnalyticsTest, ChainAnalyticsMarketSourceTest, ChainAnalyticsLiveTest (a live tick equals the backtest value of the same snapshot), LiveSymbolChecksTest
A45 Option structures (OPEN … = OPTIONS ON …) choose their legs from the latest stored snapshot, submit them as one margined group of market orders, and unwind filled legs when a later leg is cancelled. In backtest each leg fills at a snapshot's bid or ask on its own, so legs can fill on different snapshots, and a structure can be unwound at the next quotes. While legs are pending, later orders' margin judges them one by one, which is conservative. Live, legs are chosen from the recorded snapshots (A47) and fill on the venue's continuous book; a venue's combo orders are not used DECLARED (phase 45) — StructureBacktestTest (real book snapshots, group margin), StructureCoordinatorTest, RiskEngineGroupTest, LiveGatewayStructureTest (the same two legs chosen live from the same chain); structures survive a restart (StructurePersistenceTest)
A46 Structure fields (POSITION.ps.delta … pnl_pct) are computed by qkt in both modes: Greeks by Black-76 from the latest stored snapshot (rate 0, median forward), P&L from premium before fees at the equity marks, max loss per expiry. A venue's own position Greeks (portfolio margin, its own forward and rate) differ. CLOSE ps and FLATTEN close a structure as one group of market orders DECLARED (phase 43.8) — StructureGreeksTest (independent Python), StructureFieldCompilerTest, StructurePositionBacktestTest (real snapshots)
A47 Live option chains are recorded by the gateway account from its quotes: each fed root declared chains: book gets a book snapshot every chain_snapshot_seconds (default 300) of each contract's latest quote, aged to the boundary, appended to the same store backtests read. A backtest replays whatever cadence its stored series has. A root declared chains: trade records nothing live (a warning says so) DECLARED (phase 45) — ChainRecorderTest, GatewayChainRecordingTest (real store), LiveGatewayStructureTest
A48 A gateway account is account-wide in live, even with one strategy: startup trusts each strategy's persisted books (a venue position no book holds is never adopted), and each time a strategy comes up, once every strategy deployed on the account is running, their holdings must add up to the venue's; until they do only reduce-only orders are sent. Backtest has no venue to disagree with DECLARED (phase 45) — GatewayHoldersTest, LiveSessionAccountWideReconcileTest
A49 Live option contracts are judged by the market-data gate per contract for staleness and crossed books, from the gateway's quote refreshes (the wire spec requires a refresh at least every 5 s while a quote holds), but not for price outliers: a premium moves several times its underlying, and a backtest fills on the same quotes; backtest has no gate DECLARED (phase 45) — wire spec §4a; MarketDataGateOptionTest; a gateway that does not refresh quiet quotes makes their legs stale and new orders on them wait
A50 On a Deribit account (qkt-venue-gateway deribit adapter) a stop fires when Deribit's stop_trigger price (last_price by default; mark_price or index_price by setting) crosses it; a backtest stop fires on the touch (bid/ask) DECLARED — venue rule, recorded on testnet 2026-10-01 (buy-stop-market-untriggered.json); a stop can fire later or earlier live than in a backtest of the same quotes
A51 Deribit refuses a stop already on the far side of its trigger price (10034 trigger_price_too_high, 10035 trigger_price_too_low); a backtest stop placed beyond the touch fires at once DECLARED — the refusal reaches the strategy as an order rejection with Deribit's reason (fixture error-trigger-price-too-low.json)
A52 Deribit accepts market and stop orders only as GTC or DAY; one sent IOC or FOK is refused (-32602, time_in_force), never remapped. qkt sends market orders GTC unless the strategy sets IOC/FOK; a backtest fills them in any time in force DECLARED — probed on testnet 2026-10-01; the refusal reaches the strategy as an order rejection
A53 A continuous stream (VENUE:ROOT@front) trades live on a gateway account. Its rolls are measured by the rule qkt fetch --rolls uses (each contract's last closed 1-minute bar at or before the roll, from the venue's own bars), appended to the history on disk, so a later backtest of the window reads the same adjustment; its roll legs trade at the market when the venue answers, where a backtest's fill at the reference prices plus modelled slippage DECLARED (phase 46) — LiveRollMeasurerTest (live record = builder record on the same bars), RollInFlightTest (a leg answered later fills at the venue's price), LiveContinuousGatewayTest (end to end)
A54 Live, a continuous stream serves nothing for the new contract until its roll is measured (the roll minute must close at the venue, about a minute); ticks meanwhile are stale and dropped. A backtest switches at the roll instant DECLARED (phase 46) — ContinuousLiveFeedTest
A55 Live, a roll that cannot be measured (no closed bar within ten minutes, or a history that would skip a roll) stops the session, fail-closed, until qkt fetch <ROOT> --rolls measures it; a backtest stops only the holders of that stream DECLARED (phase 46) — ContinuousLiveFeedLifecycleTest
A56 Live, a stream refuses new orders while its roll legs are out at the venue; a backtest's legs fill inside the roll DECLARED (phase 46) — RollInFlightTest
A57 Live, each continuous stream's lane persists its state (orders, contract book, roll legs and an in-flight roll) and resumes it after a restart, taking its orders back from the venue; a backtest never restarts, so its lanes persist nothing DECLARED (phase 46) — LaneStateSavedTest, LaneRestartTest, LaneRestartEdgesTest, LiveContinuousRestartGatewayTest
A58 Perpetual funding. Live, qkt books what the venue charged (each strategy amount × holding / position, as FINANCING), when the venue realizes it (Deribit accrues by the millisecond and realizes it into its transaction log at every fill that changes the position, the close included, and at its 08:00 UTC settlement, measured on testnet 2026-10-05; see the gateway's Deribit adapter README). A backtest charges each stored rate on the legs held through it, at the rate's own price, on the strategies' net position as a venue does (strategies netting to zero pay nothing; partial netting shares by holding exactly as live), a rate stamped at the run's end included, hourly for Deribit and 8-hourly for Binance: a leg opened or closed between two rates pays the whole interval or none of it, an error of at most one interval's funding at each entry and exit; over a held period the totals agree to the venue's rounding. qkt trades perpetuals live only on a gateway declaring funding (Bybit included, through the gateway's Bybit adapter); a backtest holding one without stored rates covering the run (no gap over three of the series' own intervals) is refused unless --funding off. Live, funding a record carries no position for is shared by what the attached sessions hold; a strategy that closed before the venue realized a day's funding gets its share from the holding it closed DECLARED — FundingBookingTest, GatewayFundingTest, PerpetualFundingBacktestTest, GatewayFundingBookingTest; live: scripts/live-validation/funding-soak (paper gateway) and funding-soak-deribit (Deribit testnet 2026-10-05: 20 SOL held through the 08:00 settlement, realized -3.25148584 = venue net, and a 5-minute hold whose close realized 0.00006975, booked; the runner fails any venue funding left unbooked); Bybit funding: see the gateway's Bybit adapter README
A59 Margin and liquidation. A backtest checks a futures or option position's initial margin when an order opens or adds to it, and on every tick compares account equity, marked at that tick's prices, with the maintenance margin of every position whose root declares margin (futures, and options whose root declares it; an option root without margin is margined only on entry, at its worst-case expiry loss, which is not a maintenance level and is never a liquidation trigger). Below it, the venue liquidates every such position at once, in symbol order, at the tick's executable price (no slippage) with the root's taker fee, as a venue close with exit reason LIQUIDATION, cancelling the contract's working orders, and records it in liquidations.csv; orders that add risk are refused while equity stays below. Live is unchanged: the venue liquidates and qkt books the fill it reports. Remaining differences: a venue's liquidation engine may close only part of a position or step down by risk tier, closes at its bankruptcy or mark price and charges an insurance-fund or liquidation fee rather than the taker fee, and triggers on its mark price (an index average) rather than the last traded price; a venue also margins options without declared terms, which a backtest never liquidates; an option is liquidated only at a chain snapshot that quotes it, so a backtest can close it later than a venue would DECLARED (#1307) — MarginLiquidationBacktestTest, ContinuousLiquidationBacktestTest, ExchangeLiquidationTest, OptionLiquidationTest, MaintenanceMarginGateTest, MarginRequirementTest
A60 Mark and index (<alias>.mark, .index). Live reads the newest quote that carried them when the rule evaluates (on a gateway declaring mark_prices); a backtest reads the stored series of the stream's window, the newest sample strictly before the evaluation instant. The stored series holds one sample per window, the venue's last report in it (Deribit: the last trade's mark and index), so at a bar close the backtest sees the value as of the window's last trade while live sees the venue's newest quote (which a quote refreshes between trades): equal at a trade, otherwise apart by the mark's move since the last trade. Live and backtest refuse a strategy reading them where no marks are served (a gateway without mark_prices, MT5, a continuous stream), and a backtest whose stored marks miss a day of the run DECLARED — MarkStreamFieldsTest, StoredMarkPricesTest, MarkIndexBacktestTest, GatewayMarksTest; the gateway's MarkRoutesTest and Deribit's DeribitMarksTest (recorded tape); live: not yet soaked
A61 Open interest (<alias>.open_interest). Both modes see each figure from the instant the venue made it known (time on the wire and in open_interest/<VENUE>/<NAME>.csv), as an observation stream like HUB:. A backtest replays the stored figures exactly; live polls the account's gateway every minute, so a figure is seen up to a minute (plus the request) after it was known, where the backtest sees it at once. Binance figures are stored at the end of the five minutes they are stamped with (published about 95 s after the stamp), a conservative bound. Deribit publishes no history: its gateway records the present figure on each read, so a stored Deribit series holds the figures a live poll saw, at their ticker times, and starts when the gateway first read it. qkt reads it live only from a gateway declaring open_interest (refused at start otherwise), and a backtest without stored figures covering the run (no gap over three of the series' own intervals) is refused naming the fetch DECLARED — OpenInterestBacktestTest, GatewayOpenInterestTest, OpenInterestPollTest; gateway: OpenInterestRoutesTest, DeribitOpenInterestTest, Deribit testnet contract suite
A62 Option mark IV and Greeks (<alias>.iv, .delta, .gamma, .vega, .theta). Both price the Greeks the same way: Black-76 on the quote's mark IV and forward, rate 0, per contract, as POSITION.<structure> does; the venue's own Greeks are not used (Deribit's agree to 0.05% but are published to 5 decimals, and no stored series holds them). Live reads the newest quote the gateway sent (on a gateway declaring option_marks); a backtest reads the contract's quote in the root's newest chain snapshot at or before the bar's close, so a book series recorded every N seconds lags live by up to N, and a trade-built series carries the trade's IV and the index instead of the mark IV and forward. Both are Undefined while the mark is older than the root's maxQuoteAgeMinutes and from expiry, and refuse a strategy reading them on a stream that is not a catalogued option contract or a feed without option marks DECLARED (#1299) — OptionStreamFieldsTest, OptionFieldsBacktestTest (real book snapshots), GatewayOptionMarksTest, DeribitGreeksAgreementTest (recorded Deribit ticker); the gateway's contract kit (optionCode quoted with mark IV and forward) on Deribit testnet
A63 Trade flow and liquidations (<alias>.buy_volume[n], .sell_volume[n], .long_liq_volume[n], .short_liq_volume[n]). Both sum the same prints (the gateway's /v1/trades and /v1/liquidations, which qkt fetch --tape/--liquidations stores as served) over the same epoch-aligned windows of the stream's timeframe, so a bar's sums are equal. They are read only n >= 1 bars back: live reads a bar's prints from the gateway about two seconds after the bar closes, so at the close itself it could see fewer than the backtest; the bar being closed is refused at compile. Remaining differences: live, a bar not yet read (the first bars after a start, the gateway unreachable, or a read slower than one bar) is Undefined where the backtest has a value; a print the venue publishes more than two seconds late is in the backtest's sum and missing from live's. A backtest is refused when a day its reads reach (run, warmup and lookback) is not stored, and live refuses a strategy whose feed serves no tape or whose gateway lacks the capability DECLARED (#1301, #1303) — FlowStreamFieldsTest, StoredTradeFlowTest, FlowCoverageTest, TradeFlowBacktestTest, GatewayTradeFlowTest, GatewayTapeReadsTest, TapeFetchTest; the gateway's TapeRoutesTest and Deribit's DeribitTapeTest (recorded tape); live: the gateway tape matched Deribit's own on testnet and mainnet history (no qkt live session soaked)
A64 Order-book depth (<alias>.bid_depth, .ask_depth, .book_imbalance). Both modes see each snapshot of the ten best levels a side from the instant the venue stamped it (time on the wire and in depth/<VENUE>/<NAME>/<day>.csv.gz), as an observation stream like HUB:, the three fields always from one snapshot. Live polls the account's gateway every 10 seconds, so a rule reads the book as of the last poll (up to 10 seconds plus the request old), not the book at the instant it evaluates; between polls the book is unseen in both modes. The other way, a live evaluation runs a little after its bar's end, so it can read a snapshot stamped between the end and the evaluation, which a backtest (reading only what was stamped by the close) first reads at the next close: in the live check below, 25 of 26 one-minute closes read the same values in both modes and the 26th read, live, a snapshot stamped 200 ms after the close. No venue publishes book history: the gateway records the book on each read and serves what it recorded, so a stored series holds exactly the snapshots a live poll saw, at their venue stamps, and starts when a live strategy first read the contract on that gateway; a backtest replays them exactly. The book is read as published (ten levels), never as the depth an order would actually consume: a backtest fill does not walk it. qkt reads depth live only from a gateway declaring depth (refused at start otherwise), and a backtest without stored snapshots covering the run (no gap over three of the series' own intervals) is refused naming the fetch DECLARED — BookDepthBacktestTest, GatewayBookDepthTest, BookDepthPollTest, BookDepthStoreTest; gateway: DepthRoutesTest, DeribitDepthTest (recorded books), Deribit testnet contract suite (read-only optional histories case); live: paper gateway on Deribit testnet public data, a 26-close live run replayed by a backtest over the fetched snapshots (qkt#1302 PR notes)

2026-07-03 hardening pass — parity-audit rows resolved (#658)

The 2026-07-02 parity audit (issues #614-#643) was resolved in one hardening PR. Statuses below supersede any older row that disagrees; each FIXED row cites the test class that pins it.

Issue Resolution Pinned by
#614 Live deploy replays seeded candles through the full per-alias update path (indicators, aggregates, rolling snapshots) with rules and position transitions suppressed; session/anchored indicators declare timeframe-aware warmup horizons instead of warmupBars = 1 CompiledStrategyAutoWarmupTest, WarmupRequirementsTest
#615 Live fills book the venue-reported executed volume (quantized, partial-aware); a partial response without a volume resolves as unknown-outcome instead of booking the full request MT5BrokerIntegrationTest, MT5ClientTest
#616 Engine-initiated closes attach venue deal costs (commission + swap + fee) to the fill; the shared pipeline nets them from realized PnL and halt inputs in both modes MT5BrokerIntegrationTest
#617/#618 Live armed trails cancel when their venue position ticket no longer exists (never a naked market order), and fall back to the strategy's PRIMARY position ticket when the leg map has no entry OrderManagerAttachedBracketTest, StrategyPositionTrackerStackTest
#619 RESIZE quantizes deltas to volume_step, floors at volume_min, shrinks by closing the primary's exact venue ticket, and reuses a stable order id so an in-flight resize cannot double-submit ActionCompiler resize tests
#620 Portfolio live sessions share one BookRiskController (exposure limit rule + sizing scale), sampled on the portfolio candle cadence from real child legs; per-child maxDailyLoss became book-wide to match the backtest PortfolioRiskAggregatorTest, BacktestBookRiskTest
#621 time_msc fields are UTC epoch millis and are no longer offset-shifted; only naive datetime strings use the broker offset (one rule, one boundary) MT5ClientTest, Mt5BarFetcherTest
#622 A configured live session fails closed: no silent PaperBroker fallback for unrouted symbols LiveSessionBrokerCoverageTest
#623 Session-scoped indicators (SessionRange, SessionVwap, AnchoredReturn) refuse to latch partial initial windows — Undefined until the first complete window SessionRangeTest, SessionVwapTest, AnchoredReturnTest
#624 The tick-fills classifier expands the mid bar range by the slice's max half-spread, so levels crossed only by the executable quote resolve on real (side-aware) ticks OrderManagerIntrabarFillTest, BarResolvedFeedTest
#625 Backtest sims never fill an order cancelled earlier in the same tick PaperBrokerTest, MT5BrokerSimulatorTest
#626 Backtest and live honor each halt event's cancelWorkingOrders; cancellation is strategy-scoped and retains protective exits OrderManagerHaltCancelTest, BacktestRiskParityTest
#627/#628 Portfolio backtests accept always-run CAPITAL/WEIGHT and RISK OF BOOK topologies. They refuse conditional WHEN..RUN gates and portfolio --bars/--bar-tf/--tick-fills rather than silently changing topology BacktestCommandPortfolioTest, PortfolioDeployerBacktestParityTest
#629 qkt sweep --tick-fills errors instead of silently downgrading SweepCommandTest
#630/#641 --bars validates bar-store coverage per trading day (fail-loud, --allow-incomplete escape); non-Dukascopy streams are completeness-validated; empty feeds error instead of replaying nothing BarCompletenessValidatorTest, BacktestFromStoreTest
#631 MT5 warmup bars normalize bid OHLC to mid via half-spread, matching the backtest's mid bars Mt5BarFetcherTest
#632/#633 NOW.* and schedule actions evaluate at event time (bar close / scheduled fire time), and missed schedule occurrences replay one-by-one instead of coalescing NowAccessorEvalTest, ScheduleRunnerTest
#634 CandleAggregator never reopens a closed window; late ticks are dropped and counted CandleAggregatorTest
#635 Sim StopLimit/IfTouched-LIMIT activate a resting limit (no instant fill at the limit); limit fills are limit-or-better, never slipped adversely PaperBrokerTest, MT5BrokerSimulatorTest
#636 Expiry wins the deadline instant in both venue-held (sim expireGtd before the trigger pass) and engine-held (now >= deadline) paths OrderManagerGtdSweepTest
#637 A triggered order re-checks its live state before broker submission — a same-pass cancel can no longer double-submit OrderManagerBracketDecompositionTest
#639 Crossed stored quotes (bid > ask) are dropped identically at read time by CSV and binary feeds, warn-counted, instead of crashing the replay CsvTickFeedTest, BinaryTickParityTest
#640 Fetch persists tick volume; old cached rows derive volume from stored side volumes at read time DukascopyTickFetcherTest, TickAssemblerTest
#643 Plain --bars stops that gap through their level fill at the adverse opening print, not the level PaperBrokerTest
#390 Bracket exits re-anchor on the actual fill price (fallback OCO and venue-attached modify both) OrderManagerAttachedBracketProtectionModifyTest, OrderManagerTier2FallbackConversionTest

2026-07-31 parity verification (#948)

Every open issue in the parity epic was rechecked against dev. Stale reports are resolved by existing shared-code evidence; confirmed residuals were fixed without introducing a second execution, risk, accounting, or warmup pipeline.

Issue Verified result Evidence
#934 Book-risk annualization uses the routed calendar. Conditional and always-run portfolios with book streams sample on portfolio candle close; streamless books use the documented heartbeat fallback PortfolioDeployerE2ETest, PortfolioRiskAggregatorTest
#935 Net costs stay in cash P&L, while win rate, profit factor, streaks, and Monte Carlo use exposure-reducing fills only. financing.csv exports swap cost and its signed P&L impact for CSV reconciliation ReportBuilderTest, BacktestReportWriterTest
#936 All five configured MaxStrategy* limits are built once by StrategyRiskRuleFactory and consumed by live and replay StrategyRiskRuleFactoryTest, BacktestRiskParityTest
#937 daily_dd_basis is threaded through global and per-strategy replay risk state BacktestRiskParityTest
#938 A portfolio-wide replay halt cancels entries and emits deterministic close orders for every child-owned leg on the breach tick, matching live flatten-before-halt BacktestRiskParityTest
#939 Standalone live equity remains venue-based by default and can be pinned to modeled parity with risk.live_equity_basis: modeled ConfigTest, LiveSessionBrokerEquityTest
#940 Margin floor and measured-usage ramp remain intentionally live-only and are declared in A15/A17; neither restriction is implied by a backtest MarginFloorTest, MeasuredUsageTest
#941 Live child/standalone feeds subscribe configured FX conversion symbols in addition to traded streams; conversion symbols do not become tradable streams StrategyHandleTest, PortfolioDeployerE2ETest
#942 Halt cancellation honors cancelWorkingOrders, scopes by strategy id, and retains protective exits OrderManagerHaltCancelTest, BacktestRiskParityTest
#943 A live deploy with a drawdown limit refuses a non-positive starting_balance instead of silently making static drawdown inert StrategyHandleTest
#944 MT5 volume_max is parsed and enforced in live preflight and MT5_SIM MT5ClientTest, MT5BrokerIntegrationTest, MT5BrokerSimulatorTest
#945 qkt instruments verify compares YAML contract size, volume bounds/step, point size, digits, and stops level against /symbol_info, exiting non-zero on drift InstrumentsCommandTest
#946 The catalog cadence/topology claims are corrected and a real PortfolioDeployer topology with CAPITAL, WEIGHT, RISK OF BOOK, and book allocation is compared with backtest output PortfolioDeployerBacktestParityTest
#947 CLI/store replay derives the same exact-stream warmup plan as live and seeds pre-window closed bars before DSL binding BacktestFromStoreTest, CompiledStrategyAutoWarmupTest
#1071 Backtest position model is venue-derived: CLI runs default to HEDGING (per-leg books, exits close their own leg) matching the retail-MT5 accounts, --position-mode netting for netted venues; expected_margin_mode asserts the live account matches; stale netting exits are retired and a reduce-only tripwire alerts on any exit that adds exposure (#1069/#1070) HedgingModeBacktestTest, StaleBracketExitAfterReversalTest, MT5AccountVerifierTest, OrderManagerReduceOnlyExitTest

2026-09-02 position ledger — one book, derived P&L (#1096, #1097, #1098)

Row Behavior Proof
Leg intent on the order Every leaf order carries LegIntent (Open/Close/Net); the fill resolver reads it first, then the owned leg by ticket, then the venue default. Backtest and live book from the same intent, so a venue-detected close and a backtest close realize the same leg LegIntentResolverTest, LegIntentPlannerTest, TradingPipelineOcoEntryLegTrackingTest
One leg per venue ticket A re-report of an execution on an owned ticket (restart recovery) books only the venue's cumulative increment; a close naming a leg the book does not hold books nothing StrategyPositionTrackerReplayTest, Mt5CommentMatchTest
Account book derived The account position view is an index over the strategy ledger, never a second writer; account and strategy realized are the same number from one ledger LedgerAccountingCharacterizationTest, report column pairs byte-identical on the fixture set
One accounting fold Every realized amount (execution, financing, boot reconcile) is one FillAccountedEvent folded once into both accumulators, the daily tracker, trade history, pacer and halts LedgerAccountingCharacterizationTest, TradingPipelineVenueCostsTest
Flatten leg by leg Halt-flatten closes each ledger leg with Close(legId, ticket) on every venue; no account-net path LiveSessionFlatten* suites, MT5Broker close-by-ticket
Vanished-ticket retirement A ledger leg whose venue ticket is gone from two consecutive clean snapshots is closed from deal history through the ordinary fill path MT5PositionPollerCloseTest

Residual divergences (known, accepted, tracked)

Residual Behavior Tracking
CROSSES cold start Warmup replay does not evaluate rule expressions, so a CROSSES node's prev-state is unset on the first post-deploy bar — it returns Undefined (rule does not fire) for exactly one bar. Fail-safe: a missed signal, never a wrong one inherent to replay-without-firing
Freeze-level in backtest Live modifyPosition rejects SL/TP moves inside SYMBOL_TRADE_FREEZE_LEVEL (surfaced + logged); the mt5-sim does not model freeze-level, so a backtest trail always tightens where live may be refused #638 residual
Tick-fills synthetic marks A symbol with an open position but no live orders resolves SYNTHETIC under --tick-fills, so its intrabar equity marks come from synthetic points (fills are exact; drawdown sampling is approximate) #642 residual
Venue partials on fallback exits When a venue partial fills a fallback (non-attached) bracket, exits are sized to the first fill's volume; a later remainder fill has no engine exit follow-up if partial-fill venues go live
Already-crossed native stops The fill decision is aligned: MT5 converts a STOP already through the latest ask/bid to MARKET (StopLimit to LIMIT), matching the engine-held path. Backtest fills on its crossing tick, while live fills after dispatch at the venue's later executable price, so latency/slippage can still change the fill price #815; decision pinned by AlreadyCrossedStopParityTest, live wire/protection by MT5BrokerIntegrationTest

With the same input event stream, calendar, instrument metadata, cost and risk configuration, modeled live-equity basis, and venue-shaped broker model, the backtest is faithful for live dollar/trade replication within the declared bounds above. This is a shared-runtime claim, not a claim that PaperBroker or historical ticks predict venue latency, retcodes, partial fills, feed sampling, or other explicitly listed live-only effects. Use mt5-sim and retained venue evidence when the result will be cited as MT5-shaped rather than research-tier output.

File pointers

  • Pipeline contract — docs/phases/phase-4-backtest.md (the "Same pipeline, live execution" section)
  • Pipeline parity test — src/test/kotlin/com/qkt/parity/BacktestLiveParityTest.kt
  • Live-pipeline construction — src/main/kotlin/com/qkt/app/LiveSession.kt (broker = buildBroker(paperBroker, ...))
  • Backtest-pipeline construction — src/main/kotlin/com/qkt/backtest/Backtest.kt:fromStore
  • PaperBroker fills — src/main/kotlin/com/qkt/broker/PaperBroker.kt
  • MT5Broker quantization (v0.26.3 + v0.26.4) — src/main/kotlin/com/qkt/connector/mt5/MT5Broker.kt (quantizeForPlacement)
  • Strategy-port parity (separate concern) — qkt-prod/docs/PARITY.md
  • Data-source parity (the prices, separate concern) — docs/parity/parity-dukascopy-vs-mt5-xauusd.md (dukascopy is the backtest source); docs/parity/parity-bars-xauusd-m5.md, docs/parity/parity-ticks-xauusd.md (TV vendor cross-check)