Back to Community
QuantCon 2016: Dual Momentum Strategy

For the latest release of the QuantCon talks, I've implemented Gary Antonacci's Dual Momentum Strategy on Quantopian. You can edit the algorithm in the IDE and tinker with it yourself. Attached are the algorithm and a tearsheet analyzing the algorithm's performance. Watch to his talk and review his slide deck for deeper understanding of the notebook.

Dr. Antonacci's Dual Momentum is a strategy based on the relative momentum of the S&P 500, World Equities without the U.S., and the yields of Treasury Bills. Because there are Exchange Traded Funds (ETFs) that track these benchmarks, the strategy can be implemented on the Quantopian platform.

Now that you've got this framework, you should play around with it. You can test it with different momentum ETFs against treasury bills and check the results of your updated strategy. Also, you can check out other community posts which relate to or expand on the effectiveness of this strategy:

  1. Experimenting with GEM and Dual Momentum
  2. Dual-Momentum Now Goes to Cash
  3. Quantopian Lecture Series: Momentum Strategies

Best,

Lotanna Ezenwa

Loading notebook preview...
Notebook previews are currently unavailable.
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.

12 responses

And the related backtest

Clone Algorithm
1138
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Backtest ID: 57b74a64fc177d10093c9514
There was a runtime error.

Hi, I'm a newbie here. I cloned, full backtested and deployed as Quantopian paper trade yesterday. I was expecting the algo to run and fill the order this morning. Am I just confused on when the schedule function runs?

Quick inspection of the schedule function calls shows that they are run at the beginning of the month, despite the comment that says daily:

    # Rebalance every day, 1 hour after market open.  
    schedule_function(my_assign_weights, date_rules.month_start(),time_rules.market_open())  
    schedule_function(my_rebalance, date_rules.month_start(), time_rules.market_open(hours=1))  
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.

I'm currently running a live paper trading cloned version of the Quant Con 2016 Dual Momentum strategy. The first purchase was made on 10/3/16 buying VEU. Looking at PerfCharts and comparing SPY against VEU for the last 12 months it appears that the algo should have purchased SPY instead as the return has been higher over that time period. Is there something I'm missing here?

As soon I remove 2008-09 financial crisis period from backtesting, the returns are lower than SPY.
May be my Python is not top-notch but I am not clear why the SPY returns are equated wrt zero? Can you pls explain?

Thanks
Hardik

if returns[context.mom1] > returns[context.mom2]:  

    if returns[context.mom1] < 0:  
       context.weights[context.tbill] = .5  
       context.weights[context.agg] = .5  

    if returns[context.mom1] > returns[context.tbill]:  
        context.weights[context.mom1] = 1  

    else:  
        context.weights[context.agg] = 1  

else:  

    if returns[context.mom2] > returns[context.tbill]:  
        context.weights[context.mom2] = 1  
    else:  
        context.weights[context.agg] = 1  

context.weights.fillna(0,inplace=True)  

Hi Hardik. There have been periods of time (several year stretches) where a momentum strategy has under performed a broad market index such as the S&P 500. This is also true of many value strategies as well. However over longer periods (10 - 20 years) momentum strategies display significant out performance. The key is sticking with the strategy through both good and bad times. Please check out Gary Antonacci's website http://www.optimalmomentum.com/

Accidentally sent above post before I finished. I too have been working with the dual momentum algo. I made a couple of changes to better conform to Mr. Antonacci's strategy. I've included my changes below. I noticed a few things. One is the monthly rebalance should occur on the last trading day of the month instead of the first. I increased the return period to 254 days from 100. Finally I noticed that when going to bonds Mr. Antonacci's strategy is 100% in AGG so I commented out a line and changed the weighting to 1.0 for AGG.

""" This is a template algorithm on Quantopian for you to adapt and fill in.
""" from future import division
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import AverageDollarVolume, CustomFactor, Returns
from quantopian.pipeline import CustomFilter
import numpy as np
import pandas as pd
from scipy import stats

class SecurityInList(CustomFactor):
inputs = []
window_length = 1
securities = []
def compute(self, today, assets, out):
out[:] = np.in1d(assets, self.securities)

def initialize(context):
"""
Called once at the start of the algorithm.
"""
set_benchmark(sid(21513))
# Rebalance every month, 1 hour after market open.
schedule_function(my_assign_weights, date_rules.month_end(),time_rules.market_open())
schedule_function(my_rebalance, date_rules.month_end(), time_rules.market_open(hours=1))

# Record tracking variables at the end of each day.  
schedule_function(my_record_vars, date_rules.every_day(), time_rules.market_close())  

# Create our dynamic stock selector.  
context.return_period = 254  

"""  

sid 21513 = IVV: iShares Core S&P 500 ETF
sid 33486 = VEU: Vanguard FTSE All-World ex-US ETF
sid 23911 = SHY: iShares 1-3 year Treasury Bond ETF
sid 25485 = AGG: iShares Core U. S. Aggregate Bond ETF
"""

context.mom1 = mom1 = sid(21513)  
context.mom2 = mom2 = sid(33486)  
context.tbill = tbill = sid(23911)  
#context.tlt = tlt = sid(23921)  
context.agg = agg = sid(25485)  

sec_list = [mom1,mom2,tbill,agg]  
attach_pipeline(make_pipeline(sec_list, context), 'my_pipeline')

