Streams (SYMBOLS)¶
A stream is a single instrument on a single venue at a single timeframe. The SYMBOLS block declares every stream a strategy listens to. Each stream gets an alias — a short name you use throughout the rest of the file.
Shape¶
Three parts after =:
- Broker prefix — uppercase, ASCII letters + underscores
- Symbol — uppercase, the venue's name for the instrument
EVERY+ timeframe — candle window
The <alias> is what your strategy code uses. Pick something short and meaningful.
Single-stream example¶
btc becomes a first-class reference for the rest of the file. btc.close, btc.high, btc.volume, etc. all work.
Multi-stream example¶
SYMBOLS
btc = BACKTEST:BTCUSDT EVERY 1m
eur = BACKTEST:EURUSD EVERY 15m
gold = BACKTEST:XAUUSD EVERY 1h
Three streams, three different timeframes. Rules can reference any of them, including in the same condition:
RULES
WHEN btc.close / btc.close[20] > 1.05 -- BTC up 5% in 20 minutes
AND gold.close < sma(gold.close, 20) -- gold below 1h average
THEN BUY btc SIZING 0.1 -- buy BTC
Strategies that combine signals across instruments are common — momentum on one asset gated by regime on another.
Broker prefixes¶
The broker prefix tells the engine which venue this stream lives on. Built-in prefixes:
| Prefix | What it means | When to use |
|---|---|---|
BACKTEST |
The historical data store (~/.qkt/data/) |
Backtesting; qkt backtest, qkt run in paper mode |
BYBIT_SPOT |
Bybit Spot via REST + WebSocket | Live trading spot crypto |
BYBIT_LINEAR |
Bybit USDT-denominated perpetuals | Live trading futures |
EXNESS, ICMARKETS, FTMO, PEPPERSTONE |
MT5 brokers via mt5-gateway |
Live trading FX, indices, commodities |
Plus any custom profile you define in qkt.config.yaml:
You'd then write MYALPACA:SPY in your strategy.
Run qkt brokers list to see what's configured in the current environment.
Symbol names¶
The symbol is whatever the venue calls the instrument. Different brokers may use different names for the same underlying.
| Underlying | BACKTEST | BYBIT | EXNESS (MT5) |
|---|---|---|---|
| Bitcoin/USD | BTCUSDT |
BTCUSDT |
BTCUSDm (suffix m) |
| Ether/USD | ETHUSDT |
ETHUSDT |
ETHUSDm |
| EUR/USD | EURUSD |
n/a | EURUSDm |
| Gold | XAUUSD |
n/a | XAUUSDm |
| S&P 500 | SPX500 |
n/a | US500m |
The DSL uses the qkt-side name. The broker integration layer (Phase 17, Phase 7) translates to the venue's actual symbol via the broker profile's symbolPolicy (suffix, alias map). Exness adds m automatically; ICMarkets/FTMO/Pepperstone don't.
If a venue rejects a symbol you wrote, check the actual venue name vs the qkt-side name. The config schema covers symbolPolicy overrides.
Timeframes (EVERY <window>)¶
How often a candle closes for this stream. The candle aggregator collects ticks into OHLC bars; rules fire on candle close.
Supported windows:
| Token | Means |
|---|---|
1s, 5s, 15s, 30s |
Sub-minute (mostly for HF testing) |
1m, 5m, 15m, 30m |
Intraday |
1h, 2h, 4h, 6h, 12h |
Hourly |
1d |
Daily |
1w |
Weekly |
The parser is liberal — EVERY 7m and EVERY 3h work fine, even though they're non-standard. But your data fetcher may not have data at non-standard resolutions; check.
Stream field access¶
Every stream exposes these fields:
btc.open -- open price of the current closed candle
btc.high -- high
btc.low -- low
btc.close -- close
btc.volume -- volume
btc.timestamp -- candle start time (ms since epoch)
For historical lookback (the N-th candle ago):
btc.close[0] -- current candle (same as btc.close)
btc.close[1] -- previous candle
btc.close[20] -- 20 candles ago
Negative indices and out-of-range indices return null; any comparison with null evaluates to false (so you don't get exceptions during warmup).
Same symbol, different timeframes¶
If you want BTC on both 1m and 1h to detect short-term moves within long-term context, declare two aliases:
SYMBOLS
btc_1m = BACKTEST:BTCUSDT EVERY 1m
btc_1h = BACKTEST:BTCUSDT EVERY 1h
RULES
WHEN btc_1m.close CROSSES ABOVE btc_1m.close[5] -- short-term up
AND btc_1h.close > sma(btc_1h.close, 20) -- long-term up too
THEN BUY btc_1m SIZING 0.1
The candle hub deduplicates ticks — both aggregators read from the same underlying tick stream. There's no double cost.
Multiple brokers, same symbol¶
Currently not supported. The DSL parser rejects:
-- this fails to compile:
SYMBOLS
btc_bybit = BYBIT_SPOT:BTCUSDT EVERY 1m
btc_exness = EXNESS:BTCUSDm EVERY 1m
Position reconciliation becomes ambiguous when the same underlying instrument has positions on two venues simultaneously. See Broker integration for the deferred limitation and the workarounds (separate strategies, separate daemons).
FOR EACH over streams¶
To apply the same rule to many streams without copy-paste:
SYMBOLS
btc = BACKTEST:BTCUSDT EVERY 1m
eth = BACKTEST:ETHUSDT EVERY 1m
sol = BACKTEST:SOLUSDT EVERY 1m
FOR EACH s IN btc, eth, sol DO
WHEN ema(s.close, 9) CROSSES ABOVE ema(s.close, 21)
THEN BUY s SIZING 0.1
s is a textual substitution at compile time, not a runtime variable. See FOR EACH.
Common gotchas¶
- Forgetting
EVERY.btc = BACKTEST:BTCUSDT(no timeframe) is a parse error. - Lowercase broker prefix.
bybit_spot:btcusdtis a parse error — broker prefixes must be uppercase. - Mixing case in the symbol.
BTCusdtorbtcusdtwill fail at the broker boundary because the venue's symbol is case-sensitive (typically all-uppercase). - Forgetting the
msuffix on Exness. Theexnessbroker profile auto-adds it viasymbolPolicy.suffix: "m". You writeEURUSDin the DSL; the broker seesEURUSDm. If your broker profile doesn't have this set, the order fails at submission. - Stream alias collisions in
FOR EACH. Pickingsas the iterator and also having a stream namedscauses shadowing — change the iterator name.
What this composes with¶
- Conditions — references like
btc.closeand thePOSITION.btcfunction expect stream aliases declared here - Indicators — indicator function calls take stream-field expressions
- FOR EACH — iterates over streams
- SIZING and BRACKET — refer to the stream's price for percent/absolute calculations