In [5]:
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 [6]:
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 [7]:
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 [8]:
# 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 [9]:
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 :
print (symbol)
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 [10]:
data[:3]
Out[10]:
In [11]:
prices = data.copy().dropna()
prices[:5]
Out[11]:
In [12]:
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 [13]:
print(symbols)
In [17]:
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')
In [18]:
returns[:3]
Out[18]:
In [19]:
# 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[19]:
In [20]:
# equal weight allocations
elligible = elligible.multiply(1./elligible.sum(1), axis=0)
elligible[:3]
Out[20]:
In [21]:
prices.columns
Out[21]:
In [22]:
cash_proxy
Out[22]:
In [23]:
# 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[23]:
In [24]:
weights[cash_proxy] += 1 - weights[symbols].sum(axis=1)
weights[:10]
Out[24]:
In [25]:
date = rebalance_dates[0]
weights.loc[date]
Out[25]:
In [26]:
prices.loc[date]
Out[26]:
In [432]:
(10000 * weights.loc[date] / prices.loc[date]).astype(int)
Out[432]:
In [27]:
# backtest
p_value, p_holdings, p_weights = backtest(prices, weights, 10000., offset=0, commission=10.)
p_value.plot(figsize=(15,10), grid=True)
Out[27]:
In [28]:
p_weights.loc[rebalance_dates]
Out[28]:
In [ ]: