LET and DEFAULTS¶
Two ways to factor out repeated values so your strategy stays readable as it grows.
LET— name an expression, reuse it in conditions and actionsDEFAULTS— pre-set fields that every action inherits unless overridden
They look similar but serve different purposes. LET is for values you compute and reference in conditions; DEFAULTS is for repeated action parameters (sizing, bracket, time-in-force).
LET <name> = <expression>¶
Binds a name to an expression. The expression is evaluated each time the name is referenced (lazy), so it always sees fresh data.
Basic — name an indicator¶
LET fastMa = ema(btc.close, 9)
LET slowMa = ema(btc.close, 21)
RULES
WHEN fastMa CROSSES ABOVE slowMa
THEN BUY btc SIZING 0.1
Without LET the rule reads ema(btc.close, 9) CROSSES ABOVE ema(btc.close, 21) — fine for two indicators, ugly for more.
Tunable parameters¶
LET fast = 9
LET slow = 21
LET rrRatio = 3.0
RULES
WHEN ema(btc.close, fast) CROSSES ABOVE ema(btc.close, slow)
THEN BUY btc SIZING 0.1
BRACKET {
STOP_LOSS BY 1 PCT,
TAKE_PROFIT BY 1 * rrRatio PCT
}
The --param CLI flag overrides any PARAM or LET by name, just for that run:
The file's value is the default; --param only applies to the run. Pass a comma-list
(--param fast=8,12,16) to qkt sweep to grid-search instead — see
Run a parameter sweep.
Composing LETs¶
A LET can reference earlier LETs and any declared streams:
LET vol = atr(btc, 14)
LET volStop = btc.close - vol * 2 -- references vol
LET volTarget= btc.close + vol * 4
RULES
WHEN ema(btc.close, 9) CROSSES ABOVE ema(btc.close, 21)
THEN BUY btc SIZING 0.1
BRACKET {
STOP_LOSS AT volStop,
TAKE_PROFIT AT volTarget
}
Order does not matter to the compiler: LETs are resolved by name, so volStop may reference a
vol declared later in the file. Declaring in reading order is still the convention. What is
rejected is recursion — a LET that references itself, directly or through another LET, is a
compile error (LET references form a cycle):
LET volStop = btc.close - vol * 2 -- references vol, declared below
LET vol = atr(btc, 14)
RULES
WHEN btc.close < volStop THEN LOG "below the vol stop"
LET runningMax = max(runningMax, btc.close) -- a LET cannot reference itself
RULES
WHEN runningMax > 0 THEN LOG "never compiles"
Booleans¶
LET works for boolean expressions too:
LET inUptrend = ema(btc.close, 20) > ema(btc.close, 100)
LET notHalted = account.equity > 5000
LET canTrade = inUptrend AND notHalted
RULES
WHEN ema(btc.close, 9) CROSSES ABOVE ema(btc.close, 21) AND canTrade
THEN BUY btc SIZING 0.1
This factors out repeatable filter logic. You can reuse canTrade in every rule.
What LET can't do¶
- No state. Each evaluation is fresh; you can't accumulate.
- No recursion. A
LETcan't reference itself, directly or through anotherLET; the compiler rejects the cycle. - No side effects.
LETdoesn't do I/O, doesn't emit signals. - No conditional definition. No
LET fast = IF something THEN 9 ELSE 12. (Use aCASEexpression on the RHS instead — see Expressions.)
DEFAULTS { ... }¶
Pre-sets parameters for every action in the file. Anything an action doesn't explicitly state falls back to the default.
STRATEGY momo VERSION 1
DEFAULTS {
SIZING = 0.1
STOP_LOSS = BY atr(SYMBOL, 14) * 2
TAKE_PROFIT = BY atr(SYMBOL, 14) * 4
TIF = GTC
}
SYMBOLS
btc = BACKTEST:BTCUSDT EVERY 1m
RULES
WHEN ema(btc.close, 9) CROSSES ABOVE ema(btc.close, 21)
THEN BUY btc -- no explicit SIZING/BRACKET/TIF
-- → all come from DEFAULTS
STOP_LOSS and TAKE_PROFIT form one bracket. If defaults supply either leg,
the merged action must supply the other leg explicitly or through DEFAULTS;
an incomplete bracket is rejected when the strategy compiles.
Available default keys¶
| Key | What it sets | Maps to |
|---|---|---|
SIZING = <size> |
Default position size | SIZING <size> |
STOP_LOSS = AT\|BY\|PCT\|RR <value> |
Default stop-loss price or distance | inside BRACKET { STOP_LOSS ... } |
TAKE_PROFIT = AT\|BY\|PCT\|RR <value> |
Default take-profit | inside BRACKET { TAKE_PROFIT ... } |
TIF = GTC\|IOC\|FOK\|DAY\|GTD <expr> |
Default time-in-force; GTD takes an epoch-ms deadline such as NOW + 2h |
TIF ... |
ORDER_TYPE = <order type> |
Default entry order type | ORDER_TYPE = ... |
TRAILING = <order type> |
Default trailing stop | trailing stop clause |
Keys are keywords, so they are not case-sensitive (STOP_LOSS and stop_loss are the same key). A stop or target leg needs a child-price form such as BY <distance>; a bare expression (STOP_LOSS = atr(SYMBOL, 14) * 2) is rejected, and so is any other key:
A GTD default sets the same deadline on every entry that does not name its own TIF:
STRATEGY day_orders VERSION 1
DEFAULTS {
SIZING = 0.1
TIF = GTD NOW + 2h
ORDER_TYPE = LIMIT AT 100
}
SYMBOLS
btc = BACKTEST:BTCUSDT EVERY 1m
RULES
WHEN btc.close > btc.open
THEN BUY btc -- resting limit, expires two hours after placement
The SYMBOL placeholder¶
Inside DEFAULTS, the literal SYMBOL substitutes for whatever stream alias the action is acting on.
DEFAULTS {
STOP_LOSS = BY atr(SYMBOL, 14) * 2 -- → atr(btc, 14) * 2 for rules on btc,
-- atr(eur, 14) * 2 for rules on eur
TAKE_PROFIT = BY atr(SYMBOL, 14) * 4
}
This is how one default works across multiple streams in a multi-asset strategy. Without SYMBOL, you'd have to write per-stream defaults.
SYMBOL is illegal outside DEFAULTS. Using it in a rule directly produces a parse error.
Override at the action level¶
Action-level values always win over defaults:
DEFAULTS { sizing = 0.1 }
RULES
WHEN ema(btc.close, 9) CROSSES ABOVE ema(btc.close, 21)
THEN BUY btc -- uses default sizing = 0.1
WHEN strong_signal_condition
THEN BUY btc SIZING 0.5 -- overrides default with 0.5
Partial defaults¶
Independent defaults can be partial. The exception is bracket protection:
STOP_LOSS and TAKE_PROFIT must be complete after the action-level override is
merged.
DEFAULTS {
TIF = IOC -- always use IOC time-in-force
}
RULES
WHEN ema(btc.close, 9) CROSSES ABOVE ema(btc.close, 21)
THEN BUY btc SIZING 0.1 -- SIZING is per-rule; TIF comes from DEFAULTS
When to use which¶
LET— for values you reference in conditions or in arithmetic inside actions. "The fast MA," "the volatility scalar," "the trend-up flag."DEFAULTS— for action parameters that repeat across many rules. "Every entry sizes at 0.1," "every stop is 2× ATR."
If you find yourself writing the same BRACKET { ... } clause in five rules, hoist it into DEFAULTS. If you find yourself writing ema(btc.close, 9) in three conditions, hoist it into LET.
Common gotchas¶
SYMBOLonly inDEFAULTS. Don't try to use it inRULES.- A
LETmay not reference itself. Forward references resolve by name, but a cycle is a compile error. DEFAULTSdoesn't apply to engine-managed wrappers. ASTACKlayer-list with explicit per-layer overrides shadows the defaultsizing. ATIME_EXITwrapper around a market order takes the inner'stif, not the default.- Broker magic is not a DSL default. Configure it on the broker profile in
qkt.config.yaml; it identifies that routed account/profile at the venue.
See also¶
- STRATEGY block — where
LETandDEFAULTSsit in the file - Conditions — the most common place to use
LET-bound booleans - Actions — what
DEFAULTSkeys map to - Run a parameter sweep —
--paramoverride flow