Notebook
In [8]:
# Import Zipline, the open source backester, and a few other libraries that we will use
import zipline
from zipline import TradingAlgorithm
from zipline.api import schedule_function, order_target, order_target_percent, order_percent, record, symbol, history, set_commission,set_slippage
from zipline.api import date_rules, time_rules, commission, slippage, order_target, get_datetime
import pytz
from datetime import datetime
import matplotlib.pyplot as pyplot
import numpy as np
import pandas as pd
from scipy.optimize import minimize
import time
import math
In [9]:
stocks = symbols(['XLF', 'XLE', 'XLU', 'XLK', 'XLB', 'XLP', 'XLY', 'XLI', 'XLV'])
data = get_pricing(
    stocks,
    start_date='2003-01-01',
    end_date = '2015-06-23',
    frequency='daily'
)
data.price.plot(use_index=True)
Out[9]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f9516ba0c90>
In [13]:
# Define the algorithm - this should look familiar from the Quantopian IDE
# For more information on writing algorithms for Quantopian
# and these functions, see https://www.quantopian.com/help

def initialize(context):
    schedule_function(trade, 
                      date_rule=date_rules.week_end(), 
                      time_rule=time_rules.market_close(minutes=1), 
                      half_days=False)
    context.lookback = 252*2 # 252*n = n years
    context.stocks = symbols(['XLF', 'XLE', 'XLU', 'XLK', 'XLB', 'XLP', 'XLY', 'XLI', 'XLV'])
    set_commission(commission.PerShare(cost=0.00))
    set_slippage(slippage.VolumeShareSlippage(volume_limit=1, price_impact=0))
    context.i = 0
    #add_history(context.lookback, '1d', 'price')

def handle_data(context, data):
    context.i += 1
    if context.i < context.lookback:
        return
    
def trade(context,data):
        if context.i < context.lookback:
            return
        w = estimate(context,data)
    
        # Close all current positions
        for stock in context.portfolio.positions:
            if stock not in w.index:
               order_target(stock, 0)  

       #Order
        for i,n in enumerate(w):
            if w.index[i] in data:
                if w.index[i] in context.portfolio.positions: #If position already exists
                    order_target_percent(w.index[i], n)
                else:
                    order_percent(w.index[i], n)
    
def estimate(context,data):
        # Structure a portfolio such that it maximizes dividend yield while lowering volatility
        # Covar estimation
        #price_history = history(context.lookback, frequency="1d", field='price')
        price_history = data.history(context.stocks, fields='price', bar_count = context.lookback, frequency="1d")
        ret = (price_history/price_history.shift(5)-1).dropna(axis=0)[5::5]
        ret.columns = context.stocks
        n = len(ret.columns)
        
        covar = ret.cov()
        
        w = min_var_weights(covar, context.stocks)

        # LOG EVERYTHING
        #esigma = np.sqrt(np.dot(w.T,np.dot(covar,w)))*math.sqrt(52)*100
        #log.info('Expected Volatility: '+str(esigma)+'%')
        #w = pd.Series(np.round(1000*w)/1000)
        #log.info('Number of Stocks: '+str(sum(abs(w)>0)))
        #log.info('Current Leverage: '+str(context.account.leverage))
        #w.index = ret.columns
        #log.info(w)
        #print(get_datetime())
        #print(w)
        return w  #Cash Cushion of X%


def min_var_weights(cov, stocks):
    '''
    Returns a dictionary of sid:weight pairs.
    '''
    cov = pd.DataFrame(2*cov)
    x = np.array([0.]*(len(cov)+1))
    #x = np.ones(len(cov) + 1)
    x[-1] = 1.0
    p = lagrangize(cov)
    weights = np.linalg.solve(p, x)[:-1]
    weights = pd.Series(weights, index=stocks)
    return weights


