Overall Statistics
Total Orders
171
Average Win
3.59%
Average Loss
-4.85%
Compounding Annual Return
-5.991%
Drawdown
20.900%
Expectancy
-0.039
Start Equity
50000
End Equity
46312.46
Net Profit
-7.375%
Sharpe Ratio
-0.363
Sortino Ratio
-0.324
Probabilistic Sharpe Ratio
6.921%
Loss Rate
45%
Win Rate
55%
Profit-Loss Ratio
0.74
Alpha
-0.152
Beta
0.498
Annual Standard Deviation
0.204
Annual Variance
0.042
Information Ratio
-1.13
Tracking Error
0.205
Treynor Ratio
-0.149
Total Fees
$424.63
Estimated Strategy Capacity
$260000.00
Lowest Capacity Asset
FBK WDY7XKOFNSRP
Portfolio Turnover
11.57%
#region imports
from AlgorithmImports import *
#endregion
# https://www.quantconnect.com/tutorials/strategy-library/intraday-dynamic-pairs-trading-using-correlation-and-cointegration-approach

import numpy as np
import pandas as pd
import datetime as datetime
import statsmodels.formula.api as sm
#from pandas.core import datetools
import statsmodels.tsa.stattools as ts
from pair import *

class PairsTrading(QCAlgorithm):

    def __init__(self):
        self.symbols = ['ASB', 'AF', 'BANC', 'BBVA', 'BBD', 'BCH', 'BLX', 'BSBR', 'BSAC', 'SAN',
        'CIB', 'BXS', 'BAC', 'BOH', 'BMO', 'NTB', 'BK', 'BNS', 'BKU', 'BCS', 'BBT'
        ,
        'BFR', 'CM', 'COF', 'C', 'CFG', 'CMA', 'CBU', 'CPF', 'BAP', 'CFR', 'CUBI',
        'DKT', 'DB', 'EVER', 'FNB', 'FBK', 'FCB', 'FBP', 'FCF', 'FHN', 'FBC', 'FSB',
        'GWB', 'AVAL', 'BSMX', 'SUPV', 'HDB', 'HTH', 'HSBC', 'IBN', 'ING', 'ITUB', 'JPM',
        'KB', 'KEY', 'LYG', 'MTB', 'BMA', 'MFCB', 'MSL', 'MTU', 'MFG', 'NBHC', 'OFG', 'PNC',
        'PVTD', 'PB', 'PFS', 'RF', 'RY', 'RBS', 'SHG', 'STT', 'STL', 'SCNB', 'SMFG', 'STI',
        'SNV', 'TCB', 'TD', 'USB', 'UBS', 'VLY', 'WFC', 'WAL', 'WBK', 'WF', 'YDKN'
        ]
        
        self.data_resolution = 10
        self.num_bar = 6.5*60*60/(self.data_resolution)
        self.one_month = 6.5*20*60/(self.data_resolution)
        self.selected_num = 10
        self.pair_num = 120
        self.pair_threshod = 0.88
        self.BIC = -3.34
        self.count = 0
        self.pair_list = []
        self.selected_pair = []
        self.trading_pairs = []
        self.generate_count = 0
        self.open_size = 2.32
        self.close_size = 0.5
        self.stop_loss = 6
        self.data_count = 0

    def Initialize(self):
        self.SetStartDate(2023,1,1)
        self.SetEndDate(2024,3,31)
        self.SetCash(50000)

        for i in range(len(self.symbols)):
            equity = self.AddEquity(self.symbols[i],Resolution.Minute).Symbol
            self.symbols[i] = equity
            self.symbols[i].prices = []
            self.symbols[i].dates = []


    def generate_pairs(self):
        for i in range(len(self.symbols)):
            for j in range(i+1,len(self.symbols)):
                self.pair_list.append(pairs(self.symbols[i],self.symbols[j]))

        self.pair_list = [x for x in self.pair_list if x.cor > self.pair_threshod]
        
        self.pair_list.sort(key = lambda x: x.cor, reverse = True)

        if len(self.pair_list) > self.pair_num:
            self.pair_list = self.pair_list[:self.pair_num]


    def pair_clean(self,list):
        l = []
        l.append(list[0])
        for i in list:
            symbols = [x.a for x in l] + [x.b for x in l]
            if i.a not in symbols and i.b not in symbols:
                l.append(i)
            else:
                pass
        return l

    def OnData(self,data):
        if not self.Securities[self.symbols[0]].Exchange.ExchangeOpen:
            return
        #data aggregation
        if self.data_count < self.data_resolution:
            self.data_count +=1
            return
        # refill the initial df
        if len(self.symbols[0].prices) < self.num_bar:
            for i in self.symbols:
                if data.ContainsKey(i) is True:
                    i.prices.append(float(data[i].Close))
                    i.dates.append(data[i].EndTime)
                else:
                    self.Log('%s is missing'%str(i))
                    self.symbols.remove(i)
            self.data_count = 0
            return
        # generate paris
        if self.count == 0 and len(self.symbols[0].prices) == self.num_bar:
            if self.generate_count == 0:
                for i in self.symbols:
                    i.df = pd.DataFrame(i.prices, index = i.dates, columns = ['%s'%str(i)])

                self.generate_pairs()
                self.generate_count +=1


            self.Log('pair list length:'+str(len(self.pair_list)))
        # correlation selection
            for i in self.pair_list:
                i.cor_update()
        # updatet the dataframe and correlation selection
            if len(self.pair_list[0].a_price) != 0:
                for i in self.pair_list:
                    i.df_update()
                    i.cor_update()

            self.selected_pair = [x for x in self.pair_list if x.cor > 0.9]
        # cointegration selection
            for i in self.selected_pair:
                i.cointegration_test()

            self.selected_pair = [x for x in self.selected_pair if x.adf < self.BIC]
            self.selected_pair.sort(key = lambda x: x.adf)

            if len(self.selected_pair) == 0:
                self.Log('no selected pair')
                self.count += 1
                return

            self.selected_pair = self.pair_clean(self.selected_pair)
            for i in self.selected_pair:
                i.touch = 0
                self.Log(str(i.adf) + i.name)

            if len(self.selected_pair) > self.selected_num:
                self.selected_pair = self.selected_pair[:self.selected_num]

            self.count +=1
            self.data_count = 0
            return

        #update the pairs
        if self.count != 0 and self.count < self.one_month:

            num_select = len(self.selected_pair)

            for i in self.pair_list:
                if data.ContainsKey(i.a) is True and data.ContainsKey(i.b) is True:
                    i.price_record(data[i.a],data[i.b])
                else:
                    self.Log('%s has no data'%str(i.name))
                    self.pair_list.remove(i)
        ## selected pairs

            for i in self.selected_pair:
                i.last_error = i.error

            for i in self.trading_pairs:
                i.last_error = i.error

        ## enter
            for i in self.selected_pair:
                price_a = float(data[i.a].Close)
                price_b = float(data[i.b].Close)
                i.error = price_a - (i.model.params[0] + i.model.params[1]*price_b)
                if (self.Portfolio[i.a].Quantity == 0 and self.Portfolio[i.b].Quantity == 0) and i not in self.trading_pairs:
                    if i.touch == 0:
                        if i.error < i.mean_error - self.open_size*i.sd and i.last_error > i.mean_error - self.open_size*i.sd:
                            i.touch += -1
                        elif i.error > i.mean_error + self.open_size*i.sd and i.last_error < i.mean_error + self.open_size*i.sd:
                            i.touch += 1
                        else:
                            pass
                    elif i.touch == -1:
                        if i.error > i.mean_error - self.open_size*i.sd and i.last_error < i.mean_error - self.open_size*i.sd:
                            self.Log('long %s and short %s'%(str(i.a),str(i.b)))
                            i.record_model = i.model
                            i.record_mean_error = i.mean_error
                            i.record_sd = i.sd
                            self.trading_pairs.append(i)
                            self.SetHoldings(i.a, 5.0/(len(self.selected_pair)))
                            self.SetHoldings(i.b, -5.0/(len(self.selected_pair)))
                            i.touch = 0
                    elif i.touch == 1:
                        if i.error < i.mean_error + self.open_size*i.sd and i.last_error > i.mean_error + self.open_size*i.sd:
                            self.Log('long %s and short %s'%(str(i.b),str(i.a)))
                            i.record_model = i.model
                            i.record_mean_error = i.mean_error
                            i.record_sd = i.sd
                            self.trading_pairs.append(i)
                            self.SetHoldings(i.b, 5.0/(len(self.selected_pair)))
                            self.SetHoldings(i.a, -5.0/(len(self.selected_pair)))
                            i.touch = 0
                    else:
                        pass
                else:
                    pass

            # close
            for i in self.trading_pairs:
                if data.ContainsKey(i.a) and data.ContainsKey(i.b):
                    price_a = float(data[i.a].Close)
                    price_b = float(data[i.b].Close)
                    i.error = price_a - (i.record_model.params[0] + i.record_model.params[1]*price_b)
                    if ((i.error < i.record_mean_error + self.close_size*i.record_sd and i.last_error >i.record_mean_error + self.close_size*i.record_sd)
                       or (i.error > i.record_mean_error - self.close_size*i.record_sd and i.last_error <i.record_mean_error - self.close_size*i.record_sd)):
                        self.Log('close %s'%str(i.name))
                        self.Liquidate(i.a)
                        self.Liquidate(i.b)
                        self.trading_pairs.remove(i)
                    elif i.error < i.record_mean_error - self.stop_loss*i.record_sd or i.error > i.record_mean_error + self.stop_loss*i.record_sd:
                        self.Log('close %s to stop loss'%str(i.name))
                        self.Liquidate(i.a)
                        self.Liquidate(i.b)
                        self.trading_pairs.remove(i)
                    else:
                        pass
    



            self.count +=1
            self.data_count = 0
            return

        if self.count == self.one_month:
            self.count = 0
            self.data_count = 0
            return
