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

STEP BY STEP BACKTEST


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]:
VCVSX VWINX VWAHX VGHCX FGOVX VWEHX FFXSX VFIIX VUSTX
Date
1986-06-17 1.21596 1.994175 1.707851 2.850821 1.753793 0.68117 NaN 1.552549 1.268947
1986-06-18 1.21596 1.998989 1.711164 2.850821 1.755530 0.68117 NaN 1.552549 1.265163
1986-06-19 1.21596 1.996582 1.716133 2.856916 1.750320 0.68043 NaN 1.549364 1.257594

In [11]:
prices = data.copy().dropna()
prices[:5]


Out[11]:
VCVSX VWINX VWAHX VGHCX FGOVX VWEHX FFXSX VFIIX VUSTX
Date
1986-11-10 1.218052 2.118463 1.814489 2.850821 1.837465 0.725289 2.362079 1.650580 1.307694
1986-11-11 1.218052 2.119707 1.814489 2.847774 1.841068 0.725289 2.362079 1.650580 1.307694
1986-11-12 1.216814 2.122196 1.817913 2.838631 1.841068 0.726066 2.362079 1.653898 1.311594
1986-11-13 1.218052 2.118463 1.821336 2.814252 1.844671 0.726842 2.362079 1.657215 1.316793
1986-11-14 1.216814 2.124686 1.823048 2.817300 1.846472 0.727619 2.362079 1.660533 1.320693

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)


['VCVSX', 'VWINX', 'VWEHX', 'VGHCX', 'VFIIX', 'VWAHX', 'FGOVX', 'FFXSX']

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]:
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 [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]:
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 [20]:
# equal weight allocations
elligible = elligible.multiply(1./elligible.sum(1), axis=0)
elligible[:3]


Out[20]:
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 [21]:
prices.columns


Out[21]:
Index(['VCVSX', 'VWINX', 'VWAHX', 'VGHCX', 'FGOVX', 'VWEHX', 'FFXSX', 'VFIIX',
       'VUSTX'],
      dtype='object')

In [22]:
cash_proxy


Out[22]:
'VUSTX'

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]:
VCVSX VWINX VWAHX VGHCX FGOVX VWEHX FFXSX VFIIX VUSTX
Date
1986-12-31 0.000000 0.000000 0.000000 0.000000 0.333333 0.333333 0.000000 0.333333 0.0
1987-01-30 0.333333 0.333333 0.000000 0.333333 0.000000 0.000000 0.000000 0.000000 0.0
1987-02-27 0.333333 0.000000 0.000000 0.333333 0.000000 0.333333 0.000000 0.000000 0.0
1987-03-31 0.000000 0.000000 0.000000 0.333333 0.000000 0.333333 0.333333 0.000000 0.0
1987-04-30 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.0
1987-05-29 0.000000 0.000000 0.000000 0.333333 0.000000 0.000000 0.333333 0.000000 0.0
1987-06-30 0.000000 0.333333 0.333333 0.333333 0.000000 0.000000 0.000000 0.000000 0.0
1987-07-31 0.333333 0.000000 0.333333 0.333333 0.000000 0.000000 0.000000 0.000000 0.0
1987-08-31 0.333333 0.333333 0.000000 0.000000 0.000000 0.333333 0.000000 0.000000 0.0
1987-09-30 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.0

In [24]:
weights[cash_proxy] += 1 - weights[symbols].sum(axis=1)
weights[:10]


Out[24]:
VCVSX VWINX VWAHX VGHCX FGOVX VWEHX FFXSX VFIIX VUSTX
Date
1986-12-31 0.000000 0.000000 0.000000 0.000000 0.333333 0.333333 0.000000 0.333333 0.000000
1987-01-30 0.333333 0.333333 0.000000 0.333333 0.000000 0.000000 0.000000 0.000000 0.000000
1987-02-27 0.333333 0.000000 0.000000 0.333333 0.000000 0.333333 0.000000 0.000000 0.000000
1987-03-31 0.000000 0.000000 0.000000 0.333333 0.000000 0.333333 0.333333 0.000000 0.000000
1987-04-30 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000
1987-05-29 0.000000 0.000000 0.000000 0.333333 0.000000 0.000000 0.333333 0.000000 0.333333
1987-06-30 0.000000 0.333333 0.333333 0.333333 0.000000 0.000000 0.000000 0.000000 0.000000
1987-07-31 0.333333 0.000000 0.333333 0.333333 0.000000 0.000000 0.000000 0.000000 0.000000
1987-08-31 0.333333 0.333333 0.000000 0.000000 0.000000 0.333333 0.000000 0.000000 0.000000
1987-09-30 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000

