Overall Statistics |
Total Trades 162246 Average Win 0.01% Average Loss -0.01% Compounding Annual Return -39.211% Drawdown 77.500% Expectancy -0.220 Net Profit -77.465% Sharpe Ratio -7.849 Probabilistic Sharpe Ratio 0% Loss Rate 57% Win Rate 43% Profit-Loss Ratio 0.83 Alpha -0.419 Beta 0.159 Annual Standard Deviation 0.05 Annual Variance 0.002 Information Ratio -2.871 Tracking Error 0.195 Treynor Ratio -2.469 Total Fees $217289.23 Estimated Strategy Capacity $84000000.00 Lowest Capacity Asset POOL R735QTJ8XC9X |
from System import * from QuantConnect import * from QuantConnect import Resolution from QuantConnect.Algorithm import QCAlgorithm from QuantConnect.Data.Consolidators import TradeBarConsolidator from QuantConnect.Data.Market import TradeBar from QuantConnect.Orders import OrderStatus from QuantConnect.Orders.Fees import AlphaStreamsFeeModel from QuantConnect.Algorithm import * from QuantConnect.Indicators import * from QuantConnect.Securities import Security, SecurityTransactionManager from QuantConnect.Orders import * from QuantConnect.Orders import OrderEvent from QuantConnect.Orders.Fills import FillModel from QuantConnect.Algorithm.Framework import * from QuantConnect.Algorithm.Framework.Risk import * from QuantConnect.Algorithm.Framework.Alphas import * from QuantConnect.Algorithm.Framework.Alphas import Insight from QuantConnect.Algorithm.Framework.Execution import * from QuantConnect.Algorithm.Framework.Portfolio import * from QuantConnect.Algorithm.Framework.Selection import * from datetime import datetime, timedelta import json from triple_barrier import TripleBarrier from pm_config import PM_Config from pattern_match_client_factory import PatternMatchClientFactory TICKERS = '''DISCK EA FB TWTR RCL WYNN MCD LB RL PVH AMZN TSCO POOL MGM MHK PENN LKQ CPB SJM CLX TAP MNST TSN EOG SLB VLO PXD GS MTB CBOE L PFG TROW CMA NDAQ UNM C HIG ICE VRTX PRGO TFX PKI PFE VAR WST DHR ANTM ABT TDOC BAX ZBH MCK CHRW LUV NDSN SWK FLS ROP CMI NSC XYL ALK TXT WM NOC SNPS PEGA FTNT MSI XLNX ADP GLW TER TXN SHOP HPE MPWR AMD PTC XRX HUBS FLT TEAM CF SEE SHW NUE MLM UDR CCI DRE EQIX VNO SO CMS D DTE SRE TTWO'''.split() class OAMPatternMatchingAlgorithm(QCAlgorithm): VERSION = '3.0.3' def Initialize(self: QCAlgorithm): qcAlg: QCAlgorithm = self tickerFileContents = self.Download(PM_Config.TICKER_CSV_URL) self.Log(tickerFileContents) self.tickers = TICKERS # tickerFileContents.split()[0] self.Log(self.tickers) self.SetStartDate(2018,1,1) #Set Start Date self.SetEndDate(2020,12,28) #Set End Date self.SetCash(2000 * 1000) # Set Strategy Cash self.PROFIT_PCT = 0.01 self.LOSS_PCT = 0.01 self.AddEquity("SPY", Resolution.Minute) self.SetBenchmark("SPY") ## Set Universe Selection universeSymbols = [Symbol.Create(t, SecurityType.Equity, Market.USA) for t in self.tickers] self.UniverseSettings.Resolution = Resolution.Minute self.SetUniverseSelection(ManualUniverseSelectionModel(universeSymbols)) self.pmc = PatternMatchClientFactory.create(qcAlg=self) for symbol in universeSymbols: equity = qcAlg.AddEquity(symbol, Resolution.Minute) ## make 15 min bars from minute bars tbc = TradeBarConsolidator(timedelta(minutes=15)) tbc.DataConsolidated += self.FifteenMinConsolidator self.SubscriptionManager.AddConsolidator(symbol, tbc) ## hold all pertinent info for each symbol NUM_PERIODS_FOR_VWAP = 10 TB = TripleBarrier(qcAlg, symbol, 0, qcAlg.PROFIT_PCT, qcAlg.LOSS_PCT) TB.vwap = self.VWAP(symbol, NUM_PERIODS_FOR_VWAP) TB.holdingPeriods = 0 TripleBarrier.SYMBOL_DICTIONARY[symbol] = TB ## Does our broker have any exisiting state (order, positions) future = self.Time + timedelta(minutes=2) dt = self.DateRules.On(future) tm = self.TimeRules.At(future.hour, future.minute) self.Schedule.On(dt, tm, self.BrokerStateInit) self.Debug("[Initialize] Initialize Done()") self.Debug("[Initialize] OAMPatternMatchingAlgorithm VERSION: {}".format(OAMPatternMatchingAlgorithm.VERSION)) self.Debug("[Initialize] Triple Barrier VERSION: {}".format(TripleBarrier.VERSION)) self.Debug("[Initialize] PM Server: {}".format(PM_Config.PM_SERVER_URL)) self.Debug("[Initialize] Ticker Urls {}".format(PM_Config.TICKER_CSV_URL)) self.Debug("[Initialize] Tickers: {}".format(self.tickers)) def BrokerStateInit(self): TripleBarrier.initFromBrokerState(self, self.PROFIT_PCT, self.LOSS_PCT) def FifteenMinConsolidator(self: QCAlgorithm, sender, bar: TradeBar): self.Debug("Consolidated bar: [{} <--> {}], bar = {}".format(bar.Time, bar.EndTime, bar)) if not bar.Symbol in TripleBarrier.SYMBOL_DICTIONARY: self.Log("15-MIN DATA BUT NO TB -- WAS ALGORITHM REDEPLOYED W/ EXISTING HOLDINGS IN PORTFOLIO") return security: Security = self.Securities[bar.Symbol] TB: TripleBarrier = TripleBarrier.SYMBOL_DICTIONARY[bar.Symbol] headers = { "TICKER" : str(bar.Symbol), "OPEN" : str(round(bar.Open, 2)), "HIGH" : str(round(bar.High,2)), "LOW" : str(round(bar.Low,2)), "CLOSE" : str(round(bar.Close,2)), "VOLUME" : str(round(bar.Volume,2)), "VOLWAVG" : str(round(TB.vwap.Current.Value,2)), "ET" : bar.Time.strftime("%Y-%m-%dT%H:%M:%S"), "ETE" : bar.EndTime.strftime("%Y-%m-%d %H:%M:%S") } insight: Insight = self.pmc.getInsight(headers, self) self.Log("{} Insight for {} is {}".format(bar.Time, bar.Symbol, insight.Direction)) # Are we last bar before market close? If so, see you in the morning # 3:30 ............... 3:45 ............... 4:00pm # |-------- BAR --------||-------- BAR -------| # mktClose: datetime = security.Exchange.Hours.GetNextMarketClose(localDateTime=bar.Time, extendedMarket=False) timeDeltaUntilClose = mktClose - self.Time if timeDeltaUntilClose <= timedelta(minutes=10): self.Debug("Market is closed for {}. Close is {}. It is now {}".format(security, mktClose, self.Time)) return if security.Invested: TB.holdingPeriods += 1 self.Log("15-MIN HOLDING PERIODS FOR {} = {}".format(bar.Symbol, TB.holdingPeriods)) if TB.holdingPeriods >= 10: TB.placeTimeBarrierExitOrder() TB.holdingPeriods = 0 else: if insight.Direction == InsightDirection.Up: weight = round(1.0 / len(self.tickers), 3) quantity = self.CalculateOrderQuantity(str(bar.Symbol), weight) TB.placeEntryOrder(quantity) def OnOrderEvent(self: QCAlgorithm, orderEvent: OrderEvent): orderId = orderEvent.OrderId order: Order = self.Transactions.GetOrderById(orderId) self.Debug("OnOrderEvent: {}, alg time: {}, brokerId: {}".format(order, self.Time, order.BrokerId)) if not orderEvent.Symbol in TripleBarrier.SYMBOL_DICTIONARY: self.Log("ORDER EVENT BUT NO TB -- WAS ALGORITHM REDEPLOYED W/ EXISTING HOLDINGS IN PORTFOLIO: {}".format(order)) return TB: TripleBarrier = TripleBarrier.SYMBOL_DICTIONARY[orderEvent.Symbol] TB.update(order, orderEvent)
from System import * from QuantConnect import * from QuantConnect import Resolution from QuantConnect.Algorithm import QCAlgorithm from QuantConnect.Algorithm.Framework.Alphas import InsightDirection from QuantConnect.Algorithm.Framework.Alphas import Insight from abc import ABC, abstractmethod class PatternMatchClient: def __init__(self, qcAlg: QCAlgorithm) -> None: self.qcAlg = qcAlg ## This is the primary abstract method implemented by derived classes @abstractmethod def getInsight(self, headers, qcAlgorithm: QCAlgorithm) -> Insight: pass
from System import * from QuantConnect import * from QuantConnect import Resolution from QuantConnect.Algorithm import QCAlgorithm from QuantConnect.Algorithm.Framework.Alphas import InsightDirection from QuantConnect.Algorithm.Framework.Alphas import Insight from datetime import timedelta from pm_config import PM_Config from pattern_match_client import PatternMatchClient import pandas as pd import numpy as np from io import StringIO ## Entry File example naming: ## - AMZN_combined_trade_entries_2018_2019_2020.csv ## Data format ## - '2019-01-07 10:15:00' class PatternMatchClientEntryFile(PatternMatchClient): VERSION = '0.0.1' BASE_ENTRY_FILE_URL = "https://oamdatastorageacct.blob.core.windows.net/pm-entry/combined_entries" ENTRY_LOOKUP_TABLE = {} def __init__(self, qcAlg: QCAlgorithm) -> None: super().__init__(qcAlg) for t in qcAlg.tickers: filename = "{}_combined_trade_entries_2018_2019_2020.csv".format(t) #url = "{}/{}".format(PatternMatchClientEntryFile.BASE_ENTRY_FILE_URL, filename) #csv_str = qcAlg.Download(url) #sio = StringIO(csv_str) #qcAlg.ObjectStore.Save(filename, csv_str) csv_str = qcAlg.ObjectStore.Read(filename) sio = StringIO(csv_str) df = pd.read_csv(sio) df.set_index('trade_entry_index', inplace=True) PatternMatchClientEntryFile.ENTRY_LOOKUP_TABLE[t] = df qcAlg.Log("READ ENTRY FILE: {}".format(t)) def getInsight(self, headers, qcAlgorithm: QCAlgorithm): try: t = headers["TICKER"] dt = headers["ETE"] df = PatternMatchClientEntryFile.ENTRY_LOOKUP_TABLE[t] bullish_consensus = df.loc[dt]['bull_gt033'] qcAlgorithm.Log("CONSENSUS: {}".format(bullish_consensus)) qcAlgorithm.Log("CONSENSUS TYPE: {}".format(type(bullish_consensus))) insightDirection = InsightDirection.Flat if not type(bullish_consensus) == np.bool_: qcAlgorithm.Error("CONSENSUS iS NOT A BOOL") exit(-1) else: if bullish_consensus == True: insightDirection = InsightDirection.Up return Insight.Price(headers["TICKER"], timedelta(minutes=15), insightDirection) except: return Insight.Price("DUMMY", timedelta(minutes=15), InsightDirection.Flat)
from QuantConnect.Algorithm import QCAlgorithm from pattern_match_client import PatternMatchClient from pattern_match_client_pm_server import PatternMatchClientPmServer from pattern_match_client_entry_file import PatternMatchClientEntryFile class PatternMatchClientFactory: USE_ENTRY_FILES = True ## The Factory Create Method def create(qcAlg: QCAlgorithm) -> PatternMatchClient: ## Entry File Usage (for backtesting) if PatternMatchClientFactory.USE_ENTRY_FILES: return PatternMatchClientEntryFile(qcAlg) ## PM Server Usage (for live and paper trading) return PatternMatchClientPmServer(qcAlg)
from System import * from QuantConnect import * from QuantConnect import Resolution from QuantConnect.Algorithm import QCAlgorithm from QuantConnect.Algorithm.Framework.Alphas import InsightDirection from QuantConnect.Algorithm.Framework.Alphas import Insight from datetime import timedelta from pm_config import PM_Config from pattern_match_client import PatternMatchClient import json class PatternMatchClientPmServer(PatternMatchClient): VERSION = '2.0.1' def __init__(self, qcAlg: QCAlgorithm) -> None: super().__init__(qcAlg) def create_url(self, headers: dict): ## qs ex: = ?TICKER=AAPL&OPEN=109.2&ET=2021-01-05T10:15:00&HIGH=109.7&LOW=108.2&CLOSE=109.23&VOLUME=20000&VOLWAVG=109.51' base = PM_Config.PM_SERVER_URL queryString = "TICKER={}".format(headers["TICKER"]) queryString += "&OPEN={}".format(headers["OPEN"]) queryString += "&HIGH={}".format(headers["HIGH"]) queryString += "&LOW={}".format(headers["LOW"]) queryString += "&CLOSE={}".format(headers["CLOSE"]) queryString += "&VOLUME={}".format(headers["VOLUME"]) queryString += "&VOLWAVG={}".format(headers["VOLWAVG"]) queryString += "&ET={}".format(headers["ET"]) url = "{}?{}".format(base, queryString) return url def getInsight(self, headers, qcAlgorithm: QCAlgorithm): url = self.create_url(headers) pattern_match_server_response = qcAlgorithm.Download(url) qcAlgorithm.Debug("{} :: Json response from PM Server: {} with url {}".format(qcAlgorithm.Time, pattern_match_server_response, url)) insightDirection = InsightDirection.Flat if len(pattern_match_server_response) > 0: decision = json.loads(pattern_match_server_response)["decision"] if decision == "BULLISH": insightDirection = InsightDirection.Up if decision == "NEUTRAL": insightDirection = InsightDirection.Flat if decision == "BEARISH": insightDirection = InsightDirection.Down return Insight.Price(headers["TICKER"], timedelta(minutes=15), insightDirection)
from QuantConnect import * from QuantConnect import Resolution from QuantConnect.Algorithm import QCAlgorithm from QuantConnect.Orders import OrderStatus from QuantConnect.Algorithm import * from QuantConnect.Securities import Security, SecurityTransactionManager from QuantConnect.Orders import * from QuantConnect.Orders import OrderEvent from QuantConnect.Orders.Fills import FillModel from datetime import datetime, timedelta class PM_Config(): ## Stock Universe DEV_TICKER_CSV_URL = "https://oamdatastorageacct.blob.core.windows.net/pattern-matching/tickers_100.csv" PROD_TICKER_CSV_URL = "https://oamdatastorageacct.blob.core.windows.net/pattern-matching/tickers_sp500.csv" ## Pattern Matching Server RANDOM_PM_SERVER_URL = 'https://oam-pattern-matching-server-webapp.azurewebsites.net/randomBullishBearishNeutral' DEV_PM_SERVER_URL = 'https://oam-pattern-matching-server-webapp-staging.azurewebsites.net/patternMatch' PROD_PM_SERVER_URL = 'https://oam-pattern-matching-server-webapp.azurewebsites.net/patternMatch' ## TODO: Change these when going live to PROD (Production) TICKER_CSV_URL = DEV_TICKER_CSV_URL PM_SERVER_URL = RANDOM_PM_SERVER_URL
from QuantConnect import * from QuantConnect import Resolution from QuantConnect.Algorithm import QCAlgorithm from QuantConnect.Orders import OrderStatus from QuantConnect.Algorithm import * from QuantConnect.Securities import Security, SecurityTransactionManager from QuantConnect.Orders import * from QuantConnect.Orders import OrderEvent from QuantConnect.Orders import Order from QuantConnect.Orders.Fills import FillModel from datetime import datetime, timedelta import math class TripleBarrier(): VERSION = '3.0.1' SYMBOL_DICTIONARY = {} def initFromBrokerState(qcAlg: QCAlgorithm, profitPct: float, stopLossPct: float, noEntryWithinTimeDeltaToClose=None): qcAlg.Log("[TB BROKER STATE INIT] SETTING UP TRIPLE BARRIER SYMBOL_DICTIONARY FROM BROKER STATE") for symbol, security in qcAlg.Securities.items(): if symbol not in TripleBarrier.SYMBOL_DICTIONARY: qcAlg.Log("[TB BROKER STATE INIT] ADDING TB FOR security: {0}".format(security)) holdingQuantity = qcAlg.Portfolio[symbol].Quantity TB = TripleBarrier(qcAlg, symbol, holdingQuantity, profitPct, stopLossPct, noEntryWithinTimeDeltaToClose) TB.enterTime = qcAlg.Time TripleBarrier.SYMBOL_DICTIONARY[symbol] = TB openOrders = qcAlg.Transactions.GetOpenOrders() for oo in openOrders: o: Order = oo symbol = o.Symbol if symbol not in TripleBarrier.SYMBOL_DICTIONARY: qcAlg.Log("[TB BROKER STATE INIT] OpenOrder, but no TB?? : {0}".format(symbol)) continue TB: TripleBarrier = TripleBarrier.SYMBOL_DICTIONARY[symbol] ticket: OrderTicket = qcAlg.Transactions.GetOrderTicket(oo.Id) if ticket.OrderType == OrderType.Limit: qcAlg.Log("[TB BROKER STATE INIT] FOUND TAKE PROFIT OpenOrderTicket : {0}".format(ticket)) TB.profitTicket = ticket if ticket.OrderType == OrderType.StopMarket: qcAlg.Log("[TB BROKER STATE INIT] FOUND STOP LOSS OpenOrderTicket : {0}".format(ticket)) TB.lossTicket = ticket def __init__(self, qcAlg: QCAlgorithm, tickerSymbol: Symbol, quantity: float, profitPct: float, stopLossPct: float, noEntryWithinTimeDeltaToClose=None) -> None: self.qcAlg: QCAlgorithm = qcAlg self.tickerSymbol = tickerSymbol self.entryPrice = 0.0 self.quantity = round(quantity, 2) self.shorting = self.quantity < 0 self.profictPct = profitPct self.profitTicket = None self.stopLossPct = stopLossPct self.lossTicket = None self.enterTime = datetime.min self.entryTicket = None self.noEntryWithinTimeDeltaToClose = noEntryWithinTimeDeltaToClose self.tag = "DOUBLE-BAR" if (self.stopLossPct == 0 or self.profictPct == 0) else "TRIPLE-BAR" self.tag += "-{}".format(TripleBarrier.VERSION) self.log("BARRIER INIT") def log(self, msg): self.qcAlg.Log("[{}] {}, {}".format(self.tag, msg, self)) def __str__(self) -> str: holdingQuantity = self.qcAlg.Portfolio[self.tickerSymbol].Quantity rval = "[" rval += "sym: {}, requested_quant: {}, current_quant: {}, ".format(self.tickerSymbol, self.quantity, holdingQuantity) rval += "fill_price: {} tp: {}, sl: {}, ".format(self.entryPrice, self.profictPct, self.stopLossPct) rval += "enterTime: {}".format(self.enterTime) rval += "]" return rval def placeEntryOrder(self, quantity): self.quantity = math.floor(quantity) security: Security = self.qcAlg.Securities[self.tickerSymbol] # is the mkt open IS_OPEN = security.Exchange.Hours.IsOpen(self.qcAlg.Time, extendedMarket=False) if not IS_OPEN: self.log("EXCHANGE NOT OPEN: NOT ENTERING POSITION WITH OPENING MKT ORDER") return ## Are we too close to close if self.noEntryWithinTimeDeltaToClose is not None: mktClose: datetime = security.Exchange.Hours.GetNextMarketClose(localDateTime=self.qcAlg.Time, extendedMarket=False) timeDeltaTilClosed = mktClose - self.qcAlg.Time if timeDeltaTilClosed <= self.noEntryWithinTimeDeltaToClose: self.log("NOT ENTERING POSITION THIS CLOSE TO MKT CLOSE: {}".format(timeDeltaTilClosed)) return self.enterTime = self.qcAlg.Time self.entryTicket = self.qcAlg.MarketOrder(self.tickerSymbol, self.quantity, asynchronous=True, tag="MKT ENTRY") self.log("PLACED ENTRY ORDER: note: FILL COULD TAKE AWHILE") def placeTimeBarrierExitOrder(self): self.log("CANCELING OPEN ORDERS AND PLACING TIME BARRIER MKT EXIT ORDER") self.qcAlg.Transactions.CancelOpenOrders(self.tickerSymbol, "TIME BARRIER CANCELING ALL OTHER ORDERS") self.qcAlg.Liquidate(self.tickerSymbol, tag="TIME BARRIER EXIT ORDER") def placeBarriers(self, price, quantity): if self.quantity != quantity: self.qcAlg.Error("QUANTITY MISMATCH ERROR") if self.quantity > 0: # LONG - the limit price needs to be above the average purchase price tp_price = round(price + (self.profictPct * price), 2) sl_price = round(price - (self.stopLossPct * price), 2) if self.quantity < 0: # SHORT - the limit price needs to be below the average purchase price tp_price = round(price - (self.profictPct * price), 2) sl_price = round(price + (self.stopLossPct * price), 2) # submit a take profit limit order - as other order (partial) fills, we need to adjust if self.profictPct != 0: self.profitTicket = self.qcAlg.LimitOrder(self.tickerSymbol, -self.quantity, tp_price, "TAKE PROFIT {}".format(self.tickerSymbol)) self.log("TAKE PROFIT ORDER (LMT) SET AT {}".format(tp_price)) # submit stop loss order - as other order (partial) fills, we need to adjust if self.stopLossPct != 0: self.lossTicket = self.qcAlg.StopMarketOrder(self.tickerSymbol, -self.quantity, sl_price, "STOP LOSS {}".format(self.tickerSymbol)) self.log("STOP LOSS ORDER (STP-MKT) SET AT {}".format(sl_price)) def update(self, order: Order, orderEvent: OrderEvent): if orderEvent.Status == OrderStatus.PartiallyFilled: self.updatePartiallyFilled(order, orderEvent) if orderEvent.Status == OrderStatus.Filled: self.updateFullyFilled(order, orderEvent) def updatePartiallyFilled(self, order: Order, orderEvent: OrderEvent): holdingQuantity = self.qcAlg.Portfolio[self.tickerSymbol].Quantity if order.Type == OrderType.Market: self.log("PARTIALLY FILLED ENTRY OR EXIT (TIME BARRIER) MKT TICKET") ## A mkt order is either the entry or the time-barrier exit - we don't care about partial fills return if self.lossTicket is None or self.profitTicket is None: self.log("NO SIBLING PRICE BARRIER ORDER - PARTIAL FILLS DO NOT REQUIRE UPDATES TO OTHER ORDERS") return orderResponse = None updateOrderFields = UpdateOrderFields() updateOrderFields.Quantity = -holdingQuantity if self.lossTicket.OrderId == order.Id and self.profitTicket is not None: self.log("PARTIALLY FILLED LOSS TICKET - UPDATE TAKE PROFIT TICKET {}") orderResponse = self.profitTicket.Update(updateOrderFields) if self.profitTicket.OrderId == order.Id and self.lossTicket is not None: self.log("PARTIALLY FILLED PROFIT TICKET - UPDATE STOP LOSS TICKET {}") orderResponse = self.lossTicket.Update(updateOrderFields) return orderResponse def updateFullyFilled(self, order: Order, orderEvent: OrderEvent): symbol = orderEvent.Symbol security = self.qcAlg.Securities[symbol] MKT_ENTRY_ORDER_FILLED = order.Type == OrderType.Market and security.Invested MKT_TIME_BARRIER_ORDER_FILLED = order.Type == OrderType.Market and not security.Invested PRICE_BARRIER_ORDER_FILLED = order.Type != OrderType.Market if MKT_ENTRY_ORDER_FILLED: ## If market entry order is now filled place the other orders self.entryPrice = order.Price quantity = order.Quantity self.log("ENTRY ORDER (MKT) FILLED: {}".format(orderEvent)) self.placeBarriers(self.entryPrice, quantity) if MKT_TIME_BARRIER_ORDER_FILLED: self.qcAlg.Transactions.CancelOpenOrders(self.tickerSymbol, "TIME BARRIER CANCELING ALL OTHER ORDERS") self.log("EXIT ORDER (MKT) TIME BARRIER FILLED (WE WILL CANCEL THE PRICE BARRIER ORDERS) {}".format(orderEvent)) if PRICE_BARRIER_ORDER_FILLED: self.qcAlg.Transactions.CancelOpenOrders(self.tickerSymbol, "PRICE BARRIER FILL CANCELING ALL OTHER ORDERS") self.log("PRICE BARRIER (TP OR SL) ORDER FILLED (WE CANCELED THE OTHER): {}".format(orderEvent))