#!/usr/bin/env python
"""ATR Indicator Module - Average True Range.
This module provides the ATR (Average True Range) indicator developed by
J. Welles Wilder, Jr. for measuring market volatility.
Classes:
TrueHigh: Records the true high for ATR calculation.
TrueLow: Records the true low for ATR calculation.
TrueRange: Calculates the True Range.
AverageTrueRange: Calculates the Average True Range (alias: ATR).
Example:
class MyStrategy(bt.Strategy):
def __init__(self):
self.atr = bt.indicators.ATR(self.data, period=14)
def next(self):
if self.atr[0] > self.atr[-1] * 1.5:
self.buy()
"""
import math
from . import Indicator, MovAv
[docs]
class TrueHigh(Indicator):
"""
Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in
Technical Trading Systems"* for the ATR
Records the "true high" which is the maximum of today's high and
yesterday's close
Formula:
- truehigh = max (high, close_prev)
See:
- http://en.wikipedia.org/wiki/Average_true_range
"""
lines = ("truehigh",)
def __init__(self):
"""Initialize the TrueHigh indicator.
Adds a minimum period of 2 to access previous close.
"""
super().__init__()
self.addminperiod(2)
[docs]
def next(self):
"""Calculate true high: max(high, previous_close)."""
self.lines.truehigh[0] = max(self.data.high[0], self.data.close[-1])
[docs]
def once(self, start, end):
"""Calculate true high in runonce mode."""
high_array = self.data.high.array
close_array = self.data.close.array
larray = self.lines.truehigh.array
while len(larray) < end:
larray.append(0.0)
if len(high_array) > 0 and len(larray) > 0:
larray[0] = high_array[0] if len(high_array) > 0 else 0.0
for i in range(1, min(end, len(high_array), len(close_array))):
high_val = high_array[i] if i < len(high_array) else 0.0
prev_close = close_array[i - 1] if i > 0 and i - 1 < len(close_array) else 0.0
if i < len(larray):
larray[i] = max(high_val, prev_close)
[docs]
class TrueLow(Indicator):
"""
Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in
Technical Trading Systems"* for the ATR
Records the "true low" which is the minimum of today's low and
yesterday's close
Formula:
- truelow = min (low, close_prev)
See:
- http://en.wikipedia.org/wiki/Average_true_range
"""
lines = ("truelow",)
def __init__(self):
"""Initialize the TrueLow indicator.
Adds a minimum period of 2 to access previous close.
"""
super().__init__()
self.addminperiod(2)
[docs]
def next(self):
"""Calculate true low: min(low, previous_close)."""
self.lines.truelow[0] = min(self.data.low[0], self.data.close[-1])
[docs]
def once(self, start, end):
"""Calculate true low in runonce mode."""
low_array = self.data.low.array
close_array = self.data.close.array
larray = self.lines.truelow.array
while len(larray) < end:
larray.append(0.0)
if len(low_array) > 0 and len(larray) > 0:
larray[0] = low_array[0] if len(low_array) > 0 else 0.0
for i in range(1, min(end, len(low_array), len(close_array))):
low_val = low_array[i] if i < len(low_array) else 0.0
prev_close = close_array[i - 1] if i > 0 and i - 1 < len(close_array) else 0.0
if i < len(larray):
larray[i] = min(low_val, prev_close)
[docs]
class TrueRange(Indicator):
"""
Defined by J. Welles Wilder, Jr. in 1978 in his book New Concepts in
Technical Trading Systems.
Formula:
- max(high - low, abs (high - prev_close), abs(prev_close - low)
Which can be simplified to
- Max(high, prev_close) - min(low, prev_close)
See:
- http://en.wikipedia.org/wiki/Average_true_range
The idea is to take the previous close into account to calculate the range
if it yields a larger range than the daily range (High - Low)
"""
alias = ("TR",)
lines = ("tr",)
def __init__(self):
"""Initialize the TrueRange indicator.
Adds a minimum period of 2 to access previous close.
"""
super().__init__()
self.addminperiod(2)
[docs]
def next(self):
"""Calculate true range: truehigh - truelow."""
truehigh = max(self.data.high[0], self.data.close[-1])
truelow = min(self.data.low[0], self.data.close[-1])
self.lines.tr[0] = truehigh - truelow
[docs]
def once(self, start, end):
"""Calculate true range in runonce mode."""
high_array = self.data.high.array
low_array = self.data.low.array
close_array = self.data.close.array
larray = self.lines.tr.array
while len(larray) < end:
larray.append(0.0)
if len(high_array) > 0 and len(low_array) > 0 and len(larray) > 0:
larray[0] = (
high_array[0] - low_array[0] if len(high_array) > 0 and len(low_array) > 0 else 0.0
)
for i in range(1, min(end, len(high_array), len(low_array), len(close_array))):
high_val = high_array[i] if i < len(high_array) else 0.0
low_val = low_array[i] if i < len(low_array) else 0.0
prev_close = close_array[i - 1] if i > 0 and i - 1 < len(close_array) else 0.0
truehigh = max(high_val, prev_close)
truelow = min(low_val, prev_close)
if i < len(larray):
larray[i] = truehigh - truelow
[docs]
class AverageTrueRange(Indicator):
"""
Defined by J. Welles Wilder, Jr. in 1978 in his book *"New Concepts in
Technical Trading Systems"*.
The idea is to take the close into account to calculate the range if it
yields a larger range than the daily range (High - Low)
Formula:
- SmoothedMovingAverage(TrueRange, period)
See:
- http://en.wikipedia.org/wiki/Average_true_range
"""
alias = ("ATR",)
lines = ("atr",)
params = (("period", 14), ("movav", MovAv.Smoothed))
def _plotlabel(self):
plabels = [self.p.period]
plabels += [self.p.movav] * self.p.notdefault("movav")
return plabels
def __init__(self):
"""Initialize the ATR indicator.
Sets up Wilder's smoothing factors.
"""
super().__init__()
self.addminperiod(self.p.period + 1)
# SMMA alpha for Wilder's smoothing
self.alpha = 1.0 / self.p.period
self.alpha1 = 1.0 - self.alpha
def _calc_tr(self, high, low, prev_close):
"""Calculate True Range
Args:
high: Current high price.
low: Current low price.
prev_close: Previous close price.
Returns:
float: True range value.
"""
truehigh = max(high, prev_close)
truelow = min(low, prev_close)
return truehigh - truelow
[docs]
def nextstart(self):
"""Seed ATR with SMA of first period TR values."""
# Seed with SMA of first period TR values
period = self.p.period
tr_sum = 0.0
for i in range(period):
if i == 0:
tr = self.data.high[-i] - self.data.low[-i]
else:
tr = self._calc_tr(self.data.high[-i], self.data.low[-i], self.data.close[-i - 1])
tr_sum += tr
self.lines.atr[0] = tr_sum / period
[docs]
def next(self):
"""Calculate ATR for the current bar.
Uses smoothed moving average: ATR = prev_ATR * alpha1 + TR * alpha
"""
tr = self._calc_tr(self.data.high[0], self.data.low[0], self.data.close[-1])
self.lines.atr[0] = self.lines.atr[-1] * self.alpha1 + tr * self.alpha
[docs]
def once(self, start, end):
"""Calculate ATR in runonce mode."""
high_array = self.data.high.array
low_array = self.data.low.array
close_array = self.data.close.array
larray = self.lines.atr.array
period = self.p.period
alpha = self.alpha
alpha1 = self.alpha1
while len(larray) < end:
larray.append(0.0)
# Pre-fill warmup with NaN (indices 0 to period-1)
for i in range(min(period, len(high_array))):
if i < len(larray):
larray[i] = float("nan")
# CRITICAL FIX: Always seed at index `period` (first valid ATR position)
# regardless of the `start` parameter. The ATR needs `period` TR values,
# and TR starts from index 1 (needs close[-1]), so first valid ATR is at index `period`.
seed_idx = period
if seed_idx < len(high_array) and seed_idx < len(low_array) and seed_idx < len(close_array):
tr_sum = 0.0
for j in range(period):
# Use TR values from indices 1 to period (inclusive)
idx = j + 1 # Start from index 1 (first valid TR)
if idx < len(high_array) and idx < len(low_array) and idx - 1 < len(close_array):
truehigh = max(high_array[idx], close_array[idx - 1])
truelow = min(low_array[idx], close_array[idx - 1])
tr = truehigh - truelow
tr_sum += tr
prev_atr = tr_sum / period
if seed_idx < len(larray):
larray[seed_idx] = prev_atr
else:
prev_atr = 0.0
# Calculate ATR using SMMA for all subsequent bars
for i in range(seed_idx + 1, min(end, len(high_array), len(low_array), len(close_array))):
truehigh = max(high_array[i], close_array[i - 1])
truelow = min(low_array[i], close_array[i - 1])
tr = truehigh - truelow
if i > 0 and i - 1 < len(larray):
prev_val = larray[i - 1]
if not (isinstance(prev_val, float) and math.isnan(prev_val)):
prev_atr = prev_val
prev_atr = prev_atr * alpha1 + tr * alpha
if i < len(larray):
larray[i] = prev_atr
TR = TrueRange
ATR = AverageTrueRange