Notebook

Engineered Momentum Strategy

Overview

Inspired from Dual Momentum I "engineered" a similar strategy with a few small adjustments, specifically an emphasis on accelerating momentum, that have significant impact on returns. All data shown is either pulled from shared algorithms or from this google doc or this google doc that has data back to 1871. Here's a blog post with more background on the strategy.

  • Momentum signal is the sum of the 1 month, 3 month, and 6 month returns
  • Use long-term treasuries as the out-of-market asset to take advantage of more exaggerated swings
  • Use small cap global stocks which are more uncorrelated to US large cap stocks Image of Momentum

Historical Performance

Run below cell for plots, will need to upload relevant CSV files to your research data area.

In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

#Plot the assets owned by month over time
ax1 = plt.subplot2grid((6, 1), (0, 0), colspan=1, rowspan=1)
weight = local_csv('Weights1.csv').set_index(['Date'])
weight.plot(kind='bar', ax=ax1, stacked=True, width=1.0,edgecolor = "none")
ax1.set_yticklabels([])
ax1.set_xticklabels([])
plt.legend(loc='upper left')
plt.xlabel('')
plt.title('Accelerating Dual Momentum Strategy', fontsize=14, fontweight='bold')

#Plot the dollar performance over time
ax0 = plt.subplot2grid((6, 1), (1, 0), colspan=1, rowspan=3)
balances = local_csv('Balances1.csv').set_index(['Date'])
balances[['S&P 500','Intl Small Stock','LT Treasury','Accelerating Dual Momentum']].plot(logy=True,ax=ax0)
plt.xlabel('')
plt.ylabel('Balance of $10,000\nInvestment', fontsize=14, fontweight='bold')
ax0.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))
ax0.set_yticks([10000, 20000, 50000, 100000, 200000, 500000])
ax0.set_ylim([8000, 500000])
ax0.set_xticklabels([])
ax0.set_xticks([1998, 2000, 2002, 2004, 2006, 2008, 2010, 2012, 2014, 2016, 2018])
ax0.set_xlim([1998, 2018])
plt.grid(b=True, which='major', linestyle='-')
plt.grid(b=True, which='minor', color='gray', linestyle=':')

#Plot the dollar performance over time
ax2 = plt.subplot2grid((6, 1), (4, 0), colspan=1, rowspan=2)
balances['Strategy/S&P 500'].plot(ax=ax2)
plt.ylabel('Strategy Relative\nPerformance to S&P 500', fontsize=14, fontweight='bold')
plt.xlabel('Date', fontsize=14, fontweight='bold')
ax2.set_xticks([1998, 2000, 2002, 2004, 2006, 2008, 2010, 2012, 2014, 2016, 2018])
ax2.set_xlim([1998, 2018])
Out[1]:
(1998, 2018)
In [26]:
balances['Strategy/S&P 500'].plot()
ax2 = plt.gca()
plt.ylabel('Strategy Relative\nPerformance to S&P 500', fontsize=14, fontweight='bold')
plt.xlabel('Date', fontsize=14, fontweight='bold')
ax2.set_xticks([1998, 2000, 2002, 2004, 2006, 2008, 2010, 2012, 2014, 2016, 2018])
ax2.set_xlim([1998, 2018])
Out[26]:
(1998, 2018)
In [11]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

#Plot the assets owned by month over time
ax1 = plt.subplot2grid((6, 1), (0, 0), colspan=1, rowspan=1)
weight = local_csv('WeightsL.csv').set_index(['Date'])
weight.plot(kind='bar', ax=ax1, stacked=True, width=1.0,edgecolor = "none")
ax1.set_yticklabels([])
ax1.set_xticklabels([])
plt.legend(loc='upper left')
plt.xlabel('')
plt.title('Accelerating Dual Momentum Strategy', fontsize=14, fontweight='bold')

#Plot the dollar performance over time
ax0 = plt.subplot2grid((6, 1), (1, 0), colspan=1, rowspan=3)
balances = local_csv('BalancesL.csv').set_index(['Date'])
balances[['S&P 500','EAFE Mid/Small','Gov Bonds','Accelerating Dual Momentum','GEM']].plot(logy=True,ax=ax0)
plt.xlabel('')
plt.ylabel('Real Balance of $1\nInvestment (after Inflation)', fontsize=14, fontweight='bold')
ax0.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))
ax0.set_xticklabels([])
plt.grid(b=True, which='major', linestyle='-')
#plt.grid(b=True, which='minor', color='gray', linestyle=':')

#Plot the relative performance to S&P500
ax2 = plt.subplot2grid((6, 1), (4, 0), colspan=1, rowspan=2)
balances[['Accel/S&P 500','GEM/S&P 500']].plot(ax=ax2,logy=True)
plt.ylabel('Strategy Relative\nPerformance to S&P 500', fontsize=14, fontweight='bold')
ax2.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))
plt.xlabel('Date', fontsize=14, fontweight='bold')
Out[11]:
<matplotlib.text.Text at 0x7fcd1bbcfe50>
In [16]:
balances[['S&P 500','EAFE Mid/Small','Gov Bonds','Accelerating Dual Momentum','GEM']].plot(logy=True)
ax0 = plt.gca()
plt.xlabel('')
plt.ylabel('Real Balance of $1\nInvestment (after Inflation)', fontsize=14, fontweight='bold')
ax0.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))
plt.grid(b=True, which='major', linestyle='-')
In [14]:
balances[['Accel/S&P 500','GEM/S&P 500']].plot(logy=True)
ax2 = plt.gca()
plt.ylabel('Strategy Relative\nPerformance to S&P 500', fontsize=14, fontweight='bold')
ax2.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))
plt.xlabel('Date', fontsize=14, fontweight='bold')
Out[14]:
<matplotlib.text.Text at 0x7fcd151a3b50>
In [24]:
balances = local_csv('30-year.csv').set_index(['Date'])
balances[['S&P 500','Gov Bonds','Accelerating Dual Momentum','GEM']].plot()
ax0 = plt.gca() 
plt.legend(loc='best')
plt.title('Trailing 30 Year Annualized Real Return (after Inflation)', fontsize=14, fontweight='bold')
ax0.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: '{:2.0f}%'.format(int(x))))
plt.xlabel('Date', fontsize=14, fontweight='bold')
plt.grid(b=True, which='major', linestyle='-')

Strategy Background

Asset Class Performance Data (January 1998 to February 2018)

All data includes dividends and the fees of the mutual funds, although there are no transaction costs. The assumed initial balance is 10,000 dollars.

Ticker Description Final Balance CAGR Stdev Best Year Worst Year Max. Drawdown Sharpe Ratio Sortino Ratio US Mkt Correlation
VFINX Vanguard S&P 500 40,087 7.13 14.89 32.18 -37.02 -50.97 0.41 0.58 0.99
VGTSX Vanguard Total Intl Stock Market 29,698 5.55 17.34 40.34 -44.10 -58.50 0.29 0.41 0.87
VBMFX Vanguard Total Bond Market 24,865 4.62 3.41 11.39 -2.26 -3.99 0.79 1.29 -0.10
- - - - - - - - - - -
VINEX Vanguard International Explorer 91,767 11.62 18.95 90.29 -46.62 -59.56 0.58 0.86 0.76
VUSTX Vanguard Long-Term US Treasury 34,048 6.26 10.31 29.28 -13.03 -16.68 0.46 0.75 -0.29

Strategy Performance

Using Portfolio Visualizer I "engineered" a timing model similar to Dual Momentum. The change from looking at only the 1 year return, to evaluating the 1, 3, & 6 month return is a major improvement, as is replacing a total bond fund with long term treasuries. Further improvement is made by using small cap global stocks which have greater "runs" of out performance and are less correlated to US stocks.

