Overall Statistics |
Total Trades 272 Average Win 0.46% Average Loss -0.46% Compounding Annual Return 0.698% Drawdown 6.500% Expectancy 0.036 Net Profit 1.936% Sharpe Ratio 0.222 Probabilistic Sharpe Ratio 5.539% Loss Rate 49% Win Rate 51% Profit-Loss Ratio 1.01 Alpha 0 Beta 0 Annual Standard Deviation 0.024 Annual Variance 0.001 Information Ratio 0.222 Tracking Error 0.024 Treynor Ratio 0 Total Fees $11782.80 Estimated Strategy Capacity $0 Lowest Capacity Asset EVT SSC0EI5J2F6T |
from datetime import timedelta from typing import NamedTuple import itertools from numpy import mean from AlgorithmImports import * from collections import namedtuple CcgTreshold = namedtuple('CcgTreshold', ['long_entry','long_exit','short_entry','short_exit']) class CcgPair: """ All the config for trading a single pair. """ def __init__(self, tickers, tresholds, lookback_period) -> None: self.tickers = tickers self.tresholds = tresholds self.lookback_period = lookback_period self.pair_identifier = tickers[0] + '-' + tickers[1] def other_ticker(self, ticker): for tick in self.tickers: if tick != ticker: return tick class CcgPairsTradingConfig: pairs = [] @classmethod def tickers(cls): return list(set(itertools.chain(*[p.tickers for p in cls.pairs]))) @classmethod def initalize(cls, pairs): cls.pairs = pairs class PairsData: def __init__(self, pair, algo) -> None: self.pair = pair self.ratios = RollingWindow[float](2) self.ups = RollingWindow[float](self.pair.lookback_period) self.downs = RollingWindow[float](self.pair.lookback_period) self.algo = algo self.pair_pnl = 0 self.prev_upa = None self.prev_dna = None self.p1 = None self.p2 = None def up(self): """Note: RollingWindow is always reversed.""" return max(self.ratios[0] - self.ratios[1], 0) if self.ratios.IsReady else 0 def down(self): """Note: RollingWindow is always reversed.""" return max(self.ratios[1] - self.ratios[0], 0) if self.ratios.IsReady else 0 def update(self, ratio, p1=0, p2=0): self.ratios.Add(ratio) self.ups.Add(self.up()) self.downs.Add(self.down()) if self.ups.IsReady and self.prev_upa is None: self.prev_upa = mean([float(r) for r in self.ups]) elif self.prev_upa is not None: self.prev_upa = (self.ups[0] + 2 * self.prev_upa) / 3 if self.downs.IsReady and self.prev_dna is None: self.prev_dna = mean([float(r) for r in self.downs]) elif self.prev_dna is not None: self.prev_dna = (self.downs[0] + 2 * self.prev_dna) / 3 self.p1 = p1 self.p2 = p2 def upa(self): return 0 if self.prev_upa is None else self.prev_upa def dna(self): return 0 if self.prev_upa is None else self.prev_dna def rs(self): return self.upa()/self.dna() if self.upa() and self.dna() else 0 def rsi(self): return 100 - 100/(1 + self.rs()) def long_entry_signal(self): return 1 if self.rsi() < self.pair.tresholds.long_entry else 0 def long_exit_signal(self): return 1 if self.rsi() > self.pair.tresholds.long_exit else 0 def short_entry_signal(self): return 1 if self.rsi() > self.pair.tresholds.short_entry else 0 def short_exit_signal(self): return 1 if self.rsi() < self.pair.tresholds.short_exit else 0 def get_insights(self): insights = [] # Entries if self.long_entry_signal(): insights.append(Insight.Price(self.pair.tickers[0], timedelta(days = 1), InsightDirection.Up)) second_leg_insight = Insight.Price(self.pair.tickers[1], timedelta(days = 1), InsightDirection.Down) second_leg_insight.Weight = self.ratios[0] insights.append(second_leg_insight) if self.short_entry_signal(): insights.append(Insight.Price(self.pair.tickers[0], timedelta(days = 1), InsightDirection.Down)) second_leg_insight = Insight.Price(self.pair.tickers[1], timedelta(days = 1), InsightDirection.Up) second_leg_insight.Weight = self.ratios[0] insights.append(second_leg_insight) # Exits holding = self.algo.Portfolio.get(self.pair.tickers[0]) holding_is_long = holding and holding.Invested and holding.IsLong holding_is_short = holding and holding.Invested and holding.IsShort if holding_is_long and self.long_exit_signal(): insights.append(Insight.Price(self.pair.tickers[0], timedelta(days = 1), InsightDirection.Down)) insights.append(Insight.Price(self.pair.tickers[1], timedelta(days = 1), InsightDirection.Up)) if holding_is_short and self.short_exit_signal(): insights.append(Insight.Price(self.pair.tickers[0], timedelta(days = 1), InsightDirection.Up)) insights.append(Insight.Price(self.pair.tickers[1], timedelta(days = 1), InsightDirection.Down)) log_string = ( f'{self.p1:.5f} ' f'{self.p2:.5f} ' f'{self.ratios[0]:.5f} ' f'{self.ups[0]:.5f} ' f'{self.downs[0]:.5f} ' f'{self.upa():.5f} ' f'{self.dna():.5f} ' f'{self.rs():.5f} ' f'{self.rsi():.5f} ' f'{self.long_entry_signal()} ' f'{self.long_exit_signal()} ' f'{self.short_entry_signal()} ' f'{self.short_exit_signal()} ' ) self.algo.Debug(log_string) return insights class PairsTradingAlpha(AlphaModel): def __init__(self, algorithm) -> None: self.pairs_data = {} self.ticker_data = {} self.algo = algorithm def Update(self, algorithm, data): insights = [] if not self.algo.can_trade: return [] for pair in self.algo.pairs_config.pairs: close_first = algorithm.Securities[pair.tickers[0]].Price close_second = algorithm.Securities[pair.tickers[1]].Price ratio = close_first/close_second pair_data = self.pairs_data[pair.tickers[0]] pair_data.update(ratio, close_first, close_second) insights.extend(pair_data.get_insights()) return insights def OnSecuritiesChanged(self, algorithm, changes) -> None: for added in changes.AddedSecurities: ticker = added.Symbol ticker_pair_data = self.pairs_data.get(ticker) if ticker_pair_data is None: for pair in self.algo.pairs_config.pairs: if ticker in pair.tickers and self.pairs_data.get(pair.other_ticker(ticker)) is None: pair_data = PairsData(pair, self.algo) self.pairs_data[ticker] = pair_data self.pairs_data[pair.other_ticker(ticker)] = pair_data return super().OnSecuritiesChanged(algorithm, changes) class PairsTradingPortfolioConstructionModel(PortfolioConstructionModel): def __init__(self, algo): self.algo = algo self.portfolioUsage = 0.5 self.weight = round(1/len(self.algo.pairs_config.pairs), 2) * self.portfolioUsage super().__init__() def CreateTargets(self, algorithm, insights): targets = {} for insight in insights: holding = algorithm.Portfolio[insight.Symbol] if holding.Invested: if ( holding.IsShort or insight.Direction != InsightDirection.Up ) and ( holding.IsLong or insight.Direction != InsightDirection.Down ): targets[str(insight.Symbol)] = PortfolioTarget(insight.Symbol, -holding.Quantity) continue targets[str(insight.Symbol)] = PortfolioTarget.Percent(algorithm, insight.Symbol, insight.Direction * self.weight * (insight.Weight or 1)) return list(targets.values()) class PairsTradingExecution(ExecutionModel): # Fill the supplied portfolio targets efficiently def Execute(self, algorithm, targets): for target in targets: algorithm.MarketOnCloseOrder(target.Symbol, target.Quantity) class PairsTrading(QCAlgorithm): def Initialize(self): self.SetStartDate(2004, 1, 29) # Set Start Date self.SetEndDate(2006, 10, 30) # Set End Date self.SetCash(100000) # Set Strategy Cash self.run_hour = 15 self.run_minute = 42 self.can_trade = False pairs = [ #pairs #treshold of entry/exit # lookback_period [('ETG', 'EVT'), CcgTreshold(long_entry=20, long_exit=30, short_entry=80, short_exit=70), 3], ] ccg_pairs = [] for pair_init in pairs: pair = CcgPair(*pair_init) ccg_pairs.append(pair) CcgPairsTradingConfig.initalize(ccg_pairs) self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin) # Universe self.UniverseSettings.Resolution = Resolution.Minute symbols = [] for ticker in CcgPairsTradingConfig.tickers(): symbol = Symbol.Create(ticker, SecurityType.Equity, Market.USA) symbols.append(symbol) self.AddUniverseSelection(ManualUniverseSelectionModel(symbols)) self.pairs_config = CcgPairsTradingConfig # Alphas self.AddAlpha(PairsTradingAlpha(self)) # Portfolio Construction self.SetPortfolioConstruction( PairsTradingPortfolioConstructionModel(self)) # Order Execution self.SetExecution( PairsTradingExecution() ) def OnData(self, data): """OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here. Arguments: data: Slice object keyed by symbol containing the stock data """ self.can_trade = (self.Time.hour == self.run_hour and self.Time.minute == self.run_minute)