Config schema and reference¶
qkt.config.yaml is the operator-facing config file for qkt commands, daemon runtime, broker profiles, risk controls, accounting, observability, and promotion gates. The file is optional for local research, but production mode is intentionally fail-closed when required controls are absent.
Resolution and substitution¶
qkt resolves config in this order:
--config <path>QKT_CONFIG./qkt.config.yaml/etc/qkt/qkt.config.yamlon non-Windows systems$XDG_CONFIG_HOME/qkt/qkt.config.yamlor the platform config equivalent~/.qkt/qkt.config.yaml
Environment and system-property substitution works in any scalar value:
Broker env overrides of the form QKT_BROKER_<NAME>_<FIELD> win over file
values for MT5 profile scalar fields and for every connector's credential fields. They are profile-scoped operational
overrides; reusable scaffolds use neutral substitution variables such as
QKT_BROKER_GATEWAY_URL.
Minimal research config¶
Local research can run with no config. This file is enough when you want explicit data and accounting defaults:
source: local
data_root: ./data
starting_balance: 10000
log_level: info
runtime:
mode: dev
account:
currency: USD
fx_conversion:
missing_policy: warn
Production-style config skeleton¶
source: local
data_root: /var/lib/qkt/data
starting_balance: 100000
log_level: info
runtime:
mode: production
waivers:
alerts:
reason: "temporary supervised launch without alert channel"
state:
enabled: true
async: false
account:
currency: USD
fx_conversion:
source: market
missing_policy: fail
symbols:
USDJPY: BACKTEST:USDJPY
EURUSD: BACKTEST:EURUSD
execution:
preset: mt5-realistic
seed: 42
# Venue position model the backtest simulates (#1071). CLI runs default to `hedging`
# (the retail-MT5 model both production accounts use): every entry books its own
# coexisting leg and each bracket exit closes only its leg. `netting` collapses
# opposite fills into one signed position for venues that truly net.
position_mode: hedging
risk:
max_daily_loss: "1000"
max_order_qty: "100"
max_order_notional: "250000"
price_collar_pct: "5"
margin_floor_pct: "200"
measured_usage_hours: "24"
measured_usage_max_qty: "0.01"
max_drawdown_pct: "8"
max_daily_drawdown_pct: "4"
total_dd_basis: static
daily_dd_basis: balance
per_strategy:
xau-ema-cross:
max_daily_loss: "300"
max_position_size: "1.0"
max_open_positions: "2"
max_drawdown_pct: "5"
max_daily_drawdown_pct: "3"
promotion:
enforce: true
required_state: production
dataset_snapshot: true
realistic_execution: true
walk_forward: true
approval: true
paper_days: 20
paper_min_trades: 30
max_paper_slippage_bps: 3.0
brokers:
mt5:
type: mt5
gateway_url: ${QKT_BROKER_GATEWAY_URL:-http://localhost:5001}
api_key: ${QKT_BROKER_API_KEY}
server_time_zone: ${QKT_BROKER_SERVER_TIME_ZONE}
symbol_suffix: ${QKT_BROKER_SYMBOL_SUFFIX:-}
magic: ${QKT_BROKER_MAGIC:-10001}
calendars:
"BTC*": crypto
"XAU*": fx pause 17:00-18:00 America/New_York
"*": fx
aliases:
NAS100: USTEC
capability_restrictions: []
instrument_overrides:
XAUUSD:
min_volume: "0.01"
max_volume: "50"
volume_step: "0.01"
point_size: "0.001"
digits: "3"
trade_stops_level_points: "50"
notify:
telegram:
enabled: true
bot_token: ${TELEGRAM_BOT_TOKEN}
chat_id: ${TELEGRAM_CHAT_ID}
events: [order_rejected, halted, resumed, strategy_error, daemon_started]
daily_summary_utc: "21:00"
commands: true
insights:
enabled: false
book_risk:
capital: "100000"
limits:
max_gross_exposure: "3.0" # x capital: 300,000 on this book
max_net_exposure: "1.5" # x capital
max_symbol_concentration: "0.35"
de_risk:
ladder:
- drawdown: "0.04"
factor: "0.50"
cooldown_bars: 24
- drawdown: "0.08"
factor: "0.00"
cooldown_bars: 72
allocation:
method: INVERSE_VOL
target_vol: "0.12"
rebalance_every_bars: 96
max_leverage: "3"
Paper daemon example¶
Use this when you want daemon behavior and alerts without production fail-closed gates:
source: tv
starting_balance: 25000
runtime:
mode: paper
state:
enabled: true
risk:
max_daily_loss: "500"
max_order_qty: "1.0"
max_order_notional: "50000"
notify:
telegram:
enabled: true
bot_token: ${TELEGRAM_BOT_TOKEN}
chat_id: ${TELEGRAM_CHAT_ID}
events: [order_rejected, halted, resumed, strategy_error]
qkt-forge-compatible research example¶
Use this for high-volume research where qkt-forge owns orchestration and qkt stays the deterministic kernel:
source: local
data_root: ../qkt-forge/run/data
starting_balance: 10000
runtime:
mode: dev
execution:
preset: paper-fast
account:
currency: USD
fx_conversion:
missing_policy: warn
risk:
max_daily_loss: "0"
qkt-forge should pass --data-root, --dataset, --execution, and --parallelism explicitly in its command layer when a gate needs stricter behavior than this fast default.
Portfolio/book-risk example¶
Use this when evaluating a multi-strategy book or a portfolio DSL file:
starting_balance: 100000
account:
currency: USD
fx_conversion:
source: market
missing_policy: fail
symbols:
USDJPY: BACKTEST:USDJPY
risk:
max_daily_loss: "1500"
max_drawdown_pct: "8"
max_daily_drawdown_pct: "4"
total_dd_basis: trailing
daily_dd_basis: equity
book_risk:
capital: "100000"
limits:
max_gross_exposure: "3.0" # x capital: 300,000 on this book
max_net_exposure: "1.5" # x capital
max_symbol_concentration: "0.35"
allocation:
method: ERC
target_vol: "0.10"
rebalance_every_bars: 96
max_leverage: "2.5"
Top-level fields¶
| Key | Type | Default | Used by | Notes |
|---|---|---|---|---|
source |
string | tv when file exists, local from built-in defaults on missing file |
daemon, run market-source fallback |
tv opens TradingView fallback. replay reads QKT_REPLAY_TICKS. Any other value uses a null fallback. MT5 and gateway-routed symbols (Bybit, Deribit) still use their own routes. |
data_root |
path string | ./data in config object, but backtest CLI defaults to DataRoot.resolve() unless --data-root is passed |
historical data commands and examples | Prefer explicit --data-root for research runs that need reproducibility. |
starting_balance |
decimal | 0 (unset) |
daemon risk, live PnL, reports, backtest basis | Must be greater than zero when a live drawdown limit is configured. A single-strategy backtest uses it too when --starting-balance is not given, so its drawdown halts and percent sizing sit on the daemon's balance; with neither set a backtest starts at 10000. The chosen source is printed to stderr. Set explicitly for production and portfolio work. |
log_level |
string | info |
process logging setup where honored | Expected values are conventional log levels such as debug, info, warn, error. |
runtime¶
Controls safety mode and explicit runtime waivers.
| Key | Type | Default | Used by | Notes |
|---|---|---|---|---|
runtime.mode |
dev, paper, or production |
dev |
config, daemon, preflight, promotion | Production mode enables fail-closed preflight and promotion enforcement defaults. |
runtime.candle_close_grace_ms |
non-negative integer | 2000 |
daemon, run, backtest, replay | How long after a bar's window ends the 1 Hz heartbeat closes a quiet symbol's bar. Live and replay read the same value, so a SYNCHRONIZE group with a sparse member decides on the same heartbeat step in both; replay records it in result.json as execution.candleCloseModel. A tick of the same stream from the next window closes the bar immediately in both modes. |
runtime.waivers.<control>.reason |
string | none | preflight | Currently alerts is used by notify.alerts production preflight. Keep reasons operator-readable. |
state¶
Controls engine state persistence.
| Key | Type | Default | Used by | Notes |
|---|---|---|---|---|
state.enabled |
boolean | true |
daemon and live sessions | false disables restart recovery and fails production preflight. |
state.async |
boolean | false |
state persistor | true moves persistence writes to a background thread. |
state.journal_retention_days |
integer | 14 |
daemon | Day-files of the engine audit journal and MT5 transport journal (plain or gzipped) older than this many UTC days are deleted at daemon start and once a day. 0 keeps everything. |
state.journal_compress_after_days |
integer | 1 |
daemon | Closed .jsonl journal day-files older than this many UTC days are gzipped in place (~10x smaller); today's and yesterday's files stay plain with the default. Golden capture and retention read both forms. 0 disables compression. |
state.disk_free_alert_gb |
integer | 10 |
daemon and preflight | Below this much free space on the state volume the daemon logs an error and raises a disk_space_low alert (once per crossing; re-arms at 110% of the floor). Preflight fails production below the floor. 0 disables. |
State root is not set in config. Use --state-dir or QKT_STATE_DIR for commands that support state directories.
risk¶
Daemon-wide and per-strategy risk controls. Values are parsed as decimals unless noted.
| Key | Default | Used by | Notes |
|---|---|---|---|
max_daily_loss |
1000 |
backtest halt rules and daemon | Set 0 to disable the global daily-loss rule. Production preflight requires an explicit risk block. |
max_order_qty |
PreTradeControls.DEFAULT_MAX_ORDER_QTY |
daemon pre-trade controls | Hard per-order quantity cap. |
max_order_notional |
PreTradeControls.DEFAULT_MAX_ORDER_NOTIONAL |
daemon pre-trade controls | Account-currency notional cap. |
price_collar_pct |
PreTradeControls.DEFAULT_PRICE_COLLAR_FRAC as percent |
daemon pre-trade controls | Percent distance from last market price for explicit-price orders. |
margin_floor_pct |
200 |
daemon pre-trade controls | Entry orders reject when reported margin level is below this floor. 0 disables. |
measured_usage_hours |
24 |
daemon pre-trade controls | New deployments can only trade up to measured_usage_max_qty during this window. 0 disables. |
measured_usage_max_qty |
0.01 |
daemon pre-trade controls | Max entry quantity during measured usage. |
max_round_trips_10m |
10 |
daemon, foreground run, and backtest disclosure | Per-strategy closing-fill threshold in 10 minutes. 0 disables. Live halts persistently on a breach; replay records the breach unless strict mode is enabled. |
max_broker_rejections_1m |
5 |
daemon, foreground run, and backtest disclosure | Per-strategy broker-rejection threshold in one minute. 0 disables. Live halts persistently on a breach; replay records the breach unless strict mode is enabled. |
max_drawdown_pct |
unset | backtest and daemon halt rules | Percent in (0, 100]. Global total-drawdown halt. |
max_daily_drawdown_pct |
unset | backtest and daemon halt rules | Percent in (0, 100]. Global daily-drawdown halt. |
total_dd_basis |
static |
halt rules | static uses initial balance. trailing uses high-water equity. |
daily_dd_basis |
balance |
halt rules | The day-start reference: balance is the closed balance at UTC midnight, equity adds the float open at midnight. Either way the day's drawdown is measured against current equity, open loss included. |
live_equity_basis |
venue |
standalone live sizing and drawdown | venue consumes broker account equity. modeled pins live to starting_balance + qkt realized + qkt unrealized, matching backtest accounting. Portfolio children always use their allocated modeled capital. |
balance is the compatibility default. With a position carried over midnight it measures the day's
drawdown from the closed balance, so overnight float already lost counts toward today; accounts whose
daily-loss mandate is measured from day-start equity (including many funded account programs) should
set daily_dd_basis: equity explicitly.
| per_strategy.<name>.max_daily_loss | unset | daemon and backtest risk layering | Per-strategy daily realized-loss halt. |
| per_strategy.<name>.max_position_size | unset | daemon pre-trade controls | Caps absolute position size for one strategy. |
| per_strategy.<name>.max_open_positions | unset | daemon pre-trade controls | Caps non-zero symbols for one strategy. |
| per_strategy.<name>.max_drawdown_pct | unset | halt rules | Per-strategy total drawdown percent. |
| per_strategy.<name>.max_daily_drawdown_pct | unset | halt rules | Per-strategy daily drawdown percent. |
| per_strategy.<name>.max_trades_per_day | unset | PACER pre-trade controls | Rejects risk-increasing entries after N entry fills in the current UTC day. |
| per_strategy.<name>.cooldown_after_loss | unset | PACER pre-trade controls and DSL | Duration such as 30m, 1h, or integer milliseconds. Rejects new risk after losses while active. |
| per_strategy.<name>.cooldown_after_loss_after_consecutive | 1 | PACER pre-trade controls and DSL | Number of consecutive losing closes required before cooldown starts. |
| per_strategy.<name>.loss_streak_halt | unset | halt rules | Halts the strategy once consecutive losing closes reach N. |
| per_strategy.<name>.loss_streak_halt_scope | persistent | halt rules | daily auto-resumes at next UTC day; persistent requires operator resume. |
Per-strategy rules layer on top of global rules. A global breach halts the whole daemon. A per-strategy breach halts only that strategy.
account and fx_conversion¶
| Key | Type | Default | Used by | Notes |
|---|---|---|---|---|
account.currency |
ISO currency string | USD |
accounting engine, reports, risk notional | Normalized to uppercase. |
fx_conversion.source |
string | market |
accounting evidence | Descriptive source label for FX conversion. |
fx_conversion.missing_policy |
warn or fail |
fail |
accounting engine | fail rejects unconvertible non-account-currency PnL/costs. warn is an explicit unsafe compatibility override. |
fx_conversion.symbols.<PAIR> |
qkt symbol | empty | backtest/accounting | Maps an FX pair such as USDJPY to a qkt symbol used to source conversion marks. |
CLI overrides are available for backtests: --account-currency, --fx-source, --fx-missing-policy, and repeated --fx-symbol PAIR=QKT_SYMBOL.
execution¶
Backtest, sweep, walk-forward, and experiment commands read execution settings through BacktestContext.
| Key | Type | Default | CLI override | Notes |
|---|---|---|---|---|
execution.preset |
paper-fast, mt5-basic, mt5-realistic, stress |
based on --broker, usually paper-fast |
--execution |
Chooses default broker simulator behavior. |
execution.seed |
long | unset except stress default 42 |
--seed |
Deterministic random slippage seed. |
execution.latency |
duration | preset default | --execution-latency |
Accepts integer milliseconds, 250ms, 1s, or fixed:250ms. Delays order placement: a delayed market order fills at the sided quote prevailing at its release (the last quote at or before submit + latency), not the next quote after it, matching the venue (45 of 46 Exness demo market deals on 2026-09-23 filled at the prevailing quote, none at the next one). A resting order's trigger-to-fill stays instantaneous. |
execution.order_spacing |
duration | 0 |
--order-spacing |
Minimum gap between consecutive order releases on the simulated venue's single send lane (mt5-sim). An order submitted at t is released at the later of t + latency and the previous release plus the spacing, and fills at the sided quote prevailing at its release. Models the serialized MT5 gateway, which places a burst's legs one after another (measured on the Exness demo: about 140-160 ms between legs; parity row A20). 0 releases every order independently. Same format as execution.latency; not valid with --tick-fills. |
execution.stop_latency |
duration | 0 |
--stop-latency |
Delay between a protective stop's trigger and its execution (mt5-sim). A crossed stop fills at the first quote at or after trigger + delay, sided, plus slippage — the venue behaviour measured on the Exness demo (median ~260 ms, #1135). 0 fills on the crossing print. Same format as execution.latency. |
execution.tp_fill |
print, level |
print |
--tp-fill |
How a gap-crossed protective take-profit is priced (mt5-sim). print credits the crossing print when it beats the level; level fills exactly at the level, as retail MT5 does. |
execution.slippage |
string | preset default | --slippage |
zero, instrument, fixed-points:N, or uniform:N. |
execution.reject_every |
int | unset | --reject-every |
Reject every Nth simulated order. |
execution.partial_fill |
decimal (0,1) |
unset | --partial-fill |
Fractional partial-fill model. |
Preset defaults:
| Preset | Broker kind | Defaults |
|---|---|---|
paper-fast |
paper |
zero latency, zero slippage, no venue rules |
mt5-basic |
mt5-sim |
instrument slippage and MT5 sizing rules |
mt5-realistic |
mt5-sim |
250 ms latency, instrument slippage, stop-distance enforcement |
stress |
mt5-sim |
500 ms latency, uniform slippage up to 20 points, reject every 10th order, 50 percent partial fills |
No preset sets execution.order_spacing. Add --order-spacing 150ms when a strategy sends several orders at once (STACK_AT bursts, TIMES, OCO_ENTRY), where a single release price overstates the fills.
promotion¶
Promotion gates protect production deploys. In runtime.mode: production, promotion.enforce defaults to true.
| Key | Type | Default | Used by | Notes |
|---|---|---|---|---|
promotion.enforce |
boolean | runtime.mode == production |
daemon deploy, qkt promotion status, qkt status --deep |
When true, missing gates block deploy unless waived. |
promotion.required_state |
promotion state | production |
gate evaluator | Valid states: draft, research, candidate, paper, shadow-live, small-capital, production, retired. |
promotion.dataset_snapshot |
boolean | false |
gate evaluator | Requires --evidence dataset_snapshot=... on the promotion record. |
promotion.realistic_execution |
boolean | false |
gate evaluator | Requires --evidence realistic_execution=.... |
promotion.walk_forward |
boolean | false |
gate evaluator | Requires --evidence walk_forward=.... |
promotion.approval |
boolean | true |
gate evaluator | Requires qkt promotion approve for the required state. |
promotion.paper_days |
int | 0 |
gate evaluator | Minimum recorded paper/live validation days. |
promotion.paper_min_trades |
int | 0 |
gate evaluator | Minimum recorded paper/live validation trades. |
promotion.max_paper_slippage_bps |
double | unset | gate evaluator | Fails if recorded p95 paper slippage is missing or above the cap. |
promotion.registry_dir |
path | state dir promotion subdir |
promotion store | Override JSONL promotion registry location. |
Promotion records are appended JSONL. Waivers require a reason and are also journaled when created through CLI or deploy waiver paths.
brokers¶
brokers is keyed by account name. Each entry is one trading account, opened by the connector
named in its type; the entry name, upper-cased, is the prefix strategies use (prop_s01 serves
PROP_S01:XAUUSD). A missing or unknown type refuses startup and lists the installed
connectors. See Broker integration for the model.
An entry may carry only the settings its connector lists below, plus type and the nested blocks
calendars, aliases, capability_restrictions and instrument_overrides. Any other key refuses
startup and names the closest known setting — a misspelled expected_account_login would
otherwise switch the account identity check off without a word.
Credential fields (api_key, api_secret) accept a literal, ${VAR} (substituted when the
file loads), env:VAR, or file:/path (trailing newline trimmed, for Docker secrets). An
environment override QKT_BROKER_<NAME>_<FIELD> wins over the file value.
type: mt5¶
MT5 entries can inherit built-in defaults.
Built-in MT5 profile names: exness, icmarkets, ftmo, pepperstone.
| Key | Type | Required | Default/inheritance | Notes |
|---|---|---|---|---|
brokers.<name>.type |
string | yes | none | mt5 (this table) or gateway (below). |
extends |
profile name | no | same-name built-in if present | Inherit from a built-in or earlier user profile. |
gateway_url |
URL | yes for fresh profile | inherited or built-in | MT5 gateway HTTP base URL. |
api_key |
string | no | inherited or empty | Bearer token matching the gateway API_KEY; use a neutral variable such as ${QKT_BROKER_API_KEY} in reusable scaffolds. |
symbol_suffix |
string | no | inherited or empty | Appended to broker symbol names. |
magic |
int | yes for fresh profile | inherited or built-in | Must be unique across MT5 profiles. |
server_time_zone |
zone id | yes for fresh profile | inherited or built-in | MT5 server clock. Use new_york_close for UTC+2/+3 following US DST, or an IANA id such as Europe/Helsinki. |
server_tz_offset_hours |
int | no | none | Legacy fixed-offset alternative. Cannot be combined with server_time_zone; does not handle DST. |
poll_interval_ms |
long | no | 1000 |
Position and pending-order polling cadence. |
tick_poll_interval_ms |
long | no | 1000 |
Live quote polling cadence. When omitted beside an explicitly configured poll_interval_ms, inherits that value for backward compatibility. Set both keys to tune quote and reconciliation load independently. |
http_timeout_ms |
long | no | 20000 |
Per-request gateway HTTP timeout. The gateway places orders serially at roughly four a second, so a burst of N entries (TIMES N, a deep STACK, N actions in one rule) queues N/4 seconds deep; size this above that or the tail of the burst fails on the qkt side with "Socket closed" while the venue still fills it. |
retry_attempts |
int | no | 3 |
Gateway retry attempts. |
deviation_points |
int | no | 20 |
Market-order price deviation tolerance. |
expected_account_login |
long | production MT5 | none | Refuses startup when /account.login differs. |
expected_account_server |
string | production MT5 | none | Refuses startup when /account.server differs. |
expected_trade_mode |
demo, contest, or real |
production MT5 | none | Prevents demo/real environment inversion. |
expected_account_currency |
currency code | no | none | Optional profile-level currency assertion; global account.currency is also checked by preflight. |
expected_leverage |
int | production MT5 | none | Refuses startup when venue leverage differs. |
expected_margin_mode |
netting or hedging |
production MT5 | none | Refuses startup when the venue's margin mode differs — the engine's position model must match the account's (#1071). |
calendars |
map pattern to fx, crypto, nyse, or <base> pause HH:MM-HH:MM [Zone] |
no | inherited or FX default | First matching pattern wins. A pause clause adds a venue-scheduled daily break on top of the base calendar's sessions; the map form { base: fx, pause: 17:00-18:00, zone: America/New_York } is equivalent. During the pause the market-data gate reports a quote gap as PAUSED (info) instead of STALE (error) and still suppresses new entries; feed polling and session gating are unchanged. Outside the symbol's session (weekends for fx, never for crypto) the gate reports a quote gap as venue closed (info), not STALE. Built-in venue windows, measured from live quote gaps and widened by one stale threshold on each side: exness and icmarkets pause metals and energy (XAU*, XAG*, XPT*, XPD*, USOIL*, UKOIL*, XTI*, XBR*, XNG*) 16:55–18:10 New York and FX 16:55–17:15 New York; exness also pauses copper (XCU*) 18:50–01:10 London and keeps crypto (BTC*, ETH*, LTC*, XRP*, BCH*, SOL*, ADA*, DOGE*, DOT*, LINK*) on the 24/7 crypto calendar so its feed polls through weekends; the5ers pauses metals and energy 16:45–18:10 and everything else 16:50–17:10 New York, with BTC on the FX calendar (it stops at weekends there). |
aliases |
map qkt symbol to broker symbol | no | inherited plus overrides | Example NAS100: USTEC. |
capability_restrictions |
list of OrderTypeCapability names |
no | inherited plus overrides | Disables venue capabilities by enum name. |
instrument_overrides.<symbol> |
map | no | inherited plus overrides | Requires min_volume, volume_step, point_size, digits, trade_stops_level_points; optional max_volume is enforced when present. |
Bybit¶
qkt reaches Bybit only through the qkt-venue-gateway
running its Bybit adapter (GATEWAY_ADAPTER=bybit), as a type: gateway entry (below), exactly as it
reaches Deribit. One gateway serves one Bybit category (GATEWAY_SETTING_CATEGORY). Name the entry after
the prefix your strategies use: bybit_linear serves BYBIT_LINEAR: symbols, bybit_spot serves
BYBIT_SPOT: symbols.
brokers:
bybit_linear:
type: gateway
gateway_url: http://127.0.0.1:8444
api_key: env:BYBIT_TRADER_TOKEN # the gateway's GATEWAY_TRADER_TOKEN
expected_adapter: bybit
expected_account_login: "<the account_login the gateway's /v1/health reports>"
expected_trade_mode: demo # real on mainnet
The Bybit API key goes to the gateway (GATEWAY_LOGIN/GATEWAY_SECRET), never to qkt. The adapter's
settings, and how it reports Bybit's fills, funding and restarts, are on its
page. qkt create template <dir> --kind bybit scaffolds this setup, gateway included.
type: gateway¶
One entry per account on a VGP v1 venue gateway
(wire format): futures, perpetuals, spot and
options at any venue the gateway's adapter serves. The entry name is the strategy prefix, and venue
codes are written with - as _: an entry named deribit trades DERIBIT:BTC_USDC_25DEC26_92000_C.
Name the entry after the venue the instrument catalogs use (deribit, binance_um), so live
strategies, structures and backtests name the same symbols.
brokers:
deribit:
type: gateway
gateway_url: https://venue-gateway.internal:8443
api_key: env:DERIBIT_GATEWAY_KEY
expected_adapter: deribit
expected_account_login: "4421"
expected_trade_mode: demo
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
gateway_url |
URL | yes | none | The gateway's base URL. |
api_key |
credential | yes | none | Bearer token; env: and file: forms resolve as for every broker. |
expected_adapter |
string | yes | none | Startup refuses a gateway reporting another adapter. |
expected_account_login |
string | yes | none | Startup refuses a gateway logged into another account. |
expected_trade_mode |
demo or real |
yes | none | Startup refuses a gateway in the other mode. |
http_timeout_ms |
long | no | 5000 |
Per request. |
retry_attempts |
int | no | 3 |
Reads, and submits, are sent again on a timeout or 503; a submit is idempotent on its client order id, so this never places a second order. |
chain_snapshot_seconds |
int | no | 300 |
How often the live chain of each fed option root declared chains: book is appended to its book series (structures and CHAIN: streams read it). |
calendars |
as above | no | crypto |
Set it for venues that close, such as CME futures. |
A perpetual's funding is booked as the venue charges it: the gateway reports each charge with the
position it was charged on, and each strategy holding the perpetual books amount × its holding /
position as financing, so a strategy long and one short each get their own sign and a position another
tool holds on the account keeps its own part. What was booked persists (funding.json in the session
owner's state), so a restart books what was charged while qkt was down, from the last 7 days, and never
twice. A gateway that does not declare the funding capability cannot report it, so qkt refuses orders
that could open or add to a perpetual there (reductions still pass). A strategy reading a contract's mark or
index (.mark, .index) starts only on a gateway that declares mark_prices, and reads the newest the
gateway quoted. A strategy reading an option's implied volatility or Greeks (.iv, .delta, .gamma,
.vega, .theta) starts only on a gateway that declares option_marks, and reads the newest quote it sent.
Several strategies may share one gateway account, as a portfolio. They share one connection; each
fill reaches the strategy whose order it was (and waits for it while its session is stopped), and a
contract settlement at expiry closes each strategy's own holding at the settlement price, sharing the
venue's costs once by holding. The venue only knows the account's total, so a gateway account is
account-wide even when one strategy trades it: startup trusts each strategy's persisted book (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 account's. While they do not,
only orders that reduce a position are sent; the check clears itself when a later one agrees (as
strategies deploy one after another, and it is judged again before any order adding risk is refused, so
a fill the venue had not yet reported when last checked blocks nothing), or after an operator resolves the difference. After a restart every restored order is resolved by id, with its complete fill
history, so a fill is never lost or booked twice. A submit the gateway never answers is sent again
until a deadline, then resolved by id. The gateway's identity is checked at startup and whenever its
event log restarts; a gateway on another account refuses every order. The kill switch at the gateway
refuses orders that add risk; an order that reduces both its strategy's and the account's position is
sent reduce_only and passes. Limit and stop levels are sent on the contract's tick grid (an option's
declared tickSteps, otherwise the tick the gateway lists), rounded as the backtest exchange rounds them: buy limits and sell stops down, sell limits
and buy stops up, so an order never fills or triggers before its level and a protective stop can sit up
to one tick wider than asked (a backtest does the same for a contract of a declared futures: root;
without one it keeps the level as computed). The account's prices come from the gateway's quotes socket: its
contracts, and its option roots as whole feeds (OPTIONS:<ACCOUNT>.<ROOT>, so name the account after
the venue, e.g. deribit, to match instruments.yaml). Venues list new option expiries every day: schedule
qkt fetch DERIBIT:<ROOT> --catalog (daily is enough) and a running daemon picks the new catalog up
within a minute; a contract missing from the catalog is never routed or recorded.
Policy-rate artifacts are also configured through the environment. QKT_RBA_POLICY_RATE_SOURCE
and QKT_RBNZ_POLICY_RATE_SOURCE accept an absolute path, file: URI, or HTTPS URL for the
authorities' official XLSX tables. The RBA and RBNZ URLs are the defaults. Use a read-only mounted
local artifact when an authority rejects server-side HTTP traffic; qkt records its SHA-256 and
fails live ingestion when the artifact is malformed or more than seven calendar days stale.
market_data¶
Thresholds for the live market-data quality gate (MarketDataGate) every daemon live session builds. Defaults are the gate's historical hard-coded values, so an absent block changes nothing.
| Key | Type | Default | Used by | Notes |
|---|---|---|---|---|
market_data.stale_age_multiple |
double | 5.0 |
live sessions | Staleness threshold as a multiple of the symbol's smoothed inter-tick gap. |
market_data.min_stale_age_ms |
long | 10000 |
live sessions | Floor for the staleness threshold. |
market_data.outlier_sigma |
double | 6.0 |
live sessions | Standard deviations from the short-window mean beyond which a tick is rejected. |
market_data.max_clock_skew_ms |
long | 60000 |
live sessions | Tolerance between broker tick timestamps and the local clock before new orders are suppressed. |
The gate suppresses new orders for a symbol in every case below; only the faults alert. A fault
logs at ERROR, sends a MarketDataUnhealthy strategy error (when strategy_error notifications
are on) and ships marketdata.stale to insights once per episode.
| Condition | Log | Alerts | marketdata.stale kind |
|---|---|---|---|
| No tick past the staleness threshold while the symbol is in session | ERROR STALE |
yes | stale |
Broker tick clock further than max_clock_skew_ms from the local clock |
ERROR CLOCK-SKEWED (or LAGGING when ticks trail the clock by less than an hour: late delivery, not a mis-set zone) |
yes | clock_skew |
| A run of rejected outlier ticks | ERROR UNHEALTHY |
yes | outlier |
Quote gap inside a calendar pause, and for one staleness threshold after the pause ends (the venue's first post-break print lags the calendar) |
INFO PAUSED |
no | none |
No tick for longer than the staleness threshold after a pause ended |
ERROR STALE (aged from the break end) |
yes | stale |
| Quote gap while the symbol's calendar is out of session, or a last print older than any server-zone offset | INFO venue closed |
no | none |
Sessions are judged per symbol from the broker profile's calendars, so on an account trading
XAUUSD and BTCUSD (with BTC*: crypto) a weekend gap is closed for gold and still STALE for
bitcoin. A gap that went STALE in session stays STALE after the session ends. Every condition
clears on the first fresh tick. When a symbol that alerted is fully healthy again (no fault left,
clock in tolerance), the gate logs recovered after <ms>ms unhealthy and ships one
marketdata.recovered event whose unhealthyForMs runs from the episode's first alert; a symbol
that raised stale and clock_skew recovers once, when both have cleared. Recovery sends no
notification.
notify¶
Notification channels are keyed by channel type. Telegram is built in.
| Key | Type | Default | Used by | Notes |
|---|---|---|---|---|
notify.<channel>.enabled |
boolean | false |
daemon notifier and preflight | Production preflight fails if no alert channel is enabled unless runtime.waivers.alerts.reason is set. |
notify.<channel>.commands |
boolean | false |
daemon command channels | Telegram command channel is enabled only when this is true. |
notify.<channel>.events |
list | empty | notifier filter | Valid event names: order_rejected, halted, resumed, position_reconciled, strategy_started, strategy_stopped, strategy_error, daemon_started, disk_space_low. |
notify.<channel>.daily_summary_utc |
string | empty | daily summary scheduler | UTC time string used by the channel. |
notify.telegram.bot_token |
string | none | Telegram provider | Required for enabled Telegram. |
notify.telegram.chat_id |
string | none | Telegram provider | Required for enabled Telegram. |
notify.telegram.queue_capacity |
int | 100 |
Telegram provider | Bounded queue size. |
Unknown notify channel keys are passed through as provider settings.
insights¶
Optional egress to a qkt-insights collector. Disabled config wires no queue and no worker thread. Enable journal_enabled for production so collector outages leave unacked batches on disk for replay instead of dropping them after retry exhaustion. Daemon live sessions also write a full engine audit JSONL under state/audit-journal/<strategy>/audit-YYYY-MM-DD.jsonl; the audit writer uses a bounded queue and its own daemon thread so durable file I/O does not run on the event bus thread.
For trade.closed envelopes, realized is retained only as a legacy alias of
netAccountRealized. Dashboards should treat netAccountRealized as the
canonical account-currency PnL after modeled commissions and venue-reported
costs. When conversion evidence is available, the same payload includes
grossAccountRealized, nativeRealized, currencies, FX rate/source fields, and
costsAccount so live trade tables and graphs can reconcile net-vs-gross values.
Market-data health ships per symbol (see market_data for when each fires).
marketdata.stale is in the lifecycle family; marketdata.recovered is in the opt-in
marketdata family, because a collector older than qkt-insights #106 rejects the whole batch that
carries it. List marketdata only once the collector runs that release:
marketdata.stale:{"source", "symbols": [symbol], "state": "stale", "reason", "ts", "kind"},kindone ofstale,clock_skew,outlier;reasonis the operator text, e.g.quote age 60553ms exceeds 60000ms threshold.marketdata.recovered:{"source", "symbols": [symbol], "state": "recovered", "reason", "ts", "unhealthyForMs"}, e.g."reason": "fresh tick after stale", "unhealthyForMs": 184000. Pair it with the openmarketdata.stalefor the same source and symbol to measure the episode.
Both use envelope ids marketdata-<state>-<source>-<ts>, like marketdata.connected,
marketdata.disconnected and marketdata.reconnected.
| Key | Type | Default | Notes |
|---|---|---|---|
insights.enabled |
boolean | false |
Must be true and url non-blank to create a sink. |
insights.url |
URL | empty | Collector ingest URL. |
insights.instance_id |
string | qkt fallback at daemon wire time |
Instance label sent with events. |
insights.token |
string | empty | Bearer or collector token as expected by the sink. |
insights.events |
list | every family except marketdata when enabled and omitted |
Valid families: trade, order, signal, risk, position, snapshot, log, state, deal, lifecycle, marketdata. snapshot is retained for old configs and wires nothing. marketdata (feed recovery events) is opt-in and needs a qkt-insights collector with #106. |
insights.flush_interval_ms |
long | 250 |
Batch flush cadence. |
insights.batch_size |
int | 200 |
Max events per HTTP batch. |
insights.queue_capacity |
int | 10000 |
In-memory queue bound before the sink worker drains events. |
insights.journal_enabled |
boolean | false |
When true, the sink worker spools serialized envelopes locally and replays unacked rows after collector downtime. |
insights.journal_dir |
path | daemon state state/insights-journal fallback when journal is enabled |
Optional journal directory. Blank uses the daemon state directory fallback. |
insights.state_poll_ms |
long | 10000 |
Broker state polling cadence. |
insights.deal_backfill_days |
long | 30 |
Broker deal backfill window on startup. |
book_risk¶
Book-risk controls apply to portfolio/book evaluation and portfolio daemon flows.
| Key | Type | Default | Notes |
|---|---|---|---|
book_risk.capital |
decimal | unset | Required for drawdown-style book risk to form a basis. |
book_riskapplies to portfolio deployments only. A strategy deployed on its own is not bounded by these limits —BookRiskControlleris built by the portfolio deployer alone. The daemon warns at start when the block is present, and total notional is otherwise unbounded for a standalone strategy. See parity catalog row A23. |book_risk.limits.max_gross_exposure| decimal | unset | Gross exposure cap as a multiple ofcapital(3.0= 3x). Not an amount of money: values above 100 are refused at load. | |book_risk.limits.max_net_exposure| decimal | unset | Net exposure cap as a multiple ofcapital. | |book_risk.limits.max_symbol_concentration| decimal | unset | Single-symbol net exposure cap as a multiple ofcapital(0.35= 35%). | |book_risk.de_risk.ladder[].drawdown| decimal | required per rung | Drawdown threshold as fraction. | |book_risk.de_risk.ladder[].factor| decimal | required per rung | Exposure scale factor at that drawdown rung. | |book_risk.de_risk.ladder[].cooldown_bars| int | unset | Bars to hold a rung after recovery. | |book_risk.allocation.method|FIXED,INVERSE_VOL,ERC,REGIME_WEIGHTED|FIXED| Portfolio allocation method. Regime portfolio DSL selectsREGIME_WEIGHTEDautomatically. | |book_risk.allocation.target_vol| decimal | unset | Target volatility for allocation. | |book_risk.allocation.rebalance_every_bars| int |0| Rebalance cadence. | |book_risk.allocation.max_leverage| decimal |4| Allocation leverage cap. |
Legacy and reserved sections¶
| Section | Status | Notes |
|---|---|---|
tv |
parsed flat map | Reserved for TradingView-related settings. Current live fallback is selected by top-level source: tv. |
fetchers |
parsed nested map | Reserved for named fetcher settings. Current backtest custom fetcher path uses CLI flags --fetcher dukascopy --fetcher-script <path>. |
qkt-forge config alignment¶
qkt-forge has its own YAML files under ../qkt-forge/config. Keep these aligned with qkt:
| qkt-forge file | qkt side to align |
|---|---|
sources.yaml |
qkt strategy symbols, data root, dataset snapshot windows |
gates.yaml |
qkt execution preset, walk-forward windows, cost stress, promotion evidence keys |
qkt.yaml |
qkt binary/image, data root, concurrency, session budget |
budget.yaml |
research spend and run cadence, not qkt runtime config |
agents.yaml |
model/agent config, not qkt runtime config |
qkt-forge should call ordinary qkt commands for backtest, sweep, walk-forward, experiment, and promotion. Production deploy governance remains in qkt.
Common command overrides¶
| Command | Config read | Useful overrides |
|---|---|---|
qkt backtest |
risk, account, fx_conversion, execution, book_risk |
--config, --dataset, --data-root, --execution, --broker, --account-currency, --fx-symbol, --starting-balance |
qkt sweep |
same as backtest | --parallelism, --param, --scenarios |
qkt walkforward |
same as backtest | --train, --test, --step, --parallelism |
qkt experiment run |
passes through backtest config | --plan, --parallelism, --dataset, --registry-dir, --out-dir |
qkt daemon start |
all runtime sections | --config, --state-dir, --load-dir |
qkt preflight |
runtime, state, risk, broker, notify | --production, --config, --state-dir |
qkt promotion |
promotion | --config, --state-dir, --registry-dir |
Validation checklist¶
Before production:
- Run
qkt preflight <strategy.qkt> --production --config qkt.config.yaml. - Run
qkt brokers list --config qkt.config.yamlwhen using MT5 profiles. - Run
qkt instruments verify --config qkt.config.yamlto detect YAML/venue metadata drift. - Run pinned backtests with
--datasetand realistic execution. - Record promotion evidence and approval.
- Run
qkt promotion status <name> --strategy <strategy.qkt> --config qkt.config.yaml. - Run
qkt status --deepafter daemon startup.