NormalizedRange — Normalized Range
The Donchian channel width as a percentage of the channel midpoint. The same measurement as PriceChannelWidth, rescaled so it means the same thing on every instrument.
NormalizedRange is the identifier a strategy writes; Normalized Range is the label the chart’s indicator picker shows. Single value — .value is the only accessor.
Signature
NormalizedRange(period, on=<source>)| Argument | Type | Required | Notes |
|---|---|---|---|
period |
int Param or literal | Yes | Lookback window. Default 20, bounds 2–200. |
on= |
symbol, OHLCV field, or indicator | No | Source. Defaults to the trade symbol’s close. |
Formula
Over the last N values of the source, with hi = max, lo = min:
$$ \text{mid} = \frac{\text{hi} + \text{lo}}{2}, \qquad \text{NormalizedRange} = \frac{\text{hi} - \text{lo}}{\max(\text{mid},\ 0.0001)} \times 100 $$
The result is a percentage: 3.0 means the last N closes spanned 3% of their own midpoint. The denominator is floored at a small epsilon so a midpoint at or near zero cannot divide by zero.
Warmup
period bars. Before the window fills the indicator returns 0 — unlike the raw channel indicators, which report their partial-window value from bar one. A strategy never observes it (execute() waits until every indicator is ready); it is what the chart plots on the leading bars.
Reading the value
nr.value # percent, e.g. 3.0 means a 3%-wide channelnr.value_at(1)nr < 2 # quietnr > 8 # widenr.crossed_above(5)Example
class QuietRangeEntry(Strategy): """Enter trends only while the normalized channel is tight.""" period = Param(20, min=2, max=200) calm = Param(4, min=1, max=50) trade = Symbol() sizing = FixedCapital()
def execute(self): nr = NormalizedRange(self.period, on=self.trade) sma = SMA(self.period, on=self.trade) self.stop_loss(2.0)
if nr.value < self.calm and self.trade.close > sma.value and self.trade.is_flat: self.buy(self.trade, reason=f"Calm channel ({nr.value:.1f}%)")
if nr.value > self.calm * 3 and self.trade.is_long: self.sell(self.trade, self.trade.position, reason="Range expanded")Notes
- This is the volatility measure to optimize over. A threshold like “below 3%” transfers between symbols and between eras; the same threshold in price units does not.
- The value is already a percentage — do not multiply by 100 again.
- Careful when chaining. The epsilon floor protects against a zero midpoint, not a negative one. Chained onto a source that goes negative (an oscillator, a spread), a negative midpoint is clamped to
0.0001and the reading explodes to an enormous positive number. Chain onto price-like sources. - Two closes set the whole value, as with the raw channel width. For a measure that weighs every bar, use
StdDevorATR; for annualized volatility,VolatilityStdDev.