Comparing OLPS algorithms on a diversified set of ETFs

Let's compare the state of the art in OnLine Portfolio Selection (OLPS) algorithms and determine if they can enhance a rebalanced passive strategy in practice. Online Portfolio Selection: A Survey by Bin Li and Steven C. H. Hoi provides the most comprehensive review of multi-period portfolio allocation optimization algorithms. The authors developed the OLPS Toolbox, but here we use Mojmir Vinkler's implementation and extend his comparison to a more recent timeline with a set of ETFs to avoid survivorship bias (as suggested by Ernie Chan) and idiosyncratic risk.

Vinkler does all the hard work in his thesis, and concludes that Universal Portfolios work practically the same as Constant Rebalanced Portfolios, and work better for an uncorrelated set of small and volatile stocks. Here I'm looking to find if any strategy is applicable to a set of ETFs.

The agorithms compared are:

Type Name Algo Reference
Benchmark BAH Buy and Hold
Benchmark CRP Constant Rebalanced Portfolio T. Cover. Universal Portfolios, 1991.
Benchmark UCRP Uniform CRP (UCRP), a special case of CRP with all weights being equal T. Cover. Universal Portfolios, 1991.
Benchmark BCRP Best Constant Rebalanced Portfolio T. Cover. Universal Portfolios, 1991.
Follow-the-Winner UP Universal Portfolio T. Cover. Universal Portfolios, 1991.
Follow-the-Winner EG Exponential Gradient Helmbold, David P., et al. On‐Line Portfolio Selection Using Multiplicative Updates Mathematical Finance 8.4 (1998): 325-347.
Follow-the-Winner ONS Online Newton Step A. Agarwal, E. Hazan, S. Kale, R. E. Schapire. Algorithms for Portfolio Management based on the Newton Method, 2006.
Follow-the-Loser Anticor Anticorrelation A. Borodin, R. El-Yaniv, and V. Gogan. Can we learn to beat the best stock, 2005
Follow-the-Loser PAMR Passive Aggressive Mean Reversion B. Li, P. Zhao, S. C.H. Hoi, and V. Gopalkrishnan. Pamr: Passive aggressive mean reversion strategy for portfolio selection, 2012.
Follow-the-Loser CWMR Confidence Weighted Mean Reversion B. Li, S. C. H. Hoi, P. L. Zhao, and V. Gopalkrishnan.Confidence weighted mean reversion strategy for online portfolio selection, 2013.
Follow-the-Loser OLMAR Online Moving Average Reversion Bin Li and Steven C. H. Hoi On-Line Portfolio Selection with Moving Average Reversion
Follow-the-Loser RMR Robust Median Reversion D. Huang, J. Zhou, B. Li, S. C.vH. Hoi, S. Zhou Robust Median Reversion Strategy for On-Line Portfolio Selection, 2013.
Pattern Matching Kelly Kelly fractional betting Kelly Criterion
Pattern Matching BNN nonparametric nearest neighbor log-optimal L. Gyorfi, G. Lugosi, and F. Udina. Nonparametric kernel based sequential investment strategies. Mathematical Finance 16 (2006) 337–357.
Pattern Matching CORN correlation-driven nonparametric learning B. Li, S. C. H. Hoi, and V. Gopalkrishnan. Corn: correlation-driven nonparametric learning approach for portfolio selection, 2011.

We pick 6 ETFs to avoid survivorship bias and capture broad market diversification. We select the longest running ETF per assset class: VTI, EFA, EEM, TLT, TIP, VNQ . We train and select the best parameters on market data from 2005-2012 inclusive (8 years), and test on 2013-2014 inclusive (2 years).


In [1]:
# You will first need to either download or install universal-portfolios from Vinkler
# one way to do it is uncomment the line below and execute
# !pip install --upgrade universal-portfolios 
# or
# !pip install --upgrade -e git+git@github.com:Marigold/universal-portfolios.git@master#egg=universal-portfolios
#
# if the above fail, git clone git@github.com:marigold/universal-portfolios.git and python setup.py install

Initialize and set debugging level to debug to track progress.


In [2]:
%matplotlib inline

import numpy as np
import pandas as pd
from pandas.io.data import DataReader
from datetime import datetime
import six
import universal as up
from universal import tools
from universal import algos
import logging
# we would like to see algos progress
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.DEBUG)

import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rcParams['figure.figsize'] = (16, 10) # increase the size of graphs
mpl.rcParams['legend.fontsize'] = 12
mpl.rcParams['lines.linewidth'] = 1
default_color_cycle = mpl.rcParams['axes.color_cycle'] # save this as we will want it back later

In [3]:
# note what versions we are on:
import sys
print('Python: '+sys.version)
print('Pandas: '+pd.__version__)
import pkg_resources
print('universal-portfolios: '+pkg_resources.get_distribution("universal-portfolios").version)


