| 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}")