Strategy: Donchian / Opening-Range Breakout
Two breakout classics - the Donchian channel and the opening-range breakout - coded and ready to test.
- ·Donchian highest/lowest
- ·Channel breakout entries
- ·Opening-range levels
- ·Time windows intraday
- ·Stops on a breakout
- ·Backtesting breakouts
Some of the most durable systems in trading history are breakouts, and the logic is disarmingly simple: when price pushes past a level it could not cross before, momentum has likely shifted, so you join the move. The Turtles built a fortune on exactly this idea. In this final playbook chapter we code two breakout classics - the Donchian channel, which trades a break of the highest high and lowest low over a lookback window, and the opening-range breakout (ORB), which trades a break of the first few minutes' range of the session.
The Donchian channel behind this breakout is the very idea the legendary "Turtle Traders" rode in the 1980s: buy the highest high of the last N days, and let the trend do the work.
They are cousins. Donchian is a swing-trading workhorse that works on daily bars and never needs a clock. ORB is its intraday sibling, anchored to the morning and governed by TimeNum(). Build both and you will own the two ends of the breakout family.
The Donchian channel
Richard Donchian's channel is just two lines: the highest high and the lowest low over the last N bars. A close - or here, a high - that crosses the upper line is a fresh breakout to the upside; a break of the lower line ends the trade.
The one subtlety that beginners miss: you must take the channel from the previous bar with Ref(..., -1). The current bar's own high is part of HHV(High, N), so without the shift the price could never "break out" of a level that always includes itself. Shifting back one bar fixes that and makes the signal non-repainting in the bargain.
_SECTION_BEGIN("Donchian Breakout System");
SetChartOptions(0, chartShowArrows | chartShowDates);
_N(Title = StrFormat("{{NAME}} - {{INTERVAL}} {{DATE}} O %g H %g L %g C %g (%.1f%%)",
O, H, L, C, SelectedValue(ROC(C, 1))));
Plot(Close, "Close", colorDefault, styleCandle | styleNoTitle | GetPriceStyle());
Length = Param("Channel Length", 20, 5, 100, 1);
// Channel from the PREVIOUS bar - so a breakout is a genuine fresh event
upper = Ref(HHV(High, Length), -1);
lower = Ref(LLV(Low, Length), -1);
Plot(upper, "Upper", colorGreen, styleThick);
Plot(lower, "Lower", colorRed, styleThick);
// Long on a break of the upper channel; exit on a break of the lower
Buy = Cross(High, upper);
Sell = Cross(lower, Low);
Buy = ExRem(Buy, Sell);
Sell = ExRem(Sell, Buy);
// Fill at the breakout level, gap-safe (never better than the open)
SetTradeDelays(0, 0, 0, 0);
BuyPrice = Max(Open, upper);
SellPrice = Min(Open, lower);
SetPositionSize(100, spsPercentOfEquity);
PlotShapes(IIf(Buy, shapeUpArrow, shapeNone), colorLime, 0, Low, -20);
PlotShapes(IIf(Sell, shapeDownArrow, shapeNone), colorOrange, 0, High, 20);
_SECTION_END();
BuyPrice = Max(Open, upper) is the honest way to fill a breakout. The idea is a stop order resting at the breakout level, but if price gaps straight through it at the open, you would never get filled at the level - you would pay the open. Taking the worse of the two keeps the backtest from claiming fills that real life would never give you.
The opening-range breakout
The ORB captures the high and low of the first few minutes of the session, then trades the first break of that range. The intuition is that the opening auction sets the day's battle lines; a decisive break of them often runs. Everything hinges on the same session-anchoring trick from the VWAP chapter, plus a frozen snapshot of the range.
_SECTION_BEGIN("Opening Range Breakout");
SetChartOptions(0, chartShowArrows | chartShowDates);
Plot(Close, "Close", colorDefault, styleCandle | styleNoTitle | GetPriceStyle());
// First N bars of the session define the range. On 5-min bars, 3 bars = 15 min.
ORBars = Param("Opening Range Bars", 3, 1, 12, 1);
newDay = Day() != Ref(Day(), -1);
barNum = 1 + BarsSince(newDay); // bar number within the session
// Freeze the range high/low once the opening range completes
ORHigh = ValueWhen(barNum == ORBars, HighestSince(newDay, High));
ORLow = ValueWhen(barNum == ORBars, LowestSince(newDay, Low));
Plot(IIf(barNum > ORBars, ORHigh, Null), "OR High", colorGreen, styleThick);
Plot(IIf(barNum > ORBars, ORLow, Null), "OR Low", colorRed, styleThick);
tn = TimeNum();
tradeWin = barNum > ORBars AND tn <= 143000; // break out after the range, before 14:30
squareOff = tn >= 151500;
Buy = Cross(High, ORHigh) AND tradeWin;
Sell = Cross(ORLow, Low) OR squareOff;
Buy = ExRem(Buy, Sell);
Sell = ExRem(Sell, Buy);
SetTradeDelays(0, 0, 0, 0);
BuyPrice = Max(Open, ORHigh);
SellPrice = Min(Open, ORLow);
SetPositionSize(100, spsPercentOfEquity);
PlotShapes(IIf(Buy, shapeUpArrow, shapeNone), colorLime, 0, Low, -20);
PlotShapes(IIf(Sell, shapeDownArrow, shapeNone), colorOrange, 0, High, 20);
_SECTION_END();
HighestSince(newDay, High) gives the highest high since the session opened; sampled exactly on the bar where barNum == ORBars, that is the opening-range high. ValueWhen then holds that frozen value across the rest of the day, so ORHigh stays put as a level to break - it does not creep upward as the day extends.
Match ORBars to your bar size. Three bars of 5-minute data is the popular 15-minute opening range; on 15-minute bars, set ORBars to 1 for the same effect. The break window (tn <= 143000) stops you from chasing a "breakout" in the last hour, which is usually noise, and the square-off flattens you before the close.
Backtesting breakouts honestly
Breakouts are the systems where sloppy fills flatter a backtest most. By definition you are entering at the exact moment price is moving fastest, so the gap between the level and your real fill - slippage - is wider than for a mean-reversion entry. Two defences:
First, the Max(Open, upper) / Min(Open, lower) fills above already stop the backtest from pretending you bought at a level the market gapped past. Second, set a realistic slippage and brokerage in the Analysis settings, then re-read the report. A breakout system that only works at zero cost is not a system.
Breakouts also produce false breaks - price pokes through the level, triggers you in, then collapses back. That is the cost of admission for catching the real moves, and it shows up as a string of small losses in the trade list. Judge a breakout over a long, varied history, add a stop, and always rehearse in sandbox trading (analyzer mode in OpenAlgo) before going live. This is education, not advice.
Try it yourself
- Run the Donchian system on NIFTY daily data with a 20-bar channel, then try 55 bars (the classic Turtle length) and compare.
- Add a 100-day EMA filter so you only take Donchian longs when price is above it - does the trend filter cut the false breaks?
- Run the ORB on BANKNIFTY 5-minute data with
ORBars = 3, then withORBars = 6, and see which range the instrument respects. - Add a fixed stop below
ORLowfor longs and measure the effect on drawdown.
Recap
- A Donchian channel is the highest high and lowest low over N bars; take it from the previous bar with
Ref(..., -1)so a breakout is a genuine event. - An opening-range breakout freezes the first few minutes' high and low with
BarsSince,HighestSinceandValueWhen, then trades a break within a time window governed byTimeNum(). - Fill breakouts gap-safely with
Max(Open, level)and include realistic slippage, because breakout entries suffer the worst fills. - Expect false breaks; manage them with a stop and a trend filter, and validate everything in sandbox first.
That completes the strategy playbook - five systems you can run, tune and trust. Next we take a system live, starting with reading a higher timeframe on a lower-timeframe chart.