Python: 2.7.9 |Anaconda 2.2.0 (x86_64)| (default, Dec 15 2014, 10:37:34) 
[GCC 4.2.1 (Apple Inc. build 5577)]
Pandas: 0.16.0
universal-portfolios: 0.1

Loading the data

We want to train on market data from 2005-2012 inclusive (8 years), and test on 2013-2014 inclusive (2 years). But at this point we accept the default parameters for the respective algorithms and we essentially are looking at two independent time periods. In the future we will want to optimize the paramaters on the train set.


In [4]:
# load data from Yahoo
# Be careful if you cange the order or types of ETFs to also change the CRP weight %'s in the swensen_allocation
etfs = ['VTI', 'EFA', 'EEM', 'TLT', 'TIP', 'VNQ']
# Swensen allocation from http://www.bogleheads.org/wiki/Lazy_portfolios#David_Swensen.27s_lazy_portfolio
# as later updated here : https://www.yalealumnimagazine.com/articles/2398/david-swensen-s-guide-to-sleeping-soundly 
swensen_allocation = [0.3, 0.15, 0.1, 0.15, 0.15, 0.15]  
benchmark = ['SPY']
train_start = datetime(2005,1,1)
train_end   = datetime(2012,12,31)
test_start  = datetime(2013,1,1) 
test_end    = datetime(2014,12,31)
train = DataReader(etfs, 'yahoo', start=train_start, end=train_end)['Adj Close']
test  = DataReader(etfs, 'yahoo', start=test_start, end=test_end)['Adj Close']
train_b = DataReader(benchmark, 'yahoo', start=train_start, end=train_end)['Adj Close']
test_b  = DataReader(benchmark, 'yahoo', start=test_start, end=test_end)['Adj Close']

In [5]:
# plot normalized prices of the train set
ax1 = (train / train.iloc[0,:]).plot()
(train_b / train_b.iloc[0,:]).plot(ax=ax1)


Out[5]:
<matplotlib.axes._subplots.AxesSubplot at 0x1067c2d90>

In [6]:
# plot normalized prices of the test set
ax2 = (test / test.iloc[0,:]).plot()
(test_b / test_b.iloc[0,:]).plot(ax=ax2)


Out[6]:
<matplotlib.axes._subplots.AxesSubplot at 0x10cbebf10>

Comparing the Algorithms

We want to train on market data from a number of years, and test out of sample for a duration smaller than the train set. To get started we accept the default parameters for the respective algorithms and we essentially are just looking at two independent time periods. In the future we will want to optimize the paramaters on the train set.


In [7]:
#list all the algos
olps_algos = [
algos.Anticor(),
algos.BAH(),
algos.BCRP(),
algos.BNN(),
algos.CORN(),
algos.CRP(b=swensen_allocation), # Non Uniform CRP (the Swensen allocation)
algos.CWMR(),
algos.EG(),
algos.Kelly(),
algos.OLMAR(),
algos.ONS(),
algos.PAMR(),
algos.RMR(),
algos.UP()
]

In [8]:
# put all the algos in a dataframe
algo_names = [a.__class__.__name__ for a in olps_algos]
algo_data = ['algo', 'results', 'profit', 'sharpe', 'information', 'annualized_return', 'drawdown_period','winning_pct']
metrics = algo_data[2:]
olps_train = pd.DataFrame(index=algo_names, columns=algo_data)
olps_train.algo = olps_algos

At this point we could train all the algos to find the best parameters for each.


In [10]:
# run all algos - this takes more than a minute
for name, alg in zip(olps_train.index, olps_train.algo):
    olps_train.ix[name,'results'] = alg.run(train)

In [11]:
# Let's make sure the fees are set to 0 at first
for k, r in olps_train.results.iteritems():
    r.fee = 0.0

In [12]:
# we need 14 colors for the plot
n_lines = 14
color_idx = np.linspace(0, 1, n_lines)
mpl.rcParams['axes.color_cycle']=[plt.cm.rainbow(i) for i in color_idx]

In [13]:
# plot as if we had no fees
# get the first result so we can grab the figure axes from the plot
ax = olps_train.results[0].plot(assets=False, weights=False, ucrp=True, portfolio_label=olps_train.index[0])
for k, r in olps_train.results.iteritems():
    if k == olps_train.results.keys()[0]: # skip the first item because we have it already
        continue
    r.plot(assets=False, weights=False, ucrp=False, portfolio_label=k, ax=ax[0])



In [14]:
def olps_stats(df):
    for name, r in df.results.iteritems():
        df.ix[name,'profit'] = r.profit_factor
        df.ix[name,'sharpe'] = r.sharpe
        df.ix[name,'information'] = r.information
        df.ix[name,'annualized_return'] = r.annualized_return * 100
        df.ix[name,'drawdown_period'] = r.drawdown_period
        df.ix[name,'winning_pct'] = r.winning_pct * 100
    return df

