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