| Overall Statistics |
|
Total Trades 36 Average Win 2.52% Average Loss -0.10% Compounding Annual Return 4.351% Drawdown 2.900% Expectancy 6.454 Net Profit 11.558% Sharpe Ratio 0.688 Probabilistic Sharpe Ratio 17.981% Loss Rate 72% Win Rate 28% Profit-Loss Ratio 25.84 Alpha 0.027 Beta 0.03 Annual Standard Deviation 0.045 Annual Variance 0.002 Information Ratio -0.443 Tracking Error 0.196 Treynor Ratio 1.036 Total Fees $6966.29 |
import numpy as np
import statsmodels.api as sm
from arch.unitroot import PhillipsPerron
from statsmodels.tsa.stattools import adfuller
import numpy as np
import pandas as pd
def is_stationary(data, sig_level=.05):
if adfuller(data)[1] > sig_level:
return False
elif PhillipsPerron(data).pvalue > sig_level:
return False
else:
return True
def optimize(data_anchor:pd.Series, data:pd.DataFrame):
data_train = data.copy()
data_train['ones'] = 1 # adds a column of 1's for the alpha term of OLS
res = sm.OLS(data_anchor, data).fit()
if not is_stationary(res.resid):
return None
weights = res.params[:-1] # remove the alpha term
portfolio_values = data_anchor - (data * weights).sum(axis=1)
return portfolio_values.mean(), portfolio_values.std(), weights# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2020 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from enum import Enum
import portfolio_construction as po
import numpy as np
from System.Drawing import Color
class VerticalTransdimensionalAutosequencers(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2018, 5, 1) # Set Start Date
self.SetCash(1000000) # Set Strategy Cash
# GDAX (aka Coinbase) doesn't allow shorting
self.SetBrokerageModel(BrokerageName.Bitfinex)
self.anchor = 'BTCUSD'
tickers = [self.anchor, 'ETHUSD', 'BCHUSD', 'LTCUSD']
self.symbols = []
for ticker in tickers:
symbol = self.AddCrypto(ticker, Resolution.Daily, Market.Bitfinex).Symbol
self.symbols.append(symbol)
self.data = RollingWindow[Slice](100) # stores historical price data
self.weights = None
self.portfolio = None
self.state = State.NEUTRAL
self.SetWarmup(100)
self.Train(self.DateRules.WeekStart('BTCUSD'), self.TimeRules.Midnight, self.Retrain)
# below is for fancy charting
stockPlot = Chart('Custom')
stockPlot.AddSeries(Series('long', SeriesType.Scatter, '$', Color.Green, ScatterMarkerSymbol.Triangle))
stockPlot.AddSeries(Series('short', SeriesType.Scatter, '$', Color.Red, ScatterMarkerSymbol.TriangleDown))
#stockPlot.AddSeries(Series('liquidate', SeriesType.Scatter, '$', Color.Blue, ScatterMarkerSymbol.Diamond))
self.AddChart(stockPlot)
def OnData(self, data):
self.data.Add(data)
if self.IsWarmingUp:
return
if self.portfolio is None:
return
if not all([data.Bars.ContainsKey(symbol) for symbol in self.symbols]):
return
anchor_price = data.Bars[self.anchor].Close
others_prices = []
for ticker in self.weights.index:
close = data.Bars[ticker].Close
others_prices.append(close)
signal = self.portfolio.update(self, anchor_price, others_prices)
scale_factor = 20
# go long if not already
if signal == 1 and self.state != State.LONG:
self.state = State.LONG
self.Liquidate()
self.MarketOrder('BTCUSD', 1 * scale_factor)
for ticker, weight in self.weights.iteritems():
self.MarketOrder(ticker, -weight * scale_factor)
# go short if not already
elif signal == -1 and self.state != State.SHORT:
self.state = State.SHORT
self.Liquidate()
self.MarketOrder('BTCUSD', -1 * scale_factor)
for ticker, weight in self.weights.iteritems():
self.MarketOrder(ticker, weight * scale_factor)
def Retrain(self):
if not self.data.IsReady:
return
try:
# since RollingWindow is recent at top, we need to reverse it
data = self.PandasConverter.GetDataFrame(self.data).iloc[::-1]
except:
self.reset()
return
# turn the closing prices for each equity into columns
data = data['close'].unstack(level=0)
data_wo_anchor = data.drop(self.anchor, axis=1)
res = po.optimize(data[self.anchor], data_wo_anchor)
if res is None:
self.reset()
return
mean, std, self.weights = res
self.portfolio = Portfolio(mean, std, self.weights)
def reset(self):
self.Liquidate()
self.weights = None
self.portfolio = None
self.Log('Resetting')
class Portfolio:
def __init__(self, mean, std, weights, std_factor=1.5):
self.mean = mean
self.std = std
self.weights = np.array(weights)
self.std_factor = std_factor
self.tripped = Tripped.NEUTRAL
def update(self, algorithm, anchor_price, others_prices):
spread = anchor_price - np.sum(np.array(others_prices) * np.array(self.weights))
upper = self.mean + self.std * self.std_factor
lower = self.mean - self.std * self.std_factor
algorithm.Plot('Custom', 'Spread Value', spread)
algorithm.Plot('Custom', 'Short Value', upper)
algorithm.Plot('Custom', 'Long Value', lower)
if spread > upper:
self.tripped = Tripped.UPPER
# go short
elif self.tripped == Tripped.UPPER and spread <= upper:
self.tripped = Tripped.NEUTRAL
algorithm.Plot('Custom', 'short', spread)
return -1
elif spread < lower:
self.tripped = Tripped.LOWER
# go long
elif self.tripped == Tripped.LOWER and spread >= lower:
self.tripped = Tripped.NEUTRAL
algorithm.Plot('Custom', 'long', spread)
return 1
return 0
class State(Enum):
LONG = 0
NEUTRAL = 1
SHORT = 2
class Tripped(Enum):
LOWER = 0
NEUTRAL = 1
UPPER = 2