In [15]:
olps_stats(olps_train)
olps_train[metrics].sort('profit', ascending=False)


Out[15]:
profit sharpe information annualized_return drawdown_period winning_pct
RMR 1.344883 1.417639 1.581531 57.1563 108 55.55556
CWMR 1.30964 1.29867 1.419667 49.69137 134 54.7047
PAMR 1.307667 1.284661 1.405146 49.15968 113 54.55912
OLMAR 1.303725 1.275474 1.358441 49.43 128 54.95495
BNN 1.16316 0.7406326 0.597467 24.49535 516 53.74625
ONS 1.124495 0.5324337 0.4103257 12.69247 301 54.77137
CORN 1.120664 0.5868915 0.3335868 16.03551 619 54.42346
BCRP 1.113103 0.5587799 0.3926754 12.47399 493 52.53479
EG 1.095291 0.4654109 -0.5754063 8.796127 691 54.02584
UP 1.093509 0.4591855 -0.5701021 8.627557 720 54.12525
CRP 1.08585 0.4206153 0.1402307 9.493241 725 54.12525
BAH 1.075043 0.3852003 -0.646195 6.960725 874 54.12525
Anticor 1.06997 0.279532 0.04357109 10.00163 836 53.43968
Kelly 0.8724567 -0.7194364 -0.7692659 -73.22097 2012 51.58002

In [16]:
# Let's add fees of 0.1% per transaction (we pay $1 for every $1000 of stocks bought or sold).
for k, r in olps_train.results.iteritems():
    r.fee = 0.001

In [17]:
# plot with fees
# get the first result so we can grab the figure axes from the plot
ax = olps_train.results[0].plot(assets=False, weights=False, ucrp=True, portfolio_label=olps_train.index[0])
for k, r in olps_train.results.iteritems():
    if k == olps_train.results.keys()[0]: # skip the first item because we have it already
        continue
    r.plot(assets=False, weights=False, ucrp=False, portfolio_label=k, ax=ax[0])


Notice how Kelly crashes right away and how RMR and OLMAR float to the top after some high volatility.


In [18]:
olps_stats(olps_train)
olps_train[metrics].sort('profit', ascending=False)


Out[18]:
profit sharpe information annualized_return drawdown_period winning_pct
RMR 1.132369 0.5880135 0.4408147 20.23296 800 51.57107
ONS 1.116896 0.5017011 0.3281846 11.91852 303 54.64481
BCRP 1.110684 0.5474909 0.3639968 12.20745 496 52.50869
OLMAR 1.103785 0.4692946 0.2657746 15.62398 808 50.97354
EG 1.09293 0.4544007 -1.921296 8.579679 720 53.94933
UP 1.091364 0.4491232 -1.002317 8.43104 721 54.04868
CRP 1.083823 0.4110887 0.08867675 9.268793 731 54.09836
BAH 1.074893 0.3844829 -0.6506861 6.947344 874 54.09836
Anticor 1.019879 0.08123565 -0.2464923 2.801569 1108 51.76881
CWMR 1.013093 0.06186626 -0.3053956 1.904562 995 47.28991
PAMR 1.007116 0.03362724 -0.3451079 1.033495 996 46.54401
BNN 0.9001447 -0.5143973 -1.059632 -14.07822 1975 47.01789
CORN 0.8659736 -0.7379091 -1.434188 -16.89046 1975 47.54098
Kelly 0.750211 -1.459831 -1.510671 -93.97284 2012 50.5349

Run on the Test Set


In [19]:
# create the test set dataframe
olps_test  = pd.DataFrame(index=algo_names, columns=algo_data)
olps_test.algo  = olps_algos

In [20]:
# run all algos
for name, alg in zip(olps_test.index, olps_test.algo):
    olps_test.ix[name,'results'] = alg.run(test)

In [21]:
# Let's make sure the fees are 0 at first
for k, r in olps_test.results.iteritems():
    r.fee = 0.0

In [22]:
# plot as if we had no fees
# get the first result so we can grab the figure axes from the plot
ax = olps_test.results[0].plot(assets=False, weights=False, ucrp=True, portfolio_label=olps_test.index[0])
for k, r in olps_test.results.iteritems():
    if k == olps_test.results.keys()[0]: # skip the first item because we have it already
        continue
    r.plot(assets=False, weights=False, ucrp=False, portfolio_label=k, ax=ax[0])


Kelly went wild and crashed, so let's remove it from the mix


In [23]:
# plot as if we had no fees
# get the first result so we can grab the figure axes from the plot
ax = olps_test.results[0].plot(assets=False, weights=False, ucrp=True, portfolio_label=olps_test.index[0])
for k, r in olps_test.results.iteritems():
    if k == olps_test.results.keys()[0] or k == 'Kelly': # skip the first item because we have it already
        continue
    r.plot(assets=False, weights=False, ucrp=False, portfolio_label=k, ax=ax[0])



