DUAL MOMENTUM (DM)


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

DM0001


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>

DM0002


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>

STEP BY STEP BACKTEST


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>