Venue plugins — futures, crypto, and any broker — research¶
Date: 2026-09-18
Question: How do we make qkt able to trade futures (and more crypto venues, and eventually
anything) through installable adapters — e.g. qkt add --broker ampfuture dropping in a JAR —
with one global mapping for instruments, orders, fills, positions, account data and credentials?
What is feasible, what scales, and what is sound enough to put real money behind?
Builds on: 2026-06-08 MT5 as the universal provider.
Both blockers that document named (per-symbol calendars, config-driven profiles) have since
shipped (broker/mt5/SymbolCalendars.kt, MT5BrokerProfileLoader.kt:34-37). This document is
the next step it deferred: the venues MT5 cannot reach, and the deep asset-class concepts.
Status: research / proposal. Step 1a (the contracts, MT5 and Bybit behind them, live wiring) is
implemented — see trading account contracts.
The design settled on these names: Connector, TradingAccount, AccountDirectory; the research's
Venue is TradingAccount.
TL;DR¶
- Feasible — the seams mostly exist already.
Broker(broker/Broker.kt) is a narrow, asset-agnostic venue interface;CompositeBrokerroutes by symbol prefix;MarketSource+MarketSourceCapabilitydo the same for data;InstrumentRegistryis an interface;PositionAccountingMode(NETTING/HEDGING),CostKind.FUNDING/EXCHANGE_FEEandBrokerEvent.ConnectionChangedare already modelled. qkt ships as a plain JVM 21 app (installDist+ jlink), not a native image, so loading code at runtime is possible. - What blocks plugins today is small and specific:
LiveSessioncasts toMT5Brokerin three places to get instrument metadata; broker construction is hand-assembled inDaemonCommand(mt5Factories + bybitFactories); credentials come from three different mechanisms; the broker YAML reader is flat; the jlink runtime lacks modules plugins commonly need; andOrderRequest(20 subtypes, many engine-internal) is too rich to be a public contract. - Your JAR idea is right for the adapter code, not for where live money runs. The whole safety model you built — guardrails polling an independent gateway, the kill switch enforced in the gateway on order paths — depends on a choke point outside qkt. A Rithmic adapter loaded straight into the qkt JVM has no such choke point: the guardian could no longer stop it.
- Recommendation — write each adapter once as a JAR, run it in one of two hosts:
- in-process inside qkt, for backtest, paper and data; or
- inside
qkt-venue-gateway, a small generic gateway host that loads the same JAR and exposes a versioned Venue Gateway Protocol (VGP v1) — the mt5-gateway's endpoints, generalized, with the kill switch implemented once in the host. Live money always goes through (2). Guardrails talks VGP and works against every venue unchanged. - Venue ≠ protocol ≠ account ≠ data vendor. "AMP" is a futures clearing firm (FCM) you reach
through a connectivity protocol (Rithmic, CQG, …). The plugin is the protocol
(
rithmic); AMP is configuration. Soqkt add --broker ampfuturebecomes two verbs:qkt plugin install rithmic(code) andqkt brokers add amp --plugin rithmic(account). - Futures and dated crypto futures share one core model. Expiry, roll, margin, settlement and funding belong in the venue-neutral engine; plugins only report those facts. Build them once and Deribit/Binance quarterlies get them for free.
- Soundness comes from three things, not from the loader: certification levels enforced at deploy (DATA / PAPER / LIVE), a conformance test kit built from the defects you have already hit live, and plugin provenance recorded in promotion. Without these, plugins become the easiest way to lose money silently.
- Start with the seams, not the loader. Phase 1 is pure refactoring inside the monolith with zero behaviour change — and it is valuable even if plugins never ship.
1. What exists today¶
Verified in source on 2026-09-18 (branch fix-live-parity-audit, head 107bb211).
Already plugin-shaped¶
| Seam | File | Why it helps |
|---|---|---|
| Venue interface | broker/Broker.kt |
submit / cancel / modify / modifyPosition, capabilities, getOpenPositions, recoverPendingOrders, watchBookedLegs, deals, accountState, pendingOrders, positionAccountingMode, shutdown. Asset-agnostic. |
| Per-session construction | app/BrokerFactory.kt |
(EventBus, Clock, MarketPriceTracker, PositionProvider, String?) -> Broker. Its doc already states the principle this design needs: "the venue label is the public identity, the protocol is an implementation detail." |
| Multi-venue routing | broker/CompositeBroker.kt |
First-match SymbolPattern routing, order-id and ticket → leaf tracking, merged deals / pending orders. |
| Market data | marketdata/source/MarketSource.kt |
liveTicks / bars / ticks / tickSlice, per-symbol capabilities. cli/MarketSourceFactory.kt composes by prefix. |
| Data capabilities | MarketSourceCapability |
LIVE_TICKS, BARS, TICKS, VOLUME — volume-weighted indicators are already rejected on a feed without VOLUME. |
| Instruments | instrument/InstrumentRegistry.kt |
Interface with a hard error on missing meta (no silent contractSize = 1). |
| Order capabilities | broker/OrderTypeCapability.kt |
The engine decomposes composites (Bracket, OCO, OTO…) into what a venue accepts. |
| Position modes | broker/BrokerState.kt |
PositionAccountingMode.NETTING / HEDGING / UNKNOWN. Futures are netting. |
| Cost kinds | accounting/Accounting.kt |
CostKind: COMMISSION, SWAP, FUNDING, BORROW, EXCHANGE_FEE, SPREAD_COST, TAX — perps (funding) and futures (exchange fees) already have a slot. |
| Connection events | events/BrokerEvent.kt |
ConnectionChanged, GatewayUnreachable, AccountEquityStale, PositionReconciled. |
| Calendars | broker/mt5/SymbolCalendars.kt, common/DailyBreakCalendar.kt |
Per-symbol rules, "<base> pause HH:MM-HH:MM <Zone>". |
| Runtime | build.gradle.kts |
JVM 21, application + installDist, jlink runtime, jpackage on Windows. No native image. |
Existing venues: MT5 (broker/mt5, ~6.6k lines; MT5Broker.kt alone 2,842), Bybit spot and
linear (broker/bybit, ~2.1k lines), PaperBroker, LogBroker, MT5BrokerSimulator.
What blocks plugins¶
LiveSessionreaches into MT5.app/LiveSession.kt:636,:1004and:1355usefilterIsInstance<MT5Broker>()to buildMT5InstrumentRegistry. A third-party broker has no way to supply instrument metadata to a live session.- Broker assembly is hardcoded.
cli/DaemonCommand.kt:~270-350buildsmt5Factoriesfrom profiles andbybitFactoriesonly whenBYBIT_API_KEYis set, under hardcoded keys"bybit_spot"/"bybit_linear".MarketSourceFactory.compositerepeats the same hardcoding for data.BotSessionCommand.kt:186constructsMT5Brokerdirectly as well. - Credentials come from three places. MT5: config +
${VAR}+QKT_BROKER_<NAME>_<FIELD>overrides (MT5BrokerProfileLoader). Bybit: directSystem.getenv("BYBIT_API_KEY")insideBybitClient(:119-139). FRED: direct env. A plugin has no host service to ask. - The broker YAML reader is flat.
brokers:arrives asMap<String, Map<String, String>>; nested blocks (calendars, aliases, overrides) are parsed separately per broker name (MT5BrokerProfileLoaderdocstring). A plugin cannot declare nested config such as acredentials:block.snakeyaml-engineis already a dependency, so this is a parser choice, not a missing capability. - The runtime has a fixed module set. Dockerfile and
jlinkRuntimeship onlyjava.base, java.logging, java.naming, java.xml, jdk.httpserver, jdk.crypto.ec, jdk.unsupported.java.net.http(the JDK HTTP/WebSocket client),java.sqlandjava.managementare absent. A plugin that uses them works in development and throwsNoClassDefFoundErrorinside the production image. OrderRequestis not a public contract. It is a sealed interface with 20 subtypes (execution/OrderRequest.kt), including engine-internal shapes such asArmedTrailingStop,SteppedStop,TimeTighteningStop,ScaleOut,TimeExitandStack. Every broker today has its own translator (MT5OrderTranslator,BybitOrderTranslator) over that full type. Exposing it would freeze engine internals into the plugin API, and adding a subtype would break every compiled plugin's exhaustivewhen.- Defaults that vouch for everything. Several
Brokermethods default to "trust the engine":recoverPendingOrdersreturns every id ("Brokers without venue truth vouch for every order"),getOpenPositionsreturns empty,dealsreturns empty. Correct forPaperBroker; dangerous for a third-party live adapter that simply forgets to override them — it would start trading on assumed state without error. - No discovery. No
ServiceLoader, noURLClassLoaderanywhere insrc/main. CLI commands are a hand-writtenwhenincli/Main.kt.
None of these is deep. Items 1–4 are refactors inside the monolith.
2. The first decision: where does an adapter run?¶
Option A — in-process JAR (the original idea)¶
qkt loads qkt-venue-rithmic.jar from a plugins directory and calls it directly.
- ✅ Typed, low latency, one deployable, easy to test, fits Kotlin/JVM venues.
- ❌ No independent choke point. The kill switch lives in
mt5-gatewaytoday (the path-gated/order,/modify_sl_tp,/close_position… set), and the guardian enforces prop limits by flipping it. An in-process adapter talks to the exchange directly; the guardian has nothing to flip. You would be re-implementing the safety model inside the process it is supposed to protect against. - ❌ Crash / leak / dependency-conflict blast radius is the whole trading daemon.
- ❌ No sandbox. The Java
SecurityManagerwas deprecated for removal in JDK 17 and is permanently disabled from JDK 24; plugin code runs with qkt's full authority. - ❌ JVM-only. Rithmic's and CQG's first-party SDKs, and many crypto SDKs, are C++/.NET/Python first.
Option B — out-of-process gateway per venue (generalize mt5-gateway)¶
Each venue runs as its own service speaking a common HTTP/WS protocol; qkt has one generic client.
- ✅ Kill switch and guardian work exactly as today, for every venue.
- ✅ Crash isolation; any language; independently deployable and restartable.
- ✅ You already operate this pattern (mt5-gateway + guardrails + insights all consume it).
- ❌ A protocol to design, version and test. One more container per account.
- ❌ An extra network hop — irrelevant at your holding periods (1h–21d), relevant only for HFT.
Recommendation — both, from one codebase¶
┌──────────── same adapter JAR (qkt-venue-rithmic) ────────────┐
│ │
backtest / paper / data live money
┌───────────────────────┐ ┌────────────────────────────────────┐
│ qkt (JVM) │ │ qkt-venue-gateway (JVM, one per account) │
│ └─ InProcessHost │ │ ├─ loads the same adapter JAR │
│ └─ adapter │ │ ├─ kill switch (path + symbol) │
└───────────────────────┘ │ ├─ VGP v1 HTTP/WS server │
│ └─ adapter → exchange │
└───────────────▲────────────────────┘
│ VGP v1
┌──────────────────────────┼─────────────────────┐
│ qkt daemon (VgpBroker) │ qkt-guardrails │ qkt-insights
- The adapter is written once, against the plugin SPI (§5).
- In-process host: for backtest, paper and data plugins. Fast, typed, no ops cost.
qkt-venue-gateway: a small generic JVM service that loads adapter JARs and exposes VGP v1 (§6). The kill switch, auth, idempotency, event journal and health are implemented once here, not per venue.- Live deploys refuse in-process adapters. Enforced by certification level (§10), not by convention.
- MT5 needs nothing new.
mt5-gatewayalready is a gateway; it becomes the reference VGP implementation later (Phase 6), or stays as-is behind the existingMT5Broker. - Non-JVM venues (a Python
ib_insyncbridge, a C++ Rithmic bridge) can implement VGP directly without being a JAR at all. The protocol is the real contract; the JAR SPI is the convenient path.
This keeps what the JAR idea is good at (one artifact, typed, easy to add) and keeps what your safety stack depends on (an independent choke point).
3. The mental model: four different things¶
Futures break the "one broker = one thing" intuition that MT5 taught.
| Concept | What it is | Examples | In qkt |
|---|---|---|---|
| Protocol / connectivity | The API you speak | MT5 (via gateway), Rithmic R|Protocol, CQG WebAPI, TT, Tradovate REST/WS, IBKR TWS, Bybit v5, Binance, Deribit | the plugin (type: rithmic) |
| Venue / clearing | Who holds the account and clears | AMP, Optimus, NinjaTrader Clearing, a prop firm, Exness | config on a broker entry |
| Account | One login, one balance | AMP demo #123, The5ers HS 50k | one broker entry, one qkt-venue-gateway, one guardian |
| Data vendor | Where research data comes from | Databento, FirstRate, Norgate, Dukascopy, your hub | a data plugin, independent of execution |
So there is no ampfuture plugin. AMP offers several connectivity options; you pick one (say
Rithmic), and AMP becomes the fcm: / system: fields on a Rithmic broker entry. The same
Rithmic plugin then serves a Rithmic-routed futures prop account with no new code — exactly as
the one MT5 adapter already serves both Exness and The5ers.
4. The venue-neutral domain model (core changes)¶
These live in core, not in plugins. Plugins report facts; the engine owns meaning.
4.1 Instrument identity¶
Today a symbol is a string PREFIX:SYMBOL and InstrumentMeta is keyed by it. Futures need a
distinction between what a strategy names and what the exchange trades:
/** What a strategy writes. Stable across rolls. */
data class InstrumentRef(val venue: String, val root: String, val selector: ContractSelector)
// AMP:ES@front AMP:ES@next AMP:ESZ5 (explicit) BYBIT_LINEAR:BTCUSDT (perp, selector = None)
sealed interface ContractSelector {
object None : ContractSelector // spot, CFD, perpetual
data class Front(val offset: Int = 0) : ContractSelector // @front, @next
data class Explicit(val code: String) : ContractSelector // ESZ5
}
/** What an order is sent on. Changes at every roll. */
data class VenueSymbol(val venue: String, val code: String) // AMP / ESZ5
Every existing symbol maps to ContractSelector.None, so nothing about FX, metals, CFDs or
crypto changes. Positions, P&L and attribution key on InstrumentRef (the economic identity);
orders and venue reconciliation key on VenueSymbol. This is the structural fix for the
"one economic position spans many symbols" problem.
4.2 Instrument kind and metadata¶
InstrumentMeta gains optional fields, all defaulted so every existing YAML and registry keeps
working:
| Field | Why |
|---|---|
kind |
Drives everything below; default CFD for MT5, SPOT/PERPETUAL for Bybit. |
quoteCurrency (explicit) |
Closes the QuoteCurrencyGuard hole: symbols with no currency tail (ES, FDAX, FGBL) currently pass unchecked, so EUR-quoted futures would book as USD. |
expiry, lastTradeDay, firstNoticeDay |
Expiry guard; no entry the strategy cannot hold to its horizon. |
settlement (CASH / PHYSICAL) |
Physical-delivery contracts must be flat before first notice. |
initialMargin, maintenanceMargin (+ asOf) |
The margin model; nothing in qkt reads margin today (verified: zero hits). |
priceLimitBand |
Limit up/down: a stop may not be executable. |
fundingIntervalHours |
Perpetual funding, booked as CostKind.FUNDING. |
volumeSemantics (TICK_COUNT / TRADED) |
Stops a volume rule fitted on CFD tick counts running unchanged on exchange volume. |
4.3 Data capabilities¶
Extend MarketSourceCapability (it already has VOLUME): TRADES (prints with aggressor
side), DEPTH, OPEN_INTEREST, SETTLEMENT, CONTRACT_CHAIN. Extend Bar/Candle with
optional openInterest and settlement, and the bar CSV with optional trailing columns
(the header already makes the format self-describing).
4.4 Core futures behaviour¶
Covered in depth in the 2026-09-18 conversation analysis ("what is missing for futures"); in short, all venue-neutral:
- Settlement marking —
EquityTrackermarks continuously today; futures accounts move at the daily settlement price. Book the variation at settlement so the ledger matches the venue. - Margin model — pre-trade "can I hold this", including revised margins.
- Expiry guard — refuse entries inside the roll window unless rolling is configured.
- Roll engine — atomic from the strategy's point of view: close old + open new under one
InstrumentRefposition, using a native calendar-spread order where the venue has one (OrderTypeCapability.CALENDAR_SPREAD, new), legged otherwise with explicit exposure checks. - Limit state — "stop not executable" as a first-class risk state.
Deribit and Binance quarterly futures are DATED_FUTURE too, so crypto inherits all of it.
5. The plugin SPI¶
A new Gradle module, qkt-api, holds only what plugins compile against. It is semver'd and
locked with the Kotlin binary-compatibility-validator plugin, so an accidental break fails CI.
5.1 Entry point¶
package com.qkt.api
/** Discovered with java.util.ServiceLoader from each plugin JAR. */
interface VenuePlugin {
val id: String // "rithmic" — the `type:` in config
val spiVersion: Int // major version of qkt-api it was built for
fun describe(): PluginDescriptor
fun open(config: VenueConfig, host: HostServices): Venue
}
data class PluginDescriptor(
val id: String,
val version: String,
val kinds: Set<InstrumentKind>, // what it can trade
val certification: Certification, // DATA, PAPER, LIVE — §10
val configSchema: List<ConfigField>, // validated by the host before open()
val requiredJdkModules: Set<String>, // checked against the running runtime
)
data class ConfigField(val key: String, val type: FieldType, val required: Boolean, val secret: Boolean, val doc: String)
5.2 One Venue per account, thin per-session views¶
Mirrors what the daemon already does for MT5 (one shared MT5Client + MT5ReadCache per
gateway, one MT5Broker per session):
interface Venue : AutoCloseable {
val name: String // "amp-demo" — the broker entry name
val prefix: String // "AMP" — the DSL stream prefix
fun instruments(): InstrumentProvider // replaces filterIsInstance<MT5Broker>
fun calendars(): CalendarProvider
fun marketData(): MarketSource? // null when data comes from elsewhere
fun session(ctx: SessionContext): VenueSession
fun truth(): VenueTruth? // required for LIVE, no defaults — §10
fun onConnection(listener: (ConnectionState, String?) -> Unit)
}
interface InstrumentProvider {
fun meta(symbol: VenueSymbol): InstrumentMeta?
fun resolve(ref: InstrumentRef, at: java.time.Instant): VenueSymbol // ES@front -> ESZ5
fun chain(root: String): List<ContractSpec> // futures only
}
5.3 A small, stable order model¶
Plugins never see OrderRequest. The host lowers it, using the capabilities the plugin
declares — the same decomposition the engine does today, moved in front of a stable type:
sealed interface VenueOrder {
val clientId: String; val symbol: VenueSymbol; val side: Side
val quantity: java.math.BigDecimal; val tif: TimeInForce
data class Market(...) : VenueOrder
data class Limit(..., val price: BigDecimal) : VenueOrder
data class Stop(..., val stopPrice: BigDecimal) : VenueOrder
data class StopLimit(..., val stopPrice: BigDecimal, val limitPrice: BigDecimal) : VenueOrder
}
data class VenueSubmit(
val order: VenueOrder,
val protection: Protection? = null, // attached SL/TP, only if BRACKET/POSITION_MODIFY declared
val ocoGroup: String? = null, // only if OCO declared
val reduceOnly: Boolean = false,
val closes: String? = null, // position/ticket id on hedging venues
)
interface VenueSession {
val capabilities: (VenueSymbol) -> Set<OrderTypeCapability>
fun submit(s: VenueSubmit): SubmitAck // async result via events
fun cancel(clientId: String)
fun modify(clientId: String, change: OrderModification): SubmitAck
fun events(sink: (VenueEvent) -> Unit) // accepted, filled, partial, cancelled, rejected
}
Capability negotiation doubles as forward compatibility. The host only ever sends shapes a plugin declared. When qkt later adds a new engine order type, it is lowered onto the existing four; an old plugin never receives something it cannot handle, and an undeclared shape is rejected in the host rather than thrown inside the plugin.
Built-ins bridge in through an adapter so nothing is rewritten up front:
class PluginBroker(session: VenueSession, lowering: OrderLowering) : Broker.
5.4 Host services — plugins never reach out¶
interface HostServices {
val clock: Clock
fun logger(name: String): org.slf4j.Logger
fun secret(ref: String): Secret // env:, file:, ${VAR} — §8
fun executor(name: String): java.util.concurrent.ScheduledExecutorService // host owns shutdown
fun http(): okhttp3.OkHttpClient // shared, pooled, TLS already in the runtime
fun stateDir(): java.nio.file.Path // per-plugin, per-account
fun rateLimiter(key: String, perSecond: Double): RateLimiter
fun metrics(): Metrics
}
Plugins do not call System.getenv, do not start raw threads and do not open their own
HttpClient. That makes them testable, keeps secrets in one place, and sidesteps the missing
java.net.http module entirely.
5.5 Data plugins¶
A separate, smaller SPI for research data (Databento, FirstRate imports, your hub):
interface DataPlugin { val id: String; fun open(config: VenueConfig, host: HostServices): MarketSource }
Reusing MarketSource means backtests, qkt fetch, completeness validation and the bar store
work with no new code paths. A data plugin can never be certified above DATA.
6. Venue Gateway Protocol (VGP v1)¶
The contract between qkt / guardrails / insights and any live venue. It is the mt5-gateway's surface, generalized and versioned, plus the two things futures venues need that MT5 polling never did: a push channel and idempotent submits.
| Method | Path | Notes |
|---|---|---|
| GET | /v1/health |
venue_connected, kill_switch, server_time, protocol + adapter versions |
| GET | /v1/account |
balance, equity, currency; margin used/available; futures initial/maintenance |
| GET | /v1/instruments[/{code}] |
normalized InstrumentMeta superset incl. kind, expiry, margins |
| GET | /v1/contracts/{root} |
futures chain |
| GET | /v1/positions |
per ticket (hedging) or per contract (netting), with accounting mode |
| GET | /v1/orders |
working orders |
| POST | /v1/orders |
idempotent on client_order_id — a retried submit must never double-fill |
| PATCH / DELETE | /v1/orders/{id} |
modify / cancel |
| POST | /v1/positions/close |
by ticket, or by symbol + optional quantity |
| GET | /v1/deals?from=&to= |
executions with typed costs |
| GET | /v1/stream?since=<seq> |
WebSocket: orders, fills, positions, account, quotes — every event carries a sequence number; reconnect resumes from since, so a fill is never lost across a disconnect |
| POST | /v1/kill |
{"scope":"all"} or {"scope":"symbols","symbols":["ES","NQ"]} |
| POST | /v1/kill/release |
same scopes |
| GET | /v1/bars, /v1/ticks |
historical, when the venue has them |
Semantics that must be written into the spec, not left to implementations:
- Kill switch gates paths that add or change risk, as mt5-gateway's
_KILL_GATEDdoes today — and scoped kill is native. The weekend work showed the need: v0.5.0 had to emulate "everything except BTCUSD" in the guardian by listing positions and closing them one by one. VGP makes that one call. - Closing positions must stay possible under an engaged switch through an explicit
flatten path; the weekend fix had to release a guardian-held switch before closing because
/close_positionis gated. VGP should separate "flatten" (always allowed) from "open or enlarge" (gated). - Timestamps are UTC epoch-ms on the wire. The raw MT5 broker-time offset
(
mt5-gateway-server-offset) is a gateway concern, never a client one. - Auth: Bearer token, as the gateway already does.
- Compatibility:
qkt-venue-gatewayalso serves the legacy paths (/account,/health,/kill,/kill/release,/get_positions,/close_position) so today's guardrailsGatewayClientworks against a futures venue on day one. Aprotocol: vgp1option in guardrails comes later.
7. Loading, packaging and the CLI¶
7.1 Modules¶
qkt-api the SPI (§5) — semver, binary-compat locked
qkt-core engine: DSL, pipeline, risk, P&L, backtest
qkt-cli commands, daemon, plugin manager
qkt-venue-gateway the gateway host (§2, §6)
qkt-plugin-tck conformance kit (§10)
adapters/bybit first adapter moved out (dogfood)
adapters/mt5 stays in-tree and built-in until VGP is proven (§11)
Moving an existing adapter out first is the real test of the API: if Bybit cannot be written
against qkt-api alone, no third party can.
7.2 Discovery and isolation¶
- One
URLClassLoaderper plugin JAR, discovered withServiceLoader<VenuePlugin>/ServiceLoader<DataPlugin>. - Parent-first for
com.qkt.api.*,kotlin.*,org.slf4j.*,okhttp3.*(shared types must come from one loader, otherwise you get the classicX cannot be cast to X). Child-first for everything else, so two plugins can carry different protobuf or Netty versions. - Plugins compile against a Kotlin version ≤ the host's and do not bundle the stdlib.
- JPMS module layers are not needed; qkt runs on the classpath and should stay there.
7.3 The JAR¶
A shaded JAR containing:
META-INF/services/com.qkt.api.VenuePluginqkt-plugin.json:id,version,spiVersion,requiredJdkModules,certification, SHA-256 of the payload, optional signature.
7.4 The runtime module gap¶
Two fixes, both cheap:
- Add
java.net.httpto the jlink set (Dockerfile andjlinkRuntime) — about 1 MB. SDKs use it a lot. qkt plugin verifychecks everyrequiredJdkModulesentry withModuleLayer.boot().findModule(name)against the runtime that will actually run it, and refuses to install on a miss. This turns a production-onlyNoClassDefFoundErrorinto an install-time error.
7.5 Pinning and deployment¶
qkt.plugins.lockrecordsid,version,sha256, source. The daemon refuses to start if an installed JAR does not match the lock. Same idea as your GHCR image pinning.- Docker: plugins are baked into a derived image, never downloaded at container start.
FROM ghcr.io/elitekaycy/qkt:vX+COPY plugins/ /opt/qkt/plugins/, tagged and pinned like every other image in the fleet. The same applies toqkt-venue-gatewayimages per adapter.
7.6 CLI¶
Your qkt add --broker ampfuture splits into "install code" and "add an account", because one
plugin serves many accounts:
qkt plugin install [email protected] # fetch, verify sha + modules, write lock
qkt plugin install ./qkt-venue-rithmic-0.1.0.jar
qkt plugin list | verify | remove
qkt brokers add amp-demo --plugin rithmic --prefix AMP # writes the config entry, asks for secret *references*
qkt brokers check amp-demo # login, account list, instruments, clock skew, kill-switch reachability
qkt brokers certify amp-demo --level paper # runs the TCK live scenarios under the account lock
qkt brokers already exists (cli/BrokersCommand.kt, MT5 listing) and keeps the current
vocabulary.
8. Configuration and credentials¶
8.1 One entry shape for every venue¶
brokers:
exness-demo: # unchanged — built-in
type: mt5
extends: exness
gateway_url: http://127.0.0.1:5001
magic: 20001
amp-demo:
type: rithmic # plugin id
prefix: AMP # DSL: AMP:MES@front
host: venue-gateway # venue-gateway (live-capable) | in-process (paper/data only)
gateway_url: http://venue-gateway-amp:5101
environment: test # test | live
system: "Rithmic Paper Trading"
fcm: AMP
account: ${RITHMIC_ACCOUNT}
credentials:
user: env:RITHMIC_USER
password: file:/run/secrets/rithmic_password
calendars:
- ["*", "fx pause 17:00-18:00 America/New_York"]
roll:
policy: volume_crossover # or days_before_expiry: 8
instruments:
MES: { commissionPerLot: 1.04 }
binance-futures:
type: binance
prefix: BINANCE_UM
host: venue-gateway
gateway_url: http://venue-gateway-binance:5102
credentials:
api_key: env:BINANCE_API_KEY
api_secret: env:BINANCE_API_SECRET
The plugin's configSchema is validated before open(); an unknown key, a missing required field
or a secret written inline is an error at load, not a surprise at 20:00 UTC.
8.2 One resolver in core¶
Resolution order, highest first:
QKT_BROKER_<NAME>_<FIELD>— the conventionMT5BrokerProfileLoaderalready implements, kept so nothing existing changes.- The config value, which may be a literal (non-secrets only) or a reference:
${VAR}(existing),env:VAR,file:/path(Docker/Kubernetes secrets).
Secrets are a type, not a string:
@JvmInline value class Secret(private val value: String) {
fun reveal(): String = value
override fun toString(): String = "Secret(***)"
}
A Secret never reaches logs, state files, insights, qkt brokers output or promotion records.
Schema fields marked secret: true must be references; a literal is rejected. Bybit keeps
reading BYBIT_API_KEY as a fallback so existing deployments do not break.
8.3 Different venues, different logins¶
The resolver supplies credentials; the plugin owns the session lifecycle and reports it with
the existing ConnectionChanged event:
| Pattern | Examples | Lifecycle |
|---|---|---|
| API key + HMAC signing | Bybit, Binance, OKX | stateless; clock skew matters (recvWindow) |
| User/password → session token, refresh | Rithmic, Tradovate, most futures props | login, renew before expiry, re-login on reject |
| Local terminal login | IBKR TWS / IB Gateway, MT5 | the human logs in once (often with 2FA and a periodic restart); the adapter only connects to a socket |
| Wallet signature | Hyperliquid (EIP-712) | the key is the account — treat as highest-sensitivity secret |
qkt brokers check exercises the login path so a bad credential fails at setup, not at the
first signal.
9. Order and trade pipeline, end to end¶
.qkt strategy
└─ DSL compile ─► OrderRequest (rich: Bracket, OCO, Trailing, ScaleOut, Stack…)
└─ OrderManager + risk (venue-neutral; reads InstrumentMeta incl. margin/expiry/limits)
└─ resolve InstrumentRef → VenueSymbol (ES@front → ESZ5)
└─ lower by capabilities(VenueSymbol) ─► VenueSubmit (4 order shapes ± protection/OCO)
├─ in-process: VenueSession → SDK → exchange (paper, data)
└─ VgpBroker ─► qkt-venue-gateway ─► kill-switch gate ─► adapter ─► exchange (live)
◄─ VenueEvent stream (seq-numbered) ─► BrokerEvent (Accepted/Filled/Partial/Cancelled/Rejected)
└─ positions keyed by InstrumentRef, legs by VenueSymbol + ticket
└─ P&L, typed costs (COMMISSION/EXCHANGE_FEE/SWAP/FUNDING), settlement variation
└─ risk, halts, insights, promotion evidence
Startup / reconnect (LIVE only, no defaults):
VenueTruth.openPositions / workingOrders / deals / account
└─ LegBookReconciler + recoverPendingOrders + watchBookedLegs (existing machinery)
Normalization rules that make the mapping global:
- Identity: qkt client order id is the idempotency key end to end; the venue order id and position ticket are recorded, never assumed stable across venues.
- Quantities: always in venue units (
volumeStep), quantized down, rejected belowvolumeMin— today's behaviour (PaperBroker.kt:159,ExecutionBehavior.kt:65,ActionCompiler.kt:126). - Prices: rounded to tick size by the host, never by the adapter.
- Position mode: declared per symbol (
PositionAccountingMode); reconciliation follows it. - Protection: attached (MT5
POSITION_MODIFY), native bracket (futures OCO), or engine-held, chosen by capability — the engine already decides this per broker. - Time: UTC epoch-ms everywhere past the adapter.
- Costs: always typed
VenueCost; an adapter that cannot report a cost says so, rather than reporting zero.
10. What makes it sound¶
10.1 Certification levels, enforced at deploy¶
| Level | Requires | Allowed |
|---|---|---|
DATA |
MarketSource passes data TCK |
research, backtest, qkt fetch |
PAPER |
+ VenueSession, order-lifecycle TCK against the venue's sandbox |
paper / demo |
LIVE |
+ VenueTruth implemented (no defaults), restart/reconnect/idempotency TCK, served through qkt-venue-gateway with a reachable kill switch, a recorded qkt brokers certify --level live run on a demo account |
real money |
qkt deploy in production mode refuses a broker whose plugin level is below LIVE. This is the
direct answer to §1 item 7: a live adapter cannot inherit "vouch for everything" defaults,
because the VenueTruth interface it must implement has none.
10.2 Conformance test kit (qkt-plugin-tck)¶
A JUnit suite any adapter runs, built from failures you have already hit live rather than from imagination:
- submit → accept → fill; reject; cancel; cancel-after-fill race
- partial fills and cumulative quantity (rare on CFDs, normal on exchanges)
- duplicate submit with the same
client_order_id→ exactly one order - process killed mid-order → restart → no phantom or duplicate position
(
qkt-restart-over-positions-fixes, #1146) - venue-side stop fires while the engine closes → one close, resolved from deals
(
venue-stop-engine-stop-race) - restart inside the entry bar after a stop-flatten (
qkt-stop-flatten-rule-edges) - several children entering inside one risk sample (
qkt-book-cap-race) - the 20-scenario position matrix (
qkt-position-matrix-audit) and the 40-case portfolio matrix - disconnect → reconnect from
since=<seq>→ no lost fill - kill switch: gated paths rejected, flatten allowed, scoped kill leaves other symbols tradeable
- clock skew, stale feed, scheduled break
The existing BacktestLiveParityTest and MT5GoldenVerifierTest generalize into a per-venue
golden fill check.
10.3 Provenance¶
Promotion records the plugin id@version and SHA-256 next to strategyHash, and the daemon
refuses a promoted live strategy when the installed plugin differs. Today's strategyHash
already misses changes to imported portfolio children (hg20k-live-book-deploy-facts); do not
repeat that blind spot with adapters, which change behaviour far more than a sizing line does.
10.4 Fail closed¶
- Unknown plugin
type:→ config error, never a fallback. - Plugin fails to load → the brokers that use it are unavailable and their strategies refuse to deploy; the rest of the daemon keeps running.
- Missing instrument meta → hard error (existing Phase 30 rule).
- Undeclared order shape → rejected in the host.
VenueTruthread fails at startup → no trading (existing rule for MT5).
11. Feasibility by venue¶
External facts below are from general knowledge, not verified in this session — confirm each before committing.
| Venue | Connectivity | Auth | Fit | Notes to verify |
|---|---|---|---|---|
| Tradovate | REST + WebSocket, JSON | user/password → access token, renewed | good first futures adapter: simplest protocol | API access may need a funded account and/or a paid add-on |
| Rithmic (AMP, many futures props) | R|Protocol: WebSocket + protobuf | user/password, system + gateway selection | the one that unlocks most FCMs and futures props | conformance testing with Rithmic before production is typically required |
| CQG | WebAPI: WebSocket + protobuf | user/password | similar to Rithmic | licensing and fees per FCM |
| IBKR | TWS API socket to a locally logged-in TWS / IB Gateway | human login, 2FA, periodic restart | broad asset coverage | API jar distributed by IBKR (do not bundle it — the plugin model helps here); limited historical depth |
| ProjectX / TopstepX | REST + realtime hub | API key | relevant to your prop-payout thesis | availability and terms change; confirm current API access |
| Binance / OKX | REST + WS | API key + HMAC | close to the existing Bybit code | both have dated quarterlies → DATED_FUTURE |
| Deribit | JSON-RPC over WS | API key | dated BTC/ETH futures + options | good second test of the futures core |
| Hyperliquid | REST + WS | wallet signature (EIP-712) | perps | key custody is the whole security model |
| Databento (data) | HTTP historical, live stream | API key | first data plugin | check for a JVM client; the HTTP API is language-neutral |
12. Roadmap¶
Sizes are relative (S ≈ days, M ≈ 1–2 weeks, L ≈ several weeks). They are guesses.
| Phase | Scope | Size | Value on its own |
|---|---|---|---|
| 1 — Seams in the monolith | Broker.instruments() replaces the three filterIsInstance<MT5Broker> sites; a VenueRegistry fed by type: replaces hand assembly in DaemonCommand / MarketSourceFactory / BotSessionCommand; unified credential resolver + Secret; nested YAML for broker entries; optional InstrumentMeta fields (kind, explicit quoteCurrency) |
M | Removes MT5 coupling, fixes the quote-currency hole, one credential path. Zero behaviour change; worth doing even without plugins. |
2 — qkt-api + loader |
module split; VenuePlugin/DataPlugin; ServiceLoader + isolated class loaders; qkt plugin CLI; lockfile; add java.net.http to jlink; move Bybit out as the dogfood adapter; binary-compat validator in CI |
M–L | Proves the API on real code. |
| 3 — Futures research path | Databento or file-import data plugin; exchange-futures execution simulation in backtest (whole contracts, tick rounding, per-contract commission, no swap, limit bands); CME calendar in backtest (BacktestContext.defaultCalendars() is hardcoded today); expiry guard |
M | Tier 0 on real futures data, properly. |
| 4 — VGP + first live-capable futures venue | VGP v1 spec; qkt-venue-gateway; VgpBroker; kill switch incl. scoped kill; qkt-plugin-tck; first adapter (Tradovate or Rithmic) on demo; guardrails pointed at it via legacy paths; certify PAPER then LIVE |
L | First real futures account, with the same guardian protection hg20k has. |
| 5 — Futures depth | settlement marking, margin model, roll engine + @front refs, calendar-spread capability, OI/settlement data; Deribit or Binance quarterlies as the second consumer |
L | Multi-month futures holds; dated crypto futures. |
| 6 — Converge MT5 (optional) | mt5-gateway speaks VGP v1; MT5Broker becomes a VGP client or stays |
L | Only if one code path is worth the risk to live MT5 accounts. Do not do this early. |
MT5 is deliberately last. It is ~6.6k lines of hard-won venue truth (ticket and comment matching, OCO recovery, server-time handling) running real money; the plugin system should grow beside it (strangler pattern), not start by rewriting it.
13. Risks and open questions¶
- API surface creep. The biggest long-term risk is leaking engine types into
qkt-api. Keep it to §5; anything else is host-internal. - Two hosts, one behaviour. In-process and
qkt-venue-gatewaymust run the same adapter identically; the TCK must run against both. - Event ordering across the network. Sequence numbers solve loss; the engine must also tolerate a fill arriving before the accept.
- Roll semantics are a product decision, not only engineering: legged vs spread, who owns the roll (engine vs strategy), what happens to stops and trailing state across a roll.
- Operational load. One
qkt-venue-gateway+ one guardian per futures account. Fine at your scale; worth a compose template per venue. - Licensing. Some vendor SDKs cannot be redistributed; the plugin model (user installs the vendor JAR) handles it, but check each licence.
- Does the edge exist? None of this is worth building before a futures backtest (Tier 0) says the strategies survive real futures data and costs.
Files of record¶
broker/Broker.kt, broker/CompositeBroker.kt, broker/OrderTypeCapability.kt,
broker/BrokerState.kt, app/BrokerFactory.kt, app/LiveSession.kt (636, 1004, 1355),
cli/DaemonCommand.kt (~270–350), cli/MarketSourceFactory.kt, cli/bot/BotSessionCommand.kt
(186), broker/mt5/MT5BrokerProfileLoader.kt, broker/bybit/BybitClient.kt (119–139),
marketdata/source/MarketSource.kt, marketdata/source/MarketSourceCapability.kt,
instrument/InstrumentRegistry.kt, instrument/InstrumentMeta.kt,
instrument/QuoteCurrencyGuard.kt, execution/OrderRequest.kt, events/BrokerEvent.kt,
accounting/Accounting.kt, cli/BacktestContext.kt (1122), cli/Main.kt, build.gradle.kts,
Dockerfile.