In [24]:
olps_stats(olps_test)
olps_test[metrics].sort('profit', ascending=False)


Out[24]:
profit sharpe information annualized_return drawdown_period winning_pct
BCRP 1.323158 1.687346 1.75396 21.09194 36 59.04573
Anticor 1.213151 1.119712 0.9792509 17.03448 86 56.88623
PAMR 1.198056 1.066684 0.8037021 16.56317 127 53.90782
CWMR 1.192981 1.038446 0.7643975 16.02498 127 54.10822
CORN 1.187963 1.042635 0.625535 12.58479 136 55.4672
BNN 1.18066 0.9848432 0.5932079 13.03065 122 56.6
BAH 1.150074 0.8247777 0.5070828 7.09091 193 55.666
UP 1.14739 0.8091129 0.3482341 6.841178 193 55.86481
EG 1.1467 0.8053731 0.491308 6.826939 194 54.87078
ONS 1.121838 0.6733858 -0.5299079 5.597352 229 53.87674
CRP 1.0989 0.5622322 -0.6019041 5.547421 224 53.28032
RMR 1.094977 0.5463773 0.1040624 7.934969 247 54.50902
OLMAR 1.032958 0.1938946 -0.38272 2.766207 291 54.30862
Kelly 0 NaN 0 -100 443 58.0574

Wow, ONS and OLMAR are at the bottom of the list. Remember, we really didn't do any training, but if we had selected ONS or OLMAR at the beginning of 2013 based on past performance, we would not have beat BAH. Hm.

Focusing on OLMAR

Instead of using the default parameters, we will test several window parameters to see if we can get OLMAR to improve.


In [25]:
# we need need fewer colors so let's reset the colors_cycle
mpl.rcParams['axes.color_cycle']= default_color_cycle

In [26]:
train_olmar = algos.OLMAR.run_combination(train, window=[3,5,10,15], eps=10)
train_olmar.plot()


Out[26]:
<matplotlib.axes._subplots.AxesSubplot at 0x10ea53050>

In [27]:
print(train_olmar.summary())


Summary for window=3:
    Profit factor: 1.30
    Sharpe ratio: 1.24
    Information ratio (wrt UCRP): 1.34
    Annualized return: 48.33%
    Longest drawdown: 132 days
    Winning days: 55.0%
        
Summary for window=5:
    Profit factor: 1.30
    Sharpe ratio: 1.28
    Information ratio (wrt UCRP): 1.36
    Annualized return: 49.43%
    Longest drawdown: 128 days
    Winning days: 55.0%
        
Summary for window=10:
    Profit factor: 1.29
    Sharpe ratio: 1.18
    Information ratio (wrt UCRP): 1.31
    Annualized return: 47.25%
    Longest drawdown: 125 days
    Winning days: 55.5%
        
Summary for window=15:
    Profit factor: 1.26
    Sharpe ratio: 1.06
    Information ratio (wrt UCRP): 1.15
    Annualized return: 41.48%
    Longest drawdown: 169 days
    Winning days: 54.6%
        

In [28]:
train_olmar = algos.OLMAR.run_combination(train, window=5, eps=[3,5,10,15])
train_olmar.plot()


Out[28]:
<matplotlib.axes._subplots.AxesSubplot at 0x10e921d50>

In [29]:
print(train_olmar.summary())


Summary for eps=3:
    Profit factor: 1.30
    Sharpe ratio: 1.27
    Information ratio (wrt UCRP): 1.36
    Annualized return: 49.15%
    Longest drawdown: 128 days
    Winning days: 54.9%
        
Summary for eps=5:
    Profit factor: 1.31
    Sharpe ratio: 1.28
    Information ratio (wrt UCRP): 1.37
    Annualized return: 49.73%
    Longest drawdown: 128 days
    Winning days: 55.0%
        
Summary for eps=10:
    Profit factor: 1.30
    Sharpe ratio: 1.28
    Information ratio (wrt UCRP): 1.36
    Annualized return: 49.43%
    Longest drawdown: 128 days
    Winning days: 55.0%
        
Summary for eps=15:
    Profit factor: 1.30
    Sharpe ratio: 1.27
    Information ratio (wrt UCRP): 1.36
    Annualized return: 49.37%
    Longest drawdown: 128 days
    Winning days: 55.0%
        

We find that a window of 5 and eps are 5 are optimal over the train time period, but the default of w=5 and eps=10 were also fine for our purposes.


In [30]:
# OLMAR vs UCRP
best_olmar = train_olmar[1]
ax1 = best_olmar.plot(ucrp=True, bah=True, weights=False, assets=False, portfolio_label='OLMAR')
olps_train.loc['CRP'].results.plot(ucrp=False, bah=False, weights=False, assets=False, ax=ax1[0], portfolio_label='CRP')