set_commission(commission.PerShare(cost=0, min_trade_cost=0))  
# Momentum ETFs  

def make_pipeline(sec_list, context):
"""
A function to create our dynamic stock selector (pipeline). Documentation on
pipeline can be found here: https://www.quantopian.com/help#pipeline-title
"""

# Return Factors  
mask = SecurityInList()  
mask.securities = sec_list  
mask = mask.eq(1)  
yr_returns = Returns(window_length=context.return_period, mask=mask)  

pipe = Pipeline(  
    screen = mask,  
    columns = {  
        'yr_returns': yr_returns  
    }  
)  
return pipe  

def before_trading_start(context, data):
"""
Called every day before market open.
"""
context.output = pipeline_output('my_pipeline')

def my_assign_weights(context, data):

context.weights = pd.Series(index=context.output.index)  
returns = context.output['yr_returns']  

if returns[context.mom1] > returns[context.mom2]:  

    if returns[context.mom1] < 0:  
      #context.weights[context.tbill] = .5  
      context.weights[context.agg] = 1  

    elif returns[context.mom1] > returns[context.tbill]:  
        context.weights[context.mom1] = 1  

    else:  
        context.weights[context.agg] = 1  

else:  

    if returns[context.mom2] > returns[context.tbill]:  
        context.weights[context.mom2] = 1  
    else:  
        context.weights[context.agg] = 1  

context.weights.fillna(0,inplace=True)  

def my_rebalance(context,data):
"""
Execute orders according to our schedule_function() timing.
"""

for stock,weight in context.weights.iteritems():  
    if data.can_trade(stock):  
        order_target_percent(stock,weight)  

def my_record_vars(context, data):
"""
Plot variables at the end of each day.
"""
record(leverage=context.account.leverage)

def handle_data(context,data):
"""
Called every minute.
"""
pass

Thanks David, Appreciate your help.

I am still unsure about need to check if SPY returns are < 0, and assign weight to AGG. I think this algo will do it anyway - unless I am missing something here OR AGG returns can also go negative but remain higher than SPY - has that happened in past?

What are your thoughts about making it multi-momentum strategy instead of dual-momentum?

I am trying to modify this to multi-dimensional momentum strategy and use SPY, GLD, UUP (Dollar Index) and DBA (Agri commodity ETF) and attempt to ride the best momentum. Although I am using If loops, but is becoming cumbersome. Max function will make it much simpler - need some help with this.

Also, Need to optmize # of momentum parameters, their quality per Gary's requirements (ie. liquidity, volatility etc.) and ensure not too many transactions are done.

Thanks
Hardik

Hardik, the first piece of the strategy is relative momentum. Lotanna's post displays a great flow chart of the complete dual momentum strategy. You're initially comparing SPY vs ACWI ex-U.S. For example say SPY > ACWI ex-U.S, then compare SPY to the T Bill return. If T Bill > SPY, then you would go to AGG.

I'm inclined to stick with the dual momentum approach personally. Mr. Antonacci's research, back testing and empirical evidence make it a compelling strategy. However he also provides evidence in his book that momentum has worked historically across all asset classes. I think you have a great idea with incorporating multiple asset classes. I would still be inclined to apply it in a dual momentum strategy as the standard deviation and max drawdown should be lower.

Check out the following post: https://www.quantopian.com/posts/dual-momentum-now-goes-to-cash-if-everything-is-down
This might be a good place to start with multiple asset types. However, it's a little older code and I believe the history function (at least as written) was deprecated. You can run a back test against it to verify that.

Good luck,
David

Hello, I'm very new to Quantopian (and quant process in general) - and came across this post by Lotanna after reading "Dual Momentum Investing" by Dr. Gary Antonacci that offered a great starting point for my own research and backtesting of the strategy (nice work, Lotanna!).

I posted some questions about various details I had to the book's website at http://www.optimalmomentum.com/contact%20us.html, and was pleasantly surprised that Dr. Gary himself had graciously responded within a few hours. During an email thread with him, Dr. Gary clarified that the actual method he himself uses is detailed on page 98 of the printed book (2015 edition), and differs slightly from the flowchart depicted on page 101.

Basically, the first step is to determine absolute momentum by comparing the 12 month return of the SP500 (since the U.S. leads world equity markets) against that of the T-Bills, and if positive - to then compare the relative strengths of the SP500 and the ACWX (Global Stocks ex U.S.) - thus investing in the higher performing of the two. Otherwise, one would invest in AGG. The actual vehicles to be used can vary although Dr. Gary does have his own recommendations in the book's FAQ - http://www.optimalmomentum.com/faq.html - which contains a bunch of compact gems that really clarify DMI beyond his excellent book.

This modification produced higher returns against the flowchart on page 101 for the few backtests that I executed. At his suggestion, I'm posting this to Quantopian to pass this on to anyone interested. It actually simplifies the rebalancing logic in my opinion.

https://www.quantopian.com/posts/dual-momentum-investing-strategy-according-to-dr-gary-antonacci

Hi Guys - don t you think 12 months return should be normalized by volatility ? I m not sure I saw this in above codes - thx for your insight kind regards, Antoine

Sorry I'm not great at python, could someone explain what mask.eq(1) does?

    # Return Factors  
    mask = SecurityInList()  
    mask.securities = sec_list  
    mask = mask.eq(1)