Problem Description
Solution
Here is the Python code to calculate the Average True Range (ATR) in QuantConnect.
I have provided two approaches within the algorithm:
- Built-in Indicator (Recommended): The most efficient way to stream ATR values during a backtest or live trading.
- Manual Calculation (Pandas): A custom function to calculate ATR from a raw DataFrame of historical prices. This is useful for research or batch processing.
QuantConnect Strategy Code
# region imports
from AlgorithmImports import *
import pandas as pd
import numpy as np
# endregion
class AverageTrueRangeCalculation(QCAlgorithm):
def initialize(self):
self.set_start_date(2023, 1, 1)
self.set_end_date(2024, 1, 1)
self.set_cash(100000)
self.symbol = self.add_equity("SPY", Resolution.DAILY).symbol
self.period = 14
# ------------------------------------------------------------
# METHOD 1: Built-in Indicator (Recommended for Streaming)
# ------------------------------------------------------------
# Automatically updates with every new data point.
# Uses Wilder's Smoothing by default.
self.atr_indicator = self.ATR(self.symbol, self.period, MovingAverageType.WILDERS, Resolution.DAILY)
# Warm up the indicator so it is ready immediately
self.set_warm_up(self.period + 1)
def on_data(self, data: Slice):
if not self.atr_indicator.is_ready:
return
# Accessing the built-in indicator value
current_atr = self.atr_indicator.current.value
self.plot("ATR Analysis", "Built-in ATR", current_atr)
# ------------------------------------------------------------
# METHOD 2: Manual Calculation from Raw History
# ------------------------------------------------------------
# Useful if you need to calculate it on a dataframe for research
# or if you are not using the indicator system.
# 1. Get raw history (High, Low, Close)
# We need period + 1 to calculate the previous close for the first TR
history_df = self.history(self.symbol, self.period + 5, Resolution.DAILY)
if not history_df.empty:
manual_atr = self.calculate_manual_atr(history_df, self.period)
# Note: Values might differ slightly due to Smoothing initialization (Wilder's vs Simple)
if manual_atr is not None:
self.plot("ATR Analysis", "Manual Pandas ATR", manual_atr)
def calculate_manual_atr(self, df: pd.DataFrame, period: int) -> float:
"""
Calculates ATR manually using Pandas.
Assumes df contains 'high', 'low', 'close' columns.
"""
# Ensure we have enough data
if len(df) < period + 1:
return None
# 1. Calculate True Range (TR)
# TR = Max(High-Low, abs(High-PrevClose), abs(Low-PrevClose))
prev_close = df['close'].shift(1)
# High - Low
hl = df['high'] - df['low']
# Abs(High - PrevClose)
hpc = (df['high'] - prev_close).abs()
# Abs(Low - PrevClose)
lpc = (df['low'] - prev_close).abs()
# Combine into a DataFrame to find the max per row
tr_df = pd.concat([hl, hpc, lpc], axis=1)
true_range = tr_df.max(axis=1)
# 2. Calculate Average True Range (ATR)
# Standard ATR uses Wilder's Smoothing.
# For simplicity in Pandas, a Simple Moving Average (SMA) is often used,
# but here is the Exponential (Wilder's) approximation:
# Drop the NaN created by the shift
true_range = true_range.dropna()
if len(true_range) < period:
return None
# Calculate Rolling Mean (Simple ATR)
# atr_series = true_range.rolling(window=period).mean()
# OR Calculate Wilder's Smoothing (Standard ATR)
# alpha = 1/period is equivalent to Wilder's smoothing
atr_series = true_range.ewm(alpha=1/period, adjust=False).mean()
return atr_series.iloc[-1]
Key Concepts
-
True Range (TR) Formula:
$$TR = \max[(High - Low), |High - PreviousClose|, |Low - PreviousClose|]$$ -
Smoothing:
The standard ATR indicator (developed by J. Welles Wilder) uses Wilder's Smoothing, not a simple arithmetic mean.- In the built-in
self.ATR, this is handled automatically viaMovingAverageType.WILDERS. - In the manual Pandas example,
df.ewm(alpha=1/period)is used to approximate Wilder's smoothing. If you prefer a simple average, usedf.rolling(window=period).mean().
- In the built-in
-
Data Resolution:
Ensure the resolution passed toself.historymatches the resolution of your data (e.g.,Resolution.DAILYorResolution.MINUTE). ATR requires High, Low, and Close data, so it will not work accurately with Tick data unless aggregated into bars first.
Q&A
Q: Why is the built-in indicator preferred over manual calculation?
A: The built-in self.ATR is computationally optimized for the LEAN engine. It updates incrementally with every new data point (O(1) complexity) rather than recalculating the entire history (O(N) complexity) at every time step, making backtests significantly faster.
Q: Can I use this for Intraday data?
A: Yes. Simply change Resolution.DAILY to Resolution.MINUTE or Resolution.HOUR in both the add_equity call and the self.ATR definition.
Q: How do I access the previous day's ATR value?
A: When using the built-in indicator, you can use the window indexer. self.atr_indicator[0] is the current value, and self.atr_indicator[1] is the value from one period ago.