Out[30]:
[<matplotlib.axes._subplots.AxesSubplot at 0x10e412a50>]

On the train set OLMAR really delivers over CRP !


In [31]:
# let's print the stats
print(best_olmar.summary())


Summary:
    Profit factor: 1.31
    Sharpe ratio: 1.28
    Information ratio (wrt UCRP): 1.37
    Annualized return: 49.73%
    Longest drawdown: 128 days
    Winning days: 55.0%
        

Let's see how individual ETFs contribute to portfolio equity.


In [32]:
best_olmar.plot_decomposition(legend=True, logy=True)


Out[32]:
<matplotlib.axes._subplots.AxesSubplot at 0x10e82e690>

Let's highlight the magnitude of the highest contributing ETF by removing the log scale and looking at it directly.


In [33]:
best_olmar.plot_decomposition(legend=True, logy=False)


Out[33]:
<matplotlib.axes._subplots.AxesSubplot at 0x10cd918d0>

So VNQ (Real Estate) is the big driver after the market crash of 2008, which makes sense.

Let's look at portfolio allocations


In [34]:
best_olmar.plot(weights=True, assets=True, ucrp=False, logy=True, portfolio_label='OLMAR')


Out[34]:
[<matplotlib.axes._subplots.AxesSubplot at 0x10de2b910>,
 <matplotlib.axes._subplots.AxesSubplot at 0x10e144150>]

VNQ is the big driver of wealth (log scale). Let's test the strategy by removing the most profitable stock and comparing Total Wealth.


In [35]:
# find the name of the most profitable asset
most_profitable = best_olmar.equity_decomposed.iloc[-1].argmax()

# rerun algorithm on data without it
result_without = algos.OLMAR().run(train.drop([most_profitable], 1))

# and print results
print(result_without.summary())
result_without.plot(weights=False, assets=False, bah=True, ucrp=True, logy=True, portfolio_label='OLMAR-VNQ')


Summary:
    Profit factor: 1.23
    Sharpe ratio: 1.02
    Information ratio (wrt UCRP): 0.99
    Annualized return: 32.37%
    Longest drawdown: 153 days
    Winning days: 54.7%
        
Out[35]:
[<matplotlib.axes._subplots.AxesSubplot at 0x10e1ea310>]

In [36]:
result_without.plot_decomposition(legend=True, logy=False)


Out[36]:
<matplotlib.axes._subplots.AxesSubplot at 0x10ec18850>

Let's add fees of 0.1% per transaction (we pay \$1 for every \$1000 of stocks bought or sold).


In [37]:
best_olmar.fee = 0.001
print(best_olmar.summary())
best_olmar.plot(weights=False, assets=False, bah=True, ucrp=True, logy=True, portfolio_label='OLMAR')


Summary:
    Profit factor: 1.11
    Sharpe ratio: 0.48
    Information ratio (wrt UCRP): 0.28
    Annualized return: 15.97%
    Longest drawdown: 808 days
    Winning days: 51.1%
        
Out[37]:
[<matplotlib.axes._subplots.AxesSubplot at 0x10f919850>]

The results now fall, with a Sharpe Ratio below the ~0.5 market Sharpe, and an annualized return that has been cut in half due to fees. It's as if all the trading makes OLMAR underperform for the first 4 years until it can grab some volatility in 2008 to beat UCRP.

Let's look at OLMAR in the test time frame


In [38]:
test_olmar = algos.OLMAR(window=5, eps=5).run(test)
#print(train_olmar.summary())
test_olmar.plot(ucrp=True, bah=True, weights=False, assets=False, portfolio_label='OLMAR')


Out[38]:
[<matplotlib.axes._subplots.AxesSubplot at 0x111f2b690>]

With fees


In [39]:
test_olmar.fee = 0.001
print(test_olmar.summary())
test_olmar.plot(weights=False, assets=False, bah=True, ucrp=True, logy=True, portfolio_label='OLMAR')


Summary:
    Profit factor: 0.76
    Sharpe ratio: -1.62
    Information ratio (wrt UCRP): -3.05
    Annualized return: -19.52%
    Longest drawdown: 461 days
    Winning days: 49.1%
        
Out[39]:
[<matplotlib.axes._subplots.AxesSubplot at 0x1121ad290>]

OLMAR Starting in 2010

The 2008-2009 recession was unique. Let's try it all again starting in 2010, with a train set from 2010-2013 inclusive, and a test set of 2014.


In [40]:
# set train and test time periods
train_start_2010= datetime(2010,1,1)
train_end_2010 = datetime(2013,12,31)
test_start_2010 = datetime(2014,1,1)
test_end_2010 = datetime(2014,12,31)

