Order Types in Action: Brackets, OCO, and Risk Management Written into the Order Book¶
Strategy Compendium · No. 25 · Category
order_types(6 strategies) · 2026-09-02
Strategy decides when to buy or sell; order type decides how. Most backtesting tutorials teach you self.buy() and then pretend it’s free: instant, full-size, no slippage. In real markets, the execution details of stops, limits, and OCO groups often matter more to net P&L than the signal itself. The classic tragedy: perfect signal, precise entry — then lunch runs long and nobody placed the stop.
This article covers the 6 order-type backtests in tests/functional/strategies/order_types/. They are not six “strategy ideas” but six interface contracts between your strategy and the market — how the bracket trio makes “every entry carries a stop” an atomic operation, how OCO makes a group of orders mutually exclusive. Consider this the “framework feature + strategy” episode of the series: a working tour of backtrader’s order API.
Category at a Glance¶
Strategy |
Data |
Core idea |
Source |
|---|---|---|---|
Stop order |
Convertible-bond index, daily |
After a golden-cross fill, auto-place a 3% stop; exit on dead cross |
|
Bracket trio |
2005-2006 daily |
Limit main + stop + target submitted as one package; child orders arm on parent fill |
|
OCO orders |
2005-2006 daily |
Three limit buys at different depths; one fill cancels the rest |
|
StopTrail |
2005-2006 daily |
MA-cross entry template with a |
|
Order Target |
YHOO 2005-2006 daily |
Compute target position percent by date; |
|
Order Close |
2005-2006 daily |
|
|
Deep Dive 1: Bracket — Making the Stop an Atomic Operation¶
In a backtest, “forgot the stop” never happens — code always remembers. The bracket order’s value is sinking that memory into the order structure itself: main order, stop-loss, and take-profit submitted as one unit; the moment the main order fills, both children arm; when either child fills, the other is cancelled. The human loophole is closed by the order book.
The implementation (test_37_bracket_order_strategy.py) builds the trio in one shot on a golden cross:
if self.cross > 0.0:
close = self.data.close[0]
p1 = close * (1.0 - self.p.limit) # main: limit buy 0.5% below
p2 = p1 - 0.02 * close # stop: 2% of close below p1
p3 = p1 + 0.02 * close # target: 2% of close above p1
o1 = self.buy(exectype=bt.Order.Limit, price=p1,
valid=valid1, transmit=False)
o2 = self.sell(exectype=bt.Order.Stop, price=p2,
valid=valid2, parent=o1, transmit=False)
o3 = self.sell(exectype=bt.Order.Limit, price=p3,
valid=valid3, parent=o1, transmit=True) # last order ships the group
The keys are transmit and parent: the first two orders are held back with transmit=False until the third one’s transmit=True releases the whole group; parent=o1 declares the hierarchy the engine uses to arm children on fill and cancel the sibling when one side executes. On 2005-2006 data this yields 8 completed round trips (4 wins, 4 losses — a clean 50%), final value 99,875.56, pinned by abs(final_value - 99875.56) < 0.01. Note the main order is a limit valid for 3 days: if price never pulls back, the whole package expires — in a bull run you miss the move. That’s the bracket’s price of discipline.
Deep Dive 2: OCO — One Group of Orders, Only One Future¶
OCO (One-Cancels-Other) solves the opposite problem: you want to buy the dip, but you don’t know how deep the dip runs. Instead of guessing one price, place a limit order at each of three depths and declare them mutually exclusive — first to fill cancels the rest.
test_41_oco_order_strategy.py hangs three buys on a golden cross, depths growing with the square and cube of the offset:
p1 = self.data.close[0] * (1.0 - self.p.limit) # 0.5% below
p2 = self.data.close[0] * (1.0 - 2 * 2 * self.p.limit) # 2% below
p3 = self.data.close[0] * (1.0 - 3 * 3 * self.p.limit) # 4.5% below
o1 = self.buy(exectype=bt.Order.Limit, price=p1, valid=valid1, size=1)
o2 = self.buy(exectype=bt.Order.Limit, price=p2, valid=valid2, oco=o1, size=1)
o3 = self.buy(exectype=bt.Order.Limit, price=p3, valid=valid3, oco=o1, size=1)
oco=o1 links the later orders into the first one’s group. The near order gets only 3 days of validity (limdays=3), the far ones get 1,000 — betting that shallow pullbacks come fast while deep ones are worth waiting for. After a fill, the position is held 10 bars and closed by time. The backtest ends at 99,936.20 with a Sharpe of -728 — an extreme value that is not a bug but the arithmetic of 1-share positions with sparse trades under tiny annualized volatility. The test’s own comment says it plainly: these numbers confirm the OCO cancellation mechanism works, not that the strategy profits. That is a regression test doing its actual job — validating framework behavior, not returns.
The Rest of the Bench¶
Stop order (
test_05): on the convertible-bond index, the fill triggersself.sell(exectype=bt.Order.Stop, price=buy_price * 0.97)insidenotify_order; a dead cross cancels the stop before the market close — cancel-then-close ordering is the classic detail of managing resting orders. 211 buys over the run, 106 stopped out.StopTrail (
test_42): descended from the official stoptrail sample, params keeptrailpercent=0.02; this version runs on cross-driven market orders (final 105,190.30, Sharpe 1.19). Rewriting it as a truesell(exectype=bt.Order.StopTrail, trailpercent=0.02)is the best exercise on this page.Order Target (
test_43): declare targets, not trades — odd months holddate/100percent, even months(31-date)/100, andorder_target_percentcomputes and places the difference. The on-ramp from “trade thinking” to “position thinking.”Order Close (
test_61):exectype=bt.Order.Closefills at the current bar’s close (paired withseteosbar(True)), removing the one-bar next-open delay; final value 102,995.50.
Run It Yourself¶
# The whole category (6 strategies, runonce/runnext dual-mode parity)
pytest tests/functional/strategies/order_types/ -v
# Just the bracket trio
pytest tests/functional/strategies/order_types/test_37_bracket_order_strategy.py -v
Why Study Order Types Here¶
Order semantics are where backtest fidelity quietly dies: whether a limit fills on a touch, the gap between a stop’s trigger and its fill price, the exact timing of OCO cancellations — all depend on broker-simulator precision. cloudQuant/backtrader freezes these behaviors into asserted baselines across 1,152 strategy regression tests, so any drift in order semantics trips an alarm; runonce/runnext dual-mode parity guarantees vectorized speed never changed a single fill. The pure-Python engine is 46% faster than the original, and the C++ backend (pip install back-trader-cpp) adds a median 128x speedup — enough to permute all six order types over the same data and find the execution details that belong to your strategy.
Find it useful? Star the project on GitHub. Start from the series overview for the full map. A deeper (Chinese) treatment lives here.
Risk disclaimer: for education and research only. Backtests use historical data and do not constitute investment advice; algorithmic trading carries substantial risk of loss.