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
-1.693
Tracking Error
0.104
Treynor Ratio
0
Total Fees
$0.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
Portfolio Turnover
0%
from AlgorithmImports import *

class EarningsATRUniverseAlgorithm(QCAlgorithm):

    def Initialize(self):
        # sample start date
        self.set_start_date(2024, 1, 1)
        # sample end date
        self.set_end_date(2024, 10, 31)

        self.set_cash(100000)

        # get 2024 earnings calendar from object store, can store custom data in object store
        self.df_earnings = self.get_df_from_store('sylvaint/earnings_2024.csv')
        
        # List to store filtered symbols and dates for earnings and ATR checks
        self.filtered_symbols = []
        # set resolution to Daily
        self.universe_settings.resolution = Resolution.DAILY
        
        # add stock universe with coarse filter function
        self.add_universe(self.CoarseSelectionFunction)

        # Dictionary to track symbols' ATR and earnings previous close
        self.symbol_data = {}
        self.prev_close = {}

    # check symbol has earnings for date
    def is_stock_earnings(self, date_string, symbol):
        try:
            stock_array = (
                self.df_earnings
                .query('date==@date_string')
                .assign(symbols=lambda df_: df_['symbols'].apply(lambda x: x.split(',')))
                .symbols
                .to_list()[0]
            )
        except:
            return False
            
        return symbol in stock_array

    # read earnings dataframe from store
    def get_df_from_store(self, key):
        file_path = self.object_store.get_file_path(key)
        return pd.read_csv(file_path)

    # convert self.time to date string
    def time_to_date_string(self, t):
        return t.strftime('%Y-%m-%d')

    # this is the coarse filter to our stock universe, it is run for every date in our sample range
    def CoarseSelectionFunction(self, coarse):
        # universe selection occurs at midnight so previous day data
        # print the current algorithm date
        self.Debug(f"Coarse selection date: {self.time}")

        selected = []
        
        for stock in coarse:
            # Filter by price > 10 and volume > 500,000 and earnings today
            if (stock.Price > 10 and 
                stock.Volume > 500000 and
                # this checks if stock reported earnings yesterday
                self.is_stock_earnings(self.time_to_date_string(self.time - timedelta(days=1)), stock.symbol.value)):

                # store previous close
                self.prev_close[stock.Symbol.value] = stock.adjusted_price
                selected.append(stock.Symbol)
        # self.debug(f"{len(selected)} stocks from coarse")
        
        return selected

    # this is called every day at midnight and registers added and removed securities according to the coarse filter
    def OnSecuritiesChanged(self, changes):
        # Subscribe to selected securities from universe filter
        for security in changes.AddedSecurities:
            symbol = security.Symbol
            # Initialize ATR and fundamental data tracking
            atr = self.atr(symbol, 14, Resolution.DAILY)
            # Populate ATR with 14 days of historical data
            indicator_history = self.indicator_history(atr, symbol, 14, Resolution.DAILY)
            if indicator_history.Current:
                atr_value = indicator_history.Current[0].value
            else:
                atr_value = None
            self.symbol_data[symbol] = {
                'ATR': atr_value,
            }

        # remove atr until next occurence
        for security in changes.RemovedSecurities:
            symbol = security.Symbol
            del self.symbol_data[symbol]
    
    # this is where data comes in, in this case daily data
    def on_data(self, data):
        # Process earnings and ATR conditions for filtered stocks
        for symbol, sym_data in self.symbol_data.items():
            if symbol in data.Bars and sym_data['ATR'] is not None:
                # Get the current open, close, and ATR values
                open_price = data.Bars[symbol].Open
                prev_close = self.prev_close[symbol.value]
                atr_value = sym_data['ATR']
                
                # Check if the stock had earnings yesterday and opened >2 ATRs above the close
                if open_price > (prev_close + 2 * atr_value):
                    self.filtered_symbols.append({
                        'Symbol': symbol.Value,
                        'Date': self.Time,
                        'ATR': atr_value,
                        'PrevClose': prev_close,
                        'Open': open_price
                    })
                
    # this is called at the end of the algo
    def on_end_of_algorithm(self):
        # Display filtered results at the end of the backtest
        for result in self.filtered_symbols:
            # self.log(f"{result['Date']} - {result['Symbol']} - ATR: {result['ATR']} - prev close {result['PrevClose']} - Open {result['Open']} - opened more than 2 ATRs above the previous close.")
            self.log(f"{result['Date']},{result['Symbol']},{result['ATR']},{result['PrevClose']},{result['Open']}")