Tutorial

Donchian Channel Breakout Strategy in Pine Script v6

Build a Donchian channel in Pine Script v6, test breakouts without repainting, and wrap them in a strategy with a channel and ATR exit.

•12 min read

Most breakout code you find online fails in the same quiet way. The script looks fine, the arrows line up with every new high on the chart, and then you notice that the trigger never fires on the bar where price actually breaks out, or that it fires on every bar of a trend because the channel keeps moving up with price. A Donchian channel is one of the simplest ideas in technical analysis, just the highest high and lowest low over a window, and it is also a very good way to learn how Pine Script handles history, repainting, and order timing.

This guide builds the channel, turns it into a breakout rule, wraps that rule in a Pine Script v6 strategy with an exit, and then walks through the mistakes that make a Donchian script behave differently from what you meant. Nothing here predicts price. It is a coding exercise, and the point is to end up with a script you understand line by line.

What a Donchian channel actually is

A Donchian channel has three lines. The upper line is the highest high of the last N bars. The lower line is the lowest low of the last N bars. The middle line, when you draw it, is the average of the two. Richard Donchian is credited with the idea, and the channel is the engine behind many classic trend-following rules, including the well-known 20 and 55 bar breakout systems associated with the Turtle traders.

Because the upper line is a rolling maximum, it can only do two things. It stays flat while the old high is still inside the window, or it steps up when a new high arrives, or it drops when the old high ages out of the window. That staircase shape is what makes the channel useful. When price closes beyond the previous staircase level, something unusual happened relative to the last N bars, and that is the whole definition of a breakout in this approach.

The default length most people reach for is 20, which on a daily chart is roughly one trading month. On a crypto chart, which trades every day, 20 daily bars is 20 calendar days. The length is the one input that changes the personality of the script, so we will make it an input from the start rather than hard coding it.

Drawing the channel in Pine Script v6

Pine Script gives you ta.highest() and ta.lowest(), which return the highest and lowest value of a series over a number of bars. Passing high into the first and low into the second gives you the channel directly. Here is a complete indicator.

pine
//@version=6
indicator("Donchian Channel", overlay = true)

length = input.int(20, "Length", minval = 2)

upper = ta.highest(high, length)
lower = ta.lowest(low, length)
basis = math.avg(upper, lower)

pUpper = plot(upper, "Upper", color = color.teal)
pLower = plot(lower, "Lower", color = color.red)
plot(basis, "Basis", color = color.gray)
fill(pUpper, pLower, color = color.new(color.teal, 92), title = "Channel fill")

Paste this into the Pine Editor and add it to a chart. You will see the staircase described above. Notice one thing about this version: the upper line on the current bar includes the current bar's high. If today's high is the highest in 20 bars, then the upper line is exactly today's high, and today's close can never be above it. That is the first trap, and it matters as soon as you try to detect a breakout.

Why the breakout test needs the previous bar's channel

To say that price broke above the channel, you compare the current close with the channel as it stood before this bar existed. If you compare close > upper using the version above, the condition can never be true, because upper already contains the current high and the close can never exceed the high of the same bar.

The fix is the history operator. Writing ta.highest(high, length)[1] asks for the value of the rolling maximum as it was on the previous bar, which covers the N bars that ended before the current one. Now close > upper means the current close is above the highest high of the previous N bars, which is the breakout you wanted.

pine
//@version=6
indicator("Donchian Breakout Triggers", overlay = true)

length = input.int(20, "Length", minval = 2)

upper = ta.highest(high, length)[1]
lower = ta.lowest(low, length)[1]

longBreak  = close > upper
shortBreak = close < lower

plot(upper, "Prior upper", color = color.teal)
plot(lower, "Prior lower", color = color.red)

plotshape(longBreak, "Long break", shape.triangleup, location.belowbar, color.teal, size = size.small)
plotshape(shortBreak, "Short break", shape.triangledown, location.abovebar, color.red, size = size.small)

alertcondition(longBreak, "Donchian long break", "Close broke above the prior channel high")
alertcondition(shortBreak, "Donchian short break", "Close broke below the prior channel low")

Now the triangles appear only on bars that close outside the previous channel. In a strong move you may still see several in a row, because each new bar can close above the prior bar's channel. That is correct behavior for this definition, and later we will make the strategy ignore the repeats.

Closed bars, realtime bars, and repainting

The breakout test uses close. On a historical bar the close is final, so the triangle you see on history is what the script computed. On the live bar, close is the current price and it changes with every tick. A triangle can appear mid-bar when price pokes above the level and then vanish if the bar closes back inside the channel. That is repainting in the plain sense: the live bar showed one thing and the finished bar shows another.