In [41]:
# load data from Yahoo
train_2010 = DataReader(etfs, 'yahoo', start=train_start_2010, end=train_end_2010)['Adj Close']
test_2010  = DataReader(etfs, 'yahoo', start=test_start_2010,  end=test_end_2010)['Adj Close']

In [42]:
# plot normalized prices of these stocks
(train_2010 / train_2010.iloc[0,:]).plot()


Out[42]:
<matplotlib.axes._subplots.AxesSubplot at 0x11225cf10>

In [43]:
# plot normalized prices of these stocks
(test_2010 / test_2010.iloc[0,:]).plot()


Out[43]:
<matplotlib.axes._subplots.AxesSubplot at 0x10d0b9f90>

In [44]:
train_olmar_2010 = algos.OLMAR().run(train_2010)
train_crp_2010 = algos.CRP(b=swensen_allocation).run(train_2010)
ax1 = train_olmar_2010.plot(assets=True, weights=False, ucrp=True, bah=True, portfolio_label='OLMAR')
train_crp_2010.plot(ucrp=False, bah=False, weights=False, assets=False, ax=ax1[0], portfolio_label='CRP')


Out[44]:
[<matplotlib.axes._subplots.AxesSubplot at 0x10cfbffd0>]

In [45]:
print(train_olmar_2010.summary())


Summary:
    Profit factor: 1.19
    Sharpe ratio: 1.00
    Information ratio (wrt UCRP): 0.75
    Annualized return: 23.83%
    Longest drawdown: 209 days
    Winning days: 55.1%
        

In [46]:
train_olmar_2010.plot_decomposition(legend=True, logy=True)


Out[46]:
<matplotlib.axes._subplots.AxesSubplot at 0x10ced8d50>

Not bad, with a Sharpe at 1 and no one ETF dominating the portfolio. Now let's see how it fairs in 2014.


In [47]:
test_olmar_2010 = algos.OLMAR().run(test_2010)
test_crp_2010 = algos.CRP(b=swensen_allocation).run(test_2010)
ax1 = test_olmar_2010.plot(assets=True, weights=False, ucrp=True, bah=True, portfolio_label='OLMAR')
test_crp_2010.plot(ucrp=False, bah=False, weights=False, assets=False, ax=ax1[0], portfolio_label='CRP')


Out[47]:
[<matplotlib.axes._subplots.AxesSubplot at 0x112e24650>]

In [48]:
print(test_olmar_2010.summary())


Summary:
    Profit factor: 1.06
    Sharpe ratio: 0.36
    Information ratio (wrt UCRP): -0.63
    Annualized return: 4.72%
    Longest drawdown: 83 days
    Winning days: 57.9%
        

We just happen to be looking at a different time period and now the Sharpe drops below 0.5 and OLMAR fails to beat BAH. Not good.


In [49]:
test_olmar_2010.plot_decomposition(legend=True, logy=True)


Out[49]:
<matplotlib.axes._subplots.AxesSubplot at 0x114cd8550>

SPY / TLT portfolio comparison

Let's step back and simplify this by looking at OLMAR on a SPY and TLT portfolio. We should also compare this portfolio to a rebalanced 70/30 mix of SPY and TLT.


In [50]:
# load data from Yahoo
spy_tlt_data = DataReader(['SPY', 'TLT'], 'yahoo', start=datetime(2010,1,1))['Adj Close']

# plot normalized prices of these stocks
(spy_tlt_data / spy_tlt_data.iloc[0,:]).plot()


Out[50]:
<matplotlib.axes._subplots.AxesSubplot at 0x115057790>

In [51]:
spy_tlt_olmar_2010 = algos.OLMAR().run(spy_tlt_data)
spy_tlt_olmar_2010.plot(assets=True, weights=True, ucrp=True, bah=True, portfolio_label='OLMAR')


Out[51]:
[<matplotlib.axes._subplots.AxesSubplot at 0x112ef2d50>,
 <matplotlib.axes._subplots.AxesSubplot at 0x115745410>]

In [52]:
spy_tlt_olmar_2010.plot_decomposition(legend=True, logy=True)


Out[52]:
<matplotlib.axes._subplots.AxesSubplot at 0x11581d310>

In [53]:
print(spy_tlt_olmar_2010.summary())


Summary:
    Profit factor: 1.22
    Sharpe ratio: 1.18
    Information ratio (wrt UCRP): 0.48
    Annualized return: 21.35%
    Longest drawdown: 210 days
    Winning days: 56.2%
        

In [54]:
spy_tlt_2010 = algos.CRP(b=[0.7, 0.3]).run(spy_tlt_data)

ax1 = spy_tlt_olmar_2010.plot(assets=False, weights=False, ucrp=True, bah=True, portfolio_label='OLMAR')
spy_tlt_2010.plot(assets=False, weights=False, ucrp=False, bah=False, portfolio_label='CRP', ax=ax1[0])


Out[54]:
[<matplotlib.axes._subplots.AxesSubplot at 0x115797ed0>]

