In [2]:
import pandas as pd
import itable
import ffn
import talib
%matplotlib inline
def side_by_side(*objs, **kwds):
from pandas.formats.printing import adjoin
space = kwds.get('space', 4)
reprs = [repr(obj).split('\n') for obj in objs]
print (adjoin(space, *reprs))
In [3]:
def monthly_return_table (daily_prices) :
#monthly_returns = daily_prices.resample('M').last().pct_change()
monthly_returns = daily_prices.resample('M', how='last').pct_change()
df = pd.DataFrame(monthly_returns.values, columns=['Data'])
df['Month'] = monthly_returns.index.month
df['Year']= monthly_returns.index.year
table = df.pivot_table(index='Year', columns='Month').fillna(0).round(4) * 100
#annual_returns = daily_prices.resample('12M').last().pct_change()[1:].values.round(4) * 100
annual_returns = daily_prices.resample('12M', how='last').pct_change()[1:].values.round(4) * 100
if len(table) > len(annual_returns) :
table = table[1:]
table['Annual Returns'] = annual_returns
return table
In [4]:
def endpoints(start=None, end=None, period='m', trading_days=None) :
if trading_days is not None:
dates = trading_days
# the following 2 lines cause python 3.4.2 to crash, so removed them
# elif start is not None and end is not None:
# dates = tradingcalendar.get_trading_days(start, end)
else:
print ('\n** ERROR : must either provide pandas series (or df) of trading days \n')
print (' or a start and end date\n')
if isinstance(period, int) :
dates = [dates[i] for i in range(0, len(dates), period)]
else :
if period == 'm' : months = 1
elif period == 'q' : months = 3
elif period == 'b' : months = 6
elif period == 'y' : months = 12
e_dates = [dates[i - 1] for i in range(1,len(dates))\
if dates[i].month > dates[i-1].month\
or dates[i].year > dates[i-1].year ]+ list([dates[-1]])
dates = [e_dates[i] for i in range(0,len(e_dates),months)]
return dates
In [5]:
# THIS ONE MATCHES PV
# SEE PV backtest :https://goo.gl/lBR4K9
# AND spreadsheet : https://goo.gl/8KGp58
# and Quantopian backtest : https://goo.gl/xytT5L
def backtest(prices, weights, capital, offset=1, commission=0.) :
rebalance_dates = weights.index
buy_dates = [prices.index[d + offset] for d in range(len(prices.index)-1) if prices.index[d] in rebalance_dates ]
print ('FIRST BUY DATE = {}\n'.format(buy_dates[0]))
p_holdings = pd.DataFrame(0, index=prices.index, columns=prices.columns)
cash = 0.
for i, date in enumerate(prices.index):
if date in rebalance_dates :
# print ('--------------------------------------------------------------------')
new_weights = weights.loc[date]
p_holdings.iloc [i] = p_holdings.iloc [i - 1]
if date in buy_dates :
if date == buy_dates[0] :
p_holdings.loc[date] = (capital * weights.iloc[0] / prices.loc[date])
# print ('INIT', cash, p_holdings.iloc[i-1],prices.loc[date], new_weights)
else :
portfolio_value = cash + (p_holdings.iloc[i - 1] * prices.loc[date]).sum() * new_weights
p_holdings.iloc[i] = (portfolio_value / prices.loc[date]).fillna(0)
# print ('{} BUY \n{}\n{}\n{}\n{}\n{}\nHOLDINGS\n{}\n'.format(date,cash,portfolio_value,p_holdings.iloc[i-1],
# prices.loc[date],new_weights,p_holdings.iloc[i]))
cash = (portfolio_value - p_holdings.iloc[i] * prices.loc[date]).sum()
# print ('{}\nPORTFOLIO VALUE\n{}\nCASH = {}'.format(date, portfolio_value,cash))
else :
p_holdings.iloc [i] = p_holdings.iloc [i - 1]
#print ('{} HOLDINGS UNCHANGED'.format(date))
p_value = (p_holdings * prices).sum(1)[p_holdings.index>=buy_dates[0]]
# print(p_holdings, )
p_weights = p_holdings.mul(prices).div(p_holdings.mul(prices).sum(axis=1), axis=0).fillna(0)
return p_value, p_holdings, p_weights
In [6]:
symbols =['VCVSX','VWINX','VWEHX','VGHCX','VUSTX','VFIIX','VWAHX','FGOVX','FFXSX']
cash_proxy = 'CASHX'
risk_free = 'FFXSX'
rs_lookback = 1
risk_lookback = 1
n_top = 3
# get data
tickers = symbols.copy()
if cash_proxy != 'CASHX' :
tickers = list(set(tickers + [cash_proxy]))
if isinstance(risk_free, str) :
tickers = list(set(tickers + [risk_free]))
data = pd.DataFrame (columns=tickers)
for symbol in tickers :
url = 'http://chart.finance.yahoo.com/table.csv?s=' + symbol + '&ignore=.csv'
data[symbol] = pd.read_csv(url, parse_dates=True, index_col='Date').sort_index(ascending=True)['Adj Close']
inception_dates = pd.DataFrame([data[ticker].first_valid_index() for ticker in data.columns],
index=data.keys(), columns=['inception'])
print (inception_dates)
prices = data.copy().dropna()
end_points = endpoints(period='m', trading_days=prices.index)
prices_m = prices.loc[end_points]
returns = prices_m[symbols].pct_change(rs_lookback)[rs_lookback:]
absolute_momentum_rule = returns > 0
if isinstance(risk_free, int) :
excess_returns = returns
else :
risk_free_returns = prices_m[risk_free].pct_change(rs_lookback)[rs_lookback:]
excess_returns = returns.subtract(risk_free_returns, axis=0).dropna()
rebalance_dates = excess_returns.index.join(absolute_momentum_rule.index, how='inner')
# relative strength ranking
ranked = excess_returns.loc[rebalance_dates][symbols].rank(ascending=False, axis=1, method='dense')
# elligibility rule - top n_top ranked securities
elligible = ranked[ranked<=n_top] > 0
# equal weight allocations
elligible = elligible.multiply(1./elligible.sum(1), axis=0)
# downside protection
weights = pd.DataFrame(0.,index=elligible.index, columns=prices.columns)
if cash_proxy == 'CASHX' :
weights[cash_proxy] = 0
prices[cash_proxy] = 1.
weights[symbols] = (elligible * absolute_momentum_rule).dropna()
weights[cash_proxy] += 1 - weights[symbols].sum(axis=1)
# backtest
p_value, p_holdings, p_weights = backtest(prices, weights, 10000., offset=0, commission=10.)
p_value.plot(figsize=(15,10), grid=True)
inception
VWEHX 1980-01-02
VWAHX 1980-01-02
FFXSX 1986-11-10
VCVSX 1986-06-17
VGHCX 1984-05-23
VUSTX 1986-05-19
VWINX 1980-01-02
VFIIX 1980-06-27
FGOVX 1980-01-02
FIRST BUY DATE = 1986-12-31 00:00:00
Out[6]:
<matplotlib.axes._subplots.AxesSubplot at 0xb777a20>
In [7]:
# algo stats
ffn.calc_perf_stats(p_value).display()
Stats for None from 1986-12-31 00:00:00 - 2016-12-06 00:00:00
Annual risk-free rate considered: 0.00%
Summary:
Total Return Sharpe CAGR Max Drawdown
-------------- -------- ------ --------------
2365.40% 1.83 11.30% -7.99%
Annualized Returns:
mtd 3m 6m ytd 1y 3y 5y 10y incep.
------ ------ ------ ----- ----- ----- ----- ----- --------
-0.12% -3.55% -2.71% 1.89% 2.32% 4.90% 7.93% 9.34% 11.30%
Periodic:
daily monthly yearly
------ ------- --------- --------
sharpe 1.83 1.84 1.56
mean 10.88% 10.90% 11.51%
vol 5.95% 5.93% 7.38%
skew -0.06 0.16 0.43
kurt 4.81 0.15 -0.50
best 3.79% 6.48% 27.31%
worst -3.37% -3.47% 0.47%
Drawdowns:
max avg # days
------ ------ --------
-7.99% -0.93% 19.39
Misc:
--------------- -------
avg. up month 1.83%
avg. down month -0.92%
up year % 100.00%
12m up % 96.29%
--------------- -------
In [12]:
p_value.index
Out[12]:
DatetimeIndex(['1986-12-31', '1987-01-02', '1987-01-05', '1987-01-06',
'1987-01-07', '1987-01-08', '1987-01-09', '1987-01-12',
'1987-01-13', '1987-01-14',
...
'2016-11-22', '2016-11-23', '2016-11-25', '2016-11-28',
'2016-11-29', '2016-11-30', '2016-12-01', '2016-12-02',
'2016-12-05', '2016-12-06'],
dtype='datetime64[ns]', name='Date', length=7547, freq=None)
In [19]:
import empyrical as e
print((e.cagr(p_value.pct_change()[1:]) * 100).round(2))
print((e.sharpe_ratio(p_value.pct_change()[1:])).round(2))
print((e.sortino_ratio(p_value.pct_change()[1:])).round(2))
11.3
1.83
2.78
In [20]:
e.annual_volatility(p_value.pct_change())
Out[20]:
0.059470047305524215
In [7]:
def highlight_pos_neg (s) :
is_positive = s > 0
return ['background-color : rgb(127,255,0)' if v else 'background-color : rgb(255,99,71)' for v in is_positive]
df = monthly_return_table (p_value)
df.style.\
apply(highlight_pos_neg)
Out[7]:
Data
Data
Data
Data
Data
Data
Data
Data
Data
Data
Data
Data
Annual Returns
1
2
3
4
5
6
7
8
9
10
11
12
1987
1.57
2.3
1.48
-1.66
0.0
1.75
2.08
0.8
-2.93
0.0
0.68
1.81
8.02
1988
6.48
2.8
-0.3
0.0
-0.33
3.51
-1.33
0.31
2.45
2.31
-1.74
0.34
15.17
1989
3.61
-0.61
-0.04
3.14
2.86
2.5
1.9
1.38
-0.18
0.04
0.94
1.73
18.58
1990
-3.03
0.0
1.06
-1.44
1.3
1.04
1.17
-2.86
0.23
1.6
6.17
3.04
8.23
1991
4.53
4.12
3.73
2.16
1.64
-3.47
1.13
2.99
1.49
0.75
-2.12
2.26
20.62
1992
-0.31
1.17
-0.76
0.56
1.99
-0.63
2.81
-1.2
1.09
-0.51
2.5
0.8
7.66
1993
2.35
2.63
-0.06
-0.11
2.0
0.33
1.01
2.84
1.04
3.15
-0.36
1.31
17.3
1994
2.3
-3.13
0.0
0.0
1.64
-1.85
0.67
3.37
-0.76
0.38
-1.75
0.92
1.62
1995
2.42
3.03
0.99
0.99
5.18
0.9
2.93
0.27
1.51
1.62
1.88
2.42
26.89
1996
1.81
1.03
0.91
1.78
0.9
-0.46
-0.22
0.52
4.13
0.09
2.85
-0.59
13.39
1997
1.57
0.87
-2.28
0.0
3.52
3.98
4.61
-1.54
1.67
-1.2
0.69
1.44
13.89
1998
1.23
2.4
3.51
0.61
0.52
1.26
-0.86
-0.56
2.33
2.01
3.62
3.46
21.24
1999
1.07
-1.92
0.0
1.34
-0.37
2.23
-1.11
-0.75
-2.22
0.32
3.11
3.18
4.82
2000
0.49
5.22
4.02
0.6
0.72
3.79
-0.62
2.22
1.42
1.16
2.53
3.01
27.31
2001
-1.12
-1.5
-0.18
-0.24
1.1
0.18
2.42
1.26
0.46
1.66
-0.93
-0.3
2.78
2002
-0.54
1.13
-1.72
-0.23
0.57
0.32
2.1
2.4
1.76
-2.02
2.69
-0.46
6.02
2003
-0.43
0.43
-0.57
4.64
3.68
1.07
0.12
-0.25
3.03
-1.35
2.38
2.9
16.57
2004
2.62
1.05
0.44
-3.07
0.1
0.24
-0.19
2.37
0.57
1.01
-0.76
3.07
7.56
2005
-0.74
-0.67
-1.54
0.0
1.9
1.2
0.97
0.75
-0.96
-1.24
0.0
2.17
1.75
2006
2.24
0.31
-0.54
-0.35
-0.31
-0.12
2.79
2.18
1.22
1.25
1.53
-0.32
10.24
2007
1.26
0.43
-0.3
3.19
1.25
-1.68
-0.39
1.46
1.96
1.53
1.45
-0.05
10.5
2008
1.23
0.73
0.32
-0.59
1.53
-2.69
0.33
0.93
-2.05
-0.6
0.68
4.5
4.2
2009
-1.68
-1.29
1.02
1.34
4.38
1.21
5.58
1.69
4.04
-1.35
0.99
2.42
19.63
2010
-0.35
0.17
2.24
-0.41
-1.19
2.5
1.3
-0.29
-0.6
2.63
-1.27
1.63
6.43
2011
1.48
2.25
0.86
2.9
2.22
-0.6
0.76
2.51
3.88
-1.35
-1.38
1.91
16.41
2012
1.61
1.76
1.31
-0.16
3.39
-0.55
1.09
0.09
2.36
-0.07
0.95
-0.46
11.85
2013
3.79
0.93
2.47
2.11
-1.95
-0.34
1.88
-1.81
0.0
2.19
1.89
1.04
12.71
2014
0.92
3.5
-0.43
1.51
1.91
1.04
-1.08
1.91
-1.1
0.25
1.92
0.67
11.49
2015
3.91
-0.71
1.02
-0.9
0.4
-1.07
0.0
-2.91
0.22
-0.07
-0.03
0.75
0.47
2016
-2.2
1.29
1.05
1.71
1.4
1.76
1.12
-0.84
0.33
-3.03
-0.46
0.0
2.01
In [8]:
frame = df['Annual Returns'].to_frame()
frame['positive'] = df['Annual Returns'] >= 0
frame['Annual Returns'].plot(figsize=(15,10),kind='bar',color=frame.positive.map({True: 'g', False: 'r'}), grid=True)
Out[8]:
<matplotlib.axes._subplots.AxesSubplot at 0x10bb5c0>
In [9]:
symbols =['VCVSX','VUSTX','VWEHX','VFIIX','VGHCX','FRESX']
cash_proxy = 'VFIIX'
risk_free = 'FFXSX'
rs_lookback = 1
risk_lookback = 1
n_top = 5
# get data
tickers = symbols.copy()
if cash_proxy != 'CASHX' :
tickers = list(set(tickers + [cash_proxy]))
if isinstance(risk_free, str) :
tickers = list(set(tickers + [risk_free]))
data = pd.DataFrame (columns=tickers)
for symbol in tickers :
url = 'http://chart.finance.yahoo.com/table.csv?s=' + symbol + '&ignore=.csv'
data[symbol] = pd.read_csv(url, parse_dates=True, index_col='Date').sort_index(ascending=True)['Adj Close']
inception_dates = pd.DataFrame([data[ticker].first_valid_index() for ticker in data.columns],
index=data.keys(), columns=['inception'])
print (inception_dates)
prices = data.copy().dropna()
end_points = endpoints(period='m', trading_days=prices.index)
prices_m = prices.loc[end_points]
returns = prices_m[symbols].pct_change(rs_lookback)[rs_lookback:]
absolute_momentum_rule = returns > 0
if isinstance(risk_free, int) :
excess_returns = returns
else :
risk_free_returns = prices_m[risk_free].pct_change(rs_lookback)[rs_lookback:]
excess_returns = returns.subtract(risk_free_returns, axis=0).dropna()
rebalance_dates = excess_returns.index.join(absolute_momentum_rule.index, how='inner')
# relative strength ranking
ranked = excess_returns.loc[rebalance_dates][symbols].rank(ascending=False, axis=1, method='dense')
# elligibility rule - top n_top ranked securities
elligible = ranked[ranked<=n_top] > 0
# equal weight allocations
elligible = elligible.multiply(1./elligible.sum(1), axis=0)
# downside protection
weights = pd.DataFrame(0.,index=elligible.index, columns=prices.columns)
if cash_proxy == 'CASHX' :
weights[cash_proxy] = 0
prices[cash_proxy] = 1.
weights[symbols] = (elligible * absolute_momentum_rule).dropna()
weights[cash_proxy] += 1 - weights[symbols].sum(axis=1)
# backtest
p_value, p_holdings, p_weights = backtest(prices, weights, 10000., offset=0, commission=10.)
p_value.plot(figsize=(15,10), grid=True)
inception
FFXSX 1986-11-10
FRESX 1986-11-14
VGHCX 1986-11-10
VCVSX 1986-11-10
VFIIX 1986-11-10
VUSTX 1986-11-10
VWEHX 1986-11-10
FIRST BUY DATE = 1986-12-31 00:00:00
Out[9]:
<matplotlib.axes._subplots.AxesSubplot at 0x122e358>
In [10]:
# algo stats
ffn.calc_perf_stats(p_value).display()
Stats for None from 1986-12-31 00:00:00 - 2016-11-23 00:00:00
Annual risk-free rate considered: 0.00%
Summary:
Total Return Sharpe CAGR Max Drawdown
-------------- -------- ------ --------------
2045.86% 1.71 10.80% -7.91%
Annualized Returns:
mtd 3m 6m ytd 1y 3y 5y 10y incep.
------ ------ ----- ----- ----- ----- ----- ----- --------
-1.82% -3.83% 0.79% 1.66% 2.12% 5.07% 7.49% 8.14% 10.80%
Periodic:
daily monthly yearly
------ ------- --------- --------
sharpe 1.71 1.79 1.56
mean 10.44% 10.46% 10.97%
vol 6.10% 5.84% 7.02%
skew 0.04 -0.01 0.44
kurt 10.92 1.95 -0.67
best 4.98% 8.90% 24.31%
worst -3.30% -5.06% 0.69%
Drawdowns:
max avg # days
------ ------ --------
-7.91% -0.83% 17.67
Misc:
--------------- -------
avg. up month 1.59%
avg. down month -1.19%
up year % 100.00%
12m up % 96.56%
--------------- -------
In [11]:
def highlight_pos_neg (s) :
is_positive = s > 0
return ['background-color : rgb(127,255,0)' if v else 'background-color : rgb(255,99,71)' for v in is_positive]
df = monthly_return_table (p_value)
df.style.\
apply(highlight_pos_neg)
Out[11]:
Data
Data
Data
Data
Data
Data
Data
Data
Data
Data
Data
Data
Annual Returns
1
2
3
4
5
6
7
8
9
10
11
12
1987
1.49
2.24
0.95
-3.11
-0.21
1.94
1.17
0.18
-3.29
4.43
0.81
1.83
8.48
1988
6.12
3.45
-0.8
-0.25
-1.06
3.25
-0.76
-0.16
2.4
1.7
-1.44
0.05
12.91
1989
2.72
-0.46
0.11
3.17
2.87
1.83
2.2
0.42
-0.23
1.36
1.02
0.74
16.85
1990
-2.65
0.55
1.55
-1.33
2.97
1.59
1.16
-4.87
0.69
1.34
4.23
2.48
7.61
1991
4.82
3.28
3.7
2.02
1.35
-2.1
1.83
2.18
1.65
0.78
-1.07
3.61
24.15
1992
0.64
0.71
-0.82
0.8
1.89
-0.78
1.89
0.05
1.4
-0.43
2.1
1.56
9.34
1993
2.81
1.36
1.96
-0.9
1.66
1.21
0.78
1.39
1.62
2.17
-0.76
1.49
15.77
1994
1.92
-1.06
-2.7
-0.59
1.21
-1.44
1.65
2.93
-0.41
-0.01
-1.18
1.23
1.43
1995
1.19
2.45
1.11
1.32
3.57
1.45
2.11
0.79
2.23
0.76
1.96
2.07
23.11
1996
1.67
0.48
0.46
1.0
0.37
0.12
0.39
1.17
3.46
0.77
2.62
2.15
15.62
1997
1.47
0.79
-1.59
0.73
3.14
3.53
3.78
-1.42
1.93
-0.76
0.57
1.45
14.3
1998
0.6
1.51
1.89
-0.16
0.82
1.39
0.13
0.05
1.63
0.45
2.08
1.78
12.84
1999
1.36
-2.7
0.17
1.66
-0.14
0.49
-1.04
-0.31
-0.39
0.4
2.23
1.73
3.42
2000
0.76
3.7
2.57
0.34
1.01
4.01
1.47
0.8
0.27
-0.12
2.22
3.25
22.14
2001
0.36
-1.03
-1.38
0.06
1.17
0.68
1.46
0.45
-0.95
2.12
0.03
0.1
3.03
2002
0.18
0.79
0.61
0.34
0.83
1.35
0.32
1.58
-0.22
-0.45
1.93
0.25
7.75
2003
-0.39
0.57
0.52
3.52
4.23
1.06
0.48
0.4
2.93
0.56
2.35
2.67
20.5
2004
2.89
1.02
1.19
-5.06
-0.1
1.28
0.44
3.21
0.45
1.89
0.45
2.87
10.77
2005
-2.02
-0.55
-1.28
0.97
1.86
1.87
2.11
-0.2
-0.53
-1.38
0.3
2.1
3.17
2006
2.67
0.66
0.58
-1.07
-0.29
-0.07
2.15
2.22
1.52
2.09
2.16
-0.64
12.57
2007
0.93
0.31
-0.03
1.99
0.29
-1.23
0.26
1.39
2.03
1.19
-1.32
-0.81
5.03
2008
0.97
-0.2
0.37
0.66
0.72
-1.38
0.09
1.28
-4.33
-1.94
3.77
3.17
2.98
2009
-4.45
-0.68
1.6
8.9
4.12
0.17
3.86
4.99
3.68
-2.18
1.23
1.4
24.31
2010
-1.19
0.23
4.15
1.14
-1.59
2.01
1.24
-0.77
-0.01
2.8
-1.18
0.49
7.4
2011
1.86
2.3
0.26
2.43
1.83
-1.37
1.1
0.53
2.26
-0.66
-1.42
1.12
10.61
2012
3.54
1.12
0.84
0.5
0.57
-0.23
1.14
0.38
1.11
-0.25
0.14
0.32
9.52
2013
2.86
0.68
2.21
2.62
-1.8
-0.89
0.96
-2.32
1.73
2.61
-0.6
0.48
8.69
2014
1.86
3.56
-0.17
1.65
1.67
1.23
-0.66
1.96
-2.54
1.06
1.63
0.73
12.52
2015
3.64
-0.58
0.82
-1.62
0.15
-1.62
0.56
-2.62
0.67
1.24
-0.09
0.27
0.69
2016
-1.7
0.82
1.26
0.78
0.92
2.54
1.74
-1.37
0.51
-1.9
-1.82
0.0
1.66
In [12]:
frame = df['Annual Returns'].to_frame()
frame['positive'] = df['Annual Returns'] >= 0
frame['Annual Returns'].plot(figsize=(15,10),kind='bar',color=frame.positive.map({True: 'g', False: 'r'}), grid=True)
Out[12]:
<matplotlib.axes._subplots.AxesSubplot at 0x10ec908>
In [417]:
symbols =['VCVSX','VWINX','VWEHX','VGHCX','VFIIX','VWAHX','FGOVX','FFXSX']
cash_proxy = 'VUSTX'
risk_free = 0
# get data
tickers = symbols.copy()
if cash_proxy != 'CASHX' :
tickers = list(set(tickers + [cash_proxy]))
if isinstance(risk_free, str) :
tickers = list(set(tickers + [risk_free]))
data = pd.DataFrame (columns=tickers)
for symbol in tickers :
url = 'http://chart.finance.yahoo.com/table.csv?s=' + symbol + '&ignore=.csv'
data[symbol] = pd.read_csv(url, parse_dates=True, index_col='Date').sort_index(ascending=True)['Adj Close']
inception_dates = pd.DataFrame([data[ticker].first_valid_index() for ticker in data.columns],
index=data.keys(), columns=['inception'])
In [418]:
data[:3]
Out[418]:
FFXSX
VWINX
VWEHX
VGHCX
VFIIX
FGOVX
VUSTX
VCVSX
VWAHX
Date
1986-11-10
2.362079
2.118463
0.725289
2.850821
1.650580
1.837465
1.307694
1.218052
1.814489
1986-11-11
2.362079
2.119707
0.725289
2.847774
1.650580
1.841068
1.307694
1.218052
1.814489
1986-11-12
2.362079
2.122196
0.726066
2.838631
1.653898
1.841068
1.311594
1.216814
1.817913
In [419]:
prices = data.copy().dropna()
prices[:5]
Out[419]:
FFXSX
VWINX
VWEHX
VGHCX
VFIIX
FGOVX
VUSTX
VCVSX
VWAHX
Date
1986-11-10
2.362079
2.118463
0.725289
2.850821
1.650580
1.837465
1.307694
1.218052
1.814489
1986-11-11
2.362079
2.119707
0.725289
2.847774
1.650580
1.841068
1.307694
1.218052
1.814489
1986-11-12
2.362079
2.122196
0.726066
2.838631
1.653898
1.841068
1.311594
1.216814
1.817913
1986-11-13
2.362079
2.118463
0.726842
2.814252
1.657215
1.844671
1.316793
1.218052
1.821336
1986-11-14
2.362079
2.124686
0.727619
2.817300
1.660533
1.846472
1.320693
1.216814
1.823048
In [420]:
end_points = endpoints(period='m', trading_days=prices.index)
prices_m = prices.loc[end_points]
rs_lookback = 1
risk_lookback = 1
n_top = 3
In [421]:
print(symbols)
['VCVSX', 'VWINX', 'VWEHX', 'VGHCX', 'VFIIX', 'VWAHX', 'FGOVX', 'FFXSX']
In [422]:
returns = prices_m[symbols].pct_change(rs_lookback)[rs_lookback:]
absolute_momentum_rule = returns > 0
if isinstance(risk_free, int) :
excess_returns = algo_data
else :
risk_free_returns = prices_m[risk_free].pct_change(rs_lookback)[rs_lookback:]
excess_returns = returns.subtract(risk_free_returns, axis=0).dropna()
rebalance_dates = excess_returns.index.join(absolute_momentum_rule.index, how='inner')
In [423]:
returns[:3]
Out[423]:
VCVSX
VWINX
VWEHX
VGHCX
VFIIX
VWAHX
FGOVX
FFXSX
Date
1986-12-31
-0.003081
-0.032117
0.004210
-0.054663
0.005719
-0.002187
0.002659
0.002315
1987-01-30
0.048554
0.046097
0.023809
0.151928
0.013589
0.031439
0.009466
0.007580
1987-02-27
0.056158
0.001175
0.018740
0.011527
0.009351
0.004154
0.003434
0.003128
In [424]:
# relative strength ranking
ranked = excess_returns.loc[rebalance_dates][symbols].rank(ascending=False, axis=1, method='dense')
# elligibility rule - top n_top ranked securities
elligible = ranked[ranked<=n_top] > 0
elligible[:3]
Out[424]:
VCVSX
VWINX
VWEHX
VGHCX
VFIIX
VWAHX
FGOVX
FFXSX
Date
1986-12-31
False
False
True
False
True
False
True
False
1987-01-30
True
True
False
True
False
False
False
False
1987-02-27
True
False
True
True
False
False
False
False
In [425]:
# equal weight allocations
elligible = elligible.multiply(1./elligible.sum(1), axis=0)
elligible[:3]
Out[425]:
VCVSX
VWINX
VWEHX
VGHCX
VFIIX
VWAHX
FGOVX
FFXSX
Date
1986-12-31
0.000000
0.000000
0.333333
0.000000
0.333333
0.0
0.333333
0.0
1987-01-30
0.333333
0.333333
0.000000
0.333333
0.000000
0.0
0.000000
0.0
1987-02-27
0.333333
0.000000
0.333333
0.333333
0.000000
0.0
0.000000
0.0
In [426]:
prices.columns
Out[426]:
Index(['FFXSX', 'VWINX', 'VWEHX', 'VGHCX', 'VFIIX', 'FGOVX', 'VUSTX', 'VCVSX',
'VWAHX'],
dtype='object')
In [427]:
cash_proxy
Out[427]:
'VUSTX'
In [428]:
# downside protection
weights = pd.DataFrame(0.,index=elligible.index, columns=prices.columns)
if cash_proxy == 'CASHX' :
weights[cash_proxy] = 0
prices[cash_proxy] = 1.
weights[symbols] = (elligible * absolute_momentum_rule).dropna()
weights[:10]
Out[428]:
FFXSX
VWINX
VWEHX
VGHCX
VFIIX
FGOVX
VUSTX
VCVSX
VWAHX
Date
1986-12-31
0.000000
0.000000
0.333333
0.000000
0.333333
0.333333
0.0
0.000000
0.000000
1987-01-30
0.000000
0.333333
0.000000
0.333333
0.000000
0.000000
0.0
0.333333
0.000000
1987-02-27
0.000000
0.000000
0.333333
0.333333
0.000000
0.000000
0.0
0.333333
0.000000
1987-03-31
0.333333
0.000000
0.333333
0.333333
0.000000
0.000000
0.0
0.000000
0.000000
1987-04-30
0.000000
0.000000
0.000000
0.000000
0.000000
0.000000
0.0
0.000000
0.000000
1987-05-29
0.333333
0.000000
0.000000
0.333333
0.000000
0.000000
0.0
0.000000
0.000000
1987-06-30
0.000000
0.333333
0.000000
0.333333
0.000000
0.000000
0.0
0.000000
0.333333
1987-07-31
0.000000
0.000000
0.000000
0.333333
0.000000
0.000000
0.0
0.333333
0.333333
1987-08-31
0.000000
0.333333
0.333333
0.000000
0.000000
0.000000
0.0
0.333333
0.000000
1987-09-30
0.000000
0.000000
0.000000
0.000000
0.000000
0.000000
0.0
0.000000
0.000000
In [429]:
weights[cash_proxy] += 1 - weights[symbols].sum(axis=1)
weights[:10]
Out[429]:
FFXSX
VWINX
VWEHX
VGHCX
VFIIX
FGOVX
VUSTX
VCVSX
VWAHX
Date
1986-12-31
0.000000
0.000000
0.333333
0.000000
0.333333
0.333333
0.000000
0.000000
0.000000
1987-01-30
0.000000
0.333333
0.000000
0.333333
0.000000
0.000000
0.000000
0.333333
0.000000
1987-02-27
0.000000
0.000000
0.333333
0.333333
0.000000
0.000000
0.000000
0.333333
0.000000
1987-03-31
0.333333
0.000000
0.333333
0.333333
0.000000
0.000000
0.000000
0.000000
0.000000
1987-04-30
0.000000
0.000000
0.000000
0.000000
0.000000
0.000000
1.000000
0.000000
0.000000
1987-05-29
0.333333
0.000000
0.000000
0.333333
0.000000
0.000000
0.333333
0.000000
0.000000
1987-06-30
0.000000
0.333333
0.000000
0.333333
0.000000
0.000000
0.000000
0.000000
0.333333
1987-07-31
0.000000
0.000000
0.000000
0.333333
0.000000
0.000000
0.000000
0.333333
0.333333
1987-08-31
0.000000
0.333333
0.333333
0.000000
0.000000
0.000000
0.000000
0.333333
0.000000
1987-09-30
0.000000
0.000000
0.000000
0.000000
0.000000
0.000000
1.000000
0.000000
0.000000
In [430]:
date = rebalance_dates[0]
weights.loc[date]
Out[430]:
FFXSX 0.000000
VWINX 0.000000
VWEHX 0.333333
VGHCX 0.000000
VFIIX 0.333333
FGOVX 0.333333
VUSTX 0.000000
VCVSX 0.000000
VWAHX 0.000000
Name: 1986-12-31 00:00:00, dtype: float64
In [431]:
prices.loc[date]
Out[431]:
FFXSX 2.379192
VWINX 2.085360
VWEHX 0.728181
VGHCX 2.687786
VFIIX 1.692999
FGOVX 1.876035
VUSTX 1.349658
VCVSX 1.209364
VWAHX 1.849020
Name: 1986-12-31 00:00:00, dtype: float64
In [432]:
(10000 * weights.loc[date] / prices.loc[date]).astype(int)
Out[432]:
FFXSX 0
VWINX 0
VWEHX 4577
VGHCX 0
VFIIX 1968
FGOVX 1776
VUSTX 0
VCVSX 0
VWAHX 0
Name: 1986-12-31 00:00:00, dtype: int32
In [433]:
# backtest
p_value, p_holdings, p_weights = backtest(prices, weights, 10000., offset=0, commission=10.)
p_value.plot(figsize=(15,10), grid=True)
FIRST BUY DATE = 1986-12-31 00:00:00
Out[433]:
<matplotlib.axes._subplots.AxesSubplot at 0x14ee75f8>