def lagrangize(df):
    '''
    Utility funcion to format a DataFrame 
    in order to solve a Lagrangian sysem. 
    '''
    df = df
    df['lambda'] = np.ones(len(df))
    z = np.ones(len(df) + 1)
    x = np.ones(len(df) + 1)
    z[-1] = 0.0
    x[-1] = 1.0
    m = [i for i in df.as_matrix()]
    m.append(z)    
    return pd.DataFrame(np.array(m))
In [14]:
## WORKING COPY
# Analyze is a post-hoc analysis method available on Zipline. 
# It accepts the context object and 'perf' which is the output 
# of a Zipline backtest.  This API is currently experimental, 
# and will likely change before release.

def analyze(context, perf):
    fig = pyplot.figure()
    
    # Make a subplot for portfolio value.
    ax1 = fig.add_subplot(211)
    perf.portfolio_value.plot(ax=ax1, figsize=(16,12))
    ax1.set_ylabel('portfolio value in $')
    pyplot.legend(loc=0)
    pyplot.show()
In [15]:
#WORKING COPY
# NOTE: This cell will take a few minutes to run.

# Create algorithm object passing in initialize and
# handle_data functions
algo_obj = TradingAlgorithm(
    initialize=initialize, 
    handle_data=handle_data
)

# HACK: Analyze isn't supported by the parameter-based API, so
# tack it directly onto the object.
algo_obj._analyze = analyze

# Run algorithm
perf_manual = algo_obj.run(data.transpose(2,1,0))
In [21]:
perf_manual = perf_manual[perf_manual.index >= datetime(2005,1,1)]
perf_manual.returns.plot()
Out[21]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f94fff06e90>
In [22]:
    def calculate_max_drawdown(rets):
        compounded_returns = []
        cur_return = 0.0
        for r in rets:
            try:
                cur_return += math.log(1.0 + r)
            # this is a guard for a single day returning -100%, if returns are
            # greater than -1.0 it will throw an error because you cannot take
            # the log of a negative number
            except ValueError:
                log.debug("{cur} return, zeroing the returns".format(
                    cur=cur_return))
                cur_return = 0.0
            compounded_returns.append(cur_return)

        cur_max = None
        max_drawdown = None
        for cur in compounded_returns:
            if cur_max is None or cur > cur_max:
                cur_max = cur

            drawdown = (cur - cur_max)
            if max_drawdown is None or drawdown < max_drawdown:
                max_drawdown = drawdown

        if max_drawdown is None:
            return 0.0

        return 1.0 - math.exp(max_drawdown)
In [23]:
calculate_max_drawdown(perf_manual.returns)
Out[23]:
0.3347186528388948
In [24]:
import pyfolio as pf

returns, positions, transactions, gross_lev = pf.utils.extract_rets_pos_txn_from_zipline(perf_manual)
In [25]:
pf.create_full_tear_sheet(returns, positions=positions, 
                          transactions=transactions,
                          gross_lev=gross_lev
                         )
Entire data start date: 2005-01-03
Entire data end date: 2015-06-23


Backtest Months: 125
Performance statistics Backtest
annual_return 0.06
annual_volatility 0.14
sharpe_ratio 0.51
calmar_ratio 0.19
stability_of_timeseries 0.65
max_drawdown -0.33
omega_ratio 1.10
sortino_ratio 0.72
skew 0.24
kurtosis 16.04
tail_ratio 0.94
common_sense_ratio 1.00
information_ratio -0.00
alpha 0.03
beta 0.49
Worst Drawdown Periods net drawdown in % peak date valley date recovery date duration
0 33.47 2007-12-24 2009-03-09 2012-06-14 1169
1 10.38 2012-07-30 2012-12-28 2013-04-02 177
2 10.11 2013-04-30 2013-08-28 2014-01-16 188
3 5.76 2014-09-05 2014-10-16 2014-10-28 38
4 5.73 2005-10-03 2005-10-27 2006-07-24 211

[-0.017 -0.033]