Name Change/Description Final Balance CAGR Stdev Best Year Worst Year Max. Drawdown Sharpe Ratio Sortino Ratio US Mkt Correlation
Dual Momentum Buy best option from looking at 1 Year return of US and Global Stocks, if negative, buy total bond fund. Popularized/Developed by Gary Antonacci 80,296 10.88 11.81 29.87 -18.27 -19.70 0.78 1.20 0.69
Accelerating Simple Dual Momentum Instead of only 1 year, look at 1 month, 3 months, and 6 months return. Weight these time ranges equally 159,464 13.98 11.65 48.99 -8.98 -16.74 1.01 1.74 0.63
Long-Term Treasuries instead Total Bond Replace total bond fund for long term treasuries, it moves much more aggressively and out-of-phase to stocks. 206,161 15.37 12.98 49.14 -8.39 -16.28 1.01 1.78 0.47
Small-Cap instead of Large Cap Global Stocks Replace the total ex-US fund with a small cap version which is much less correlated to large US stocks. 331,003 18.95 13.43 77.49 -3.81 -21.19 1.23 2.36 0.51
Accelerating Dual Momentum Replace the total ex-US fund with a small cap version which is much less correlated to large US stocks, also replace total bond with long-term treasuries. 426,408 20.45 14.60 77.49 0.15 -20.63 1.23 2.35 0.38
In [9]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

#Plot the assets owned by month over time
ax1 = plt.subplot2grid((6, 1), (0, 0), colspan=1, rowspan=1)
weight = local_csv('Weights2.csv').set_index(['Date'])
weight.plot(kind='bar', ax=ax1, stacked=True, width=1.0,edgecolor = "none")
ax1.set_yticklabels([])
ax1.set_xticklabels([])
plt.legend(loc='upper left')
plt.xlabel('')
plt.title('Engineered Simple Dual Momentum Strategy', fontsize=14, fontweight='bold')

#Plot the dollar performance over time
ax0 = plt.subplot2grid((6, 1), (1, 0), colspan=1, rowspan=3)
balances = local_csv('Balances2.csv').set_index(['Date'])
balances[['S&P 500','Total Intl Stock','Total US Bond','Simple Momentum']].plot(logy=True,ax=ax0)
plt.xlabel('')
plt.ylabel('Balance of $10,000\nInvestment', fontsize=14, fontweight='bold')
ax0.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))
ax0.set_yticks([10000, 20000, 50000, 100000, 200000, 500000])
ax0.set_ylim([8000, 500000])
ax0.set_xticklabels([])
ax0.set_xticks([1998, 2000, 2002, 2004, 2006, 2008, 2010, 2012, 2014, 2016, 2018])
ax0.set_xlim([1998, 2018])
plt.grid(b=True, which='major', linestyle='-')
plt.grid(b=True, which='minor', color='gray', linestyle=':')

#Plot the dollar performance over time
ax2 = plt.subplot2grid((6, 1), (4, 0), colspan=1, rowspan=2)
balances['Strategy/S&P 500'].plot(ax=ax2)
plt.ylabel('Strategy Relative\nPerformance to S&P 500', fontsize=14, fontweight='bold')
plt.xlabel('Date', fontsize=14, fontweight='bold')
ax2.set_xticks([1998, 2000, 2002, 2004, 2006, 2008, 2010, 2012, 2014, 2016, 2018])
ax2.set_xlim([1998, 2018])
Out[9]:
(1998, 2018)
In [10]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

#Plot the assets owned by month over time
ax1 = plt.subplot2grid((6, 1), (0, 0), colspan=1, rowspan=1)
weight = local_csv('Weights3.csv').set_index(['Date'])
weight.plot(kind='bar', ax=ax1, stacked=True, width=1.0,edgecolor = "none")
ax1.set_yticklabels([])
ax1.set_xticklabels([])
plt.legend(loc='upper left')
plt.xlabel('')
plt.title('Gary Antonacci Dual Momentum Strategy', fontsize=14, fontweight='bold')

#Plot the dollar performance over time
ax0 = plt.subplot2grid((6, 1), (1, 0), colspan=1, rowspan=3)
balances = local_csv('Balances3.csv').set_index(['Date'])
balances[['S&P 500','Total Intl Stock','Total US Bond','Dual Momentum']].plot(logy=True,ax=ax0)
plt.xlabel('')
plt.ylabel('Balance of $10,000\nInvestment', fontsize=14, fontweight='bold')
ax0.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))
ax0.set_yticks([10000, 20000, 50000, 100000, 200000, 500000])
ax0.set_ylim([8000, 500000])
ax0.set_xticklabels([])
ax0.set_xticks([1998, 2000, 2002, 2004, 2006, 2008, 2010, 2012, 2014, 2016, 2018])
ax0.set_xlim([1998, 2018])
plt.grid(b=True, which='major', linestyle='-')
plt.grid(b=True, which='minor', color='gray', linestyle=':')

#Plot the dollar performance over time
ax2 = plt.subplot2grid((6, 1), (4, 0), colspan=1, rowspan=2)
balances['Strategy/S&P 500'].plot(ax=ax2)
plt.ylabel('Strategy Relative\nPerformance to S&P 500', fontsize=14, fontweight='bold')
plt.xlabel('Date', fontsize=14, fontweight='bold')
ax2.set_xticks([1998, 2000, 2002, 2004, 2006, 2008, 2010, 2012, 2014, 2016, 2018])
ax2.set_xlim([1998, 2018])
Out[10]:
(1998, 2018)

Run Momentum Signal

Hit run on the following cell to determine which asset to buy. The asset/ETF with 1 is the asset with the momentum signal right now. Weight1 is for the simple 3 fund portfolio, Weight2 is for a modification on the 3 fund (replace total international with small-cap global, and total bond with long-term treasuries), and Weight3 is for the 6 asset portfolio.

In [1]:
import pandas as pd
import matplotlib.pyplot as plt