If you only want triggers that stay put, require a confirmed bar. The built-in barstate.isconfirmed is true on the last tick of a bar, which on historical bars is every bar. Changing the conditions to longBreak = close > upper and barstate.isconfirmed keeps the realtime behavior in line with history. We cover this variable in detail in barstate.isconfirmed in Pine Script: Signals That Stop Repainting, and the related alert timing problem in Why Your Pine Script Alert Fires on the Wrong Bar.

The trade-off is real. A confirmed trigger arrives at the close, so you act one bar later than the first touch. For a breakout rule that is usually acceptable, since many breakouts that touch a level intrabar fail to hold it at the close.

Turning the rule into a strategy

An indicator marks bars. A strategy places simulated orders that TradingView's Strategy Tester can report on. To convert the breakout, swap indicator() for strategy(), place entries with strategy.entry(), and add an exit. A breakout rule without an exit is half a system, and the exit is where most of the design choices live.

The classic pairing uses a shorter channel for the exit than for the entry. You enter long when price breaks the 20 bar high and exit when price falls through the 10 bar low. The shorter exit window lets a trend run while still getting you out when it fades. We will add one more protection, an ATR based stop, so a single failed breakout cannot sit open for a long time.

pine
//@version=6
strategy("Donchian Breakout Strategy", overlay = true,
     default_qty_type = strategy.percent_of_equity, default_qty_value = 10,
     commission_type = strategy.commission.percent, commission_value = 0.05,
     slippage = 2)

entryLen = input.int(20, "Entry channel length", minval = 2)
exitLen  = input.int(10, "Exit channel length", minval = 2)
atrLen   = input.int(14, "ATR length", minval = 1)
atrMult  = input.float(2.0, "ATR stop multiple", minval = 0.1, step = 0.1)
useShorts = input.bool(false, "Also trade the short side")

entryHigh = ta.highest(high, entryLen)[1]
entryLow  = ta.lowest(low, entryLen)[1]
exitLow   = ta.lowest(low, exitLen)[1]
exitHigh  = ta.highest(high, exitLen)[1]
atrValue  = ta.atr(atrLen)

longBreak  = close > entryHigh
shortBreak = close < entryLow

if longBreak and strategy.position_size <= 0
    strategy.entry("Long", strategy.long)

if useShorts and shortBreak and strategy.position_size >= 0
    strategy.entry("Short", strategy.short)

if strategy.position_size > 0
    longStop = math.max(exitLow, strategy.position_avg_price - atrMult * atrValue)
    strategy.exit("Long exit", "Long", stop = longStop)

if strategy.position_size < 0
    shortStop = math.min(exitHigh, strategy.position_avg_price + atrMult * atrValue)
    strategy.exit("Short exit", "Short", stop = shortStop)

plot(entryHigh, "Entry high", color = color.teal)
plot(exitLow, "Exit low", color = color.orange)

There are several details in this script worth slowing down for. The condition strategy.position_size <= 0 on the long entry means a new long order is only sent when you are flat or short, which is how the script ignores the repeated breakouts we saw earlier. Without that guard, strategy.entry() would be called on every qualifying bar. TradingView does not stack extra entries unless you enable pyramiding, but the guard keeps the intent obvious to the next person who reads the code.

The stop level takes the higher of two values for a long position: the lowest low of the exit window, and a fixed distance below the average entry price measured in ATRs. Whichever sits closer to the current price wins, so the stop is always the tighter of the two. For shorts the logic mirrors with math.min. The strategy.exit() call has to run on every bar while the position is open, which is why it lives inside the if strategy.position_size > 0 block rather than only on the entry bar.

Shorts are off by default through the useShorts input. Many assets have a long term upward drift, and a symmetric rule is a separate design decision, not a free addition. Turning the input on lets you see what the mirrored version does on whatever chart you are testing.

How orders fill, and why the numbers look slightly off

If you check your first trade on the chart, the entry does not happen at the breakout bar's close. By default a strategy evaluates on the close of a bar and fills the order on the next bar's open. The script above therefore enters at the open after the bar that broke the channel. In a breakout setup that gap can be large, and it is a normal and honest cost of the method.

You may be tempted to set process_orders_on_close = true to fill at the signal bar's close, which makes the entry price look cleaner. Be careful with it. It assumes you can transact at the closing price of a bar that you only recognized as a breakout once it closed, which is optimistic. We explain how the commission and slippage settings in the header shape results in How Slippage and Commission Actually Eat Your Returns. The strategy above sets a small percentage commission and two ticks of slippage so that the first run does not look better than it should. Treat those values as placeholders and replace them with your exchange's actual fee schedule.

