Back to Community
Problems reproducing Antonacci's dual momentum GEM strategy

I modified some code from another trader, to try and reproduce the allocations and results of Antonacci's Dual Momentum GEM strategies.

The rules are simple:

  • Compare the performance of SPY 500, a global non-US-stock index and US treasury bills over the last 252 trading days, every month
  • If one of the two equity indices shows the highest return of all three indices, take a 100% long position in that index
  • If US treasury bills show the highest return, take a 100% long position in these

The results of my backtests are way off the official results, both in allocations and returns.

Can anyone help me spot the problems?

Clone Algorithm
27
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

# Put any initialization logic here.  The context object will be passed to
# the other methods in your algorithm.
def initialize(context):
    #set_symbol_lookup_date('2002-08-01')
    context.stocks = symbols('SPY','ACWX','AGG')
    context.look_back_window = 252
    context.port_size = 1
    context.month_count = 0
    
    schedule_function(
    rebalance,
    date_rules.month_end(days_offset=0),
    time_rules.market_open(minutes=5)
    )
    pass

def rebalance(context, data):
    #get daily historical prices
    h =   data.history(context.stocks, 'price', context.look_back_window, '1d')
    #use dataframe resample to get monthly values
    h = h.resample('M').last()
    print h
    #first, get the pct_change() from the first date (0) in the dataframe and today (-1). Then splice the last row because the first row is all NaN. Drop all stocks with NaN then turn the dataframe into series. Series make it easier to sort.
    mom = h.iloc[[0,-1]].pct_change().tail(1).dropna(axis=1).squeeze()
    #order by return percentage, and take only the top 
    mom = mom.sort_values().index[-1]
    print mom
    #liquidate positions that are not in the buy list
    for s in context.portfolio.positions:
        if s != mom:
            order_target_percent(s, 0)
    
    #get weight based on how many stocks are selected
    weight = 0.95
    
    #purchase stock with largest momentum
    if data.can_trade(mom):
        order_target_percent(mom, weight)
    
# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    record(leverage = context.account.leverage)
    pass
There was a runtime error.
5 responses

I have not looked at your code but can tell you from long experience you are wasting your time trying to match his figures. You have no idea what data or software​ he was using and you are chasing your own backside for no reward.

My advice is to rely on your own skills and your own data (or Quantopian's data). Thrice check your code does what this simple system is supposed to do. Once you are satisfied if there is still a large difference contact Antonacci. Ask for his data and code or at least details of it.

It's your money on the line. Make your own judgement - I always do. Then I have only myself to blame when the system f***s up. And they usually do.

πŸ˜€πŸ˜πŸ˜‚πŸ˜ƒπŸ˜„πŸ˜…πŸ˜ŽπŸ˜‰πŸ˜ŽπŸ˜‹

" Then I have only myself to blame when the system f***s up."

See, that's what I hate - I like to blame others :-D

But jokes aside, I have already found a few emberassing mistakes. I haven't implemented the rules correctly, the absolute momentum part of dual momentum is completely missing.

@ fynn trader,

Between the paper and the results there are many discrepancies.

Try this from 2007.
It is more close to my understanding of Global Equities Dual Momentum.

# Gary Antonacci's Global Equities Dual Momentum replication

# https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2042750  
# http://www.optimalmomentum.com/gem_allocation.html

def initialize(context):  
    schedule_function(rebalance, date_rules.month_end(), time_rules.market_close(minutes=5))  
    set_commission(commission.PerShare(cost=0))  
    set_slippage(slippage.FixedSlippage(spread=0.00))

def rebalance(context, data):  
    assets = symbols('SPY', 'ACWX', 'SHV')  
    mom_period = 252 

    hist = data.history(assets, 'price', mom_period + 1, '1d')  
    mom = (hist.iloc[-1]/hist.iloc[0]) - 1.0  
    mom = mom.dropna()  
    mom = mom.sort_values().index[-1]  
    mom = mom if mom > 0 else symbols('SHV')  
    print mom

    if get_open_orders(): return

    for asset in assets:  
        if data.can_trade(asset):  
            if asset is mom:  
                order_target_percent(asset, 1.0)  
            else:  
                order_target(asset, 0)

    record(leverage = context.account.leverage)

Thanks a lot, @Vladimir!

Code looks very clear.

fynn trader, is your revised simulation working?

I still find dual momentum under performs SPY since 2011 quite a lot.