Now OLMAR looks better!

OLMAR Market Sectors comparison

Let's look at algo behavior on market sectors:

  • XLY Consumer Discrectionary SPDR Fund
  • XLF Financial SPDR Fund
  • XLK Technology SPDR Fund
  • XLE Energy SPDR Fund
  • XLV Health Care SPRD Fund
  • XLI Industrial SPDR Fund
  • XLP Consumer Staples SPDR Fund
  • XLB Materials SPDR Fund
  • XLU Utilities SPRD Fund

In [55]:
sectors = ['XLY','XLF','XLK','XLE','XLV','XLI','XLP','XLB','XLU']
train_sectors = DataReader(sectors, 'yahoo', start=train_start_2010, end=train_end_2010)['Adj Close']
test_sectors  = DataReader(sectors, 'yahoo', start=test_start_2010,  end=test_end_2010)['Adj Close']

In [56]:
# plot normalized prices of these stocks
(train_sectors / train_sectors.iloc[0,:]).plot()


Out[56]:
<matplotlib.axes._subplots.AxesSubplot at 0x115c8a8d0>

In [57]:
# plot normalized prices of these stocks
(test_sectors / test_sectors.iloc[0,:]).plot()


Out[57]:
<matplotlib.axes._subplots.AxesSubplot at 0x10dd2d510>

In [58]:
train_olmar_sectors = algos.OLMAR().run(train_sectors)
train_olmar_sectors.plot(assets=True, weights=False, ucrp=True, bah=True, portfolio_label='OLMAR')


Out[58]:
[<matplotlib.axes._subplots.AxesSubplot at 0x112d31ad0>]

In [59]:
print(train_olmar_sectors.summary())


Summary:
    Profit factor: 1.12
    Sharpe ratio: 0.64
    Information ratio (wrt UCRP): -0.13
    Annualized return: 13.94%
    Longest drawdown: 173 days
    Winning days: 53.0%
        

In [60]:
train_olmar_sectors.plot(assets=False, weights=False, ucrp=True, bah=True, portfolio_label='OLMAR')


Out[60]:
[<matplotlib.axes._subplots.AxesSubplot at 0x115e81590>]

In [61]:
test_olmar_sectors = algos.OLMAR().run(test_sectors)
test_olmar_sectors.plot(assets=True, weights=False, ucrp=True, bah=True, portfolio_label='OLMAR')


Out[61]:
[<matplotlib.axes._subplots.AxesSubplot at 0x116801050>]

In [62]:
test_olmar_sectors = algos.OLMAR().run(test_sectors)
test_olmar_sectors.plot(assets=False, weights=False, ucrp=True, bah=True, portfolio_label='OLMAR')


Out[62]:
[<matplotlib.axes._subplots.AxesSubplot at 0x11682a510>]

All OLPS Algos Market Sectors comparison


In [63]:
#list all the algos
olps_algos_sectors = [
algos.Anticor(),
algos.BAH(),
algos.BCRP(),
algos.BNN(),
algos.CORN(),
algos.CRP(),  # removed weights, and thus equivalent to UCRP
algos.CWMR(),
algos.EG(),
algos.Kelly(),
algos.OLMAR(),
algos.ONS(),
algos.PAMR(),
algos.RMR(),
algos.UP()
]

In [64]:
olps_sectors_train = pd.DataFrame(index=algo_names, columns=algo_data)
olps_sectors_train.algo = olps_algos_sectors

In [65]:
# run all algos - this takes more than a minute
for name, alg in zip(olps_sectors_train.index, olps_sectors_train.algo):
    olps_sectors_train.ix[name,'results'] = alg.run(train_sectors)

In [66]:
# we need 14 colors for the plot
n_lines = 14
color_idx = np.linspace(0, 1, n_lines)
mpl.rcParams['axes.color_cycle']=[plt.cm.rainbow(i) for i in color_idx]

In [67]:
# plot as if we had no fees
# get the first result so we can grab the figure axes from the plot
olps_df = olps_sectors_train
ax = olps_df.results[0].plot(assets=False, weights=False, ucrp=True, portfolio_label=olps_df.index[0])
for k, r in olps_df.results.iteritems():
    if k == olps_df.results.keys()[0]: # skip the first item because we have it already
        continue
    r.plot(assets=False, weights=False, ucrp=False, portfolio_label=k, ax=ax[0])



In [68]:
# Kelly went wild, so let's remove it
# get the first result so we can grab the figure axes from the plot
olps_df = olps_sectors_train
ax = olps_df.results[0].plot(assets=False, weights=False, ucrp=True, portfolio_label=olps_df.index[0])
for k, r in olps_df.results.iteritems():
    if k == olps_df.results.keys()[0] or k == 'Kelly' : # skip the first item because we have it already
        continue
    r.plot(assets=False, weights=False, ucrp=False, portfolio_label=k, ax=ax[0])