#region imports
from AlgorithmImports import *
#endregion
import numpy as np
import pandas as pd
import datetime as datetime
import statsmodels.formula.api as sm
import statsmodels.tsa.stattools as ts

class pairs(object):

    def __init__(self,a,b):
        self.a = a
        self.b = b
        self.name = str(a) + ':' + str(b)
        self.df = pd.concat([a.df,b.df],axis = 1).dropna()
        self.num_bar = self.df.shape[0]
        self.cor = self.df.corr().iloc[0][1]
        self.error = 0
        self.last_error = 0
        self.a_price = []
        self.a_date = []
        self.b_price = []
        self.b_date = []

    def cor_update(self):
        self.cor = self.df.corr().iloc[0][1]

    def cointegration_test(self):
        self.model = sm.ols(formula = '%s ~ %s'%(str(self.a),str(self.b)), data = self.df).fit()
        self.adf = ts.adfuller(self.model.resid,autolag = 'BIC')[0]
        self.mean_error = np.mean(self.model.resid)
        self.sd = np.std(self.model.resid)

    def price_record(self,data_a,data_b):
        self.a_price.append(float(data_a.Close))
        self.a_date.append(data_a.EndTime)
        self.b_price.append(float(data_b.Close))
        self.b_date.append(data_b.EndTime)

    def df_update(self):
        new_df = pd.DataFrame({str(self.a):self.a_price,str(self.b):self.b_price},index = [self.a_date]).dropna()
        self.df = pd.concat([self.df,new_df])
        self.df = self.df.tail(self.num_bar)
        for i in [self.a_price,self.a_date,self.b_price,self.b_date]:
            i = []