def engineered_momentum(end, start, sp500, midvalue, world, world_small, emerging, bonds, treasuries, tips):
    assets = [sp500, midvalue, world, world_small, emerging, bonds, treasuries, tips]
    df = pd.DataFrame(columns=['Ratio','Short','Long']) 
    data = get_pricing(assets, start_date=start, end_date=end)['close_price'].dropna()
    
    #Get best asset class within each subcategory
    df = pd.DataFrame(columns=['Weight1','Weight2','Weight3','Score1','Score2','1Year','6Mon','3Mon','1Mon']) 
    
    #Calculate Momentum Ratios
    for stock in assets:
        his = data[stock][-252:]
        df.loc[stock, '1Mon'] = his[-1] / his[-21] - 1
        df.loc[stock, '3Mon'] = his[-1] / his[-63] - 1
        df.loc[stock, '6Mon'] = his[-1] / his[-126] - 1
        df.loc[stock, '1Year'] = his[-1] / his[0] - 1

    #Check Term Trend is Positive
    df = df.astype(float)
    df['Score1'] = df['1Mon'] + df['3Mon'] + df['6Mon']
    df['Score2'] = df['3Mon'] + df['6Mon'] + df['1Year']
    
    '''
    Simple Strategy
    '''
    df['Weight1'] = 0.0
    df.loc[sp500, 'Weight1'] = df.loc[sp500,'Score1']
    df.loc[world, 'Weight1'] = df.loc[world,'Score1']
    df.loc[df['Weight1'] < 0, 'Weight1'] = 0.0
    df.loc[~df.index.isin(df['Weight1'].nlargest(1).index.tolist()),'Weight1'] = 0.0
    if len(df[df.Weight1 > 0]) == 0:
        df.loc[bonds, 'Weight1'] = 1.0
    df.loc[df.Weight1 > 0, 'Weight1'] = 1.0
    
    '''
    Engineered 3 Assets
    '''    
    df['Weight2'] = 0.0
    df.loc[sp500, 'Weight2'] = df.loc[sp500,'Score1']
    df.loc[world_small, 'Weight2'] = df.loc[world_small,'Score1']
    df.loc[df['Weight2'] < 0, 'Weight2'] = 0.0
    df.loc[~df.index.isin(df['Weight2'].nlargest(1).index.tolist()),'Weight2'] = 0.0
    if len(df[df.Weight2 > 0]) == 0:
        df.loc[treasuries, 'Weight2'] = 1.0
    df.loc[df.Weight2 > 0, 'Weight2'] = 1.0
    
    '''
    Engineered 6 Assets
    '''    
    df['Weight3'] = 0.0
    if df.loc[midvalue, 'Score2'] > df.loc[sp500, 'Score2']:
        df.loc[midvalue, 'Weight3'] = df.loc[midvalue, 'Score1']
    else:
        df.loc[sp500, 'Weight3'] = df.loc[sp500, 'Score1']
    if df.loc[emerging, 'Score2'] > df.loc[world_small, 'Score2']:
        df.loc[emerging, 'Weight3'] = df.loc[emerging, 'Score1']
    else:
        df.loc[world_small, 'Weight3'] = df.loc[world_small, 'Score1']
        
    df.loc[df['Weight3'] < 0, 'Weight3'] = 0.0
    df.loc[~df.index.isin(df['Weight3'].nlargest(1).index.tolist()),'Weight3'] = 0.0
    if len(df[df.Weight3 > 0]) == 0:
        if df.loc[tips, '1Mon'] > df.loc[treasuries, '1Mon']:
            df.loc[tips, 'Weight3'] = 1.0
        else:
            df.loc[treasuries, 'Weight3'] = 1.0
    df.loc[df.Weight3 > 0, 'Weight3'] = 1.0
    
    return df, data

"""
Define end date either by now, or offsetting etc.
"""
end = pd.Timestamp.utcnow() #pd.to_datetime('2017-11-01') #
print end
start = end - 300 * pd.tseries.offsets.BDay()

#Run strategy
df, data = engineered_momentum(end,
                               start,
                               symbols('SPY'),
                         symbols('VOE'),
                         symbols('VEU'),
                         symbols('VSS'),
                         symbols('VWO'),
                         symbols('BND'),
                         symbols('VGLT'),
                         symbols('TIP'))

#Plot and display results
returns = data.dropna().pct_change().dropna()+1
fig, ax = plt.subplots()
returns[-126:].cumprod().plot(ax = ax)
df
2018-04-02 01:57:57.330359+00:00
Out[1]:
Weight1 Weight2 Weight3 Score1 Score2 1Year 6Mon 3Mon 1Mon
Equity(8554 [SPY]) 0.0 0.0 0.0 0.037717 0.186104 0.136454 0.062064 -0.012414 -0.011933
Equity(32521 [VOE]) 0.0 0.0 0.0 0.036119 0.125624 0.094685 0.050268 -0.019328 0.005180
Equity(33486 [VEU]) 1.0 0.0 0.0 0.054718 0.213334 0.165263 0.050182 -0.002111 0.006647
Equity(38272 [VSS]) 0.0 1.0 0.0 0.074541 0.260259 0.195383 0.061929 0.002948 0.009665
Equity(27102 [VWO]) 0.0 0.0 1.0 0.125031 0.330268 0.203135 0.096690 0.030444 -0.002103
Equity(33652 [BND]) 0.0 0.0 0.0 -0.021225 -0.014250 0.012131 -0.012100 -0.014281 0.005156
Equity(38988 [VGLT]) 0.0 0.0 0.0 -0.021738 -0.008711 0.034692 -0.010400 -0.033003 0.021665
Equity(25801 [TIP]) 0.0 0.0 0.0 0.001715 0.004872 0.009926 0.001267 -0.006320 0.006769
In [5]:
df = returns.copy()
df.columns = ['S&P 500', 'US Mid Cap Value', 'ex-US Large Cap', 'ex-US Small Cap', 'Emerging Markets','Total Bond','Long Term Treasuries','TIPS'
    ]
df[-252:][['S&P 500','ex-US Small Cap','Long Term Treasuries']].cumprod().plot()
Out[5]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f40391e4a50>

More Assets?

Asset Class Performance Data (January 1998 to February 2018)

All data includes dividends and the fees of the mutual funds, although there are no transaction costs. The assumed initial balance is 10,000 dollars.

Ticker Description Final Balance CAGR Stdev Best Year Worst Year Max. Drawdown Sharpe Ratio Sortino Ratio US Mkt Correlation
VFINX Vanguard S&P 500 40,087 7.13 14.89 32.18 -37.02 -50.97 0.41 0.58 0.99
VGTSX Vanguard Total Intl Stock Market 29,698 5.55 17.34 40.34 -44.10 -58.50 0.29 0.41 0.87
VBMFX Vanguard Total Bond Market 24,865 4.62 3.41 11.39 -2.26 -3.99 0.79 1.29 -0.10
- - - - - - - - - - -
TRMCX T. Rowe Price US Mid-Cap Value Total 73,354 10.39 15.28 46.68 -34.57 -49.85 0.60 0.91 0.89
VINEX Vanguard International Explorer 91,767 11.62 18.95 90.29 -46.62 -59.56 0.58 0.86 0.76
VEIEX Vanguard Emerging Market Stocks 46,980 7.97 23.55 75.98 -52.81 -62.70 0.37 0.53 0.80
VUSTX Vanguard Long-Term US Treasury 34,048 6.26 10.31 29.28 -13.03 -16.68 0.46 0.75 -0.29

Asset Class Relative Strength Strategies

Using Portfolio Visualizer a few strategies are considered that pair correlated assets together and use relative momentum to pick between them.

Name Change/Description Final Balance CAGR Stdev Best Year Worst Year Max. Drawdown Sharpe Ratio Sortino Ratio US Mkt Correlation
S&P 500 and Mid-Cap Value Rank equally the trailing 3, 6, and 12 month returns and pick the best performer. 100,184 12.11 15.36 46.68 -38.07 -51.33 0.70 1.07 0.93
Emerging Markets & Global Small-Cap Same strategy as above but with global assets 109,195 12.59 21.01 90.29 -48.53 -60.22 0.58 0.87 0.75
Long Term Treasuries & TIPS Relative strength between TIPS and long treasuries, only look at last month. Note that this starts in 2001. 34,520 7.48 9.38 26.54 -15.38 -17.06 0.67 1.23 -0.27

I have a soft spot for mid cap value and believe that it is a more logical choice in the model although it slightly reduces performance. Keep in mind that not only is this looking at historical data, which is unlikely to repeat itself exactly (or even at all), it is also evaluating the performance over the last 20 years. Looking at longer historical data suggests that mid cap value is a better option although one needs to make their own assessment and decision.

Same is true for emerging markets and TIPS. Pairing small-cap international and emerging markets, pairing mid-cap value and S&P 500, and pairing TIPS with long-term treasuries does seem to work reasonably well but doesn't beat the strategy that just has 3 assets (S&P 500, small-cap int'l, and long-term treasuries), it requires a ton more transactions, and has more volatile performance relative to the benchmark (S&P 500).

