Donchian Channels in Python — Coding It from Scratch (pandas)

The channel, the prior-bar breakout, and a Turtle-style exit

Donchian Channels computed in Python
Donchian Channels computed in Python

Donchian Channels are a rolling max and min — two lines of pandas. The only thing to get right is the prior-bar shift for breakout signals, as the explainer warned. Here’s the code.

What you’ll need

  • Python 3.9+, pandas
  • high, low series.

The channel

import pandas as pd


def donchian(high, low, period: int = 20):
    upper = high.rolling(period).max()
    lower = low.rolling(period).min()
    middle = (upper + lower) / 2
    return pd.DataFrame({"upper": upper, "middle": middle, "lower": lower})

rolling(period).max() and .min() are the whole indicator — the stepped bands from the output chart.

The breakout signal (mind the shift)

For a breakout you must compare price to the channel excluding the current bar — otherwise today’s high is inside its own channel and can never break out:

dc = donchian(df["high"], df["low"], 20)

# Prior channel: shift the bands back one bar.
prior_upper = dc["upper"].shift(1)
prior_lower = dc["lower"].shift(1)

df["breakout_long"]  = df["close"] > prior_upper   # new 20-bar high
df["breakout_short"] = df["close"] < prior_lower

breakout_long fires on a genuine new 20-bar high — the classic Donchian entry to backtest in AlgoGen.

A Turtle-style system: asymmetric channels

The Turtles entered on a long channel and exited on a shorter one:

entry = donchian(df["high"], df["low"], 20)
exit_ = donchian(df["high"], df["low"], 10)
go_long   = df["close"] > entry["upper"].shift(1)
exit_long = df["close"] < exit_["lower"].shift(1)   # 10-bar low ends the long

Gotchas

  • Shift for signals, not for display. The plotted channel uses the current bar; the signal uses the prior-bar channel. Mixing these up is the #1 Donchian bug — either your breakouts never fire, or they look like they trigger every bar.
  • Highs and lows, not closes. The channel is built from high and low; some breakout variants trigger on a close beyond the channel (fewer false breaks) vs an intrabar high/low touch (earlier, noisier). Decide which.
  • Range whipsaw. Expect many false breakouts in sideways markets — that’s the nature of the tool, not a bug.

The lazy one-liner

import pandas_ta as ta
df.ta.donchian(lower_length=20, upper_length=20, append=True)   # DCL/DCM/DCU

Same indicator elsewhere: MQL5, Pine Script, EasyLanguage, NinjaScript.


This post is educational, not financial advice. Indicators describe the past; they don’t predict the future. Backtest anything before you risk real money on it.

Sources and further reading

  1. Windowing operations (pandas documentation)

Historical research from the Algogen archive. Not investment advice.

Blog

Have a strategy idea?

Describe it in plain English, preview where your rules fire, then decide whether the evidence is worth a backtest.

Keep me postedHow it works