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.
Run below cell for plots, will need to upload relevant CSV files to your research data area.
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])
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])
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')
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='-')
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')
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='-')
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 |
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 |
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])
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])
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.
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
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()
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 |
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 |
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.
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()
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.
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()
'''
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)
'''
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)
'''
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)
Weight the two stock ETFs based on score
'''
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)