Quantopian's community platform is shutting down. Please read this post for more information and download your code.
Back to Community
Custom Factor - ATR

I've coded up a Custom Factor of ATR - thanks go to Anthony Garner, I used his code for the Kaufman Efficiency Ratio as a base. This is the code:

class Average_True_Range(CustomFactor):  
   inputs = [USEquityPricing.close, USEquityPricing.high, USEquityPricing.low]  
   window_length = 15  
   def compute(self, today, assets, out, close, high, low):  
       lb = self.window_length  
       atr = np.zeros(len(assets), dtype=np.float64)  
       a=np.array(([high[1:(lb)]-low[1:(lb)],abs(high[1:(lb)]-close[0:(lb-1)]),abs(low[1:(lb)]-close[0:(lb-1)])]))  
       b=a.T.max(axis=2)  
       c=b.sum(axis=1)  
       atr=c /(lb-1)

       out[:] = atr  

Interestingly, and a little bit annoyingly, I can't get my results to line up exactly with the results that TALIB.ATR generates. I don't if this is because TALIB uses a slightly different formulation of ATR? I haven't been able to find docs showing exactly how they calculate their version. Any ideas / suggestions / bugs spotted, please let me know, thanks.

9 responses

Try this

# APR is ATR / Price, where ATR is SMA(20) of TR.  
#  
# It works round different backadjustment paradigms used by Quantopian:  
#     https://www.quantopian.com/posts/stocks-on-the-move-by-andreas-clenow  
# Uses a SMA(20) rather than the conventional Wilder exponential smoothing:  
#     http://www.macroption.com/average-true-range-calculator/  
#  
class APR(CustomFactor):  
    inputs = [USEquityPricing.close,USEquityPricing.high,USEquityPricing.low]  
    window_length = 21  
    def compute(self, today, assets, out, close, high, low):  
        hml = high - low  
        hmpc = np.abs(high - np.roll(close, 1, axis=0))  
        lmpc = np.abs(low - np.roll(close, 1, axis=0))  
        tr = np.maximum(hml, np.maximum(hmpc, lmpc))  
        atr = np.mean(tr[1:], axis=0) #skip the first one as it will be NaN  
        apr = atr / close[-1]  
        out[:] = apr

Check out the other forum post I put in the comments. You'll need to convert APR to ATR in your pipeline:

    # Append current price and current share holding as column  
    # (This works around pipeline backadjustment issue.)  
    pool['price'] = [data[sid].price if sid in data else np.NaN for sid in pool.index]  
    pool['atr'] = (pool.apr * pool.price)  
    pool['current_shares'] = [positions[sid].amount if sid in positions else 0 for sid in pool.index] 

Will this also return APRs for ETFs?

Yes, any ticker. Note, in the new release of Quantopian (aka Q2, last month), they've sorted out the backadjustment issues in my comment above, so you can safely use ATR. The code for ATR is very similar:

# ATR is SMA(20) of TR.  
#  
# Uses a SMA(20) rather than the conventional Wilder exponential smoothing:  
#     http://www.macroption.com/average-true-range-calculator/  
#  
class ATR(CustomFactor):  
    inputs = [USEquityPricing.close,USEquityPricing.high,USEquityPricing.low]  
    window_length = 21  
    def compute(self, today, assets, out, close, high, low):  
        hml = high - low  
        hmpc = np.abs(high - np.roll(close, 1, axis=0))  
        lmpc = np.abs(low - np.roll(close, 1, axis=0))  
        tr = np.maximum(hml, np.maximum(hmpc, lmpc))  
        atr = np.mean(tr[1:], axis=0) #skip the first one as it will be NaN  
        out[:] = atr  

I've shortened Burrito Dan's method up a bit and verified it's performing the same calculation, also defaulted mine to a 10 day ATR:

class ATR(CustomFactor):  # 10 day ATR calculation  
    inputs = [USEquityPricing.high, USEquityPricing.low, USEquityPricing.close]  
    window_length = 11  # length of 11 as the earliest data point is removed because previous close is used in the ATR calculation

    def compute(self, today, assets, out, high, low, close):  
        range_high = np.maximum(high[1:], close[:-1])  # properly shift high/low and close data so that  
        range_low = np.minimum(low[1:], close[:-1])  # previous close is being used to calculate atr  
        out[:] = np.mean(range_high - range_low, axis=0)  

Nathaniel,

Here's an modified version that gives an exact match to talib.ATR.
See overlay plots in the notebook.

Here is an option of 10 days ATR as percent

class atr_10days_percent(CustomFactor):  
    inputs = [USEquityPricing.high, USEquityPricing.low, USEquityPricing.close]  
    window_length = 11  
    def compute(self, today, assets, out, high, low, close):  
        range_high = np.maximum(high[1:], close[:-1])  
        range_low = np.minimum(low[1:], close[:-1])  
        out[:] = np.mean(((range_high - range_low)/close[:-1])*100, axis=0)  

If you care about getting your indicator to match talib.ATR, you should use exponential weights for the mean.

Start with something like the following for ATR, then normalize and modify it for percent if that's what you are looking for.

Window length should be at least a factor of 5 longer than the ATR lookback. I used a factor of 10 in the code below.

class ATR10(CustomFactor):  
    inputs = [USEquityPricing.high, USEquityPricing.low, USEquityPricing.close]  
    atr_len = 10  
    window_length = 100  
    def compute(self, today, assets, out, high, low, close):  
        TrueHighs = np.maximum(high[1:], close[:-1])  
        TrueLows = np.minimum(low[1:], close[:-1])  
        count = len(TrueHighs)  
        decay_rate = (1.0 - (1.0 / (self.atr_len)))  # Use Widler smoothing  
        weights = np.full(count, decay_rate, float) ** np.arange(count + 1, 1, -1)  
        out[:] = np.average(TrueHighs-TrueLows, axis=0, weights=weights)  

Thanks for the feedback @Steve Jost