Name Change/Description Final Balance CAGR Stdev Best Year Worst Year Max. Drawdown Sharpe Ratio Sortino Ratio US Mkt Correlation
Dual Momentum Buy best option from looking at 1 Year return of US and Global Stocks, if negative, buy total bond fund. Popularized/Developed by Gary Antonacci 80,296 10.88 11.81 29.87 -18.27 -19.70 0.78 1.20 0.69
Engineered Simple Dual Momentum Instead of only 1 year, look at 1 month, 3 months, and 6 months return. Weight these time ranges equally 159,464 13.98 11.65 48.99 -8.98 -16.74 1.01 1.74 0.63
Engineered Dual Momentum Replace the total ex-US fund with a small cap version which is much less correlated to large US stocks 426,408 20.45 14.60 77.49 0.15 -20.63 1.23 2.35 0.38
S&P500, Mid-Cap Value, Large Int'l, & Small Int'l To combat any potential of over fitting, keep all four stock options. This option will result in more trading needs as the increased stock selections jockey for position. 414,317 20.28 14.97 77.49 -6.08 -21.60 1.19 2.27 0.44
S&P500, Mid-Cap Value, Small Int'l, & Emerging Markets Same as above, but emerging markets instead of total int'l stocks. 335,237 19.02 16.86 75.58 -6.08 -28.58 1.01 1.86 0.44

Run Quantopian Backtests and Load to Analyze

The "Engineered Momentum Strategy" that consists of S&P 500, small-cap global stocks, and long-term treasuries seems to slightly underperform the strategy that pairs in mid-cap value, emerging markets and TIPS. These assets are directly compared to their counterpart like those relative strength strategies shown above, then compared to the other major asset.

In [1]:
import matplotlib.pyplot as plt
import numpy as np
import pyfolio as pf
import pandas as pd
from datetime import datetime

backtest_names = {
    'Engineered 6 Asset Rotation' : '5aab26b06d5e0e4061a1e778',
    'Engineered 3 Asset Rotation' : '5aab28eadbc97543533215d5',
    'Engineered 3 Asset - Multiple' : '5ac6d296d50c0e420f361f2a',
    'Simple 3 Asset Rotation' : '5aab2ed53a02e642b9c12d6c',
    'S&P 500': '5aab2c53da3e7f413267e00d'
}

backtests = {}
returns = {}
for b in backtest_names:
    print b
    backtests[b] = get_backtest(backtest_names[b])
    returns[b] = backtests[b].cumulative_performance.ending_portfolio_value/10000.0

df = pd.DataFrame(returns)
df.plot()
Engineered 3 Asset - Multiple
100% Time: 0:00:05|###########################################################|
Engineered 6 Asset Rotation
100% Time: 0:00:03|###########################################################|
Engineered 3 Asset Rotation
100% Time: 0:00:01|###########################################################|
Simple 3 Asset Rotation
100% Time: 0:00:04|###########################################################|
S&P 500
100% Time: 0:00:02|###########################################################|
Out[1]:
<matplotlib.axes._subplots.AxesSubplot at 0x7fc4af369c50>

Determine Tracking Error

I know that it is difficult to stay with a strategy as it loses to the index. So the following plot illustrates how the strategies do relative to the index over time. In the 14 year backtest the strategies almost double the returns of the S&P 500; but throughout that 14 year stretch there are specific pockets it quickly advances (the drops in the market). There are also times it loses as it try and switch too early out of stocks.

In [2]:
df_divide = df[['Engineered 3 Asset Rotation','Engineered 6 Asset Rotation','Simple 3 Asset Rotation','Engineered 3 Asset - Multiple']].divide(df['S&P 500'], axis='index')
df_divide.plot()
Out[2]:
<matplotlib.axes._subplots.AxesSubplot at 0x7fc4abc0f050>

Quantopian Algorithm Code

Simple (S&P 500, Global Large-Cap, Total US Bond) Engineered Momentum

In [ ]:
'''
Even weight 1, 3, 6 month between S&P 500 and Small-Cap Global
Default to Long Treasuries 

https://www.portfoliovisualizer.com/test-market-timing-model?s=y&coreSatellite=false&timingModel=6&startYear=1998&endYear=2018&initialAmount=10000&symbols=VFINX+VGTSX&singleAbsoluteMomentum=false&absoluteMomentumAsset=VFINX&volatilityTarget=9.0&downsideVolatility=false&outOfMarketAssetType=2&outOfMarketAsset=VBMFX&movingAverageSignal=1&movingAverageType=1&multipleTimingPeriods=true&periodWeighting=2&windowSize=12&windowSizeInDays=105&movingAverageType2=1&windowSize2=10&windowSizeInDays2=105&volatilityWindowSize=0&volatilityWindowSizeInDays=0&assetsToHold=1&allocationWeights=1&riskControl=false&riskWindowSize=10&riskWindowSizeInDays=0&rebalancePeriod=1&separateSignalAsset=false&tradeExecution=0&benchmark=VFINX&timingPeriods[0]=1&timingUnits[0]=2&timingWeights[0]=33&timingPeriods[1]=3&timingUnits[1]=2&timingWeights[1]=33&timingPeriods[2]=6&timingUnits[2]=2&timingWeights[2]=34&timingUnits[3]=2&timingWeights[3]=0&timingUnits[4]=2&timingWeights[4]=0&volatilityPeriodUnit=2&volatilityPeriodWeight=0
'''
import pandas as pd
import math
import numpy as np
import datetime

MAX_ASSETS = 1
MIN_BUY = 0
ROBINHOOD_GOLD = 0
 
def initialize(context):
    """
    Called once at the start of the algorithm.
    """   
    set_commission(commission.PerTrade(cost=0.00))
    
    schedule_function(set_allocation, date_rules.month_start(), time_rules.market_open())
    schedule_function(my_rebalance, date_rules.month_start(days_offset=0), time_rules.market_open(hours=1))
    schedule_function(buy_longs, date_rules.month_start(days_offset=0), time_rules.market_open(hours=2))
     
    context.sp500 =   sid(8554)    #S&P 500                             SPY
    context.world =   sid(22972)   #All World ex-US Stocks              EFA
    context.bonds =   sid(25485)   #Total Bond                          AGG
    context.started = 0
    
def set_allocation(context, data):
    """
    Get our portfolio allocation
    """    
        
    assets = [context.sp500, context.world, context.bonds]
        
    #Get best asset class within each subcategory
    df = pd.DataFrame(columns=['Weight','Score1','Score2','1Year','6Mon','3Mon','1Mon']) 
    
    #Calculate Momentum Ratios
    for stock in assets:
        his = data.history(stock, "price", 252, frequency="1d")
        df.loc[stock, '1Mon'] = his[-1] / his[-21] - 1
        df.loc[stock, '3Mon'] = his[-1] / his[-63] - 1
        df.loc[stock, '6Mon'] = his[-1] / his[-126] - 1
        df.loc[stock, '1Year'] = his[-1] / his[0] - 1
    
    #Check Term Trend is Positive
    df = df.astype(float)
    df['Score1'] = df['1Mon'] + df['3Mon'] + df['6Mon']
    df['Score2'] = df['3Mon'] + df['6Mon'] + df['1Year']
    df['Weight'] = df['Score1']
    df.loc[df['Weight'] < 0, 'Weight'] = 0.0
    
    #Set bonds to 0
    df.loc[context.bonds, 'Weight'] = 0.0
    
    #Get only top assets
    df.loc[~df.index.isin(df['Weight'].nlargest(MAX_ASSETS).index.tolist()),'Weight'] = 0.0
        
    #Add Safe if none others are positive
    if len(df[df.Weight > 0]) == 0:
        df.loc[context.bonds, 'Weight'] = 1.0
    
    #Determine Weights
    sum_weight = sum(df['Weight'])
    df['Weight'] = df['Weight']/sum_weight
    
    log.info(df.round(4))
    context.good = df
    
    record(sp500 = df.Weight.loc[context.sp500]*0.3,
           world = df.Weight.loc[context.world]*0.7,
           bonds = df.Weight.loc[context.bonds]*0.9,
           score = df.loc[df.Score1.idxmax(), 'Score1'],
           leverage = context.account.leverage)
        
