In [1]:
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 [2]:
import os
os.chdir('C:\\users\\scuba\\pycharmprojects\\simplebacktester')
os.getcwd()
Out[2]:
In [30]:
import pandas as pd
import ffn
%matplotlib inline
from backtest_helpers.compute_weights_RS_DM import compute_weights_RS_DM
from backtest_helpers.compute_weights_PMA import compute_weights_PMA
from backtest_helpers.monthly_return_table import monthly_return_table
from backtest_helpers.endpoints import endpoints
from backtest_helpers.backtest import backtest
strategies = {
'RS0001': { 'symbols': ['VCVSX','VWEHX','VFIIX','FGOVX','VWAHX'], 'prices': 'yahoo',
'rs_lookback': 1, 'risk_lookback': 1, 'n_top': 2, 'frequency': 'm',
'cash_proxy': 'CASHX', 'risk_free': 0},
'RS0002': {'symbols': ['MMHYX','FAGIX','VFIIX'], 'prices': 'yahoo',
'rs_lookback': 3, 'risk_lookback': 2, 'n_top': 1, 'frequency': 'm',
'cash_proxy': 'CASHX', 'risk_free': 0},
'RS0003': {'symbols': ['MMHYX','FAGIX','VFIIX'], 'prices': 'yahoo',
'rs_lookback': 1, 'risk_lookback': 1, 'n_top': 1, 'frequency': 'q',
'cash_proxy': 'CASHX', 'risk_free': 0},
'DM0001': {'symbols': ['VCVSX','VWINX','VWEHX','VGHCX','VUSTX','VFIIX','VWAHX','FGOVX','FFXSX'],
'prices': 'yahoo',
'rs_lookback': 1, 'risk_lookback': 1, 'n_top': 3, 'frequency': 'm',
'cash_proxy': 'CASHX', 'risk_free': 'FFXSX'},
'DM0002': {'symbols': ['VCVSX','VUSTX','VWEHX','VFIIX','VGHCX','FRESX'], 'prices': 'yahoo',
'rs_lookback': 1, 'risk_lookback': 1, 'n_top': 5, 'frequency': 'm',
'cash_proxy': 'VFIIX', 'risk_free': 'FFXSX'},
'PMA001': {'symbols': ['VCVSX', 'VFIIX'], 'prices': 'yahoo',
'risk_lookback': 3, 'frequency': 'm', 'allocations': [0.6, 0.4],
'cash_proxy': 'VUSTX'},
'PMA002': {'symbols': ['VCVSX', 'VWINX', 'VWEHX'], 'prices': 'yahoo',
'risk_lookback': 3, 'frequency': 'm', 'allocations': [0.6, 0.2, 0.2],
'cash_proxy': 'VUSTX'},
'PMA003': {'symbols': ['VCVSX', 'FAGIX', 'VGHCX'], 'prices': 'yahoo',
'risk_lookback': 2, 'frequency': 'm', 'allocations': [1./3., 1./3., 1./3.],
'cash_proxy': 'VUSTX'},
}
strategy_values = pd.DataFrame(columns=strategies.keys())
security_weights = {}
security_holdings = {}
prices = {}
for name in strategies :
if 'PMA' in name :
s_value, s_holdings, s_weights, s_prices = compute_weights_PMA (name, strategies[name])
else :
s_value, s_holdings, s_weights, s_prices = compute_weights_RS_DM (name, strategies[name])
strategy_values[name] = s_value
security_weights[name] = s_weights
security_holdings[name] = s_holdings
prices[name] = s_prices
In [4]:
index = strategy_values.dropna().index
rebalance_dates = endpoints(period='m', trading_days=index)
In [5]:
# find the set of all portfolio symbols
n = len(strategies)
l = [list(security_weights[name].columns) for name in strategies]
s = []
for i in range(n) :
s = s + l[i]
aggregated_weights = pd.DataFrame(0, index=rebalance_dates, columns=list(set(s)))
all_prices = pd.DataFrame(0, index=index, columns=list(set(s)))
aggregated_weights[:1]
Out[5]:
In [6]:
# for equally weighted strategies
strategy_weights = pd.Series([1. / n for i in range(n)], index=list(strategies.keys()))
strategy_weights
Out[6]:
In [7]:
for name in strategies :
aggregated_weights[security_weights[name].columns] += security_weights[name].loc[rebalance_dates] * strategy_weights[name]
all_prices = prices[name].loc[index].combine_first(all_prices)
In [13]:
aggregated_weights[:3].round(3)
Out[13]:
In [8]:
from backtest_helpers.backtest import backtest
p_value, p_holdings, p_weights = backtest(all_prices, aggregated_weights, 10000., offset=0, commission=10.)
In [9]:
p_value.plot(figsize=(15,10), grid=True, legend=True)
Out[9]:
In [10]:
# algo stats
ffn.calc_perf_stats(p_value).display()
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]:
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]:
In [13]:
p_holdings.loc[rebalance_dates].round(0)[:5]
Out[13]:
In [14]:
transactions = (p_holdings - p_holdings.shift(1).fillna(0))
transactions = transactions[transactions.sum(1) != 0]
transactions.round(0)[:5]
Out[14]:
In [15]:
def generate_orders(transactions, prices) :
orders = pd.DataFrame()
for i in range(len(transactions)):
for j in range(len(transactions.columns)):
t = transactions.ix[i]
qty = abs(t[j])
if qty >= 1.:
if transactions.ix[i][j] < 0 :
orders = orders.append([[t.name.date().year, t.name.date().month, t.name.date().day, t.index[j],\
'Sell', -abs(t[j]), prices.ix[t.name][t.index[j]]]])
if transactions.ix[i][j] > 0 :
orders = orders.append([[t.name.date().year, t.name.date().month, t.name.date().day, t.index[j],\
'Buy', abs(t[j]), prices.ix[t.name][t.index[j]]]])
orders.columns = ['Year', 'Month', 'Day', 'Symbol', 'Action', 'Qty', 'Price']
orders
return orders
In [16]:
# del transactions['CASHX']
orders = generate_orders(transactions, all_prices)
orders[:10]
Out[16]:
In [17]:
import datetime as dt
p_value[p_value.index > dt.datetime(2010,1,1)].plot(figsize=(15,10), grid=True)
Out[17]:
In [18]:
cash_proxy = 'FFXSX'
risk_free = 'FFXSX'
if risk_free == 'CASHX' :
px = pd.DataFrame (columns=list(set([cash_proxy, risk_free])))
tickers = [cash_proxy]
elif isinstance (risk_free, str) :
px = pd.DataFrame (columns=list(set([cash_proxy, risk_free])))
tickers = [cash_proxy, risk_free]
else :
px = pd.DataFrame (columns=[cash_proxy])
prices = strategy_values.copy().dropna()
prices = prices / prices.iloc[0]
for symbol in tickers :
url = 'http://chart.finance.yahoo.com/table.csv?s=' + symbol + '&ignore=.csv'
prices[symbol] = pd.read_csv(url, parse_dates=True, index_col='Date').sort_index(ascending=True)['Adj Close']
prices = prices.dropna()
symbols = prices.columns
In [19]:
prices[:3]
Out[19]:
In [20]:
rs_lookback = 1
risk_lookback = 1
n_top = len(symbols)
frequency = 'm'
end_points = endpoints(period=frequency, 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)
print (weights[:5])
# backtest
p_value, p_holdings, p_weights = backtest(prices, weights, 10000., offset=0, commission=10.)
p_value.plot(figsize=(15,10), grid=True)
Out[20]:
The CAGR has dropped slightly but the Sharpe Ratio has increased significantly and the Max Drawdown has been reduced
In [21]:
# algo stats
ffn.calc_perf_stats(p_value).display()
In [22]:
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[22]:
In [24]:
p_value[p_value.index > dt.datetime(2010,1,1)].plot(figsize=(15,10), grid=True)
Out[24]:
In [27]:
p_holdings.loc[rebalance_dates].round(0)[:5]
Out[27]:
this is not very helpful - we need to know what to order at each rebalance date
In [29]:
p_weights.loc[rebalance_dates].round(3)[:5]
Out[29]:
In [ ]:
portfolios = {'DM0001': {'symbols': list(strategies), 'rs_lookback': 1, 'risk_lookback': 1, 'n_top': 3, 'frequency': 'm', 'cash_proxy': 'CASHX', 'risk_free': 'FFXSX'}}
p_value1, p_holdings1, p_weights1, prices1 = compute_weights_RS_DM ('DM0001', portfolios['DM0001'])
p_value1.plot(figsize=(10, 8), grid=True)