Overall Statistics |
Total Orders 348 Average Win 0.83% Average Loss -0.51% Compounding Annual Return -0.491% Drawdown 9.400% Expectancy -0.031 Start Equity 25000 End Equity 24212.70 Net Profit -3.149% Sharpe Ratio -0.922 Sortino Ratio -0.9 Probabilistic Sharpe Ratio 0.040% Loss Rate 63% Win Rate 37% Profit-Loss Ratio 1.63 Alpha -0.028 Beta 0.004 Annual Standard Deviation 0.03 Annual Variance 0.001 Information Ratio -0.78 Tracking Error 0.203 Treynor Ratio -6.48 Total Fees $0.17 Estimated Strategy Capacity $25000000.00 Lowest Capacity Asset QQQ RIWIV7K5Z9LX Portfolio Turnover 21.23% |
from AlgorithmImports import * class QQQORBTest(QCAlgorithm): """ Opening Range Breakout (ORB) Day Trading Strategy for QQQ. Reference: - https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4729284 TLDR - This strategy identifies the breakout from the opening range during the first 5 minutes of the trading session and takes positions accordingly. Strategy: - If the market moves up in the first 5 minutes, a bullish position is taken. - If the market moves down in the first 5 minutes, a bearish position is taken. - No positions are taken if the first 5-minute candle is a doji (open = close). - Stop loss is set at the low of the first 5-minute candle for long trades, and at the high for short trades. - The profit target is 10 times the risk (distance between entry price and stop). - If the profit target is not reached by the end of the day, the position is liquidated at market closure. - Trading size is calibrated to risk 1% of the capital per trade. Parameters: - Starting capital: $25,000 - Maximum leverage: 4x - Commission: $0.0005 per share - Risk per trade: 1% of capital (ie: if stop loss hit) Contact - u/shock_and_awful """ ## System method, algo entry point ## ------------------------------- def Initialize(self): self.InitParams() self.InitBacktest() self.InitData() self.InitIndicators() self.ScheduleRoutines() ## Initialize Parameters ## ---------------------- def InitParams(self): self.risk_per_trade = 0.01 # 1% risk per trade self.max_leverage = 4 # in line with US FINRA regulations self.fixed_commission = 0.0005 # fixed per trade self.minsAfterOpen = 6 # Daily trade time: 6 mins after open (after first 5 min bar has closed) self.rrMultiplier = float(self.get_parameter("rrMultiplier")) # Risk reward multiplier for take profit self.rVolPeriod = int(self.get_parameter("rVolPeriod")) self.rVolMagnitude = float(self.get_parameter("rVolMagnitude")) ## Backtest properties ## ------------------- def InitBacktest(self): self.SetStartDate(2018,1,1) # Set Start Date # self.SetEndDate(2023,2,17) # Set End Date self.SetCash(25000) ## Subscribe to asset data feed on right timeframe(s) ## -------------------------------------------------- def InitData(self): self.ticker = "QQQ" self.security = self.AddEquity(self.ticker, Resolution.Minute) self.symbol = self.security.Symbol self.SetBenchmark(self.ticker) # Set Brokerage model to IBkr self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin) # Set the feed model to fixed commission self.security.SetFeeModel(ConstantFeeModel(self.fixed_commission)) # Set the margin model to PDT self.security.margin_model = PatternDayTradingMarginModel() # Initialize time frame (Five minute consolidator) self.fiveMinConsolidator = TradeBarConsolidator(timedelta(minutes = 5)) self.fiveMinConsolidator.DataConsolidated += self.onFiveMinBarFormed self.SubscriptionManager.AddConsolidator(self.symbol, self.fiveMinConsolidator) # State Tracking self.direction = None self.stop_price = 0 self.entry_price = 0 self.highVolSpike = False ## Handle 5 min bar consolidation ## ------------------------------- def onFiveMinBarFormed(self, sender, bar): self.lastFiveMinBar = bar ## Initialize necessary indicators ## ------------------------------- def InitIndicators(self): self.openingVolMA = SimpleMovingAverage(self.rVolPeriod) ## Schedule our 'chron jobs' ## ------------------------- def ScheduleRoutines(self): ## Intraday selection self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen(self.ticker, self.minsAfterOpen), self.RunAfterMarketOpen) ## End of Day Liquidation self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.BeforeMarketClose(self.ticker, 2), self.LiquidateAtEoD) ## A convenient 'Chron job', called on the 6th minute of every trading day. ## ------------------------------------------------------------------------ def RunAfterMarketOpen(self): # Before updating with current bar volume, # check if its a volume spike if(self.openingVolMA.is_ready): if (self.lastFiveMinBar.volume > (self.rVolMagnitude * self.openingVolMA.current.value)): self.highVolSpike = True else: self.highVolSpike = False self.openingVolMA.update(self.lastFiveMinBar.Time, self.lastFiveMinBar.volume) if(not self.openingVolMA.is_ready): return if(not self.highVolSpike): return openingBar = self.lastFiveMinBar entry_price = self.current_slice[self.symbol].open coefficient = 1 # Doji candle. Don't trade. if( openingBar.close == openingBar.open ): self.Log("Doji Candle") return # Set direction based on bullish / bearish candle self.direction = PortfolioBias.LONG if (openingBar.close > openingBar.open) else PortfolioBias.SHORT # Go Long if price hasnt already moved past stop if (self.direction == PortfolioBias.LONG) and \ (entry_price > openingBar.low) : self.stop_price = openingBar.low self.tp_price = entry_price + (self.rrMultiplier * abs(entry_price - self.stop_price)) # Go Short if price hasnt already moved past stop elif (self.direction == PortfolioBias.SHORT) and \ (entry_price < openingBar.high) : self.stop_price = openingBar.high self.tp_price = entry_price - (self.rrMultiplier * abs(entry_price - self.stop_price)) coefficient = -1 num_shares = coefficient * self.CalcOrderSize(entry_price, self.stop_price) order_note = f"Opening position. going { 'long' if (self.direction == PortfolioBias.LONG) else 'short'}" self.MarketOrder(self.symbol, num_shares, tag=order_note) self.Log("Order placed") ## System method, called as every new bar of data arrives. Check for exits (stop loss) ## ----------------------------------------------------------------------------------- def OnData(self, data): # If data is available and we are invested if ((self.ticker in data ) and (data[self.ticker] is not None)) and \ (self.Portfolio.Invested): self.CheckForExits() ## Check if take profit or stop loss hit ## ------------------------------------- def CheckForExits(self): current_price = self.Securities[self.symbol].Price if (self.direction == PortfolioBias.LONG): if(current_price >= self.tp_price): self.LiquidateWithMsg("Take Profit") elif(current_price <= self.stop_price): self.LiquidateWithMsg("Stop Loss") elif (self.direction == PortfolioBias.SHORT): if(current_price <= self.tp_price): self.LiquidateWithMsg("Take Profit") elif(current_price >= self.stop_price): self.LiquidateWithMsg("Stop Loss") ## Calculate position size ## ---–---–---–---–---–--- def CalcOrderSize(self, entry_price, stop_price): # Calculate the risk per share risk_per_share = abs(entry_price - stop_price) # Get account size. # Currently using available cash but might consider using self.Portfolio.TotalPortfolioValue acct_size = self.Portfolio.Cash / 2 # Calculate the total risk for the trade total_risk = acct_size * self.risk_per_trade # Calculate the number of shares to buy/sell shares = int(total_risk / risk_per_share) # Alt position size, adjust for leverage # with modifciation: multiply by risk per trade max_shares = int(acct_size * self.max_leverage / entry_price) return min(shares, max_shares) ## 'Chron Job' to Liquidate all holdings at the end of the day ## ----------------------------------------------------------- def LiquidateAtEoD(self): self.LiquidateWithMsg("End-of-Day") ## Convenience method to liquidate with a message ## ---------------------------------------------- def LiquidateWithMsg(self, exitReason): pnl = round(100* self.Portfolio[self.symbol].UnrealizedProfitPercent,4) biasText = 'Long' if (self.direction == PortfolioBias.LONG) else 'short' winlossText = 'win' if pnl > 0 else 'loss' self.Liquidate(tag=f"{exitReason} Exiting {biasText} position with {pnl}% {winlossText}")