Quantopian's community platform is shutting down. Please read this post for more information and download your code.
Back to Community
Welles Wilder's ADX - Average Directional Index (technical indicator implementation)

An oldie but a goodie, I thought I'd share my code for a popular technical indicator, the ADX.

Introduced by Welles Wilder in his 1978 book New Concepts in Technical Trading Systems, along with the Minus Directional Indicator and the Plus Directional Indicator, the ADX forms part of a family of technical indicators developed by Wilder to help classify trends and trend strength.

Find out a little more about ADX here and here.

I've followed Wilder and used a rolling window of 14 days for the calculation window, which is used in various parts to allow for the smoothing Wilder implements. Changing the variable '''context.window_length''' should allow you to change this if you wish.

Finally, at the end of the code I implement a very crude trading strategy based on the indicator - am sure you guys can do better and be more inventive with this.

45 responses

I'm not familiar with how ADX is calculated but does line 97 and 98 need to have "context.window_length" instead of 14?

@James - yep, absolutely

I would like to code an ADX pattern that shows up on the weekly and/or the monthly charts. I have a few really terrific patterns that I would like to share how to implement and backtest. I am not overly technical, BUT I have done the research on the patterns and can explain them and share them in layman's terms. Is it possible to connect with an interested party who is interested in collaboration?

Hello Michael,

I would be interested. As a start I reproduced Tim's results using TA-Lib. There are differences in the timing of trades but the overall result is the same.

A data window of 28 days is the minimum required. Returns vary with the window length:

28 days = 170.8%  
29 days = 180.2%  
30 days = 193.1%  
31 days = 200.9%  

P.

Hi Peter,

Pretty astounding returns. I have a few patterns that are specific ADX patterns. Perhaps we can discuss and develop and backtest our strategy on here? I love this site and would like to collaborate as I have been in the markets for 13 years and might be interested in even launching a fund or a prop firm. If it goes well, we could be a big success for this site! And they could be a big boon for us as well. My email is [email protected].

Thanks for these, after reading this book I have been trying to replicate this strategy as well but have very little programming experience. From my understanding of the text, the higher the ADX of any given security, the more profitable it is to trade this system. That being said, is there a way to limit the universe to whatever stocks have the highest ADX at any given point in time? In other words, instead of simply choosing one security (in this case sid(19920)) and limiting trades to when that security has an ADX higher than 20 or 25, could we add code that would loop through all stocks available, select whichever has the highest ADX and buy/short that stock (given that all the other conditions in the model are met)?

Hello Chris,

Please see attached. Note that there is no real position sizing or money management going on here. Also, it doesn't trade the highest ADX but just all ADX over a threshold.

P.

Peter,

I appreciate the quick response. If I'm interpreting your source code correctly, this would limit the universe to stocks in the top 0.5 percentile in terms of trade volume, and trade any stocks in that universe with and ADX above 20 or 25, right? I guess my question is whether we can further limit the universe to just the stock with the highest ADX? As I mentioned, I am just starting to experiment with coding, but might this be achieved by looping through all stocks in the universe you limited it to, creating a dict of each stock name and its respective ADX, then pulling the max of this dict to pass through the ADX and DI based trading logic?

Thanks again for your help!

Hello Chris,

Yes, you are correct on all points. The 0.5% universe is around 31 stocks but it is revised each quarter. I'll have a look at selecting the highest ADX with a view to having fewer, better trades.

P.

Hello Chris,

This version trades the securities with the top N ADX values.

P.

Hi all,
Quick question. I'm new to python, so please excuse what I imagine is a stupid question.

If I'm trying to reference a changing value from one bar to the next, how would I code that?

For example, (context.mDI > context.mDI????) if I'm setting the condition so that the current mDI is greater that the mDI from the previous bar, what would it look like?

Thanks for humoring a newbie.

Best,

Justin Davis

Thanks Peter, this is extremely helpful. Sorry for being a pain, but I have a few more questions that I would really appreciate if you could answer. They are related to the code itself, so shouldn't require much effort on your part.

1) Related to the checks you added (lines 28-37). Maybe I'm interpreting the first (lines 28-30) incorrectly, but my interpretation is that it is checking whether the date/time of the last trade in the given security is less than (and therefore before) the current time. Assuming that there has been at least one trade in the given security at any point in the past, wouldn't this condition always be true? For example, if 10 shares of XYZ were purchased in 2005, even if XYZ was taken off the market in 2006, this condition would still evaluate to true wouldn't it?

As for the second and third checks that follow (lines 31-37), I'm not sure I see their relevancy. The second, as far as I can tell, just prints any open orders, but doesn't do anything with the printed list, correct? The third (lines 35-37) limits the universe of stocks to only those with a current price over $500? I must be interpreting that wrong, as I can't contemplate how this would benefit the strategy.

2) Could you please just briefly explain what the code on line 41 does (if not np.isnan(ADX))?

3) In regard to the dayCount variable, is this just so any stock positions in the portfolio are automatically sold off (or purchased to close a short position) every 30 days?

Thanks again for all of your help! I plan on adding a few other elements to this strategy that should help minimize downside risk, and would be happy to share when finished.

Hello Justin,

One way is to use a fixed-length queue. See: https://www.quantopian.com/posts/stock-price-30-days-ago

from collections import deque

# in initialize  
context.MDIvalues = deque(maxlen=2)  

# append to window  
context.MDIvalues.append(mDI)  
# access where index 0 is yesterday and index 1 is today  
if context.MDIvalues[1] > context.MDIvalues[0]  

P.

Hello Chris,

Thanks for taking the time to have a look. To try to answer:

1) 'if data[stock].datetime < get_datetime():' is checking if the price for a security is a 'new' price. Prices are timestamped and prices and their timestamps are carried forward so that the data/price object always has a entry for each security at each time interval. This is a bit of a grey area. Some algos I've written fail if this check is not made. Note that 'continue' means 'don't continue but instead go to the next iteration of the for loop'.

With 'if get_open_orders(stock):' please ignore the print statement which is leftover from debugging. What I'm trying to do here is very inelegant but I want to avoid trading a security while I still have an open position caused by a partial fill in the last trade of that security. I've had algos get into real problems where a long position is sold at time t, remains partially filled at t+1, and a short trade in the same security is entered also at t+1. [This is where I have a major conceptual problem with Quantopian which seems to be synchronous with minutely events whereas trading with Interactive Brokers via Trader Workstation or their FIX gateway would be asynchronous and would (mostly) avoid this issue. I wonder if this is because each algo running in Quantopian is single-threaded but, as I say, I have conceptual problems in this area.]

With 'if data[stock].close_price > 500.00:' I'm avoiding trading 'BRK 'A'' with a price per share sometimes around $170,000. Note that 'continue' means 'don't continue'.

2) I changed the algo after the recent security issue. When service was first resumed I had problems importing zipline in order to use the TA-Lib library so I switched to the 'supported' TA-Lib functions. For ADX based on 14 periods the first result is available at period 28 with the prior values all being NaNs i.e. Not A Number.

3) For some reason I thought the best results with ADX might come from holding positions for a few days but that is not what I found so maybe the algo is defective. The best results often came by generating a new portfolio every day or after a much larger number of days than I would have guessed i.e. 30 trading days (~ 6 weeks).

Please add some ideas. At the moment the algo does literally nothing with position sizing or handling margin. At times it goes so short that the $1 M starting cash becomes $4M.

P.

Peter - I appreciate the quick response. Thanks for your suggestion.
Best,
Justin

Can anyone take a look at this? I'm having some issues that I identified through comments. Any suggestions are appreciated.

implementation of Welles Wilder's ADX as given in spreadsheet form here

http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:average_directional_index_adx#calculation

from collections import deque

Put any initialization logic here. The context object will be passed to

the other methods in your algorithm.

def initialize(context):
#using the NASDAQ100 etf
context.ndx = sid(19920)
#container to count the number of event windows we have cycled through
context.ticks = 0
#Wilder uses a rolling window of 14 days for various smoothing within
#the indicator calculation
context.window_length = 14
#a collection of data containers that will be used during steps of the calculation
context.highs = deque([0] * 2, 2)
context.lows = deque([0] * 2, 2)
context.closes = deque([0] * 2, 2)
context.true_range_bucket = deque([0] * context.window_length, context.window_length)
context.pDM_bucket = deque([0] * context.window_length, context.window_length)
context.mDM_bucket = deque([0] * context.window_length, context.window_length)
context.dx_bucket = deque([0] * context.window_length, context.window_length)
#not sure why I had to define these here, but to print them later when debuggin
#I found that I had to declare them here
context.av_true_range = 0
context.av_pDM = 0
context.av_mDM = 0
context.di_diff = 0
context.di_sum = 0
context.dx = 0
context.adx = 0
context.pDI = 0
context.mDI = 0
context.mDIvalues = deque(maxlen=2)
context.pDIvalues = deque(maxlen=2)
context.adxvalues = deque(maxlen=2)

Having A problem here

context.mDIvalues.append(mDI)  
context.pDIvalues.append(pDI)  
context.adxvalues.append(adx)  

pass

Will be called on every trade event for the securities you specify.

def handle_data(context, data):
#iterate event window counter
context.ticks += 1
#pass high, low, close prices to our rolling containers
context.highs.appendleft(data[sid(19920)].high)
context.lows.appendleft(data[sid(19920)].low)
context.closes.appendleft(data[sid(19920)].close_price)

#ensure no calculation on first window  
if context.closes[0] == 0:  
    high_less_low = 0  
    high_less_prec_close = 0  
    low_less_prec_close = 0  
    high_less_prec_high = 0  
    prec_low_less_low = 0  
    pDM_one = 0  
    mDM_one = 0  
else:  
    high_less_low = context.highs[0]-context.lows[0]  
    high_less_prec_close = abs(context.highs[0]-context.closes[1])  
    low_less_prec_close = abs(context.lows[0]-context.closes[1])  
    high_less_prec_high = context.highs[0]-context.highs[1]  
    prec_low_less_low = context.lows[1]-context.lows[0]  


#calculate the Plus Directional Movement  
if high_less_prec_high > prec_low_less_low:  
    pDM_one = max(high_less_prec_high,0)  
else:  
    pDM_one = 0

#calculate the Minus Directional Movement  
if prec_low_less_low > high_less_prec_high:  
    mDM_one = max(prec_low_less_low,0)  
else:  
    mDM_one = 0  

#add the current pDM and mDM to the bucket to aid calculation of the first point in the  
#smoothed statistic  
context.pDM_bucket.appendleft(pDM_one)  
context.mDM_bucket.appendleft(mDM_one)  

#calculate the True Range and add to bucket  
true_range = max(high_less_low,high_less_prec_close,low_less_prec_close)  
context.true_range_bucket.appendleft(true_range)  

#once we have collected enough data to have populated the rolling windows adequately  
#we can start the meat of the calculation  
if context.ticks < (context.window_length + 1):  
    context.av_true_range = 1  
    context.av_pDM = 0  
    context.av_mDM = 0  
elif context.ticks == (context.window_length + 1):  
    context.av_true_range = sum(context.true_range_bucket)  
    context.av_pDM = sum(context.pDM_bucket)  
    context.av_mDM = sum(context.mDM_bucket)  
else:  
    context.av_true_range = context.av_true_range - (context.av_true_range/context.window_length) + true_range  
    context.av_pDM = context.av_pDM - (context.av_pDM/14) + pDM_one  
    context.av_mDM = context.av_mDM - (context.av_mDM/14) + mDM_one

if context.ticks > context.window_length:  
    context.pDI = 100 * context.av_pDM / context.av_true_range  
    context.mDI = 100 * context.av_mDM / context.av_true_range  
    context.di_diff = abs(context.pDI - context.mDI)  
    context.di_sum = context.pDI + context.mDI  
    context.dx = 100 * context.di_diff / context.di_sum  


#add to bucket to provide an average of the DX figures that will  
#act as a starting point for the rolling ADX calculation  
context.dx_bucket.appendleft(context.dx)

#the rolling ADX calculation  
if context.ticks == (context.window_length * 2):  
    context.adx = sum(context.dx_bucket) / context.window_length  
elif context.ticks > (context.window_length * 2):  
    context.adx = ((context.adx * (context.window_length - 1)) + context.dx) / context.window_length  

# Having TROUBLE with this Trading Logic
# access where index 0 is yesterday and index 1 is today
# Notice I used -1 for the day before yesterday b/c I wasn't sure what to do but I would like to include that in the buy condition

if (context.mDIvalues[1] > context.pDIvalues[1]) and (context.mDIvalues[1] > context.adxvalues[1]) and (context.mDIvalues[1] < context.mDIvalues[0]) and (context.mDIvalues[0] < context.mDIvalues[-1]):  
    order(sid(19920), 500)  
    log.debug('buy')  


elif (context.adxvalues[1] > context.mDIvalues[1]) and (context.pDIvalues[1] > context.mDIvalues[1]) and (context.pdivalues[1] < context.pDIvalues[0]) and (context.adxvalues[1] < context.adxvalues[0]):  
    order(sid(19920), -500)  
    log.debug('sell')  

log.debug('adx')  
log.debug(context.adx)  
log.debug('pDI')  
log.debug(context.pDI)  
log.debug('mDI')  
log.debug(context.mDI)  

Hello Justin,

I see you've put loads of work into this...but I thought it would be simpler to try your logic using the TA-Lib indicator library (see http://ta-lib.org/). To test current, day-1 and day-2 values I made the queue length 3.

P.

Ha! Peter, you're the best. I know I'm terrible at this, but I'm obviously working through the process.... I appreciate your feedback and the indicator library resource. Very helpful. Maybe with the support of this community there's hope for me yet.....

Cheers,

Justin

Hello:

I am new to Python, coding in general, and quant finance, but am finding this site really fascinating so far. I watch commodity markets, and would like to try and run this algorithm on the continuous front month corn contract. The data is located on quandl at:

http://www.quandl.com/OFDP-Open-Financial-Data-Project/FUTURE_C1-CBOT-Corn-Futures-Continuous-Contract-1-C1-Front-Month

I read the section on importing quandl data, but was unable to figure out how to do this. I am not sure how to run it without the sid code. I know there is a corn etf, but I would rather not use that. If anyone could provide some advice, I would appreciate it.

Thanks,

Josh

Hello Josh,

I struggle a bit with 'fetch_csv'. I've got as far as reading the data:

def initialize(context):  
    context.sid = sid(8554)  
    fetch_csv('http://www.quandl.com/api/v1/datasets/OFDP/FUTURE_C1.csv?&trim_start=1959-07-01&trim_end=2013-11-20&sort_order=desc',  
              symbol      = 'Corn',  
              date_column = 'Date',  
              date_format = '%y/%m/%d',  
              pre_func    =  show_df)

def show_df(df):  
    print df.head()  
    return df

def handle_data(context, data):  
    pass  

P.

Thanks Peter, I will play around with this over the weekend.

Reviving an old post here... this code is fantastic and has been a huge help to me. Does the OP or anyone else know why the summing on lines 92-94 of the original code are not divided by context.window_length? I would imagine these averages need to be divided by an N rather than just be a sum.

Amending this produces a negligible difference in my backtest so I'm just wondering if this summation is intentional for the subsequent moving average calculation?

Hi, I have zero knowledge on algo program but I know well on DMI.

DMI has its problems too. For example, when stock price gap up, +DI cross above -DI at a stock price that cannot be filled and therefore traders must carefully use DMI in backtesting that may give wrong results. I wish I know how to read the algo program in collaborating a better DMI.

Is that really a problem though? Taking gaps into account is part of the advantage of DMI (and "true" range in general)

If a stock price is opened gap up, say closed 20 and open gap up 22, DMI may show buy signal at 20.80 but there is no 20.80 to buy since price already gap up at 22 opening. Thus, it will have a wrong result if not carefully track down the proper prices using DMI.

I think you're getting too granular if you're telling your algorithm to buy only at the exact price the DI lines cross (slippage would make that improbable in real trading anyway). The purpose of the ADX is to indicate a trend, thus if a price gaps up to 22 and the +DI line is now above the -DI line, I would take that as a valid entry signal because the trend has turned bullish (depending on ADX readings and other factors).

Is there a way we can add to print the ROC and ADX below the backtest?

has this strategy been ported to the Quantopian2 APIs? I tried cloning it but it won't build in its current form

Can any one please post a working code for the ADX here. None of the previous codes are compiling. I tried cloning and not able to build it.

Hi Vladimir: Thank you very much for so fast reply. I will try to use this code and probably will be asking you for more help as I am new to Python. Thanks

dchdc">"njcd

Ok well number one: The post above me is ummm interesting. Also, given how many times the OG algorithm has been copied, I'm surprised that no one has bothered to check the leverage of this algorithm... It's totally out of control.

@Jacob it might be because of a difference between the Q platform 2013 and now. The original gives 193% returns and yours 3347%.

Anyways, I've tried using ADX in my algos (the talib version, not this implementation) and was unimpressed with it. Anybody gotten anything useful out of it?

@atiredmachine
It is most likely due to the fact that the starting capital is 1,000,000 and it is trading every minute
even when using :

order_target_percent(stock, 1.0)  

it still over exposes.
IMO Indicators are inherently pretty useless... I use them to determine when to decrease leverage and or pull out of the market entirely - thats it.

@Jacob I believe Quantopian used to have a dropdown for daily or minute mode. This algo was probably written for daily mode.

My results using Vladimir's code is nowhere near as good as the original post. Basically buy when adx>25 and p>n, sell when adx>25 and n>p

Wow Vladimir, your version performed 2x better than the benchmark. I guess going 100% long or short on QQQ isn't the greatest approach (like I originally did), since it is such an uptrending stock. Definitely like your 80/20 split method more. Thank you for commenting

Can anyone help creating a sample source code for me to look into soybean oil futures 2nd continuous contract month using ADX technical trend indicator? If can, please include the stop and reverse trades with minimum 1 lot, a 15-minute high low close data with ADX lines -- +DI, -DI and ADX lines. Many thanks.

Is it already the ADX a built-in function?

The ADX indicator is currently not 'built-in' as a factor. However, one can create a custom factor. Something like this

class ADX(CustomFactor):  
    """  
    Average Directional Movement Index  
    Momentum indicator. Smoothed DX  
    **Default Inputs:** USEquityPricing.high, USEquityPricing.low, USEquitypricing.close  
    **Default Window Length:** 29

    https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/DMI  
    """     

    inputs = [USEquityPricing.high, USEquityPricing.low, USEquityPricing.close]  
    window_length = 29

    def compute(self, today, assets, out, high, low, close):

        # positive directional index  
        plus_di = 100 * np.cumsum(plus_dm_helper(high, low) / trange_helper(high, low, close), axis=0)

        # negative directional index  
        minus_di = 100 * np.cumsum(minus_dm_helper(high, low) / trange_helper(high, low, close), axis=0)

        # full dx with 15 day burn-in period  
        dx_frame = (np.abs(plus_di - minus_di) / (plus_di + minus_di) * 100.)[14:]

        # 14-day EMA  
        span = 14.  
        decay_rate = 2. / (span + 1.)  
        weights = weights_long = np.full(span, decay_rate, float) ** np.arange(span + 1, 1, -1)

        # return EMA  
        out[:] = np.average(dx_frame, axis=0, weights=weights)

The code above relies on several helper functions.

def plus_dm_helper(high, low):  
    """  
    Returns positive directional movement. Abstracted for use with more complex factors

    https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/DMI

    Parameters  
    ----------  
    high : np.array  
        matrix of high prices  
    low : np.array  
        matrix of low prices

    Returns  
    -------  
    np.array : matrix of positive directional movement

    """  
    # get daily differences between high prices  
    high_diff = (high - np.roll(high, 1, axis=0))[1:]

    # get daily differences between low prices  
    low_diff = (np.roll(low, 1, axis=0) - low)[1:]

    # matrix of positive directional movement  
    return np.where(((high_diff > 0) | (low_diff > 0)) & (high_diff > low_diff), high_diff, 0.)

def minus_dm_helper(high, low):  
    """  
    Returns negative directional movement. Abstracted for use with more complex factors

    https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/DMI

    Parameters  
    ----------  
    high : np.array  
        matrix of high prices  
    low : np.array  
        matrix of low prices

    Returns  
    -------  
    np.array : matrix of negative directional movement

    """  
    # get daily differences between high prices  
    high_diff = (high - np.roll(high, 1, axis=0))[1:]

    # get daily differences between low prices  
    low_diff = (np.roll(low, 1, axis=0) - low)[1:]

    # matrix of megative directional movement  
    return np.where(((high_diff > 0) | (low_diff > 0)) & (high_diff < low_diff), low_diff, 0.)


def trange_helper(high, low, close):  
    """  
    Returns true range

    http://www.macroption.com/true-range/

    Parameters  
    ----------  
    high : np.array  
        matrix of high prices  
    low : np.array  
        matrix of low prices  
    close: np.array  
        matrix of close prices

    Returns  
    -------  
    np.array : matrix of true range

    """  
    # define matrices to be compared  
    close = close[:-1]  
    high = high[1:]  
    low = low[1:]

    # matrices for comparison  
    high_less_close = high - close  
    close_less_low = close - low  
    high_less_low = high - low

    # return maximum value for each cel  
    return np.maximum(high_less_close, close_less_low, high_less_low)

There is a great post which provides custom factors for many technical signals. Check it out https://www.quantopian.com/posts/ta-lib-for-pipeline.

Good luck.

Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

This is awesome. Thank you so much.

Where does the SID (19920) data map come from? Thanks!

The order function used in the source code is from the zipline library. Why is there no import zipline in the source code? thanks

@chuan zhang I'll try to answer your questions above.

"Where does the SID (19920) data map come from?" That equity is QQQ and is mapped (rather arbitrarily) to the internal Quantopian Security ID (SID) of 19920. That is valid when using the online platform both in the IDE and in notebooks. If one loads zipline locally then QQQ will more than likely be a different SID based upon the data bundle one uploads.

"Why is there no import zipline in the source code?" When running an algo online on the Quantopian platform, all the actual zipline code (and associated imports) is loaded and run automatically behind the scenes. This allows users to focus on simply writing scheduled functions and other functions which zipline calls. There are a few cases where one may want to import zipline to access specific modules but this is typically not necessary.

Thank you very much for your answer .
I have two more questions that I haven't found in the official documents .
How many times a day does the program run when the website self tests back?
Why does every transaction of 500 shares generate such a large leverage return?