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()