STACK_AT — conditional bracketed stacks¶
Fire independent micro-trades after a primary fill, once the position shows conviction. Each STACK_AT clause attaches its own SL/TP bracket and tracks as a separate leg — closing the primary does NOT close the stacks, and a stack hitting its own TP does not affect the primary or other stacks.
This is the multi-leg pattern from the production hedge-straddle: a directional break enters, then as MFE grows the strategy layers in three independent positions, each with its own risk and reward. Per the pa-quant analysis, this pattern roughly doubles 6-month P&L on top of the no-stack profile.
STACK_AT is distinct from STACK. STACK is pyramiding — one position, shared bracket, sequential triggers. STACK_AT is leg-based — N independent positions, each with its own bracket, fired by max-favorable-excursion thresholds.
Shape¶
BUY <stream> SIZING <primary_size>
BRACKET { STOP LOSS BY <p>, TAKE PROFIT BY <q> }
STACK_AT MFE >= <threshold> WITHIN <duration>
SIZING <stack_size>
BRACKET { STOP LOSS BY <s>, TAKE PROFIT BY <t> }
STACK_AT MFE >= <threshold2> WITHIN <duration2>
SIZING <stack_size>
BRACKET { STOP LOSS BY <s>, TAKE PROFIT BY <t> }
...
MFE >= <threshold>— the stack fires when the primary leg's max favorable excursion (high-water mark ofcurrent_price - entry_pricefor a BUY) crosses the threshold, in price unitsWITHIN <duration>— abandons the clause if the threshold isn't reached within the window since the primary fill. Each clause has its own deadline.SIZING <stack_size>— the stack leg's quantity. Currently restricted to literalSizeQty(absolute lots) — risk-based and percent sizing not yet supported.BRACKET { ... }— the stack's own SL/TP. Each leg is independent. Required.
Multiple STACK_AT clauses on one action are independent — they fire in MFE order as their thresholds cross, each abandoning on its own deadline.
Three-tier hedge-straddle example¶
BUY gold SIZING 0.20
BRACKET { STOP LOSS BY 18, TAKE PROFIT BY 15 }
STACK_AT MFE >= 10 WITHIN 30m SIZING 0.06 BRACKET { STOP LOSS BY 2, TAKE PROFIT BY 20 }
STACK_AT MFE >= 20 WITHIN 60m SIZING 0.06 BRACKET { STOP LOSS BY 2, TAKE PROFIT BY 20 }
STACK_AT MFE >= 30 WITHIN 90m SIZING 0.06 BRACKET { STOP LOSS BY 2, TAKE PROFIT BY 20 }
Primary fills at $2,000 for 0.20 lots. Then:
- Price reaches $2,010 within 30 min (MFE = 10) → tier-1 fires: a fresh 0.06-lot BUY with its own 2/20 bracket.
- Price reaches $2,020 within 60 min from primary fill (MFE = 20) → tier-2 fires.
- Price reaches $2,030 within 90 min (MFE = 30) → tier-3 fires.
If MFE peaks at 25 within 30 min and pulls back, tier-1 has already fired; tier-2 fires when MFE crosses 20 again; tier-3 is abandoned if 90 min elapses before MFE reaches 30.
When the primary's bracket fires (SL or TP), the primary leg only closes. Stack legs continue with their own brackets.
How tiers fire¶
On every market tick after the primary fills:
- The MFE tracker on the primary leg updates with the new price.
- For each
STACK_ATclause not yet fired or abandoned: - If
mfe >= thresholdANDelapsed <= within→ fire (emit a stack order). - Else if
elapsed > within→ mark abandoned (won't fire this primary's lifecycle). - A tier fires at most once per primary lifecycle.
If a single big tick crosses multiple thresholds, all qualifying tiers fire on the same tick — they're independent.
How legs track¶
After tier-1 fires and the stack market fills:
LegBook(EURUSD):
PRIMARY legId=primary-1 side=BUY qty=0.20 entry=2000.00
STACK legId=stack-tier0 side=BUY qty=0.06 entry=2010.00 parentLegId=primary-1
The position's net view (POSITION.gold) returns the combined quantity. Per-leg state is observable via the leg book.
When the stack's own TP fires at $2,030, the stack leg closes — primary leg is untouched:
PnL realizes on the stack's qty × distance, independently of the primary's PnL.
Threshold and sizing expressions¶
The threshold supports compile-time-constant arithmetic — literals and +/-/*// over literals. References, indicators, and NOW.<field> are rejected to keep the per-tick path cheap:
STACK_AT MFE >= 10 WITHIN 30m -- literal: OK
STACK_AT MFE >= 5 * 2 WITHIN 30m -- compile-folded to 10: OK
STACK_AT MFE >= atr(gold, 14) WITHIN 30m -- rejected at compile time
SIZING for STACK_AT is limited to literal lots (SizeQty). Risk-fraction (RISK 0.01), notional (100 USD), and percent-of-equity sizing are not supported for stacks in Phase 27 — they'll land in a later phase once the leg-level risk-accounting story is finished.
BRACKET for STACK_AT must use BY <distance> for both legs. AT <price>, PCT <frac>, and RR <multiplier> forms are rejected — the stack's bracket is computed from the stack's own entry price at fire time, so absolute and ratio-based forms don't translate cleanly.
Reading MFE from the DSL¶
POSITION.<stream>.mfe returns the primary leg's current MFE in price units. Useful for logging or as a condition that gates other rules:
The accessor returns 0 if no primary leg exists.
Combinability¶
- ✓
BRACKETon the primary — primary and each stack have independent brackets - ✓ Multiple
STACK_ATon one action — N tiers fire independently - ✓
OCO_ENTRYwithSTACK_ATon each leg — whichever side fills attaches its stacks - ✗ Same action with both
OCOandSTACK_AT— rejected at compile time - ✗ Same action with both
STACK(pyramiding) andSTACK_AT— rejected at compile time - ⚠ Native broker brackets — Phase 27 parent-close detection only covers PaperBroker's bracket-fallback path; live MT5 brackets need broker-side leg correlation work before the engine knows when the parent's TP/SL fires
Broker capability gate¶
A strategy that uses STACK_AT is rejected at deploy time if the routing broker doesn't declare MULTI_POSITION_PER_SYMBOL. PaperBroker and MT5 (any venue) support it natively. Bybit Spot does NOT (netting-only); Bybit Linear supports it in hedge mode only.
The error names the strategy, symbol, and broker so the fix is unambiguous:
Strategy 'hedge_straddle' uses STACK_AT on XAUUSD but routing broker 'BybitSpot'
does not declare MULTI_POSITION_PER_SYMBOL
Common gotchas¶
- Threshold is in price units, not pips/points.
STACK_AT MFE >= 10means MFE = $10, not 10 pips. For XAUUSD that's $10/oz. - Window starts at primary fill, not signal time. A 30m window for a tier means 30 minutes after the primary entry market actually fills — not 30 minutes after the rule's
WHENcondition first matched. - Abandoned clauses don't fire later in the same lifecycle. Once a tier's window expires without the threshold crossing, that tier is dead for this primary. A future primary on the same symbol gets fresh tiers.
STACK_ATdoesn't move existing brackets. The primary's bracket stays at its original SL/TP; only new stack orders are added.- No retroactive fire. The engine first evaluates on the tick after the primary fill. If the primary fills already past a tier's threshold, the tier fires on the next tick, not at fill time.
What this composes with¶
- BRACKET — each
STACK_ATcarries one - SIZING — restricted to literal lots for stacks; full surface on the primary
- OCO_ENTRY — STACK_AT on each leg is the hedge-straddle shape
- Actions —
STACK_ATattaches toBUY/SELL - Phase 27 spec — design notes and the LegBook semantics