def buy_longs(context, data):
    """
    Determine how much of each asset to buy and place orders, making sure no extra cash is used
    """    
    stocks = context.good.index.tolist()
    weight = context.good['Weight'].values.tolist()      
    n = len(weight)
    if n < 1:
        return
    
    #Determine necessary contribution
    for x in range(0, n):
        desired_balance = context.good.loc[stocks[x], 'Weight']*context.portfolio.portfolio_value
        curr_price = data.current(stocks[x],'price')
        current_balance = context.portfolio.positions[stocks[x]].amount*curr_price
        context.good.loc[stocks[x], 'Need'] = desired_balance-current_balance
        context.good.loc[stocks[x], 'Price'] = curr_price*1.005
    
    #Determine how much to get of each (truncate by share price)
    context.good['Get'] = context.good['Need']
    context.good.loc[context.good.Get < 0,'Get'] = 0 #set all gets less than 0 to 0
    get_sum = context.good['Get'].sum()
    if get_sum == 0:
        get_sum = 1
    cash = context.portfolio.cash + ROBINHOOD_GOLD
    context.good['Get'] = context.good['Get']*cash/get_sum #scale gets by available cash
    context.good.loc[context.good.Get < MIN_BUY,'Get'] = 0 #set all gets less than 0 to 0
    context.good['Shares'] = np.trunc(context.good['Get']/context.good['Price']) #determine number of shares to buy
    context.good['Get'] = context.good['Shares'] * context.good['Price'] #recalculate how much will be bought from truncated shares
    
    #Figure out remaining cash and buy more of the stock that needs it most
    cash = cash - context.good['Get'].sum()
    context.good.loc[context.good['Need'].idxmax(),'Get'] += cash #use up all cash
    context.good['Shares'] = np.trunc(context.good['Get']/context.good['Price']) #recalculate number of shares after adding left over cash back in
    context.good['Get'] = context.good['Shares'] * context.good['Price'] #recalculate how much will be bought from truncated shares
    
    #place orders for each asset
    for x in range(0, n):   
        if data.can_trade(stocks[x]):         
            order(stocks[x], context.good.loc[stocks[x], 'Shares'], style=LimitOrder(context.good.loc[stocks[x], 'Price']))
    log.info(context.good[['Weight','Need','Get']].sort_values(by='Need', ascending=False))
             

def my_rebalance(context,data):
    """
    Scale down stocks held that are in portfolio, sell any that aren't
    """
    context.started = 1
        
    context.long_turnover = 0
    good_stocks = context.good.index.tolist()
    print_out = ''
    
    #Sell stocks that are not in our lists
    for security in context.portfolio.positions:
        cost = context.portfolio.positions[security].cost_basis
        price = context.portfolio.positions[security].last_sale_price
        amount = context.portfolio.positions[security].amount
        gain = (price-cost)*amount
        if security not in good_stocks and data.can_trade(security):
            print_out += '\nSell: ' + security.symbol + ' | Gains: $' + '{:06.2f}'.format(gain) + ' | Gain: ' + '{:04.2f}'.format((price/cost-1)*100) + '%'
            order_target_percent(security,0)
            context.long_turnover += 1
                    
    #Determine weights and trim good stocks
    n = len(good_stocks)
    curr_weights = np.zeros(n)
    weight = context.good['Weight'].values.tolist()        
    for x in range(0, n):   
        security = good_stocks[x]
        curr_weights[x] = context.portfolio.positions[security].amount * context.portfolio.positions[security].last_sale_price / context.portfolio.portfolio_value
        if curr_weights[x] >  weight[x]:
            print_out += '\nTrim: ' + security.symbol
            order_target_percent(good_stocks[x],weight[x])
    log.info(print_out)

Engineered 3 Asset Momentum

In [ ]:
'''
Even weight 1, 3, 6 month between S&P 500 and Small-Cap Global
Default to Long Treasuries 

https://www.portfoliovisualizer.com/test-market-timing-model?s=y&coreSatellite=false&timingModel=6&startYear=1998&endYear=2018&initialAmount=10000&symbols=VFINX+VINEX&singleAbsoluteMomentum=false&absoluteMomentumAsset=VFINX&volatilityTarget=9.0&downsideVolatility=false&outOfMarketAssetType=2&outOfMarketAsset=VUSTX&movingAverageSignal=1&movingAverageType=1&multipleTimingPeriods=true&periodWeighting=2&windowSize=12&windowSizeInDays=105&movingAverageType2=1&windowSize2=10&windowSizeInDays2=105&volatilityWindowSize=0&volatilityWindowSizeInDays=0&assetsToHold=1&allocationWeights=1&riskControl=false&riskWindowSize=10&riskWindowSizeInDays=0&rebalancePeriod=1&separateSignalAsset=false&tradeExecution=0&benchmark=VFINX&timingPeriods[0]=1&timingUnits[0]=2&timingWeights[0]=33&timingPeriods[1]=3&timingUnits[1]=2&timingWeights[1]=33&timingPeriods[2]=6&timingUnits[2]=2&timingWeights[2]=34&timingUnits[3]=2&timingWeights[3]=0&timingUnits[4]=2&timingWeights[4]=0&volatilityPeriodUnit=2&volatilityPeriodWeight=0
'''
import pandas as pd
import math
import numpy as np
import datetime

MAX_ASSETS = 1
MIN_BUY = 0
ROBINHOOD_GOLD = 0
 
def initialize(context):
    """
    Called once at the start of the algorithm.
    """   
    set_commission(commission.PerTrade(cost=0.00))
    
    schedule_function(set_allocation, date_rules.month_start(), time_rules.market_open())
    schedule_function(my_rebalance, date_rules.month_start(days_offset=0), time_rules.market_open(hours=1))
    schedule_function(buy_longs, date_rules.month_start(days_offset=0), time_rules.market_open(hours=2))
     
    context.sp500 =   sid(8554)    #S&P 500                             SPY
    context.world =   sid(22972)   #All World ex-US Small Cap Stocks    EFA, SCZ after  7/1/2008, VSS after  1/1/2012
    context.bonds =   sid(23921)   #Long Term Treasuries                TLT
    context.started = 0
    
def set_allocation(context, data):
    """
    Get our portfolio allocation
    """    
    if get_datetime('US/Eastern').date() >= datetime.date(2008, 7, 1):
        context.world =   sid(35248)   #SCZ
    if get_datetime('US/Eastern').date() >= datetime.date(2010, 4, 1):
        context.world =   sid(38272)   #VSS
        
    assets = [context.sp500, context.world, context.bonds]
        
    #Get best asset class within each subcategory
    df = pd.DataFrame(columns=['Weight','Score1','Score2','1Year','6Mon','3Mon','1Mon']) 
    
    #Calculate Momentum Ratios
    for stock in assets:
        his = data.history(stock, "price", 252, frequency="1d")
        df.loc[stock, '1Mon'] = his[-1] / his[-21] - 1
        df.loc[stock, '3Mon'] = his[-1] / his[-63] - 1
        df.loc[stock, '6Mon'] = his[-1] / his[-126] - 1
        df.loc[stock, '1Year'] = his[-1] / his[0] - 1
    
    #Check Term Trend is Positive
    df = df.astype(float)
    df['Score1'] = df['1Mon'] + df['3Mon'] + df['6Mon']
    df['Score2'] = df['3Mon'] + df['6Mon'] + df['1Year']
    df['Weight'] = df['Score1']
    df.loc[df['Weight'] < 0, 'Weight'] = 0.0
    
    #Set bonds to 0
    df.loc[context.bonds, 'Weight'] = 0.0
    
    #Get only top assets
    df.loc[~df.index.isin(df['Weight'].nlargest(MAX_ASSETS).index.tolist()),'Weight'] = 0.0
        
    #Add Safe if none others are positive
    if len(df[df.Weight > 0]) == 0:
        df.loc[context.bonds, 'Weight'] = 1.0
    
    #Determine Weights
    sum_weight = sum(df['Weight'])
    df['Weight'] = df['Weight']/sum_weight
    
    log.info(df.round(4))
    context.good = df
    
    record(sp500 = df.Weight.loc[context.sp500]*0.3,
           world = df.Weight.loc[context.world]*0.7,
           bonds = df.Weight.loc[context.bonds]*0.9,
           score = df.loc[df.Score1.idxmax(), 'Score1'],
           leverage = context.account.leverage)
        