In [25]:
date = rebalance_dates[0]
weights.loc[date]


Out[25]:
VCVSX    0.000000
VWINX    0.000000
VWAHX    0.000000
VGHCX    0.000000
FGOVX    0.333333
VWEHX    0.333333
FFXSX    0.000000
VFIIX    0.333333
VUSTX    0.000000
Name: 1986-12-31 00:00:00, dtype: float64

In [26]:
prices.loc[date]


Out[26]:
VCVSX    1.209364
VWINX    2.085360
VWAHX    1.849020
VGHCX    2.687786
FGOVX    1.876035
VWEHX    0.728181
FFXSX    2.379192
VFIIX    1.692999
VUSTX    1.349658
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 [27]:
# 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[27]:
<matplotlib.axes._subplots.AxesSubplot at 0x62c4a20>

In [28]:
p_weights.loc[rebalance_dates]


Out[28]:
VCVSX VWINX VWAHX VGHCX FGOVX VWEHX FFXSX VFIIX VUSTX
Date
1986-12-31 0.000000 0.000000 0.000000 0.000000 0.333299 0.333404 0.000000 0.333297 0.000000
1987-01-30 0.333298 0.333322 0.000000 0.333380 0.000000 0.000000 0.000000 0.000000 0.000000
1987-02-27 0.333303 0.000000 0.000000 0.333296 0.000000 0.333401 0.000000 0.000000 0.000000
1987-03-31 0.000000 0.000000 0.000000 0.333221 0.000000 0.333427 0.333352 0.000000 0.000000
1987-04-30 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000
1987-05-29 0.000000 0.000000 0.000000 0.333354 0.000000 0.000000 0.333322 0.000000 0.333324
1987-06-30 0.000000 0.333345 0.333388 0.333267 0.000000 0.000000 0.000000 0.000000 0.000000
1987-07-31 0.333393 0.000000 0.333290 0.333317 0.000000 0.000000 0.000000 0.000000 0.000000
1987-08-31 0.333330 0.333307 0.000000 0.000000 0.000000 0.333363 0.000000 0.000000 0.000000
1987-09-30 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000
1987-10-30 0.000000 0.000000 0.000000 0.000000 0.333342 0.000000 0.333248 0.333410 0.000000
1987-11-30 0.000000 0.000000 0.333344 0.000000 0.000000 0.333363 0.000000 0.333293 0.000000
1987-12-31 0.333351 0.000000 0.000000 0.333232 0.000000 0.333418 0.000000 0.000000 0.000000
1988-01-29 0.000000 0.333441 0.000000 0.333246 0.000000 0.000000 0.000000 0.333313 0.000000
1988-02-29 0.333372 0.000000 0.000000 0.333244 0.000000 0.333384 0.000000 0.000000 0.000000
1988-03-31 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000
1988-04-29 0.333358 0.000000 0.333337 0.000000 0.000000 0.333305 0.000000 0.000000 0.000000
1988-05-31 0.000000 0.333384 0.333275 0.333341 0.000000 0.000000 0.000000 0.000000 0.000000
1988-06-30 0.333379 0.333324 0.000000 0.333297 0.000000 0.000000 0.000000 0.000000 0.000000
1988-07-29 0.000000 0.000000 0.333323 0.000000 0.000000 0.333334 0.333343 0.000000 0.000000
1988-08-31 0.000000 0.000000 0.333284 0.000000 0.333354 0.333362 0.000000 0.000000 0.000000
1988-09-30 0.000000 0.333294 0.333361 0.333344 0.000000 0.000000 0.000000 0.000000 0.000000
1988-10-31 0.333300 0.000000 0.333313 0.333387 0.000000 0.000000 0.000000 0.000000 0.000000
1988-11-30 0.000000 0.000000 0.000000 0.000000 0.000000 0.333366 0.333300 0.000000 0.333334
1988-12-30 0.333385 0.000000 0.333401 0.333215 0.000000 0.000000 0.000000 0.000000 0.000000
1989-01-31 0.333411 0.333383 0.000000 0.333205 0.000000 0.000000 0.000000 0.000000 0.000000
1989-02-28 0.333343 0.000000 0.000000 0.000000 0.000000 0.333397 0.333260 0.000000 0.000000
1989-03-31 0.000000 0.333350 0.000000 0.333280 0.333370 0.000000 0.000000 0.000000 0.000000
1989-04-28 0.333409 0.000000 0.333318 0.333273 0.000000 0.000000 0.000000 0.000000 0.000000
1989-05-31 0.333352 0.333328 0.000000 0.000000 0.000000 0.000000 0.000000 0.333319 0.000000
... ... ... ... ... ... ... ... ... ...
2014-06-30 0.333368 0.333387 0.000000 0.333245 0.000000 0.000000 0.000000 0.000000 0.000000
2014-07-31 0.000000 0.000000 0.333340 0.000000 0.000000 0.000000 0.000000 0.000000 0.666660
2014-08-29 0.000000 0.333506 0.000000 0.332945 0.000000 0.333548 0.000000 0.000000 0.000000
2014-09-30 0.000000 0.000000 0.333335 0.000000 0.000000 0.000000 0.000000 0.000000 0.666665
2014-10-31 0.000000 0.333421 0.000000 0.333092 0.000000 0.333487 0.000000 0.000000 0.000000
2014-11-28 0.000000 0.333395 0.000000 0.333195 0.333410 0.000000 0.000000 0.000000 0.000000
2014-12-31 0.000000 0.000000 0.333328 0.000000 0.333348 0.000000 0.000000 0.333324 0.000000
2015-01-30 0.000000 0.000000 0.333393 0.333226 0.333382 0.000000 0.000000 0.000000 0.000000
2015-02-27 0.333538 0.000000 0.000000 0.332894 0.000000 0.333568 0.000000 0.000000 0.000000
2015-03-31 0.333528 0.000000 0.000000 0.332966 0.333506 0.000000 0.000000 0.000000 0.000000
2015-04-30 0.333312 0.333343 0.000000 0.000000 0.000000 0.333345 0.000000 0.000000 0.000000
2015-05-29 0.333341 0.000000 0.000000 0.333291 0.000000 0.333368 0.000000 0.000000 0.000000
2015-06-30 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 1.000000
2015-07-31 0.000000 0.333508 0.000000 0.332953 0.333540 0.000000 0.000000 0.000000 0.000000
2015-08-31 0.000000 0.000000 0.333333 0.000000 0.000000 0.000000 0.000000 0.000000 0.666667
2015-09-30 0.000000 0.000000 0.333335 0.000000 0.333330 0.000000 0.000000 0.333334 0.000000
2015-10-30 0.000000 0.333384 0.000000 0.333175 0.000000 0.333440 0.000000 0.000000 0.000000
2015-11-30 0.000000 0.000000 0.333396 0.333207 0.000000 0.000000 0.000000 0.333397 0.000000
2015-12-31 0.000000 0.000000 0.333576 0.332852 0.000000 0.000000 0.000000 0.333572 0.000000
2016-01-29 0.000000 0.000000 0.333333 0.000000 0.333321 0.000000 0.000000 0.333346 0.000000
2016-02-29 0.000000 0.333343 0.000000 0.000000 0.333324 0.333333 0.000000 0.000000 0.000000
2016-03-31 0.333347 0.333301 0.000000 0.000000 0.000000 0.333352 0.000000 0.000000 0.000000
2016-04-29 0.333400 0.000000 0.000000 0.333192 0.000000 0.333408 0.000000 0.000000 0.000000
2016-05-31 0.333359 0.000000 0.333344 0.333297 0.000000 0.000000 0.000000 0.000000 0.000000
2016-06-30 0.000000 0.333301 0.333345 0.000000 0.333354 0.000000 0.000000 0.000000 0.000000
2016-07-29 0.333545 0.000000 0.000000 0.332908 0.000000 0.333547 0.000000 0.000000 0.000000
2016-08-31 0.333342 0.000000 0.333315 0.000000 0.000000 0.333343 0.000000 0.000000 0.000000
2016-09-30 0.333425 0.000000 0.000000 0.333145 0.000000 0.333430 0.000000 0.000000 0.000000
2016-10-31 0.000000 0.000000 0.000000 0.000000 0.000000 0.333333 0.000000 0.000000 0.666667
2016-11-16 0.000000 0.000000 0.000000 0.000000 0.000000 0.343167 0.000000 0.000000 0.656833

360 rows × 9 columns


In [ ]: