BRACKET¶
A BRACKET attaches a stop-loss and a take-profit to an entry as one atomic group. The broker submits them together; when either side fills, the other auto-cancels (one-cancels-other semantics).
Shape¶
BUY <stream> SIZING <size>
BRACKET {
STOP_LOSS <distance_or_price>,
TAKE_PROFIT <distance_or_price>
}
There is no bare form: STOP_LOSS and TAKE_PROFIT only exist inside the BRACKET { ... }
wrapper, and a bracket carries both legs. A stop written directly on the action is a parse error
(the parser reads BUY btc SIZING 0.1 as complete and finds STOP_LOSS where the next rule
should start):
STRATEGY bare_stop VERSION 1
SYMBOLS
btc = BACKTEST:BTCUSDT EVERY 1m
RULES
WHEN btc.close > btc.open
THEN BUY btc SIZING 0.1 STOP_LOSS BY 100
-- parse error: expected WHEN or FOR EACH in RULES, got 'STOP_LOSS' (line 6)
For a stop with no target, keep a rule-driven CLOSE, a trailing entry
(ORDER_TYPE = TRAILING BY <distance>), or an OCO
whose limit sits far away.
Distance specifications¶
Both STOP_LOSS and TAKE_PROFIT accept these forms:
BY <points> — fixed distance in quote currency¶
For BTC at $67,000 long, stop at $66,900, target at $67,300. The units match the symbol's quote (USD for crypto, pips for FX but converted to price points).
The distance must be greater than 0: a literal 0 or negative distance is a compile error, and one computed
from an expression (BY atr(btc, 14) - 50) that comes out 0 or negative skips the order, since it would put
the level on the wrong side of entry. An out-of-range computed PCT skips the order the same way.
BY <pct> PCT — percent of entry price¶
For BTC at $67,000 long, stop at $66,330, target at $69,010.
The value is a percentage, not a fraction: 1 PCT means 1%. Stop-loss percentages
must be at least 0.01 and less than 50; take-profit percentages must also be at least
0.01. Smaller values are rejected with a percent-units hint because they commonly
indicate a fraction-scale input such as 0.004 where 0.4 was intended.
Compatibility: runtimes before commit
788a90f1interpreted this operand as a fraction (0.004meant 0.4%). Current runtimes use percentage points, so the economically equivalent value is0.4. Migrate old strategy sources and parameter grids before replaying them on a current runtime.
AT <price_expression> — absolute price¶
Computed at action-execute time. btc.close is the entry price (the close of the bar that fired the rule). Arithmetic, indicator calls, and LET-bound values all work.
That bar close is only the placement-time estimate. For fill-anchored fixed brackets, live execution modifies venue protection to the actual fill geometry after acceptance. If the venue rejects that modification, qkt reports a protection failure and arms an engine-held stop at the intended fill-anchored level. Engine-held protection requires healthy tick delivery.
Mixing¶
You can mix forms in one bracket:
BRACKET {
STOP_LOSS BY 2.0 PCT, -- percent stop
TAKE_PROFIT AT btc.close + atr(btc, 14) * 6 -- ATR target
}
Scale-out (multi-leg take-profit) — not implemented¶
A bracket carries exactly one take-profit. A fractional multi-leg take-profit
(TAKE_PROFIT { 0.33 AT ..., 0.33 AT ... }) does not parse and has no AST; earlier
revisions of this page documented one that was never built.
To bank a position in pieces today, choose one of:
- Repeat the entry instead of splitting the exit.
BUY btc SIZING 0.01 TIMES 3with a differentTAKE_PROFITper action gives three independently-exiting positions and is the closest equivalent. See TIMES. RESIZEtrims an open position toward a target size, but it is not usable alongside aBRACKETon the same position — see the gotchas below.
Complete protection¶
STOP_LOSS and TAKE_PROFIT are currently a complete bracket pair. A bare
stop or take-profit, including one inherited from DEFAULTS, is rejected at
compile time. Use a BRACKET with both legs and keep any additional
rule-driven CLOSE condition alongside it.
Trailing stop¶
Trailing stops aren't a BRACKET leg — they're an order type on the entry itself:
BUY btc SIZING 0.1 ORDER_TYPE = TRAILING BY atr(btc, 14) * 2
SELL btc SIZING 0.1 ORDER_TYPE = TRAILING PCT 5
See the stop-loss recipe for semantics and engine-vs-broker routing.
Armed trailing stop¶
STOP LOSS TRAILING <distance> AFTER MFE >= <threshold> adds a one-way armed trailing stop as a bracket leg. The stop sits at a fixed distance from the entry until the trade's maximum favorable excursion (MFE) crosses <threshold>, then converts to a trailing stop at the same distance from the running favorable extreme.
Semantics:
- Pre-arm: stop sits at
entry − distance(BUY) orentry + distance(SELL). - Arming: when MFE crosses
<threshold>, the stop arms and starts tracking the favorable extreme. - Post-arm: stop sits at
hwm − distance(BUY) orhwm + distance(SELL), wherehwmis the running favorable extreme since fill. - Arming is one-way: once armed, the stop never disarms.
- The same
<distance>value applies pre and post — only the reference point shifts (entry → hwm).
<distance> must be positive and <threshold> non-negative; <threshold> of zero means "trail from inception." Either may be an expression such as atr(btc.candle, 14) * 2: it is evaluated once when the order is built, from the signal bar, and the trail keeps that fixed distance from then on. If an operand is still undefined during warm-up, or evaluates to a value outside those bounds, the order is skipped. Inside a STACK bracket both must be numeric literals. TAKE PROFIT TRAILING is rejected at parse time — armed trailing is stop-only.
Risk-based sizing (SIZING RISK $ N) sees <distance> as the worst-case stop distance regardless of arming state.
The arming logic is always engine-managed: brokers that support native brackets see only the entry and TP; the engine owns the stop and re-evaluates its level on every tick.
See Phase 36 — Armed trailing stop for the worked examples and known limitations.
Stepped stop ratchet¶
A stepped stop locks discrete, direction-relative profit milestones:
BUY btc SIZING 0.1 BRACKET {
STOP LOSS BY 50
STEP TO BREAKEVEN AFTER MFE >= 30
STEP TO ENTRY + 40 AFTER MFE >= 70,
TAKE PROFIT BY 120
}
The initial stop is 50 points from the fill. At 30 points of MFE it moves to
entry; at 70 points it moves to 40 points of locked profit. BREAKEVEN + <d>
and ENTRY + <d> are equivalent direction-relative targets, so the same source
works for long and short entries.
Thresholds must be strictly increasing and target distances non-negative. The
initial distance, thresholds and targets may be expressions (for example
STOP LOSS BY atr(btc.candle, 14) * 2 STEP TO BREAKEVEN AFTER MFE >= atr(btc.candle, 14)):
they are evaluated once when the order is built and fixed for the life of the
trade, and an undefined or invalid evaluation skips the order. Inside a STACK
bracket they must be numeric literals. Each step is consumed once. A target that
would widen the current stop is skipped with an operator warning.
Time-tightening stop¶
A time-tightening stop reduces its risk distance on the injected engine clock:
It starts 60 points from the fill, tightens by 10 points per completed 15-minute
interval, and stops tightening at a 20-point distance. The initial distance,
tightening delta, and floor must be positive, and the floor cannot exceed the
initial distance. Like the stepped stop, they may be expressions evaluated once
when the order is built (numeric literals inside a STACK bracket).
Both ratchet forms are engine-managed in paper/backtest and live execution. The engine evaluates only live stops for the tick's symbol, persists ratchet progress for restart, and never widens a stop. On venues with position modification, each tightening transition is also mirrored to the venue position so the tighter stop continues protecting it if qkt goes offline.
How the bracket reaches the broker¶
The DSL submits one BRACKET request to the order manager. From there:
-
If the broker attaches brackets (live MT5): the entry goes out with its stop and target attached. If the venue refuses to attach them to the filled position, qkt holds the stop (and a fill-anchored target) itself and alerts the operator.
-
Otherwise (
paper,mt5-sim,type: gatewayvenues such as Deribit, Bybit): the order manager submits the entry alone. On fill, it submits the target and then the stop as separate orders linked by OCO state; when one fills, the engine cancels the other. If the venue refuses the stop, qkt holds it itself (it fires a market close at the level) and keeps the target; if it refuses the target, the stop still goes out. Either way the operator is alerted, and the position is never left without its stop.
The DSL is the same either way. See Broker integration for the capability matrix.
An entry that fills only in part¶
In case 2 the stop and target wait, unsent, until the entry ends. If the entry ends with only part of it filled, what happens to them depends on what ended it:
| What ended the entry | Its exits |
|---|---|
| The venue cancelled the rest (a market remainder, an IOC, an expiry) | Sent for the filled part: sized to it, anchored on its average price. |
CANCEL <stream>, CANCEL_ALL, a risk halt |
The same: qkt cancels the rest of the entry and keeps the filled part protected. Whatever fills before the venue confirms the cancel is protected too, and an entry that fills whole first gets its full exits. |
CLOSE <stream>, CLOSE_ALL, qkt stop --flatten |
Dropped. The close flattens the filled part itself, so a stop or target left behind would face a position that is already flat. |
| Nothing filled | Dropped with the entry. |
When the entry's exits went out for its filled part and the venue later reports more of the entry
executed (it executed before the cancel took effect, and was heard after the entry's end), that
quantity gets the bracket's exits too, as their own stop and target (<bracket>-late1-sl,
<bracket>-late1-tp, then -late2-...), sized to it and anchored on its price. A repeat of the same
report arms nothing more (#1349).
The same holds after a restart: an entry whose remainder the venue cancelled while qkt was down comes back cancelled with its filled part, the position holds exactly the fills, the exits go out for that part, and nothing is sent again.
Defaults via DEFAULTS¶
If most of your strategies use the same bracket pattern, hoist it:
DEFAULTS {
SIZING = 0.1
STOP_LOSS = BY atr(SYMBOL, 14) * 2 -- 2-ATR stop
TAKE_PROFIT = BY atr(SYMBOL, 14) * 6 -- 6-ATR target (3R)
}
RULES
WHEN signal_condition
THEN BUY btc -- inherits sizing + stop + target
SYMBOL substitutes for the rule's stream at compile time. See LET and DEFAULTS.
Common gotchas¶
- Wrong side stop direction. For a
BUY, the stop must be below the entry price. The parser does check this for absolute prices but can't always check expressions (btc.close + 100for a long stop is a logic error). Test on backtest before live. - Bracket stop too close — MT5 brokers enforce
tradeStopsLevelminimum distance. Orders too tight reject at the venue. Useatr * <multiplier>to scale; if the multiplier produces too-tight stops in low-vol regimes, the order rejects. - There is no
TRAILING_STOPclause. Use a trailing entry (ORDER_TYPE = TRAILING BY <distance>) or an armed trailing stop leg (STOP LOSS TRAILING <distance> AFTER MFE >= <threshold>), both above. - Limit-entry bracket execution. When the entry is a limit order (
BUY btc SIZING 0.1 ORDER_TYPE = LIMIT AT 67000 BRACKET ...), the bracket only activates after the limit fills. If the limit never fills, the bracket never sends.
What this composes with¶
- Actions —
BRACKETis a modifier onBUY/SELL - SIZING — risk-based sizing (
PCT RISK) requires a stop in the bracket - Indicators — ATR is the canonical stop input
- Expressions — arithmetic in stop/target prices
- Mean-reversion example — heavy use of scale-out brackets