Overall Statistics |
Total Orders 213 Average Win 0.26% Average Loss -0.23% Compounding Annual Return 3.020% Drawdown 3.800% Expectancy 0.091 Start Equity 100000 End Equity 102064.02 Net Profit 2.064% Sharpe Ratio 0.301 Sortino Ratio 0.274 Probabilistic Sharpe Ratio 28.800% Loss Rate 50% Win Rate 50% Profit-Loss Ratio 1.16 Alpha 0.016 Beta -0.025 Annual Standard Deviation 0.047 Annual Variance 0.002 Information Ratio -0.243 Tracking Error 0.334 Treynor Ratio -0.554 Total Fees $320.29 Estimated Strategy Capacity $35000000.00 Lowest Capacity Asset SPY R735QTJ8XC9X Portfolio Turnover 76.17% |
from AlgorithmImports import * class OrderBookImbalanceStrategy(QCAlgorithm): def Initialize(self): self.SetStartDate(2020, 1, 4) self.SetEndDate(2020, 9, 10) self.SetCash(100000) # Add SPY with quote data explicitly requested equity = self.AddEquity("SPY", Resolution.Minute) equity.SetDataNormalizationMode(DataNormalizationMode.Raw) self.symbol = equity.Symbol # Initialize SMA indicators for bid and ask volumes self.bid_volume_sma = SimpleMovingAverage(5) self.ask_volume_sma = SimpleMovingAverage(5) # Add OBI persistence tracking - store last N OBI values self.obi_window = RollingWindow[float](3) # Track last 3 OBI values (15 minutes) # Add volume analysis - track total order book volume (bid + ask) self.total_volume_sma = SimpleMovingAverage(20) # 20-period volume SMA # Position tracking flags self.is_long = False self.is_short = False # Entry/exit thresholds self.obi_entry_threshold = 0.7 # Align with project specification self.obi_exit_threshold = 0.7 self.volume_multiplier = 1.2 # Require volume 20% above average # Schedule trading decisions every 5 minutes self.Schedule.On(self.DateRules.EveryDay(self.symbol), self.TimeRules.Every(timedelta(minutes=5)), self.ExecuteTrades) # Create custom charts self.Plot("Strategy", "OBI", 0) self.Plot("Strategy", "Entry Threshold", self.obi_entry_threshold) self.Plot("Strategy", "Exit Threshold", self.obi_exit_threshold) self.Plot("Volume", "Total Order Volume", 0) self.Plot("Volume", "Average Order Volume", 0) def OnData(self, data): # Specifically check for QuoteBars data if not data.QuoteBars.ContainsKey(self.symbol): return # Get the quote bar quote_bar = data.QuoteBars[self.symbol] # Update SMAs with bid and ask volumes self.bid_volume_sma.Update(self.Time, quote_bar.LastBidSize) self.ask_volume_sma.Update(self.Time, quote_bar.LastAskSize) # Update total order book volume SMA total_volume = quote_bar.LastBidSize + quote_bar.LastAskSize self.total_volume_sma.Update(self.Time, total_volume) def ExecuteTrades(self): # Check if indicators are ready if not self.bid_volume_sma.IsReady or not self.ask_volume_sma.IsReady or not self.total_volume_sma.IsReady: return # Get current values from SMAs bid_vol = self.bid_volume_sma.Current.Value ask_vol = self.ask_volume_sma.Current.Value # Avoid division by zero if bid_vol + ask_vol == 0: self.Debug("Skipping trade evaluation: Bid + Ask volume is zero") return # Calculate Order Book Imbalance using the formula: OBI(t) = (B(t) - A(t)) / (B(t) + A(t)) obi = (bid_vol - ask_vol) / (bid_vol + ask_vol) # Add current OBI to rolling window for persistence tracking self.obi_window.Add(obi) # Plot the current OBI value self.Plot("Strategy", "OBI", obi) # Log OBI value self.Debug(f"Time: {self.Time}, OBI: {obi}") # Check for significant order book volume total_current_volume = bid_vol + ask_vol avg_volume = self.total_volume_sma.Current.Value # Plot volume data self.Plot("Volume", "Total Order Volume", total_current_volume) self.Plot("Volume", "Average Order Volume", avg_volume) # Check if current total volume is above average (significant) has_significant_volume = total_current_volume > avg_volume * self.volume_multiplier if not has_significant_volume: self.Debug(f"Volume too low: Current={total_current_volume}, Avg={avg_volume}") # Check for persistent OBI (majority of values in window exceed threshold) has_persistent_bullish_obi = False has_persistent_bearish_obi = False if self.obi_window.IsReady: # Get values from the rolling window obi_values = [self.obi_window[i] for i in range(self.obi_window.Count)] # Check if at least 2 out of 3 recent OBI values are above threshold bullish_count = sum(1 for o in obi_values if o > self.obi_entry_threshold * 0.8) bearish_count = sum(1 for o in obi_values if o < -self.obi_entry_threshold * 0.8) has_persistent_bullish_obi = bullish_count >= 2 has_persistent_bearish_obi = bearish_count >= 2 self.Debug(f"OBI Window: [{', '.join([str(round(o, 4)) for o in obi_values])}]") self.Debug(f"Persistent Bullish: {has_persistent_bullish_obi}, Persistent Bearish: {has_persistent_bearish_obi}") # Scale position size based on OBI strength position_size = min(0.95, 0.5 + abs(obi) * 0.5) # Scale from 0.5 to 0.95 based on OBI # Trading logic based on OBI thresholds, persistence, and volume # Long signal - OBI > threshold, persistent bullish, and significant volume if obi > self.obi_entry_threshold and has_persistent_bullish_obi and has_significant_volume and not self.is_long: if self.is_short: # Close short position first self.Liquidate() self.is_short = False # Enter long position with size based on OBI strength self.SetHoldings(self.symbol, position_size) self.is_long = True self.Debug(f"LONG signal at OBI: {obi} (Position Size: {position_size:.2f}, Persistent: {has_persistent_bullish_obi}, Volume: {has_significant_volume})") # Short signal - OBI < -threshold, persistent bearish, and significant volume elif obi < -self.obi_entry_threshold and has_persistent_bearish_obi and has_significant_volume and not self.is_short: if self.is_long: # Close long position first self.Liquidate() self.is_long = False # Enter short position with size based on OBI strength self.SetHoldings(self.symbol, -position_size) self.is_short = True self.Debug(f"SHORT signal at OBI: {obi} (Position Size: {position_size:.2f}, Persistent: {has_persistent_bearish_obi}, Volume: {has_significant_volume})") # Exit long position when OBI drops below threshold or volume dries up elif self.is_long and (obi < self.obi_exit_threshold or not has_significant_volume): self.Liquidate() self.is_long = False self.Debug(f"EXIT LONG at OBI: {obi}, Volume Significant: {has_significant_volume}") # Exit short position when OBI rises above -threshold or volume dries up elif self.is_short and (obi > -self.obi_exit_threshold or not has_significant_volume): self.Liquidate() self.is_short = False self.Debug(f"EXIT SHORT at OBI: {obi}, Volume Significant: {has_significant_volume}")