def buy_longs(context, data):
    """
    Determine how much of each asset to buy and place orders, making sure no extra cash is used
    """    
    stocks = context.good.index.tolist()
    weight = context.good['Weight'].values.tolist()      
    n = len(weight)
    if n < 1:
        return
    
    #Determine necessary contribution
    for x in range(0, n):
        desired_balance = context.good.loc[stocks[x], 'Weight']*context.portfolio.portfolio_value
        curr_price = data.current(stocks[x],'price')
        current_balance = context.portfolio.positions[stocks[x]].amount*curr_price
        context.good.loc[stocks[x], 'Need'] = desired_balance-current_balance
        context.good.loc[stocks[x], 'Price'] = curr_price*1.005
    
    #Determine how much to get of each (truncate by share price)
    context.good['Get'] = context.good['Need']
    context.good.loc[context.good.Get < 0,'Get'] = 0 #set all gets less than 0 to 0
    get_sum = context.good['Get'].sum()
    if get_sum == 0:
        get_sum = 1
    cash = context.portfolio.cash + ROBINHOOD_GOLD
    context.good['Get'] = context.good['Get']*cash/get_sum #scale gets by available cash
    context.good.loc[context.good.Get < MIN_BUY,'Get'] = 0 #set all gets less than 0 to 0
    context.good['Shares'] = np.trunc(context.good['Get']/context.good['Price']) #determine number of shares to buy
    context.good['Get'] = context.good['Shares'] * context.good['Price'] #recalculate how much will be bought from truncated shares
    
    #Figure out remaining cash and buy more of the stock that needs it most
    cash = cash - context.good['Get'].sum()
    context.good.loc[context.good['Need'].idxmax(),'Get'] += cash #use up all cash
    context.good['Shares'] = np.trunc(context.good['Get']/context.good['Price']) #recalculate number of shares after adding left over cash back in
    context.good['Get'] = context.good['Shares'] * context.good['Price'] #recalculate how much will be bought from truncated shares
    
    #place orders for each asset
    for x in range(0, n):   
        if data.can_trade(stocks[x]):         
            order(stocks[x], context.good.loc[stocks[x], 'Shares'], style=LimitOrder(context.good.loc[stocks[x], 'Price']))
    log.info(context.good[['Weight','Need','Get']].sort_values(by='Need', ascending=False))
             

def my_rebalance(context,data):
    """
    Scale down stocks held that are in portfolio, sell any that aren't
    """
    context.started = 1
        
    context.long_turnover = 0
    good_stocks = context.good.index.tolist()
    print_out = ''
    
    #Sell stocks that are not in our lists
    for security in context.portfolio.positions:
        cost = context.portfolio.positions[security].cost_basis
        price = context.portfolio.positions[security].last_sale_price
        amount = context.portfolio.positions[security].amount
        gain = (price-cost)*amount
        if security not in good_stocks and data.can_trade(security):
            print_out += '\nSell: ' + security.symbol + ' | Gains: $' + '{:06.2f}'.format(gain) + ' | Gain: ' + '{:04.2f}'.format((price/cost-1)*100) + '%'
            order_target_percent(security,0)
            context.long_turnover += 1
                    
    #Determine weights and trim good stocks
    n = len(good_stocks)
    curr_weights = np.zeros(n)
    weight = context.good['Weight'].values.tolist()        
    for x in range(0, n):   
        security = good_stocks[x]
        curr_weights[x] = context.portfolio.positions[security].amount * context.portfolio.positions[security].last_sale_price / context.portfolio.portfolio_value
        if curr_weights[x] >  weight[x]:
            print_out += '\nTrim: ' + security.symbol
            order_target_percent(good_stocks[x],weight[x])
    log.info(print_out)

Engineered 6 Asset Momentum

In [ ]:
'''
Even weight 1, 3, 6 month
Default to Long Treasuries or Tips depending on 1 month relative strength

Compare 3, 6, 12 month score of S&P 500 vs Mid Value, and Small International vs Emerging Markets

https://www.portfoliovisualizer.com/test-market-timing-model?s=y&coreSatellite=false&timingModel=6&startYear=1998&endYear=2018&initialAmount=10000&symbols=VFINX+VINEX+TRMCX+VEIEX&singleAbsoluteMomentum=false&absoluteMomentumAsset=VFINX&volatilityTarget=9.0&downsideVolatility=false&outOfMarketAssetType=2&outOfMarketAsset=VUSTX&movingAverageSignal=1&movingAverageType=1&multipleTimingPeriods=true&periodWeighting=2&windowSize=12&windowSizeInDays=105&movingAverageType2=1&windowSize2=10&windowSizeInDays2=105&volatilityWindowSize=0&volatilityWindowSizeInDays=0&assetsToHold=1&allocationWeights=1&riskControl=false&riskWindowSize=10&riskWindowSizeInDays=0&rebalancePeriod=1&separateSignalAsset=false&tradeExecution=0&benchmark=VFINX&timingPeriods[0]=1&timingUnits[0]=2&timingWeights[0]=33&timingPeriods[1]=3&timingUnits[1]=2&timingWeights[1]=33&timingPeriods[2]=6&timingUnits[2]=2&timingWeights[2]=34&timingUnits[3]=2&timingWeights[3]=0&timingUnits[4]=2&timingWeights[4]=0&volatilityPeriodUnit=2&volatilityPeriodWeight=0
'''
import pandas as pd
import math
import numpy as np
import datetime

MAX_ASSETS = 1
MIN_BUY = 0
ROBINHOOD_GOLD = 0
 
def initialize(context):
    """
    Called once at the start of the algorithm.
    """   
    set_commission(commission.PerTrade(cost=0.00))
    
    schedule_function(set_allocation, date_rules.month_start(), time_rules.market_open())
    schedule_function(my_rebalance, date_rules.month_start(days_offset=0), time_rules.market_open(hours=1))
    schedule_function(buy_longs, date_rules.month_start(days_offset=0), time_rules.market_open(hours=2))
     
    context.sp500 =         sid(8554)    #S&P 500                             SPY
    context.midvalue =      sid(21770)   #US Midcap Value                     IJJ, VOE after  7/1/2008
    context.world_small =   sid(22972)   #All World ex-US Small Cap Stocks    EFA, SCZ after  7/1/2008, VSS after  1/1/2012
    context.emerging =      sid(24705)   #Emerging Markets                    EEM, VWO after 7/1/2008
    context.treasuries =    sid(23921)   #Long Term Treasuries                TLT
    context.tips =          sid(25801)   #Inflation Protected (TIPS)          TIP
    
    context.started = 0
    
