Introduction

It has been almost a year since I released alpha version of universal-portfolios. Although I didn't expect anyone to be interested in such niche area, people like Paul Perry took the idea and pushed it further and started a fruitful discussion on Quantopian. Paul's ideas have waken me up from sleep and so I decided it's time to get serious about this and spent some weeks refining my approach to portfolio management. New version of universal-portfolios contains all tools I use to manage my own portfolio. Feel free to get inspired, but please stay suspicious, don't believe anything unless you try it yourself and know that the saying "History repeats itself" doesn't really apply to stock market.

Load data

Nothing fancy, just init things and load ETFs we're going to work with SPY, TLT, XLF, XLE, XLU, XLK, XLB, XLP, XLY, XLI, XLV (S&P ETF and its components plus ETF for Treasury Bonds).

In [1]:
%matplotlib inline
%load_ext autoreload
%autoreload 2

import numpy as np
import pandas as pd
from datetime import datetime, date
import universal as up
from universal import tools
from universal import algos
import logging
import trading
reload(logging)
logging.basicConfig(level=logging.INFO)

from universal.algos import *

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 [2]:
from pandas.io.data import DataReader

# load assets
symbols = ['SPY', 'TLT', 'XLF', 'XLE', 'XLU', 'XLK', 'XLB', 'XLP', 'XLY', 'XLI', 'XLV']
S = DataReader(symbols, 'yahoo', start=datetime(2003,1,1))['Adj Close']

# create other frequencies
mS = S.resample('M', how='last')
wS = S.resample('W', how='last')

Can we beat uniform allocation (UCRP)?

Uniform portfolio, $\frac{1}{N}$ portfolio or uniform constant rebalanced portfolio (UCRP) means you invest equal proportion (that is $\frac{1}{N}$ where $N$ is number of assets) of your capital to each asset. Sounds too simple? In practice, it's very hard to beat uniform portfolio.

Note that we use weekly data. Daily data can be used as well, but the results are almost the same and doing analysis on weekly data is faster.

In [40]:
algo = CRP()
result = algo.run(wS)
print(result.summary())
_ = result.plot()
Summary:
    Profit factor: 1.29
    Sharpe ratio: 0.63
    Information ratio (wrt UCRP): 0.00
    Annualized return: 10.13%
    Annualized volatility: 15.12%
    Longest drawdown: 835 days
    Max drawdown: 47.76%
    Winning days: 58.9%
        

Some algorithms such as Universal portfolio even use best constant rebalanced portfolio (BCRP) as benchmark. BCRP is a portfolio whose weights are constant in time and is determined in hindsight (=can't be traded) to maximize total return. For our portfolio BCRP puts 57% to XLY and 43% to XLE.

In [41]:
algo = BCRP()
result = algo.run(wS)
print(result.summary())
_ = result.plot()
Summary:
    Profit factor: 1.24
    Sharpe ratio: 0.55
    Information ratio (wrt UCRP): 0.22
    Annualized return: 12.26%
    Annualized volatility: 21.03%
    Longest drawdown: 666 days
    Max drawdown: 52.70%
    Winning days: 57.5%
        

BCRP is determined in hindsight, but we can use historical data to find BCRP for e.g. last year and hold it, hoping that history will repeat itself. Let's use last year (52 weeks) to determine BCRP.

Not that bad actually, return is indeed slightly higher than that of CRP, but sharpe ratio decreased. Adding transaction costs would kill us though. From my experience it usually performs worse than UCRP in both return and sharpe.

In [46]:
algo = DynamicCRP(n=52, min_history=8)
result = algo.run(wS)
print(result.summary())
_ = result.plot()
Summary:
    Profit factor: 1.21
    Sharpe ratio: 0.52
    Information ratio (wrt UCRP): 0.02
    Annualized return: 10.47%
    Annualized volatility: 18.97%
    Longest drawdown: 550 days
    Max drawdown: 32.54%
    Winning days: 58.5%
        

So what's wrong with this approach? We shouldn't care about return alone, but about both return and volatility which is summarized in sharpe ratio. We can always use leveraged ETFs to increase return or use margin. Let's focus on sharpe ratio then.

First idea that comes into your mind is optimizing BCRP for sharpe ratio instead of return. This idea got quite popular by Frank Grossman on seekingalpha.

There's one technicality we need to take care of. Sharpe ratio with zero risk free rate is defined as $$ sh = \frac{\mu}{\sigma}, $$ where $\mu$ is expected value of our strategy and $\sigma$ is its standard deviation (volatility). Optimization can get hard since $\sigma$ can go to zero in case of portfolio holding only cash. We will therefore add "regularization" term $\alpha > 0$ $$ sh_{reg} = \frac{\mu}{\sigma + \alpha}, $$ Setting $\alpha$ to zero will give you zero portfolio with zero volatility and zero return (assuming zero risk-free rate). Setting $\alpha \rightarrow \infty$ will its maximize return. Thus $\alpha$ works as a risk-aversion parameter controlling tradeoff between volatility and return. Try setting $\alpha$ to different values to see how it influences your return and sharpe ratio.

Another way of fixing ill-posed problem of maximizing sharpe ratio is to force no cash constraint, that is all weights must sum to 1.

In [65]:
algo = DynamicCRP(n=52, min_history=8, metric='sharpe', alpha=0.01)
result = algo.run(wS)
print(result.summary())
_ = result.plot(title='Using risk-aversion(alpha) parameter')
Summary:
    Profit factor: 1.36
    Sharpe ratio: 0.81
    Information ratio (wrt UCRP): -0.09
    Annualized return: 8.63%
    Annualized volatility: 10.21%
    Longest drawdown: 758 days
    Max drawdown: 20.44%
    Winning days: 61.6%
        
In [60]:
algo = DynamicCRP(n=52, min_history=8, metric='sharpe', no_cash=True)
result = algo.run(wS)
print(result.summary())
_ = result.plot(title='Using no cash constraint')
Summary:
    Profit factor: 1.36
    Sharpe ratio: 0.80
    Information ratio (wrt UCRP): -0.08
    Annualized return: 8.69%
    Annualized volatility: 10.40%
    Longest drawdown: 719 days
    Max drawdown: 25.20%
    Winning days: 62.1%