In [69]:
olps_stats(olps_sectors_train)
olps_sectors_train[metrics].sort('profit', ascending=False)


Out[69]:
profit sharpe information annualized_return drawdown_period winning_pct
BCRP 1.226919 1.168603 1.148355 24.06737 134 55.667
ONS 1.170012 0.8586938 0.4181884 16.35309 134 55.8209
UP 1.166523 0.8495217 0.3003208 15.33203 145 55.32338
CRP 1.166265 0.8482206 0 15.31564 145 55.42289
EG 1.166244 0.8481597 -0.3349534 15.30911 145 55.52239
BAH 1.166026 0.8479684 -0.2642631 15.21354 145 55.72139
Anticor 1.14944 0.7725771 0.1426251 16.78452 178 53.60721
OLMAR 1.12479 0.6402547 -0.1256112 13.94468 173 52.97679
RMR 1.118902 0.614074 -0.1753967 13.41708 216 53.08392
CORN 1.095282 0.5037723 -0.6879739 9.378424 398 56.0199
CWMR 1.095178 0.4939886 -0.3888658 10.98823 249 53.18504
PAMR 1.092942 0.4832585 -0.4119575 10.72079 252 53.5497
BNN 1.050962 0.2778638 -1.042531 5.400526 536 52.12121
Kelly 0 NaN 0 -100 948 53.71728

In [70]:
# create the test set dataframe
olps_sectors_test  = pd.DataFrame(index=algo_names, columns=algo_data)
olps_sectors_test.algo  = olps_algos_sectors

In [71]:
# run all algos
for name, alg in zip(olps_sectors_test.index, olps_sectors_test.algo):
    olps_sectors_test.ix[name,'results'] = alg.run(test_sectors)

In [72]:
# plot as if we had no fees
# get the first result so we can grab the figure axes from the plot
olps_df = olps_sectors_test
ax = olps_df.results[0].plot(assets=False, weights=False, ucrp=True, portfolio_label=olps_df.index[0])
for k, r in olps_df.results.iteritems():
    if k == olps_df.results.keys()[0] : #or k == 'Kelly': # skip the first item because we have it already
        continue
    r.plot(assets=False, weights=False, ucrp=False, portfolio_label=k, ax=ax[0])



In [73]:
# drop Kelly !
# get the first result so we can grab the figure axes from the plot
olps_df = olps_sectors_test
ax = olps_df.results[0].plot(assets=False, weights=False, ucrp=True, portfolio_label=olps_df.index[0])
for k, r in olps_df.results.iteritems():
    if k == olps_df.results.keys()[0] or k == 'Kelly': # skip the first item because we have it already
        continue
    r.plot(assets=False, weights=False, ucrp=False, portfolio_label=k, ax=ax[0])



In [74]:
olps_stats(olps_sectors_test)
olps_sectors_test[metrics].sort('profit', ascending=False)


Out[74]:
profit sharpe information annualized_return drawdown_period winning_pct
BCRP 1.372667 1.999838 1.068856 30.93763 79 56.45161
BAH 1.238424 1.255041 0.274977 14.64473 39 59.36255
EG 1.235216 1.239731 0.2910832 14.5555 39 58.96414
CRP 1.235039 1.238862 0 14.55038 39 58.96414
UP 1.234865 1.237929 -0.5728829 14.53445 39 58.96414
CORN 1.233172 1.290861 0.4536957 19.69628 35 54.58167
ONS 1.208885 1.10624 -0.3155632 13.80497 43 58.16733
RMR 1.141392 0.7458201 -0.06157608 13.71809 33 59.83936
OLMAR 1.135582 0.7181107 -0.07290339 13.54462 47 60.64257
PAMR 1.130602 0.7027512 -0.1423942 12.65033 41 58.63454
CWMR 1.121708 0.6561141 -0.20992 11.76061 41 58.23293
BNN 1.049022 0.2901784 -0.9299598 4.194964 125 54.61847
Anticor 0.9770185 -0.1272582 -1.357038 -2.208917 74 53.38645
Kelly 0 NaN 0 -100 251 53.23383

Further work

  • More algo's could be optimized for parameters before they are run against the test set
  • In addition to the BAH, CRP and BCRP benchmarks, we could consider holding SPY at 100% as a benchmark.
  • Could look into BAH(OLMAR) and other combinations as this framework supports combining approaches directly
  • Experiment with the run_subsets feature

Conclusion

RMR and OLMAR do add value to a Lazy Portfolio if tested or run over a long enough period of time. This gives RMR and OLMAR a chance to grab onto a period of volatility. But in an up market (2013-1014) you want to Follow-the-Leader, not Follow-the-Looser. Of the other algo's, CRP or BAH are decent, and maybe it's worth understanding what ONS is doing.


In [ ]: