What is inside the channel window?
The calculation uses chart-bar highs and lows, with length 20 in the copied example. Pine's one-argument highest and lowest calls use high and low respectively. There is no daily request: 20 means 20 chart bars. Session breaks and partial bars make fixed conversions from four-hour bars to trading days unreliable.
TradingView attributes the channel to Richard Donchian and describes the same extreme-price formula. This page explains the actual indicator calculation and a separate research specification; it does not claim to reproduce a complete Turtle trading system or its performance.
For a fully populated window, the upper and lower lines are extreme values in the input series. They are not necessarily tradable prints if the input chart is synthetic or adjusted. The middle line is the average of those two extremes only. It is neither the sample mean nor the median of the closes.
Why the current close cannot be above the current upper band
On valid OHLC bars, close ≤ high ≤ Upper. Likewise, close ≥ low ≥ Lower. At a new high the upper band moves to include it; price can equal that band, but a strict close above it is impossible. The same logic applies to a strict low or close below the current lower band.
A research rule can instead compare the current close with Upper from the previous bar. That previous value contains the N completed highs before the current bar. A high above that prior value is a wick breakout; a close above it is a close breakout. Equality does not pass a strict rule.
If the current close exceeds the previous upper band, it already exceeds the previous close because that prior band includes the previous high. Adding “current close greater than previous close” is therefore redundant under these valid-data assumptions. It is not an independent momentum filter.
Five bars show both a rolling-window change and a breakout
These hypothetical bars use N = 3. The first complete current window is at C. Prior thresholds are shown only after that complete window exists.
| Bar | High | Low | Close | Current upper | Current lower | Basis | Width | Prior upper |
|---|---|---|---|---|---|---|---|---|
| A | 110 | 90 | 100 | — | — | — | — | — |
| B | 104 | 98 | 102 | — | — | — | — | — |
| C | 105 | 99 | 103 | 110 | 90 | 100 | 20 | — |
| D | 106 | 100 | 105.5 | 106 | 98 | 102 | 8 | 110 |
| E | 107 | 101 | 106.5 | 107 | 99 | 103 | 8 | 106 |
At D, A leaves the window. The channel shrinks from width 20 to 8 even though the close rises from 103 to 105.5. No prior-upper breakout occurs: 105.5 is still below 110. The new basis is 102, while the three closes 102, 103 and 105.5 have a mean of 103.5 and median of 103.
At E, close 106.5 is above the prior upper value 106. It is still below the current upper value 107. These are compatible observations, not a repainting contradiction. If E instead closed exactly at 106, its high would still exceed the prior band but the strict close-breakout rule would fail.
Pine Script v6: plotting the current-inclusive channel
The retained source contains the generated helper and plotting calls. The time-range and timeframe conversion helpers are not called by the Donchian calculation; their presence does not apply a session filter or request another timeframe. The indicator plots an orange basis, blue extremes and a blue fill with transparency 95.
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Pineify
//======================================================================//
// ____ _ _ __ //
// | _ (_)_ __ ___(_)/ _|_ _ //
// | |_) | | '_ \ / _ \ | |_| | | | //
// | __/| | | | | __/ | _| |_| | //
// |_| |_|_| |_|\___|_|_| \__, | //
// |___/ //
//======================================================================//
//@version=6
indicator(title="Donchian Channel", overlay=true, max_labels_count=500)
//#region —————————————————————————————————————————————————— Custom Code
//#endregion ————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Common Dependence
p_comm_time_range_to_unix_time(string time_range, int date_time = time, string timezone = syminfo.timezone) =>
int start_unix_time = na
int end_unix_time = na
int start_time_hour = na
int start_time_minute = na
int end_time_hour = na
int end_time_minute = na
if str.length(time_range) == 11
// Format: hh:mm-hh:mm
start_time_hour := math.floor(str.tonumber(str.substring(time_range, 0, 2)))
start_time_minute := math.floor(str.tonumber(str.substring(time_range, 3, 5)))
end_time_hour := math.floor(str.tonumber(str.substring(time_range, 6, 8)))
end_time_minute := math.floor(str.tonumber(str.substring(time_range, 9, 11)))
else if str.length(time_range) == 9
// Format: hhmm-hhmm
start_time_hour := math.floor(str.tonumber(str.substring(time_range, 0, 2)))
start_time_minute := math.floor(str.tonumber(str.substring(time_range, 2, 4)))
end_time_hour := math.floor(str.tonumber(str.substring(time_range, 5, 7)))
end_time_minute := math.floor(str.tonumber(str.substring(time_range, 7, 9)))
start_unix_time := timestamp(timezone, year(date_time, timezone), month(date_time, timezone), dayofmonth(date_time, timezone), start_time_hour, start_time_minute, 0)
end_unix_time := timestamp(timezone, year(date_time, timezone), month(date_time, timezone), dayofmonth(date_time, timezone), end_time_hour, end_time_minute, 0)
[start_unix_time, end_unix_time]
p_comm_time_range_to_start_unix_time(string time_range, int date_time = time, string timezone = syminfo.timezone) =>
int start_time_hour = na
int start_time_minute = na
if str.length(time_range) == 11
// Format: hh:mm-hh:mm
start_time_hour := math.floor(str.tonumber(str.substring(time_range, 0, 2)))
start_time_minute := math.floor(str.tonumber(str.substring(time_range, 3, 5)))
else if str.length(time_range) == 9
// Format: hhmm-hhmm
start_time_hour := math.floor(str.tonumber(str.substring(time_range, 0, 2)))
start_time_minute := math.floor(str.tonumber(str.substring(time_range, 2, 4)))
timestamp(timezone, year(date_time, timezone), month(date_time, timezone), dayofmonth(date_time, timezone), start_time_hour, start_time_minute, 0)
p_comm_time_range_to_end_unix_time(string time_range, int date_time = time, string timezone = syminfo.timezone) =>
int end_time_hour = na
int end_time_minute = na
if str.length(time_range) == 11
end_time_hour := math.floor(str.tonumber(str.substring(time_range, 6, 8)))
end_time_minute := math.floor(str.tonumber(str.substring(time_range, 9, 11)))
else if str.length(time_range) == 9
end_time_hour := math.floor(str.tonumber(str.substring(time_range, 5, 7)))
end_time_minute := math.floor(str.tonumber(str.substring(time_range, 7, 9)))
timestamp(timezone, year(date_time, timezone), month(date_time, timezone), dayofmonth(date_time, timezone), end_time_hour, end_time_minute, 0)
p_comm_timeframe_to_seconds(simple string tf) =>
float seconds = 0
tf_lower = str.lower(tf)
value = str.tonumber(str.substring(tf_lower, 0, str.length(tf_lower) - 1))
if str.endswith(tf_lower, 's')
seconds := value
else if str.endswith(tf_lower, 'd')
seconds := value * 86400
else if str.endswith(tf_lower, 'w')
seconds := value * 604800
else if str.endswith(tf_lower, 'm')
seconds := value * 2592000
else
seconds := str.tonumber(tf_lower) * 60
seconds
p_custom_sources() =>
[open, high, low, close, volume]
//#endregion —————————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Ta Dependence
p_ta_dc(simple int length) =>
lower = ta.lowest(length)
upper = ta.highest(length)
basis = math.avg(upper, lower)
[basis, upper, lower]
//#endregion —————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Constants
// Input Groups
string P_GP_1 = ""
//#endregion —————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Inputs
//#endregion ———————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Price Data
//#endregion ———————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Indicators
[p_ind_1_basis, p_ind_1_upper, p_ind_1_lower] = p_ta_dc(20) // DC
//#endregion ———————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Conditions
//#endregion ———————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Indicator Plots
// DC
plot(p_ind_1_basis, "DC - Basis", color.rgb(255, 109, 0, 0), 1)
p_ind_1_upper_plot = plot(p_ind_1_upper, "DC - Upper", color.rgb(41, 98, 255, 0), 1)
p_ind_1_lower_plot = plot(p_ind_1_lower, "DC - Lower", color.rgb(41, 98, 255, 0), 1)
fill(p_ind_1_upper_plot, p_ind_1_lower_plot, title = "DC - Background", color=color.rgb(33, 150, 243, 95))
//#endregion ————————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Custom Plots
//#endregion —————————————————————————————————————————————————————————————
//#region —————————————————————————————————————————————————— Alert
//#endregion ——————————————————————————————————————————————————————The length is fixed at 20; there is no input control, marker, alert condition or strategy order. The code does not explicitly enforce N consecutive valid high/low pairs before plotting. If a strict full-window policy matters, add that as a separate requirement rather than assuming an early line proves complete history.
The live high and low can change before the bar closes, so the current channel can change too. A prior completed boundary is useful for an observation-time rule, but the live close comparison still needs bar confirmation if the event is defined at close.