def set_allocation(context, data):
    """
    Get our portfolio allocation
    """    
    if get_datetime('US/Eastern').date() >= datetime.date(2008, 7, 1):
        context.world_small =   sid(35248)   #SCZ
        context.midvalue =      sid(32521)   #VOE
        context.emerging =      sid(27102)   #VWO
    if get_datetime('US/Eastern').date() >= datetime.date(2010, 4, 1):
        context.world_small =   sid(38272)   #VSS
        
    assets = [context.sp500, context.midvalue, context.world_small, context.emerging, context.treasuries, context.tips]
        
    #Get best asset class within each subcategory
    df = pd.DataFrame(columns=['Weight','Score1','Score2','1Year','6Mon','3Mon','1Mon']) 
    
    #Calculate Momentum Ratios
    for stock in assets:
        his = data.history(stock, "price", 252, frequency="1d")
        df.loc[stock, '1Mon'] = his[-1] / his[-21] - 1
        df.loc[stock, '3Mon'] = his[-1] / his[-63] - 1
        df.loc[stock, '6Mon'] = his[-1] / his[-126] - 1
        df.loc[stock, '1Year'] = his[-1] / his[0] - 1
    
    #Check Term Trend is Positive
    df = df.astype(float)
    df['Score1'] = df['1Mon'] + df['3Mon'] + df['6Mon']
    df['Score2'] = df['3Mon'] + df['6Mon'] + df['1Year']
    df['Weight'] = df['Score1']
    df.loc[df['Weight'] < 0, 'Weight'] = 0.0
    
    #Set outofmarket, and allworld to 0
    df.loc[context.treasuries, 'Weight'] = 0.0
    df.loc[context.tips, 'Weight'] = 0.0
    
    #Check long term trend of mid-value vs S&P 500
    if df.loc[context.midvalue, 'Score2'] > df.loc[context.sp500, 'Score2']:
        df.loc[context.sp500, 'Weight'] = 0.0
        
    #Check long term trend of emerging markets and small international
    if df.loc[context.emerging, 'Score2'] > df.loc[context.world_small, 'Score2']:
        df.loc[context.world_small, 'Weight'] = 0.0
    
    #Get only top assets
    df.loc[~df.index.isin(df['Weight'].nlargest(MAX_ASSETS).index.tolist()),'Weight'] = 0.0
        
    #Add Safe if none others are positive
    if len(df[df.Weight > 0]) == 0:
        if df.loc[context.tips, '1Mon'] > df.loc[context.treasuries, '1Mon']:
            df.loc[context.tips, 'Weight'] = 1.0
        else:
            df.loc[context.treasuries, 'Weight'] = 1.0
    
    #Determine Weights
    sum_weight = sum(df['Weight'])
    df['Weight'] = df['Weight']/sum_weight
    
    log.info(df.round(4))
    context.good = df
    
    record(sp500 = df.Weight.loc[context.sp500]*0.2,
           midvalue = df.Weight.loc[context.midvalue]*0.4,
           world_small = df.Weight.loc[context.world_small]*0.6,
           emerging = df.Weight.loc[context.emerging]*0.8,
           bonds = df.Weight.loc[context.treasuries] + df.Weight.loc[context.tips])
        
def buy_longs(context, data):
    """
    Determine how much of each asset to buy and place orders, making sure no extra cash is used
    """    
    stocks = context.good.index.tolist()
    weight = context.good['Weight'].values.tolist()      
    n = len(weight)
    if n < 1:
        return
    
    #Determine necessary contribution
    for x in range(0, n):
        desired_balance = context.good.loc[stocks[x], 'Weight']*context.portfolio.portfolio_value
        curr_price = data.current(stocks[x],'price')
        current_balance = context.portfolio.positions[stocks[x]].amount*curr_price
        context.good.loc[stocks[x], 'Need'] = desired_balance-current_balance
        context.good.loc[stocks[x], 'Price'] = curr_price*1.005
    
    #Determine how much to get of each (truncate by share price)
    context.good['Get'] = context.good['Need']
    context.good.loc[context.good.Get < 0,'Get'] = 0 #set all gets less than 0 to 0
    get_sum = context.good['Get'].sum()
    if get_sum == 0:
        get_sum = 1
    cash = context.portfolio.cash + ROBINHOOD_GOLD
    context.good['Get'] = context.good['Get']*cash/get_sum #scale gets by available cash
    context.good.loc[context.good.Get < MIN_BUY,'Get'] = 0 #set all gets less than 0 to 0
    context.good['Shares'] = np.trunc(context.good['Get']/context.good['Price']) #determine number of shares to buy
    context.good['Get'] = context.good['Shares'] * context.good['Price'] #recalculate how much will be bought from truncated shares
    
    #Figure out remaining cash and buy more of the stock that needs it most
    cash = cash - context.good['Get'].sum()
    context.good.loc[context.good['Need'].idxmax(),'Get'] += cash #use up all cash
    context.good['Shares'] = np.trunc(context.good['Get']/context.good['Price']) #recalculate number of shares after adding left over cash back in
    context.good['Get'] = context.good['Shares'] * context.good['Price'] #recalculate how much will be bought from truncated shares
    
    #place orders for each asset
    for x in range(0, n):   
        if data.can_trade(stocks[x]):         
            order(stocks[x], context.good.loc[stocks[x], 'Shares'], style=LimitOrder(context.good.loc[stocks[x], 'Price']))
    log.info(context.good[['Weight','Need','Get']].sort_values(by='Need', ascending=False))
             

def my_rebalance(context,data):
    """
    Scale down stocks held that are in portfolio, sell any that aren't
    """
    context.started = 1
        
    context.long_turnover = 0
    good_stocks = context.good.index.tolist()
    print_out = ''
    
    #Sell stocks that are not in our lists
    for security in context.portfolio.positions:
        cost = context.portfolio.positions[security].cost_basis
        price = context.portfolio.positions[security].last_sale_price
        amount = context.portfolio.positions[security].amount
        gain = (price-cost)*amount
        if security not in good_stocks and data.can_trade(security):
            print_out += '\nSell: ' + security.symbol + ' | Gains: $' + '{:06.2f}'.format(gain) + ' | Gain: ' + '{:04.2f}'.format((price/cost-1)*100) + '%'
            order_target_percent(security,0)
            context.long_turnover += 1
                    
    #Determine weights and trim good stocks
    n = len(good_stocks)
    curr_weights = np.zeros(n)
    weight = context.good['Weight'].values.tolist()        
    for x in range(0, n):   
        security = good_stocks[x]
        curr_weights[x] = context.portfolio.positions[security].amount * context.portfolio.positions[security].last_sale_price / context.portfolio.portfolio_value
        if curr_weights[x] >  weight[x]:
            print_out += '\nTrim: ' + security.symbol
            order_target_percent(good_stocks[x],weight[x])
    log.info(print_out)

Multiple Weighting

Weight the two stock ETFs based on score

