| Overall Statistics |
|
Total Orders 41 Average Win 0% Average Loss 0% Compounding Annual Return 0% Drawdown 0% Expectancy 0 Start Equity 100000 End Equity 99485.17 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 $44.84 Estimated Strategy Capacity $580000.00 Lowest Capacity Asset DWIN XLLPESHTRWX1 Portfolio Turnover 18.87% Drawdown Recovery 0 |
# region imports
from AlgorithmImports import *
# endregion
class DailyGapTrader(QCAlgorithm):
"""
ALGORITHM OVERVIEW:
This algorithm identifies small-cap stocks with the largest overnight price gaps and waits for a
confirmation of upward momentum before entering a position.
STRATEGY:
1. Universe Selection: From the US Equity Coarse Universe, daily selection filters for stocks
with a market cap under $1 billion and a price above a minimum threshold.
2. Target Identification: At 9:31 AM, it identifies the top N gappers that meet a minimum gap
percentage. It then records the high price from the first minute of trading.
3. Entry Trigger: An entry price is set at $0.01 above the first-minute high.
4. Trade Execution: The algorithm enters a position only if the stock's price crosses above this trigger.
5. Risk Management: A trailing stop loss is placed for each position.
6. Exit Strategy: All open positions are liquidated near the market close.
"""
def Initialize(self):
"""
Initial setup of the algorithm.
Reference: https://www.quantconnect.com/docs/v2/writing-algorithms/api-reference#Initialize
"""
self.SetStartDate(2025, 5, 1)
self.SetEndDate(2025, 5, 3)
self.SetCash(100000)
# --- Adjustable Parameters ---
self.min_price = 0.50
self.max_market_cap = 1000000000 # Maximum market cap to consider (e.g., $1 Billion for small-cap focus).
self.min_gap_percent = 0.10 # Minimum gap percentage (10%) to consider a stock.
self.trailing_stop_percent = 0.15
self.position_size = 1000
self.max_positions = 50
# --- End of Parameters ---
# Add SPY for market open/close checks and store its Symbol object.
self.spy = self.AddEquity("SPY", Resolution.Minute).Symbol
self.UniverseSettings.Resolution = Resolution.Minute
# This adds the default US Equity Coarse Universe data.
self.AddUniverse(self.CoarseSelectionFunction)
# Data storage dictionaries
self.closing_prices = {}
self.entry_triggers = {}
self.pending_stop_orders = []
# Schedule target setup to run at 9:31 AM, after the first bar has closed.
self.Schedule.On(self.DateRules.EveryDay(self.spy),
self.TimeRules.At(9, 31),
self.SetupTargets)
# Schedule liquidation before market close.
self.Schedule.On(self.DateRules.EveryDay(self.spy),
self.TimeRules.BeforeMarketClose(self.spy, 15),
self.LiquidatePositions)
def CoarseSelectionFunction(self, coarse: List[CoarseFundamental]) -> List[Symbol]:
"""
Filters the US Equity Coarse Universe down to a list of tradable assets.
Reference: https://www.quantconnect.com/docs/v2/writing-algorithms/universes/equity#Coarse-Selection
"""
# Sort stocks by their previous day's dollar volume in descending order.
sorted_by_volume = sorted([c for c in coarse if c.HasFundamentalData],
key=lambda c: c.DollarVolume, reverse=True)
# Filter the sorted list based on market cap and price.
# We filter for stocks with a market cap *under* our specified maximum.
filtered = [c for c in sorted_by_volume
if c.MarketCap > 0 and c.MarketCap < self.max_market_cap
and c.Price > self.min_price]
# We are interested in the top 500 of this filtered list.
top_stocks = filtered[:500]
for c in top_stocks:
self.closing_prices[c.Symbol] = c.Price
return [c.Symbol for c in top_stocks]
def OnSecuritiesChanged(self, changes: SecurityChanges):
"""
Handles additions and removals from our universe.
Reference: https://www.quantconnect.com/docs/v2/writing-algorithms/api-reference#OnSecuritiesChanged
"""
for security in changes.RemovedSecurities:
for store in [self.closing_prices, self.entry_triggers]:
if security.Symbol in store:
del store[security.Symbol]
if security.Symbol in self.pending_stop_orders:
self.pending_stop_orders.remove(security.Symbol)
def OnData(self, data: Slice):
"""
Main data handler. Executes trades when entry triggers are hit.
Reference: https://www.quantconnect.com/docs/v2/writing-algorithms/api-reference#OnData
"""
# Check for entry trigger crossovers during market hours.
if not self.entry_triggers:
return
for symbol in list(self.entry_triggers.keys()):
if data.Bars.ContainsKey(symbol):
price = data.Bars[symbol].Price
if price > self.entry_triggers[symbol]:
self.Debug(f"{symbol} crossed trigger price of {self.entry_triggers[symbol]:.2f}. Placing order.")
self.PlaceTrade(symbol)
# Remove from triggers to prevent duplicate orders.
del self.entry_triggers[symbol]
def SetupTargets(self):
"""
Scheduled for 9:31 AM. Identifies gappers, finds the first-minute high, and sets entry triggers.
"""
# Clear previous day's triggers.
self.entry_triggers.clear()
if self.Portfolio.Invested:
return
potential_targets = []
for symbol in self.ActiveSecurities.Keys:
if symbol == self.spy: continue # Exclude SPY from our gapper logic
security = self.ActiveSecurities[symbol]
if not security.HasData or security.Open == 0:
continue
previous_close = self.closing_prices.get(symbol)
if previous_close is None:
continue
gap_percent = (security.Open - previous_close) / previous_close
# Only consider stocks with a gap greater than our minimum requirement.
if gap_percent > self.min_gap_percent:
potential_targets.append({"symbol": symbol, "gap": gap_percent})
if not potential_targets:
return
sorted_targets = sorted(potential_targets, key=lambda x: x["gap"], reverse=True)
final_targets = [t["symbol"] for t in sorted_targets[:self.max_positions]]
# Log the list of identified target symbols for the day with the date.
if final_targets:
self.Log(f"Daily Targets for {self.Time.date()}: {[s.Value for s in final_targets]}")
for symbol in final_targets:
# Get the first minute bar data.
history = self.History(symbol, 1, Resolution.Minute)
if history.empty:
continue
first_minute_high = history.loc[symbol]['high'][0]
trigger_price = first_minute_high + 0.01
self.entry_triggers[symbol] = trigger_price
# Find the original gap percentage for the debug message
original_target_info = next((item for item in potential_targets if item["symbol"] == symbol), None)
if original_target_info:
self.Debug(f"Target: {symbol}. Gap: {original_target_info['gap']:.2%}. First min high: {first_minute_high:.2f}. Trigger: {trigger_price:.2f}")
def PlaceTrade(self, symbol):
"""
Calculates order quantity and places the market order.
"""
if self.Portfolio[symbol].Invested:
return
# Manually calculate the quantity of shares to achieve the desired position size.
# This is the most reliable way to set a specific dollar amount for a trade.
price = self.Securities[symbol].Price
if price == 0: return
quantity = int(self.position_size / price)
if quantity != 0:
self.MarketOrder(symbol, quantity)
# Add to list for tracking stop loss placement.
self.pending_stop_orders.append(symbol)
def OnOrderEvent(self, orderEvent: OrderEvent):
"""
Handles order events, primarily to place trailing stops after a buy order is filled.
Reference: https://www.quantconnect.com/docs/v2/writing-algorithms/api-reference#OnOrderEvent
"""
if orderEvent.Status != OrderStatus.Filled:
return
order = self.Transactions.GetOrderById(orderEvent.OrderId)
if order.Direction == OrderDirection.Buy and order.Symbol in self.pending_stop_orders:
stop_quantity = -orderEvent.FillQuantity
self.TrailingStopOrder(order.Symbol,
stop_quantity,
self.trailing_stop_percent,
trailingAsPercentage=True)
self.Debug(f"Placed {self.trailing_stop_percent:.0%} trailing stop for {order.Symbol}.")
self.pending_stop_orders.remove(order.Symbol)
def LiquidatePositions(self):
"""
Liquidates all open positions before the market closes.
"""
if self.Portfolio.Invested:
self.Debug("Market closing. Liquidating all open positions.")
self.Liquidate()