Overall Statistics
Total Trades
188
Average Win
1.56%
Average Loss
-0.79%
Compounding Annual Return
12.010%
Drawdown
18.200%
Expectancy
0.159
Net Profit
25.541%
Sharpe Ratio
0.745
Probabilistic Sharpe Ratio
32.634%
Loss Rate
61%
Win Rate
39%
Profit-Loss Ratio
1.98
Alpha
0.113
Beta
-0.039
Annual Standard Deviation
0.146
Annual Variance
0.021
Information Ratio
0.004
Tracking Error
0.284
Treynor Ratio
-2.783
Total Fees
$265.27
class EarningsReversal(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2018, 10, 1)
        self.SetEndDate(2020, 10, 1)
        self.SetCash(100000)
        
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelection, self.FineSelection)
        
        # Initialize lists/dicts
        self.longSymbols = []
        self.entryPrices = {}
        self.highestPrice = {}
        self.stopMarketTicket = {}
        
        # Add custom bar chart
        stockPlot = Chart('Positions')
        stockPlot.AddSeries(Series('Longs', SeriesType.Bar, 0))
        self.AddChart(stockPlot)
        
        # Set Benchmark
        self.AddEquity("SPY", Resolution.Daily)
        self.SetBenchmark("SPY")
        
        # Schedule EveryMarketOpen function
        self.Schedule.On(self.DateRules.EveryDay("SPY"), \
                        self.TimeRules.At(10, 00), \
                        self.EveryMarketOpen)
        
        # ------------- Paramters --------------
        # Minimum percentage decline before entry
        self.entryMove = 0.02
        # Max number of positions allowed at once (equal weighting)
        self.maxPositions = 10
        # Size of coarse selection universe
        self.numOfCoarse = 6*self.maxPositions
        # Number of days past since earnings
        self.daysSinceEarnings = 1
        # Percentage offset of trailing stop loss
        self.stopLoss = 0.10

    # Pick the top 100 liquid equities as the coarse-selected universe
    def CoarseSelection(self, coarse):
        # Sort the equities (price > 5) by Dollar Volume descendingly
        selectedByDollarVolume = sorted([x for x in coarse if x.Price > 5 and x.HasFundamentalData], 
                                        key = lambda x: x.DollarVolume, reverse = True)
        
        # Pick the top numOfCoarse liquid equities as the coarse-selected universe
        return [x.Symbol for x in selectedByDollarVolume[:self.numOfCoarse]]

    # Pick stocks with recent earnings and noticeable reaction  
    def FineSelection(self, fine):
        # filter out stocks with recent earnings
        fine = [x for x in fine if self.Time == x.EarningReports.FileDate + timedelta(days=self.daysSinceEarnings)]
        symbols = [x.Symbol for x in fine]
        # Save the stock prices around earnings
        pricesAroundEarnings = self.History(symbols, self.daysSinceEarnings+3, Resolution.Daily)

        for sec in fine:
            # Find tradeable date closest to specified number of days before earnings
            date = min(pricesAroundEarnings.loc[sec.Symbol]["close"].index, 
                       key=lambda x:abs(x-(sec.EarningReports.FileDate - timedelta(1))))
            priceOnEarnings = pricesAroundEarnings.loc[sec.Symbol]["close"][date]
            # Check if stock fell far enough
            if priceOnEarnings * (1-self.entryMove) > sec.Price:
                self.longSymbols.append(sec.Symbol)
        
        return self.longSymbols
    
    # Open equal weighted positions and create trailing stop loss
    def EveryMarketOpen(self):
        positions = [sec.Symbol for sec in self.Portfolio.Values if self.Portfolio[sec.Symbol].Invested]
        # Plot number of existing positions
        self.Plot("Positions", "Longs", len(positions))
        
        availableTrades = self.maxPositions - len(positions)
        for symbol in [x for x in self.longSymbols if x not in positions][:availableTrades]:
            if self.Securities.ContainsKey(symbol):
                # Buy stock
                self.SetHoldings(symbol, 1 / self.maxPositions)
        
        self.longSymbols = []
        
        for symbol in positions:
             # If no order exists, send stop-loss
            if not self.Transactions.GetOpenOrders(symbol):
                self.stopMarketTicket[symbol] = self.StopMarketOrder(symbol, \
                                        -self.Portfolio[symbol].Quantity, \
                                        (1-self.stopLoss) * self.entryPrices[symbol])
                        
            # Check if the asset's price is higher than earlier highestPrice
            elif self.Securities[symbol].Close > self.highestPrice[symbol]:
                # Save the new high to highestPrice
                self.highestPrice[symbol] = self.Securities[symbol].Close
                # Update the stop price
                updateFields = UpdateOrderFields()
                updateFields.StopPrice = self.Securities[symbol].Close * (1-self.stopLoss)
                self.stopMarketTicket[symbol].Update(updateFields)

    # On every order event, save fill price and current price
    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status == OrderStatus.Filled: 
            self.entryPrices[orderEvent.Symbol] = orderEvent.FillPrice
            self.highestPrice[orderEvent.Symbol] = orderEvent.FillPrice
    
    def OnData(self, data):
        pass