Choosing the lengths without fooling yourself

Once the strategy runs, the temptation is to loop over lengths until the equity curve looks smooth. Resist it. Every combination you try is another chance of finding a setting that fit the past by luck. A few habits keep you honest.

First, change one input at a time and look at a range, not a single winner. If 20 works and 19 and 21 fall apart, the result probably depends on noise. If 15 through 30 all behave similarly, the idea is at least stable across settings.

Second, look at the number of trades before looking at anything else. A breakout rule on a daily chart with a 20 bar entry produces a modest number of trades per year, and a small sample tells you very little. The guide to every metric in the Pine Script Strategy Tester explains how to read trade counts, drawdown, and profit factor together, and maximum drawdown is worth reading before you trust any smooth looking curve.

Third, test on a period the settings were not tuned on. Pick the lengths using one stretch of history and then look at a later stretch without changing them. That is a manual form of an out-of-sample check, and it is the cheapest protection against fitting noise.

Common mistakes in Donchian scripts

A handful of errors account for most broken versions of this script.

The first is forgetting the [1], which we covered. The condition then never triggers, or in some variants it triggers on the wrong bar. The second is mixing the entry window and the exit window, for example using the same variable for both, so that the stop is the same line as the entry and the position closes immediately. Giving the two windows separate names, as above, prevents it.

The third is using ta.highest(close, length) when you mean the high. The two are different channels. Using closes gives a smoother, narrower band, and the breakout test then compares a close with prior closes. Either is a legitimate design, but mixing them by accident changes the results without any error message. The fourth is hard coding the length in several places. Put it in one input.int() so that changing it changes everything together.

The fifth is the opposite of the first. Someone sees a missing trigger, removes the [1] and adds >= instead of >. The condition close >= ta.highest(high, length) can only be true when the close equals the high of a bar that also set the new window high, which is a very different and much rarer event. Fix the shift, not the operator.

The last is ignoring warm up. The first length bars of the chart do not have a full window, so early values are based on fewer bars. Pine handles the calculation, but a trade on bar 12 of a 20 bar channel is not the rule you described. You can skip these bars with a condition such as bar_index >= entryLen.

Comparing the channel with Bollinger Bands

Donchian channels and Bollinger Bands both wrap price, but they measure different things. Bollinger Bands are built from a moving average and a standard deviation, so they widen when volatility rises and narrow when it falls. A Donchian channel is built from extremes, so it is defined by the single highest and lowest bar in the window. A Bollinger band can be touched without a new N bar high. A Donchian breakout by definition is a new N bar high.

For testing ideas, that difference is useful. Put both on the same chart and you can see when a Bollinger touch coincides with a Donchian breakout and when it does not. If you want a quick check of the Bollinger numbers, the free Bollinger Bands calculator shows the values and the Pine Script behind them, and the ATR calculator does the same for the volatility measure used in the stop above.

Adapting the script without starting over

The scripts above are a starting point. Real use means changes: a volume filter on the breakout, a different exit, a session restriction, alerts with custom text. In PineScripter, the AI code editor writes Pine Script v6 from a description and edits only the lines that change, so a request like "only take the long breakout when volume is above its 20 bar average" changes the entry condition and leaves the rest alone. The Code Changes view shows exactly which lines moved.

You can also run the indicator version on a chart inside PineScripter, step through it with Replay one bar at a time to see when each trigger fires, and use Indicator Analysis to look at what price did 1, 5, 10, and 20 bars after each trigger. Those describe the past only. PineScripter does not simulate trades or report returns, so run the strategy version in TradingView's Strategy Tester when you want trade level statistics. Plans and credits are listed on the pricing page.

PineScripter is a coding tool for Pine Script and is not a financial advisor. Nothing in this post is a recommendation to buy or sell anything, and past results of any rule do not predict future results.

Frequently asked questions

What length should a Donchian channel use?

20 is the common starting point for the entry and 10 for the exit, but length is an input you should test across a range instead of picking one value that fits past data.

Why does my Donchian breakout never trigger?

The rolling highest high includes the current bar, so the close can never exceed it. Shift it with [1] so you compare against the prior bars.

Does the Donchian breakout repaint?

On the live bar a close-based trigger can appear and disappear. Add barstate.isconfirmed to keep it fixed once the bar closes.

Where does the strategy enter a trade?

By default orders fill on the open of the bar after the breakout bar closes, so the fill price can differ from the channel level.

Can PineScripter backtest this strategy?

No. PineScripter writes and edits the Pine Script and can run it on a chart, but it does not simulate trades. Use TradingView's Strategy Tester for trade statistics.

Describe your indicator in plain English and get Pine Script that compiles →


Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.