Overall Statistics
Total Trades
27
Average Win
1.23%
Average Loss
-1.70%
Compounding Annual Return
-46.339%
Drawdown
5.900%
Expectancy
-0.139
Net Profit
-1.803%
Sharpe Ratio
-2.047
Probabilistic Sharpe Ratio
26.797%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
0.72
Alpha
-0.274
Beta
0.423
Annual Standard Deviation
0.208
Annual Variance
0.043
Information Ratio
-0.262
Tracking Error
0.261
Treynor Ratio
-1.006
Total Fees
$27.00
from sklearn import linear_model
import numpy as np
import pandas as pd
from scipy import stats
from math import floor
from datetime import timedelta
from datetime import time

class PairsTradingAlgo(QCAlgorithm):
    
    def Initialize(self):
        
        self.SetStartDate(2020,6,1)
        self.SetEndDate(self.StartDate + timedelta(10))
        self.SetCash(10000)
        # self.Settings.FreePortfolioValuePercentage = 0.05
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)
        
        tickers = ['XOM', 'CVX', 'SPY']
        
        for t in tickers:
            self.AddEquity(t, Resolution.Hour).Symbol
            
        self.SetBenchmark('SPY')
        
        self.SetAlpha(PairsAlpha())
        self.SetExecution(ImmediateExecutionModel())
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        self.SetRiskManagement(TrailingStop())

class PairsAlpha(AlphaModel):
    
    def __init__(self):
        self.window_length = 13
        self.threshold = 1.5
        self.symbol_data = {}

    def Update(self, algo, data):
        insights = []
        
        for security in data.Keys:
            symbol = security.Value
            if symbol == 'SPY': continue
            if symbol not in self.symbol_data.keys():
                self.symbol_data[symbol] = RollingWindow[TradeBar](self.window_length)
            if data.Bars.ContainsKey(symbol):
                self.symbol_data[symbol].Add(data[security])
        
        symbols = [x for x in self.symbol_data.keys()]
        
        if not (self.symbol_data[symbols[0]].IsReady and self.symbol_data[symbols[1]].IsReady):
            return insights
        
        price_x = pd.Series([float(i.Close) for i in self.symbol_data[symbols[0]]], 
                             index = [i.Time for i in self.symbol_data[symbols[0]]])
                             
        price_y = pd.Series([float(i.Close) for i in self.symbol_data[symbols[1]]], 
                             index = [i.Time for i in self.symbol_data[symbols[1]]])

        spread_regr = self.regr(np.log(price_x), np.log(price_y))
        self.spread_mean = np.mean(spread_regr)
        self.spread_std = np.std(spread_regr)
        
        algo.Log('%s: %s' % (str(symbols[0]), str(algo.Portfolio[symbols[0]].Invested)))
        algo.Log('%s: %s' % (str(symbols[1]), str(algo.Portfolio[symbols[1]].Invested)))
        if not (algo.Portfolio[symbols[0]].Invested or algo.Portfolio[symbols[1]].Invested):
            if spread_regr[-1] > self.spread_mean + self.threshold * self.spread_std:
                insights.append(Insight.Price(symbols[0], timedelta(days=30), InsightDirection.Up))
                insights.append(Insight.Price(symbols[1], timedelta(days=30), InsightDirection.Down))
                algo.Log('insights emitted')
        
            elif spread_regr[-1] < self.spread_mean - self.threshold * self.spread_std:
                insights.append(Insight.Price(symbols[1], timedelta(days=30), InsightDirection.Up))
                insights.append(Insight.Price(symbols[0], timedelta(days=30), InsightDirection.Down))
                algo.Log('insights emitted')
                
        return insights
    
    def regr(self, x, y):
        regr = linear_model.LinearRegression()
        x_constant = np.column_stack([np.ones(len(x)), x])
        regr.fit(x_constant, y)
        beta = regr.coef_[0]
        alpha = regr.intercept_
        spread = y - x*beta - alpha
        return spread
        
class TrailingStop(RiskManagementModel):
    def __init__(self, drawdown_threshold = 0.01):
        self.drawdown_threshold = -abs(drawdown_threshold)
        self.max_profits = {}
        
    def ManageRisk(self, algo, targets):
        targets = []
        
        for kvp in algo.Securities:
            security = kvp.Value
            symbol = security.Symbol.Value
            current_profit = security.Holdings.UnrealizedProfitPercent
            
            if not security.Invested:
                continue
            
            if symbol not in self.max_profits.keys():
                self.max_profits[symbol] = current_profit
            self.max_profits[symbol] = np.maximum(self.max_profits[symbol], current_profit)
            
            drawdown = current_profit - self.max_profits[symbol]
            # algo.Log('%s - Current profit: %.3f, Max profit: %.3f, Current drawdown: %.3f' % (str(symbol), current_profit, self.max_profits[symbol], drawdown))
            if drawdown < self.drawdown_threshold:
                targets.append(PortfolioTarget(symbol, 0))
                algo.Log('%s position to be liquidated' % str(symbol))
                
                # Reset dictionary
                del self.max_profits[symbol]
        
        return targets