| Overall Statistics |
|
Total Trades 43 Average Win 0.45% Average Loss -0.98% Compounding Annual Return -7.428% Drawdown 4.900% Expectancy -0.062 Net Profit -3.775% Sharpe Ratio -0.911 Loss Rate 36% Win Rate 64% Profit-Loss Ratio 0.46 Alpha -0.053 Beta -0.031 Annual Standard Deviation 0.067 Annual Variance 0.004 Information Ratio -2.551 Tracking Error 0.123 Treynor Ratio 1.946 Total Fees $4931.65 |
"""
A simple Statistial Arbitrage strategy from Quantopian
(credit to: "Aqua Rooster" https://www.quantopian.com/posts/a-very-simple-1-dot-25-sharpe-algorithm)
1. Select shares universe (e.g. i. a static list or ii. a dynamic one, like most liquid shares in same sector)
and get historical prices
2. Find risk factors (i.e. common drivers for the returns), using e.g. PCA, ICA, ...
3. Regress returns vs. risk factors to get individual factor exposure (betas)
4. find shares weights such that
i. maximise your alpha (e.g. z-score of regression residuals * weights)
ii. subject to some constraints, e.g.
zero net exposure,
gross exposure <= 100%,
neutralised betas
"""
import numpy as np
import pandas as pd
import scipy as sp
import cvxpy as cvx
from sklearn.covariance import OAS
from sklearn.decomposition import PCA
import statsmodels.api as smapi # deprecation warning on pandas.core.datetools
# from sklearn.linear_model import LinearRegression # first X then Y, vs. statsmodels where Y is first
# from decimal import Decimal
# from datetime import datetime, timedelta
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")
from System import *
from QuantConnect import *
from QuantConnect.Data import *
from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *
from System.Collections.Generic import List
class StatisticalArbitrage_v1(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2012,12,1) #Set Start Date
self.SetEndDate(2013,6,1) #Set End Date
self.SetCash(10000000) #Set Strategy Cash
self.coarse_symbols=[]; self.fine_symbols = []
self.rebalence_flag =True # to rebalance the fist time anyway
self.trade_flag = True
self.SPY = self.AddEquity("SPY").Symbol #NB: Self.SPY = SPY R735QTJ8XC9X vs. Self.SPY.Value = SPY
# charts: these lines will make it appear together with Strategy
stPlot = Chart('Strat Plot')
stPlot.AddSeries(Series('Longs', SeriesType.Line, 2))
stPlot.AddSeries(Series('Shorts', SeriesType.Line, 2))
self.AddChart(stPlot)
self.UniverseSettings.Resolution = Resolution.Daily #; self.UniverseSettings.Leverage = 2
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
# params
self.min_price = 10.0
self.number_coarse = 100 # should be 500, but keeps getting maxout errors
self.back_period = 90
# rebalancing every 3 months (via counter in Rebalancing body fncts)
self.counter = 2 # no. of months
self.Schedule.On(self.DateRules.MonthStart("SPY"),
self.TimeRules.AfterMarketOpen(self.SPY, 0),
Action(self.Rebalancing))
self.Schedule.On(self.DateRules.EveryDay(self.SPY),
self.TimeRules.AfterMarketOpen(self.SPY, 10),
Action(self.Trade))
def Rebalancing(self):
# every 3rd month (e.g. Jan, March, June, ...)
if self.counter < 2:
self.counter += 1
self.rebalence_flag = False
return
self.counter = 0
self.rebalence_flag = True
def CoarseSelectionFunction(self, coarse):
""" Get universe selection, i.e. shares s.t.
(i) min price > self.min_price" and
(ii) top #(self.number_coarse) liquid """
if self.rebalence_flag:
AboveMinPrice = [x for x in coarse if float(x.Price) > self.min_price]
sortedByDollarVolume = sorted(AboveMinPrice, key=lambda x: x.DollarVolume, reverse=True)
top = sortedByDollarVolume[:self.number_coarse]
self.coarse_symbols = [i.Symbol for i in top]
# degug
self.Debug("num of COARSE shares: %d" %len(self.coarse_symbols) )
return self.coarse_symbols
else:
return [] if (not self.coarse_symbols) else self.coarse_symbols
def FineSelectionFunction(self, fine):
if self.rebalence_flag:
# get only Primary & Manufacturing shares
filtered_fine = [x for x in fine if x.SecurityReference.IsPrimaryShare
and x.CompanyReference.IndustryTemplateCode=='N'
] # more at: https://www.quantconnect.com/data#fundamentals/usa/morningstar
self.fine_symbols = [i.Symbol for i in filtered_fine]
# degug
self.Debug("num of FINE shares: %d" %len(self.fine_symbols) )
# reset
self.rebalence_flag = False
self.trade_flag = True
return self.fine_symbols
else:
return [] if (not self.fine_symbols) else self.fine_symbols
def Trade(self):
# checks
if not self.fine_symbols: return
if not self.trade_flag: return
# get log returns for the universe selection (NB: hist should be a pandas Panel)
symbols = [x.Value for x in self.fine_symbols] # x.Symbol.Value
for tkr in symbols:
self.AddEquity(tkr, Resolution.Daily)
hist = self.History(symbols, self.back_period, Resolution.Daily)
if hist is None: return
prices = hist["close"].unstack(level=0).dropna(axis=1)
logRtrn = np.log(prices / prices.shift(1)).dropna() # (num_rtrn, symbols)
# = np.diff(np.log(prices.values), axis=0)
# model: #1 risk factor
factors = PCA(1).fit_transform(logRtrn) # fit + transform # (num_rtrn, [1])
X_c = smapi.add_constant(factors) # factor(s) + const
model = smapi.OLS(logRtrn, X_c).fit()
# NB: need np.asarray(), contrarily to Quantopian (otherwise rtrns a Python list which is not hashable)
betas = np.asarray(model.params.T)[:, 1:] # model.params is (2, symbols) array; returns beta (but not constant) for all symbols [[beta0], [beta1], ...,]
# model.resid is (days, symbols) array :=>
# sum across days (.sum(axis=0)) for two periods [-x:,:],
# standardise resulting 1-d array across symbols (by zscore) and
# subtract older score from more recent
signal = sp.stats.zscore(np.asarray(model.resid)[-2:, :].sum(axis=0)) \
- sp.stats.zscore(np.asarray(model.resid)[-20:, :].sum(axis=0))
# optimised weights
w = self.get_weights(signal, betas)
# get some charts
self.ShowChart()
# w_i re-based to 100% gross
denom = np.sum(np.abs(w))
if denom == 0: denom = 1.
w = w / denom
# remove tkrs we don't have (NB: prices.columns == self.fine_symbols)
for tkr in self.Portfolio.Values:
if (tkr.Invested) and (tkr not in self.fine_symbols):
self.Liquidate(tkr.Symbol)
# submit orders
for i, tkr in enumerate(prices.columns):
self.SetHoldings(tkr, w[i])
self.trade_flag = False
def get_weights(self, signal, betas):
(m, n) = betas.shape # (symbols, 1) if PCA(1)
x = cvx.Variable(m)
objective = cvx.Maximize(signal.T * x)
constraints = [cvx.abs(sum(x)) < 0.001, # net ~= 0
sum(cvx.abs(x)) <= 1, # gross <= 100%
x <= 3.5 / m, -x <= 3.5 / m] # weights upper/lower bounds
# neutralise all risk factors exposures (here just #1): abs(beta * weights) = 0
for i in range(0, n):
constraints.append(cvx.abs(betas[:, i].T * x) < 0.001)
probl = cvx.Problem(objective, constraints)
probl.solve(solver=cvx.CVXOPT)
# if no solution
if probl.status <> 'optimal':
print prob.status
return np.asarray([0.0] * m) # return 0. array
return np.asarray(x.value).flatten()
def OnData(self, data):
pass
def ShowChart(self):
longs=shorts=0
longs = np.random.randint(0, 10, size=1)
shorts = np.random.randint(0, 10, size=1)
for tkr in self.Portfolio.Values:
if tkr.Quantity > 0: longs += 1
if tkr.Quantity < 0: shorts += 1
self.Plot("Strat Plot", 'Longs', 1.*longs); self.Log("Longs: %d" %longs)
self.Plot("Strat Plot", 'Shorts', 1.*shorts); self.Log("Shorts: %d" %shorts)
# show chart