| Overall Statistics |
|
Total Orders 0 Average Win 0% Average Loss 0% Compounding Annual Return 0% Drawdown 0% Expectancy 0 Start Equity 100000 End Equity 100000 Net Profit 0% Sharpe Ratio 0 Sortino Ratio 0 Probabilistic Sharpe Ratio 0% Loss Rate 0% Win Rate 0% Profit-Loss Ratio 0 Alpha 0 Beta 0 Annual Standard Deviation 0 Annual Variance 0 Information Ratio 0 Tracking Error 0 Treynor Ratio 0 Total Fees $0.00 Estimated Strategy Capacity $0 Lowest Capacity Asset Portfolio Turnover 0% Drawdown Recovery 0 |
# region imports
from AlgorithmImports import *
# endregion
class PremarketScanner(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2025, 9, 5)
self.SetEndDate(2025, 10, 1)
self.SetCash(100000)
self.symbolData = {}
self.currentDay = None
# Schedule watchlist compilation a few seconds after market open
self.Schedule.On(
self.DateRules.EveryDay(),
self.TimeRules.AfterMarketOpen("SPY", 1), # 1 minute after open
self.CompileWatchlist
)
# Reset premarket data at 4 AM each day
self.Schedule.On(
self.DateRules.EveryDay(),
self.TimeRules.At(4, 0),
self.ResetDailyData
)
self.AddEquity("SPY", Resolution.Minute)
self.AddUniverse(self.CoarseSelectionFilter)
self.UniverseSettings.ExtendedMarketHours = True
self.UniverseSettings.Resolution = Resolution.Minute
def OnData(self, data: Slice):
# Track premarket data for all securities
for symbol, symbolData in self.symbolData.items():
if symbol in data.Bars:
symbolData.UpdateData(data.Bars[symbol])
def CoarseSelectionFilter(self, coarse):
# Filter for liquid stocks with price >= $0.05
filtered = [
x.Symbol for x in coarse
if x.HasFundamentalData
# THE FIX: Ensure it is a standard Equity type supported by Schwab
and x.SecurityType == SecurityType.Equity
and x.Price >= 0.05
and x.DollarVolume > 5 # Basic liquidity filter: $500,000+
]
return filtered # Limit universe size for performance
def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
if security.Symbol not in self.symbolData.keys():
self.symbolData[security.Symbol] = SymbolData(self, security)
for security in changes.RemovedSecurities:
if security.Symbol in self.symbolData.keys():
self.symbolData.pop(security.Symbol)
def ResetDailyData(self):
"""Reset premarket data at the start of each trading day"""
for symbolData in self.symbolData.values():
symbolData.ResetDaily()
def CompileWatchlist(self):
"""Compile watchlist of stocks gapping up 10%+ with premarket volume filter"""
self.Debug(f"\n{'='*80}")
self.Debug(f"PREMARKET GAP SCANNER - {self.Time}")
self.Debug(f"{'='*80}\n")
gappers = []
for symbol, symbolData in self.symbolData.items():
gap_percent = symbolData.GetGapPercent()
premarket_dollar_volume = symbolData.GetPremarketDollarVolume()
current_price = symbolData.GetCurrentPrice()
# Filter criteria matching the websites
if (gap_percent is not None
and gap_percent >= 10.0
and premarket_dollar_volume >= 5 # $500K equivalent (since DollarVolume is scaled)
and current_price >= 0.05):
gappers.append({
'symbol': symbol,
'gap_percent': gap_percent,
'pm_volume': premarket_dollar_volume,
'price': current_price,
'prev_close': symbolData.previousClose
})
# Sort by gap percentage (highest first)
gappers.sort(key=lambda x: x['gap_percent'], reverse=True)
# Display watchlist
if gappers:
self.Debug(f"Found {len(gappers)} stocks gapping up 10%+ with $500K+ premarket volume:\n")
self.Debug(f"{'Symbol':<10} {'Gap %':<10} {'Price':<10} {'Prev Close':<12} {'PM $ Volume':<15}")
self.Debug(f"{'-'*70}")
for g in gappers:
self.Debug(
f"{str(g['symbol'].Value):<10} "
f"{g['gap_percent']:>7.2f}% "
f"${g['price']:>7.2f} "
f"${g['prev_close']:>9.2f} "
f"${g['pm_volume']:>13,.0f}"
)
self.Debug(f"\n{'='*80}\n")
else:
self.Debug("No stocks found matching criteria (10%+ gap, $500K+ PM volume, $0.05+ price)\n")
class SymbolData:
def __init__(self, algo, security):
self.security = security
self.symbol = security.Symbol
self.algo = algo
# Track previous day's close
self.previousClose = None
# Track premarket data
self.premarketBars = []
self.marketOpenTime = None
self.currentPrice = None
self.lastUpdateDay = None
# Get previous close using history
self.UpdatePreviousClose()
def UpdatePreviousClose(self):
"""Update the previous day's close price"""
# Requesting 2 bars to ensure we get *at least* one complete bar's close
history = self.algo.History(self.symbol, 2, Resolution.Daily)
if not history.empty and len(history) >= 1:
self.previousClose = history['close'].iloc[-1]
def ResetDaily(self):
"""Reset daily tracking variables at 4 AM"""
self.premarketBars = []
self.UpdatePreviousClose()
def UpdateData(self, bar):
"""Update data with incoming bars"""
current_time = self.algo.Time
# Check if we need to reset for a new day
if self.lastUpdateDay is None or self.lastUpdateDay.date() != current_time.date():
# Only reset if it's after 4 AM, which is handled by the scheduler's ResetDailyData
self.lastUpdateDay = current_time
# Set market open time (9:30 AM ET)
if self.marketOpenTime is None:
# Note: QC time is UTC, but the simulator handles the market-open-time conversion
self.marketOpenTime = current_time.replace(hour=9, minute=30, second=0, microsecond=0)
# Track current price
self.currentPrice = bar.Close
# Collect premarket bars (4:00 AM to 9:29 AM ET)
# Assuming current_time is converted to exchange time by the platform logic for comparison
if current_time.hour >= 4 and current_time < self.marketOpenTime:
self.premarketBars.append({
'time': current_time,
'close': bar.Close,
'volume': bar.Volume
})
def GetGapPercent(self):
"""Calculate gap percentage from previous close to current price"""
if self.previousClose is None or self.previousClose == 0 or self.currentPrice is None:
return None
gap_percent = ((self.currentPrice / self.previousClose) - 1) * 100
return gap_percent
def GetPremarketDollarVolume(self):
"""Calculate total premarket dollar volume"""
if not self.premarketBars:
return 0
# Dollar volume in the filter is x.DollarVolume > 5, which means $500,000 when using the QC default
# x.DollarVolume = (Volume * Price) / 100,000. So 5 = $500K
# Your internal tracking is raw dollar volume, so to be comparable, you'd divide by 100,000 if needed.
# But for an internal calculation of gappers, raw dollar volume is fine.
dollar_volume = sum(bar['close'] * bar['volume'] for bar in self.premarketBars)
return dollar_volume
def GetCurrentPrice(self):
"""Get current price"""
return self.currentPrice if self.currentPrice is not None else 0