qkt Full Engine Audit — 2026-06-10¶
Seven-pillar audit at v0.38.0 (
11c9349e, origin/dev): architecture, money math, backtest=live parity, test quality, production readiness, DSL semantics, and industry research (FIA / SEC 15c3-5 / MiFID II RTS 6 / Knight Capital). Seven parallel auditor agents, findings adversarially re-verified by the orchestrator against source.Markers: [V] = orchestrator read the cited code and confirmed the finding. [A] = accepted on auditor evidence (file:line cited, spot-checked or dual-agent agreement).
Context for severity: the operator intends to risk personal savings on DSL strategies. Losses from a strategy's edge are tolerable; losses from an engine fault are not. "CRITICAL" below means: an engine fault that loses or misstates real money silently.
Verdict¶
Not ready for real money. Ready to become ready: the deterministic core (BigDecimal money, event-driven replay, idempotent order IDs, single-consumer engine queue) is sound, and nothing found is architecturally fatal — but 13 verified critical defects currently stand between this engine and a savings account, several of them silent-wrong-number bugs of exactly the class the audit was commissioned to find.
The five pillar questions:
- Sound, scalable design without silent leaks? Single-strategy: yes, post-#281. Multi-strategy: no — the daemon-shared CandleHub (#360) breaks the single-consumer invariant, and one strategy exception silently kills a session's engine thread (#361).
- Money math correct throughout? No. Engine-fired exits book to no strategy (#356), venue-side closes are priced from fiction (#357), three sizing modes are off by contractSize — 100x to 100,000x (#358), and live PnL is cost-blind (#392).
- Backtest = live? Not yet. Backtest has no halt rules (#362), triggers on mid (#382), warmup differs (#383), GTD differs (#368), and the divergence catalog under-reports (#389).
- Tested well? Strong where tests exist (real types, hand-computed constants, no mocks). The risk layer's most important behavior — halted strategy's orders rejected — is untested behind a comment claiming otherwise (#386); broker-failure paths untested (#359).
- Production ready? No — gateway outage reads as "all closed" (#359), halts don't survive restart (#380), feed death is undetectable (#381), and the industry checklist shows the standard pre-trade control layer (caps, collar, kill switch, runaway breaker) is missing (#393-#396).
Go/no-go: fix the 13 P0s, then run the staged ramp (#401). The latch-stack strategy currently live on qkt-prod is affected by #365 (its 4-hour time exit fires in 14.4 seconds) and should be considered not-as-designed until that lands.
Critical findings (P0)¶
| ID | Finding | Where | Issue |
|---|---|---|---|
| MATH-1 [V] | Engine-fired exits (trailing conversions, scale-outs, time exits) omit strategyId; fills short-circuit StrategyPositionTracker.applyFill — per-strategy positions/PnL/risk silently diverge |
OrderManager.fireFallbackTrigger ~1695-1760 |
#356 |
| MATH-2 [V] | Venue-side closes booked at lastPrice(qktSymbol) ?: p.priceOpen — engine's last tick, or a fabricated break-even; deal commission/swap never captured |
MT5PositionPoller ~142 |
#357 |
| MATH-3 / DSL-5 [V] | SizeNotional/SizePctEquity/SizePctBalance omit contractSize → 100x (XAUUSD) to 100,000x (FX) oversize; risk-based paths in the same file are correct |
SizingCompiler |
#358 |
| PROD-2 / TEST-1/2 [V] | Gateway outage → getPositions/getPendingOrders return emptyList → pollers synthesize close fills for every position and cancel every pending; untested |
MT5Client:125,136; MT5PositionPoller:121-122; MT5PendingOrderPoller:80 |
#359 |
| ARCH-1 [V] | One sharedHub for all sessions: engine threads execute each other's rule evaluation on plain HashMaps — double aggregation, duplicate fires, CME |
DaemonCommand:59-61,180 |
#360 |
| ARCH-2/5, PROD-6, DSL-10 [V] | Engine loop arms FeedTick/BusEvent unwrapped — any strategy exception kills the thread silently; Heartbeat/Flatten failures swallowed in bare runCatching |
LiveSession ~633-642 |
#361 |
| PAR-1 [V] | Backtest constructs RiskEngine(rules, emptyList(), ...) — zero halt rules; live wires MaxDailyLoss ($1000 default) + DD halts |
ReplayEngine:145 |
#362 |
| PAR-2 / MATH-5 [V] | setStartingBalance only called via PortfolioDeployer's map — standalone deploys have ACCOUNT.equity = 0; %-of-equity sizing broken (risk rules do get the balance via StrategyHandle) |
LiveSession:449; PortfolioDeployer:232; StrategyHandle:91,139,181 |
#363 |
| DSL-1 [V] | Update-then-fire + RollingHigh includes current update → close > highest(close,N) structurally never true; docs claim exclusion |
AstCompiler:342-343; indicators.md:131 |
#364 |
| DSL-2 [V] | holding_duration is milliseconds; docs + both shipped examples treat it as seconds — time exits fire 1000x early. latch-stack is LIVE on qkt-prod with > 14400 |
ExprCompiler:287-288; both example .qkt files |
#365 |
| DSL-3 [V] | Rules re-fire every bar while condition holds — no edge memory; conditions.md:31 promises edge-driven actions |
CompiledRule.fire() |
#366 |
| DSL-4 [V] | parseStrategy never checks EOF — everything after the first unrecognized top-level token silently discarded; half a strategy deploys "ok" |
Parser:268-330 |
#367 |
| PAR-3 [V] | GTD end-to-end still inert: gateway order.py hardcodes ORDER_TIME_GTC, supportsNativeGtd = true disables the engine sweep. #308 fixed the wire only |
MT5Broker:65; mt5-gateway |
#368 |
Plus three P0-labeled enhancements that gate go-live: pre-trade caps + collar (#393), true kill switch (#394), and the ramp policy (#401).
High findings (P1)¶
| ID | Finding | Issue |
|---|---|---|
| MATH-4 [V] | EquityTracker peak starts at ZERO on a 0-based PnL series — trailing DD is fraction-of-peak-profit, not equity (affects #348 rules) |
#369 |
| MATH-6 [V] | LegBook.netView() zeroes net-flat hedged pairs — locked-in straddle loss invisible to unrealized/equity/halts |
#370 |
| MATH-7/11 [V] | Backtest equity curve is 0-based → maxDD/Sharpe/daily-DD distorted; Calmar divides dollars by a fraction | #371 |
| MATH-8 / DSL-9 [V] | PCT RISK divides by 100; % OF EQUITY consumes a raw fraction — same surface number, 100x apart |
#372 |
| MATH-10 [A] | No quote→account currency conversion; registry ships JPY/CHF/CAD pairs — JPY PnL booked as USD (~150x) | #373 |
| ARCH-3 [A] | OrderFilled chain: venue mutated (sibling cancelled, children sent) before books written; mid-chain exception → permanent venue/book divergence | #374 |
| ARCH-4 [V] | Unbounded LinkedBlockingQueue<Inbound> — stalled consumer OOMs the whole daemon |
#375 |
| ARCH-6 [V] | Boot reconcile: getOpenPositions failure → emptyMap() → session trades believing it is flat; CompositeBroker leaves swallow per-leaf |
#376 |
| ARCH-9 [V] | StackOrchestrator subscribes before the fill-applying handler — stack children risk-approved against pre-fill book | #377 |
| PROD-3/13 [V] | IO error → synthetic retcode -1 → "rejected" (maybe filled); isOrderSuccessful = retcode == 10009 only — 10008/10010 treated as rejects |
#378 |
| PROD-4 / TEST-6 [V] | Tick has zero validation; zero/negative/crossed quotes reach indicators, PnL marks, triggers; untested |
#379 |
| PROD-5 [V] | Halt state + daily PnL not persisted — restart un-halts and resets the daily budget | #380 |
| PROD-7 [V] | onDisconnect only on thread exit; poll loop never exits — reconnect budget unreachable, stale feed undetectable |
#381 |
| PAR-4 [V] | Triggers evaluate mid in PaperBroker, MT5_SIM, and live engine-held path; venue uses bid/ask — systematic half-spread optimism | #382 |
| PAR-6 / DSL-7 [V] | Bind (gate credit from cold hub, LiveSession:529) before seed (:590) — dead warmup window after every deploy; backtest silently consumes the window |
#383 |
| DSL-6 [V] | All buildRequest sites use ec.candle.symbol — cross-stream/SCHEDULE actions order the wrong instrument |
#384 |
| DSL-8 [V] | orderType ?: Market at parse time — DEFAULTS ORDER_TYPE dead (family of #247) |
#385 |
| MATH-9/12/13, PAR-5 [A] | Live PnL/halts cost-blind (no commission/swap/execFee); MaxDailyLoss realized-only (open bleed never trips it); contractSize ?: ONE silent fallback |
#392 |
| TEST-3/4/5/7/8 [V] | Halt rejection untested behind a false comment; exact-threshold, cost-blind-halt, determinism-replay, DST all undefined-by-test | #386 |
Medium/low tails (grouped, P2)¶
- OrderManager/candle lifecycle — terminal states not sinks (ARCH-7), satellite-state
growth vs #255's GC invariant (ARCH-8),
persistAllO(N)+swallowed failures (ARCH-10), next-tick-only candle close (ARCH-11): #387 - Ops hardening — blind-broker init window (PROD-11), pre-bind event race (PROD-12), no fsync / async loss window (PROD-16), trust boundary + NTP undocumented (PROD-18/19): #388
- Parity tail + catalog gaps — bar-synthesis short-side optimism vs KDoc (PAR-7), tick sampling (PAR-8), SCHEDULE timing (PAR-10), calendar defaults crypto-vs-fx (PAR-12), bid/ask Undefined on bar data (PAR-15), catalog missing rows (PAR-14): #389
- DSL semantics tail — bracket anchored to signal-bar mid-close (DSL-12), SCHEDULE DST
(DSL-13), warmup ignores action-side indicators (DSL-14), STACK_AT dead after restart
(DSL-15/PROD-17),
TRUE OR Undefinedswallow (DSL-16), CROSSES-in-CASE staleness (DSL-17), legacy onCandle cross-feed (DSL-19): #390 - Doc drift — every doc claim found false, incl. the two semantics promises above: #391
Industry research (FIA, SEC 15c3-5, RTS 6, Knight, CME, RTS 25)¶
54-point checklist built from primary sources (FIA Guide 2015 full text, SEC market-access rule, MiFID II RTS 6/25, Knight Capital SEC order, CME Globex controls, Bybit DCP, MT5 stop-out mechanics, Bailey & López de Prado overfitting statistics). Scores against qkt: roughly 14 have / 19 partial / 12 missing / 9 unknown. qkt is strong on the parts most hobby engines skip (walk-forward, parity catalog, state recovery, deterministic replay, deploy verification) and missing the standard institutional pre-trade control layer.
Top gaps → issues: per-order qty/notional caps + price collar (#393), stale-quote/outlier/crossed-book gate (#395), true kill switch (#394), runaway circuit breaker (#396), unknown-order-state protocol (folded into #378), external deadman watchdog (#397), margin headroom + weekend policy (#398), measured-usage deploys (#399), immutable trade journal + daily reconcile (#400).
Burn-in recommendation¶
Staged ramp, codified in #401: 30+ day demo burn-in with explicit exit criteria (zero unexplained order states, zero reconciliation deltas, kill switch exercised, demo equity inside the cost-calibrated backtest band) → 2-4 weeks micro-real at 0.01 lots → 25/50/100% ramp, ≥1 month per step, with a reset rule on any engine-state incident. P0 fixes land before first real money;
398/#399/#400 before scaling.¶
Verified solid (no issue needed)¶
- Money representation: BigDecimal DECIMAL64, scale 8, HALF_EVEN throughout; no float on any money path.
- Position/leg arithmetic test suites (PositionTracker/LegBook/StrategyPositionTracker): hand-computed constants, no tautologies. Signed-quantity handling correct.
- Candle construction parity: same classes, same epoch anchoring, same tick-driven close in both modes — identical given identical ticks.
- Order idempotency plumbing (clientOrderId/orderLinkId on Bybit; magic+comment on MT5).
- Intrabar SL+TP resolution under tick replay (single price can't trip both).
- CompositeBroker delegation (PROD-1/#283) and pending-order reload (PROD-8) — fixed in the 125 commits between audit start and v0.38.0; re-verified fixed, dropped.
Dedupe notes¶
- DSL-11 (VWAP volume-less feed) ≈ existing #301 — not refiled.
- Commission/swap modeling: backtest side shipped (v0.35.0, #335 closed, #338 documented conventions); the live cost-blindness is new and tracked in #392.
- PROD-14 (Bybit recovery
limit=50) not filed:nextPageCursoris consumed atBybitLinearStateRecovery:114; pagination appears handled. Re-check only if recovery misses orders in practice. -
352 (broker-equity sizing) remains the deeper fix behind #363; #351 (portfolio DD)¶
unaffected by these findings.
Method¶
Seven auditor agents (one per pillar + research) ran against origin/dev with file:line evidence required for every claim. The orchestrator then re-read every cited site for the CRITICAL/HIGH set, attempted to refute each finding, corrected two stale ones (PROD-1, PROD-8 — fixed upstream mid-audit), resolved one agent conflict (warmup seeding order — the DSL agent was right), and downgraded/folded where evidence was weaker than the claim. Issues filed: #356–#401, 46 total.