Phase 11d1 — Order Types, Brackets, OCO, Child Prices, Equity-Free Sizing¶
Summary¶
Phase 11d1 makes the DSL usable for real trading. Strategies can submit LIMIT, STOP, STOP-LIMIT, TRAILING BY, and TRAILING PCT orders. They can attach a BRACKET { STOP LOSS, TAKE PROFIT } or an OCO { STOP, LIMIT } to any entry. They can size with USD notional, fixed-dollar risk (RISK $50 with stop-distance lookup), or full-position close. They can specify TIF as GTC, IOC, FOK, or DAY. Every emitted order goes out as Signal.Submit(OrderRequest.X(...)) with a unique per-strategy ID.
The headline acceptance test: a DSL strategy with LIMIT entry + BRACKET { STOP LOSS BY 5, TAKE PROFIT RR 3 } + RISK $50 sizing emits a Signal.Submit(OrderRequest.Bracket) with entry: OrderRequest.Limit, stopLoss = entry - 5, takeProfit = entry + 3 * stop_distance, and quantity = $50 / stop_distance.
% OF EQUITY, % OF BALANCE, RISK <fraction> sizing, and the DEFAULTS block are deferred to Phase 11d2 — they need engine-side equity() / balance() surface that doesn't yet exist. GTD TIF is also deferred (engine TimeInForce enum has 4 variants; adding GTD requires a deadline-bearing order surface).
What's new¶
TifTranslator— translatesTifAstto engineTimeInForce. DAY/GTC/IOC/FOK supported; GTD throws "deferred".ChildPriceResolver— resolvesChildPriceAst(AT/BY/PCT/RR) given side, entry price, and optional stop distance to an absolute price. Side-aware sign for stop-loss (subtract on long, add on short) and take-profit (add on long, subtract on short).RRrequires a stop distance.OrderTypeCompiler— compilesOrderTypeAstto a pair of closures: aBuildRequestthat constructs the engineOrderRequestat fire time, and anEntryPriceRefthat returns the entry-price reference used by BRACKET children.Marketusescandle.closeas the reference;Limit/Stopuse the order's price.SizingCompiler— compilesSizingAstto a(EvalContext, entryPrice) -> BigDecimalquantity computer. SupportsSizeQty,SizeNotional,SizeRiskAbs,SizePositionFull. DefersSizePctEquity,SizePctBalance,SizeRiskFracto 11d2 with explicit "deferred" message.ActionCompiler.compileBuySell— fast path for plain market+default-TIF emitsSignal.Buy/Signal.Sell; submit path for any non-trivial option emitsSignal.Submit(OrderRequest.X). Handles BRACKET and OCO via the new compilers.- Per-strategy
IdGenerator—AstCompilerbuildsSequentialIdGenerator(prefix = "dsl-${ast.name}-")and threads it intoActionCompiler. Every emittedOrderRequestgets a unique ID prefixed by strategy name. - Kotlin DSL helpers:
- Order types:
market,limitAt(price),stopAt(price),stopLimit(stop, limit),trailingBy(distance),trailingPct(frac). - Child prices:
childAt(price),childBy(distance),childPct(frac),childRr(multiplier). - Brackets / OCO:
bracket(stopLoss, takeProfit),oco(stop, limit). - Sizing:
usdNotional(usd),riskAbs(amount),positionFull(stream). Plus existing1.bddirect-quantity. - TIF:
gtc,ioc,fok,dayconstants. ActionScope.buy/sellextended with namedorderType,tif,bracket,ocoparameters; second overload accepts aSizingAstinstead of a numericqty.
Migration from previous phase¶
None breaking. Plain BUY btc QTY 1 continues to emit Signal.Buy(symbol, qty) (the existing fast path). Anything with a non-default option emits Signal.Submit(OrderRequest.X). Existing tests that assert on Signal.Buy/Signal.Sell for plain-market actions stay green.
Usage cookbook¶
LIMIT entry with BRACKET (RISK $ sizing)¶
import com.qkt.dsl.compile.AstCompiler
import com.qkt.dsl.kotlin.and
import com.qkt.dsl.kotlin.bd
import com.qkt.dsl.kotlin.bracket
import com.qkt.dsl.kotlin.childBy
import com.qkt.dsl.kotlin.childRr
import com.qkt.dsl.kotlin.eq
import com.qkt.dsl.kotlin.gt
import com.qkt.dsl.kotlin.limitAt
import com.qkt.dsl.kotlin.position
import com.qkt.dsl.kotlin.riskAbs
import com.qkt.dsl.kotlin.strategy
val ast = strategy("limit_with_bracket", version = 1) {
val btc = stream("btc", broker = "BACKTEST", symbol = "BTCUSDT", every = "1m")
rule {
whenever((btc.close gt 105.bd) and (position(btc) eq 0.bd))
then {
buy(
stream = btc,
sizing = riskAbs(50.bd),
orderType = limitAt(99.bd),
bracket = bracket(
stopLoss = childBy(5.bd),
takeProfit = childRr(3.bd),
),
)
}
}
}
Emits Signal.Submit(OrderRequest.Bracket(entry=Limit@99, stopLoss=94, takeProfit=114, quantity=10)).
STOP-LIMIT order on cross down¶
import com.qkt.dsl.kotlin.crossesBelow
import com.qkt.dsl.kotlin.stopLimit
rule {
whenever(fast crossesBelow slow)
then {
sell(
stream = btc,
qty = 1.bd,
orderType = stopLimit(stop = 95.bd, limit = 94.bd),
)
}
}
Trailing stop with percent trailing¶
import com.qkt.dsl.kotlin.trailingPct
rule {
whenever(position(btc) gt 0.bd)
then {
sell(
stream = btc,
sizing = positionFull(btc),
orderType = trailingPct(0.05.bd), // 5% trail
)
}
}
The DSL fraction (0.05) is auto-scaled to the engine's PERCENT trail format (5.0).
OCO exits on entry¶
import com.qkt.dsl.kotlin.childAt
import com.qkt.dsl.kotlin.oco
rule {
whenever(entryCondition)
then {
buy(
stream = btc,
qty = 1.bd,
oco = oco(
stop = childAt(95.bd),
limit = childAt(110.bd),
),
)
}
}
Emits a parent BUY plus an OrderRequest.StandaloneOCO with two opposite-side legs (Stop@95 and Limit@110); whichever fills first cancels the other.
USD notional sizing¶
import com.qkt.dsl.kotlin.usdNotional
rule {
whenever(condition)
then { buy(stream = btc, sizing = usdNotional(1000.bd)) }
}
qty = $1000 / current_close. At a price of 100, this is 10 units.
IOC order¶
import com.qkt.dsl.kotlin.ioc
rule {
whenever(crossingCondition)
then { buy(stream = btc, qty = 1.bd, tif = ioc) }
}
Emits Signal.Submit(OrderRequest.Market(timeInForce=IOC)).
Testing patterns¶
Unit tests for each new compiler (TifTranslator, ChildPriceResolver, OrderTypeCompiler, SizingCompiler) cover all the variants in isolation. The end-to-end test (OrderSurfaceEndToEndTest) exercises the full pipeline by driving the compiled Strategy against synthetic candles and asserting on the captured Signal.Submit(OrderRequest.X) output.
For new strategies, prefer asserting on the emitted OrderRequest shape rather than the resulting BacktestResult, because the PaperBroker may not fully simulate every order type (limit fills, bracket child placement, OCO cancellation) the way a live broker would.
Known limitations¶
% OF EQUITY/% OF BALANCE/RISK <fraction>sizing modes deferred to 11d2 — needs engineequity()/balance()surface.DEFAULTSblock deferred to 11d2 — natural pairing with %-of-equity sizing.GTDTIF deferred — engine enum has 4 variants only; adding GTD needs a deadline field on the order surface.RISK $abssizing only resolves with static stop distance:BY <numeric literal>works;BY <expression>,PCT,ATdon't (depend on entry price, only available at fire time). Compile-time error if you try.- The
RRchild-price mode is take-profit-only. Errors at compile time if used for stop-loss. - The
RRresolver requires aBY <numeric literal>stop loss (the static distance) at compile time. To useRRwith a non-static stop, lift the bracket's stop into a runtime evaluation — out of scope for 11d1. - Fast path optimization: plain market actions with default TIF and no brackets still emit
Signal.Buy/Signal.Sell(cheaper, smaller diff). Anything else usesSignal.Submit. The pipeline downstream handles both. - Mixing
BRACKETandOCOon the same action is a compile-time error. FOR EACH, multi-stream / multi-timeframe / multi-broker: Phase 11e.- External
.qktparser: Phase 11f. - CLI runner: Phase 12.
References¶
- Spec:
docs/superpowers/specs/2026-05-07-trading-engine-phase11-master-design.md§7 Phase 11d1 - Plan:
docs/superpowers/plans/2026-05-07-trading-engine-phase11d1.md - Merge commit:
8cc6811