The retained illustration shows the overlay context, not the hypothetical five-bar calculation or a measured trading result.
What changing length can prove
At the same time, a longer complete window contains every observation in the shorter one. Its maximum cannot be lower and its minimum cannot be higher. Its width is therefore at least as large. The widths can be equal when both windows contain the same extremes.
That nesting property gives no fixed relationship between length and the number of trades, false breakouts or returns. A new high can update every length immediately; an old high remains until it expires. There is no universal “medium lag” for Donchian versus “low lag” for Bollinger Bands or Keltner Channels.
A flat positive source window can produce identical upper and lower values. A wide channel can reflect one old extreme rather than continuing high volatility. A narrow channel does not determine when expansion will occur or which direction it will take.
Compare mechanisms without ranking trading outcomes
- Donchian width is the highest high minus lowest low over its window. The basis need not be near the average close.
- A conventional close-based Bollinger calculation uses a moving-average basis and source standard deviation. The same highs and lows can have different closes and different Bollinger values while leaving Donchian values unchanged.
- An ATR-based Keltner variant combines a chosen average with a multiple of smoothed true range. Its initialization, source and multiplier are part of the specification; it is not interchangeable with an extreme-price window.
None of these mechanisms establishes a best asset, timeframe, success rate or statistical confidence interval. Adding RSI, ADX or reported volume introduces additional conditions; their benefit needs an actual comparison with unchanged execution assumptions.
A reproducible breakout research specification
- Use standard OHLC, a named feed and session, N = 20 as an explicit starting example, and N complete preceding bars. Keep the plotted current channel separate from the prior entry boundary.
- While flat, define a long candidate as a confirmed close strictly above the previous upper band. Define a short candidate as a confirmed close strictly below the previous lower band. Decide explicitly whether only the first qualifying bar is allowed.
- Choose exits and order timing before measuring results. One possible experiment exits a long after a confirmed close below the previous ten-bar low and a short above the previous ten-bar high. Those are new rules, not part of the displayed indicator.
- State costs, position sizing and gap handling. A hypothetical signal close 106.5 and stop reference 103 imply 3.5 units of planned distance; a next-open entry at 108 makes it 5. A later stop fill at 101 would lose 7 per unit before costs.
- Test the five-bar fixture, equality and a flat interval first. Then compare parameter choices on development data and reserve separate data for evaluation. Report all tried variants rather than selecting only the favorable one.
If a squeeze state arms a later breakout, specify its expiry and reset conditions. Requiring a squeeze and a breakout on the same bar is a different experiment. ATR falling after a peak does not prove a price trend has ended.
Request inputs and diagnostic output
The Visual Editor exposes a Donchian length before generation. For a version with chart inputs or alerts, the AI Coding Agent can help draft the changes. Check both syntax and bar-by-bar values; generation is not a guarantee of execution or returns.
Keep the current-inclusive Donchian plots. Add a positive integer length input, require a complete prior window for signals, and show the prior upper and lower thresholds separately. Add closed-bar alert conditions using those prior thresholds. Do not compare close with the current-inclusive extreme or add strategy orders without defined exits and costs.Open Pine Script AI Coding Agent