In [ ]:
'''
Even weight 1, 3, 6 month between S&P 500 and Small-Cap Global
Default to Long Treasuries 

https://www.portfoliovisualizer.com/test-market-timing-model?s=y&coreSatellite=false&timingModel=6&startYear=1998&endYear=2018&initialAmount=10000&symbols=VFINX+VINEX&singleAbsoluteMomentum=false&absoluteMomentumAsset=VFINX&volatilityTarget=9.0&downsideVolatility=false&outOfMarketAssetType=2&outOfMarketAsset=VUSTX&movingAverageSignal=1&movingAverageType=1&multipleTimingPeriods=true&periodWeighting=2&windowSize=12&windowSizeInDays=105&movingAverageType2=1&windowSize2=10&windowSizeInDays2=105&volatilityWindowSize=0&volatilityWindowSizeInDays=0&assetsToHold=1&allocationWeights=1&riskControl=false&riskWindowSize=10&riskWindowSizeInDays=0&rebalancePeriod=1&separateSignalAsset=false&tradeExecution=0&benchmark=VFINX&timingPeriods[0]=1&timingUnits[0]=2&timingWeights[0]=33&timingPeriods[1]=3&timingUnits[1]=2&timingWeights[1]=33&timingPeriods[2]=6&timingUnits[2]=2&timingWeights[2]=34&timingUnits[3]=2&timingWeights[3]=0&timingUnits[4]=2&timingWeights[4]=0&volatilityPeriodUnit=2&volatilityPeriodWeight=0
'''
import pandas as pd
import math
import numpy as np
import datetime

MAX_ASSETS = 2
MIN_BUY = 0
ROBINHOOD_GOLD = 0
 
def initialize(context):
    """
    Called once at the start of the algorithm.
    """   
    set_commission(commission.PerTrade(cost=0.00))
    
    schedule_function(set_allocation, date_rules.month_start(), time_rules.market_open())
    schedule_function(my_rebalance, date_rules.month_start(days_offset=0), time_rules.market_open(hours=1))
    schedule_function(buy_longs, date_rules.month_start(days_offset=0), time_rules.market_open(hours=2))
     
    context.sp500 =   sid(8554)    #S&P 500                             SPY
    context.world =   sid(22972)   #All World ex-US Small Cap Stocks    EFA, SCZ after  7/1/2008, VSS after  1/1/2012
    context.bonds =   sid(23921)   #Long Term Treasuries                TLT
    context.started = 0
    
def set_allocation(context, data):
    """
    Get our portfolio allocation
    """    
    if get_datetime('US/Eastern').date() >= datetime.date(2008, 7, 1):
        context.world =   sid(35248)   #SCZ
    if get_datetime('US/Eastern').date() >= datetime.date(2010, 4, 1):
        context.world =   sid(38272)   #VSS
        
    assets = [context.sp500, context.world, context.bonds]
        
    #Get best asset class within each subcategory
    df = pd.DataFrame(columns=['Weight','Score1','Score2','1Year','6Mon','3Mon','1Mon']) 
    
    #Calculate Momentum Ratios
    for stock in assets:
        his = data.history(stock, "price", 252, frequency="1d")
        df.loc[stock, '1Mon'] = his[-1] / his[-21] - 1
        df.loc[stock, '3Mon'] = his[-1] / his[-63] - 1
        df.loc[stock, '6Mon'] = his[-1] / his[-126] - 1
        df.loc[stock, '1Year'] = his[-1] / his[0] - 1
    
    #Check Term Trend is Positive
    df = df.astype(float)
    df['Score1'] = df['1Mon'] + df['3Mon'] + df['6Mon']
    df['Score2'] = df['3Mon'] + df['6Mon'] + df['1Year']
    df['Weight'] = df['Score1']
    df.loc[df['Weight'] < 0, 'Weight'] = 0.0
    
    #Set bonds to 0
    df.loc[context.bonds, 'Weight'] = 0.0
    
    #Get only top assets
    df.loc[~df.index.isin(df['Weight'].nlargest(MAX_ASSETS).index.tolist()),'Weight'] = 0.0
        
    #Add Safe if none others are positive
    if len(df[df.Weight > 0]) == 0:
        df.loc[context.bonds, 'Weight'] = 1.0
    
    #Determine Weights
    sum_weight = sum(df['Weight'])
    df['Weight'] = df['Weight']/sum_weight
    
    log.info(df.round(4))
    context.good = df
    
    record(sp500 = df.Weight.loc[context.sp500]*0.3,
           world = df.Weight.loc[context.world]*0.7,
           bonds = df.Weight.loc[context.bonds]*0.9,
           score = df.loc[df.Score1.idxmax(), 'Score1'],
           leverage = context.account.leverage)
        
def buy_longs(context, data):
    """
    Determine how much of each asset to buy and place orders, making sure no extra cash is used
    """    
    stocks = context.good.index.tolist()
    weight = context.good['Weight'].values.tolist()      
    n = len(weight)
    if n < 1:
        return
    
    #Determine necessary contribution
    for x in range(0, n):
        desired_balance = context.good.loc[stocks[x], 'Weight']*context.portfolio.portfolio_value
        curr_price = data.current(stocks[x],'price')
        current_balance = context.portfolio.positions[stocks[x]].amount*curr_price
        context.good.loc[stocks[x], 'Need'] = desired_balance-current_balance
        context.good.loc[stocks[x], 'Price'] = curr_price*1.005
    
    #Determine how much to get of each (truncate by share price)
    context.good['Get'] = context.good['Need']
    context.good.loc[context.good.Get < 0,'Get'] = 0 #set all gets less than 0 to 0
    get_sum = context.good['Get'].sum()
    if get_sum == 0:
        get_sum = 1
    cash = context.portfolio.cash + ROBINHOOD_GOLD
    context.good['Get'] = context.good['Get']*cash/get_sum #scale gets by available cash
    context.good.loc[context.good.Get < MIN_BUY,'Get'] = 0 #set all gets less than 0 to 0
    context.good['Shares'] = np.trunc(context.good['Get']/context.good['Price']) #determine number of shares to buy
    context.good['Get'] = context.good['Shares'] * context.good['Price'] #recalculate how much will be bought from truncated shares
    
    #Figure out remaining cash and buy more of the stock that needs it most
    cash = cash - context.good['Get'].sum()
    context.good.loc[context.good['Need'].idxmax(),'Get'] += cash #use up all cash
    context.good['Shares'] = np.trunc(context.good['Get']/context.good['Price']) #recalculate number of shares after adding left over cash back in
    context.good['Get'] = context.good['Shares'] * context.good['Price'] #recalculate how much will be bought from truncated shares
    
    #place orders for each asset
    for x in range(0, n):   
        if data.can_trade(stocks[x]):         
            order(stocks[x], context.good.loc[stocks[x], 'Shares'], style=LimitOrder(context.good.loc[stocks[x], 'Price']))
    log.info(context.good[['Weight','Need','Get']].sort_values(by='Need', ascending=False))
             

def my_rebalance(context,data):
    """
    Scale down stocks held that are in portfolio, sell any that aren't
    """
    context.started = 1
        
    context.long_turnover = 0
    good_stocks = context.good.index.tolist()
    print_out = ''
    
    #Sell stocks that are not in our lists
    for security in context.portfolio.positions:
        cost = context.portfolio.positions[security].cost_basis
        price = context.portfolio.positions[security].last_sale_price
        amount = context.portfolio.positions[security].amount
        gain = (price-cost)*amount
        if security not in good_stocks and data.can_trade(security):
            print_out += '\nSell: ' + security.symbol + ' | Gains: $' + '{:06.2f}'.format(gain) + ' | Gain: ' + '{:04.2f}'.format((price/cost-1)*100) + '%'
            order_target_percent(security,0)
            context.long_turnover += 1
                    
    #Determine weights and trim good stocks
    n = len(good_stocks)
    curr_weights = np.zeros(n)
    weight = context.good['Weight'].values.tolist()        
    for x in range(0, n):   
        security = good_stocks[x]
        curr_weights[x] = context.portfolio.positions[security].amount * context.portfolio.positions[security].last_sale_price / context.portfolio.portfolio_value
        if curr_weights[x] >  weight[x]:
            print_out += '\nTrim: ' + security.symbol
            order_target_percent(good_stocks[x],weight[x])
    log.info(print_out)