Phase 18 — LiveSession typed-broker dispatch¶
Released: 2026-05-10 Version: 0.20.0
Summary¶
Phase 18 closes the gap left by Phase 17: LiveSession now accepts a typed broker registry, so strategies declaring EXNESS:EURUSD actually route orders to the configured Exness profile (MT5 today, REST tomorrow if Exness ships one) instead of falling through to PaperBroker. The DSL broker label is the venue identity; the protocol is configured per-profile via type: in qkt.config.yaml — venues and protocols are decoupled.
The refactor is venue-agnostic: BrokerFactory = (EventBus, Clock, MarketPriceTracker) -> Broker. The daemon builds the registry from MT5 profiles at startup and threads it through StrategyHandle.RealFactory and PortfolioDeployer into each LiveSession. Each session invokes factories with its own bus/clock/price-tracker, so per-session lifecycles stay clean (each session gets its own MT5 position poller and state-recovery instance).
Existing paper-trading workflows continue to work — empty registry or unknown DSL labels fall back to PaperBroker.
What's new¶
com.qkt.app.BrokerFactorytypealias:(EventBus, Clock, MarketPriceTracker) -> Broker. Registry of factories keyed by DSL broker label (lowercased).LiveSession.brokerFactories: Map<String, BrokerFactory>parameter (default empty). When non-empty,LiveSession.start()builds aCompositeBrokerfrom the strategy's declared streams, routing each broker label to its factory output. Fallback =PaperBrokerfor unrecognized labels.StrategyHandle.RealFactory.brokerFactories: Map<String, BrokerFactory>parameter (default empty). Threads through to eachLiveSessionit creates.PortfolioDeployer.brokerFactories: Map<String, BrokerFactory>parameter (default empty). Portfolio child sessions get the same registry.DaemonCommand.startDaemon()builds the registry from MT5 profiles loaded viaConfig.brokers+MT5DefaultProfiles.all+ env vars; passes it intoRealFactoryandPortfolioDeployer.- DSL broker label resolution is case-insensitive (
EXNESS:,exness:,Exness:all resolve to profileexness).
Migration from Phase 17¶
No DSL changes. Existing strategies declaring BACKTEST: or other labels keep working — unknown labels resolve to PaperBroker fallback.
No public API breakage. All new constructor parameters have empty-map defaults. Existing LiveSession(...) / RealFactory(...) / PortfolioDeployer(...) callers continue to compile and behave identically.
Phase 17's qkt brokers list continues to work — it lists profiles loaded from config; now those same profiles actually drive live dispatch.
Usage cookbook¶
Run a strategy on Exness¶
# ./momentum.qkt
STRATEGY momentum VERSION 1
SYMBOLS
eur = EXNESS:EURUSD EVERY 1m
RULES
WHEN ema(eur, 9) crosses ABOVE ema(eur, 21)
THEN BUY eur SIZING 0.1 BRACKET STOP_LOSS BY 50 PCT TAKE_PROFIT BY 100 PCT
The strategy's BUY signal routes to MT5Broker(profile=exness) → mt5-gateway at localhost:5001 → MT5 → Exness fills it.
Mixed-venue strategy (MT5 + paper)¶
eur orders → MT5; btc orders → fallback PaperBroker (since no backtest profile is configured). Useful for paper-testing one leg while live-trading another.
Multiple Exness accounts¶
brokers:
exness-personal:
type: mt5
extends: exness
gateway_url: http://localhost:5005
magic: 10005
exness-corporate:
type: mt5
extends: exness
gateway_url: http://localhost:5006
magic: 10006
Strategy uses EXNESS-PERSONAL:EURUSD or EXNESS-CORPORATE:XAUUSD. Each profile gets its own MT5Broker per session, with its own poller scoped by magic.
Testing patterns¶
LiveSession continues to default to PaperBroker when brokerFactories is empty — so existing tests don't change. New tests opting into typed dispatch pass a factory map:
val factories: Map<String, BrokerFactory> = mapOf(
"exness" to { bus, clock, _ -> MT5Broker(profile, bus, clock) },
)
LiveSession(strategies, source, symbols, brokerFactories = factories).start()
For end-to-end testing, point the MT5 profile at a MockWebServer and assert the gateway receives the translated order.
Known limitations¶
- Cross-broker same-symbol routing still deferred. Carried from Phase 17. Two profiles handling the same symbol in one strategy would conflate at
PositionTracker. - Per-session MT5Broker construction. Each
LiveSessioninstantiates its ownMT5Broker(with its own state recovery + poller) for every profile its strategies use. CPU cost is bounded but means N pollers per N sessions per profile. Future enhancement: shared broker instances with session-scoped event filtering. - No
--strict-brokersflag yet. Unknown broker labels silently fall back toPaperBroker. Strict mode (fail on unknown labels) is a follow-on for prod safety. - Bybit not yet folded into typed registry. Existing Bybit broker keeps its own wiring. Migration to
type: bybitinqkt.config.yamlis a follow-on. - Live-broker tick source still TradingView (Phase 7c). MT5 handles execution only; the deferred tick-feed accuracy audit will quantify TV-vs-MT5 price drift.
References¶
- Phase 17 spec/plan/changelog (the broker; this phase wires it in):
docs/phases/phase-17.md - Phase 14 portfolio daemon (multi-broker context):
docs/phases/phase-14.md LiveSession:src/main/kotlin/com/qkt/app/LiveSession.ktBrokerFactory:src/main/kotlin/com/qkt/app/BrokerFactory.ktDaemonCommandstartup wiring:src/main/kotlin/com/qkt/cli/DaemonCommand.kt