Created with Highcharts 12.1.2EquityJan 2020Jul 2020Jan 2021Jul 2021Jan 2022Jul 2022Jan 2023Jul 2023Jan 2024Jul 2024Jan 2025Jul 202501M2M-10-500101205M010M20M102030
Overall Statistics
Total Orders
324
Average Win
1.70%
Average Loss
-4.24%
Compounding Annual Return
56.657%
Drawdown
9.400%
Expectancy
0.294
Start Equity
100000
End Equity
1278419.56
Net Profit
1178.420%
Sharpe Ratio
2.426
Sortino Ratio
1.132
Probabilistic Sharpe Ratio
99.996%
Loss Rate
8%
Win Rate
92%
Profit-Loss Ratio
0.40
Alpha
0.348
Beta
0.015
Annual Standard Deviation
0.144
Annual Variance
0.021
Information Ratio
1.215
Tracking Error
0.225
Treynor Ratio
22.809
Total Fees
$7692.87
Estimated Strategy Capacity
$260000.00
Lowest Capacity Asset
INTU R735QTJ8XC9X
Portfolio Turnover
10.59%
#region imports
from AlgorithmImports import *
#endregion



############################################################  BACKTEST SETTINGS  ########################################################################

# !!! Right now the historical earnings data only goes back to 2017
BACKTEST_START_YEAR = 2019                # Set start Year of the Backtest
BACKTEST_START_MONTH = 8                  # Set start Month of the Backtest
BACKTEST_START_DAY = 10                   # Set start Day of the Backtest

BACKTEST_END_YEAR = 2026                   # Set end Year of the Backtest
BACKTEST_END_MONTH = 2                      # Set end Month of the Backtest       
BACKTEST_END_DAY = 1                       # Set end Day of the Backtest

BACKTEST_ACCOUNT_CASH = 100000               # Set Backtest Strategy Cash



# Set this to "BACKTEST" when backtesting and to "LIVE" when live trading
MODE = "BACKTEST"



# In this version I have set it to only allow for one trade at a time with 100% cash allocated for that trade.
# Percentage of cash allocated per position, 100 = 100%
CASH_PERCENTAGE_PER_POSITION = 100


# Pick between "NUMERIC", "ATR", "STANDARD_DEVIATION"
EXIT_PERCENTAGE_TYPE = "NUMERIC"


# Required upwards price move before entering, 1 = 1%
PRICE_MOVE_BEFORE_ENTRY = 1


# Profit percentage, 3 = 3%
PROFIT_PERCENTAGE = 3

# Stop percentage, 5 = 5%
STOP_PERCENTAGE = 5

# Trailing percentage, 1 = 1%
TRAIL_PERCENTAGE = 1



# Set this to 1 to include beating eps estimate as a filter requirment, set to 0 to not include it as filter
USE_BEAT_EPS = 1

# Set this to 1 to include beating revenue estimate as a filter requirment, set to 0 to not include it as filter
USE_BEAT_REVENUE = 0

# Percentage of earnings per stock that must have beaten the EPS estimate when USE_BEAT_EPS = 1, 0.66 = 66%
BEAT_EPS_PERCENTAGE = 0.66

# Percentage of earnings per stock that must have beaten the Revenue estimate when USE_BEAT_REVENUE = 1, 0.66 = 66%
BEAT_REVENUE_PERCENTAGE = 0.66

# Percentage of earnings per stock that must have included a raised guidance, 0.3 = 30%
GUIDANCE_RAISED_PERCENTAGE = 0.1

# Percentage that the actual eps has to be higher than the estimate, 2 = 2%
EPS_SURPRISE_PERCENTAGE = 2

# Percentage that the actual revenue has to be higher than the estimate, 2 = 2%
REVENUE_SURPRISE_PERCENTAGE = 1
# region imports
from AlgorithmImports import *
# endregion

from io import StringIO
import pandas as pd
from symboldata import SymbolData
import config
import math

class EmotionalBlackChinchilla(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(config.BACKTEST_START_YEAR, config.BACKTEST_START_MONTH, config.BACKTEST_START_DAY)   # Set Backtest Start Date
        self.SetEndDate(config.BACKTEST_END_YEAR, config.BACKTEST_END_MONTH, config.BACKTEST_END_DAY)         # Set Backtest End Date
        self.SetCash(config.BACKTEST_ACCOUNT_CASH)
        self.mode = config.MODE
        self.cash_percent = config.CASH_PERCENTAGE_PER_POSITION/100

        self.percent_exit_type = config.EXIT_PERCENTAGE_TYPE
        self.move_pre_entry = 1 + (config.PRICE_MOVE_BEFORE_ENTRY/100)
        self.SetSecurityInitializer(self.CustomSecurityInitializer)

        self.profit_percentage = 1 + (config.PROFIT_PERCENTAGE/100)
        self.stop_percentage   = 1 - (config.STOP_PERCENTAGE/100)
        self.trail_percentage = 1 + (config.TRAIL_PERCENTAGE/100)

        self.AddUniverse(self.CoarseFilterFunction, self.FineFilterFunction)
        self.UniverseSettings.Resolution = Resolution.Minute
        self.UniverseSettings.ExtendedMarketHours = True
        self.UniverseSettings.Leverage = 4

        self.symbol_instances = {}

        # --------------------------------------------------------------------
        # LIVE MODE
        # --------------------------------------------------------------------
        if self.mode == "LIVE":
            x = self.Download("https://docs.google.com/spreadsheets/d/1KoL8P32yXQZiMbnsDAfQ7F4qT8fL73L5/export?format=csv")

            # Schedules
            self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(16, 0), self.get_updated_earnings)
            self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(16, 5), self.get_updated_earnings)
            self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(16, 10), self.get_updated_earnings)
            self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(16, 15), self.get_updated_earnings)
            self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(16, 20), self.get_updated_earnings)
            self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(16, 25), self.get_updated_earnings)
            self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(16, 30), self.get_updated_earnings)
            self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(16, 35), self.get_updated_earnings)
            self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(16, 40), self.get_updated_earnings)
            self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(16, 45), self.get_updated_earnings)
            self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(16, 50), self.get_updated_earnings)
            self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(16, 55), self.get_updated_earnings)
            self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(17, 0), self.get_updated_earnings)

            self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(8, 0), self.get_updated_database)
            x = self.Download("https://docs.google.com/spreadsheets/d/1KoL8P32yXQZiMbnsDAfQ7F4qT8fL73L5/export?format=csv")

            # Read CSV data into a DataFrame
            df = pd.read_csv(StringIO(x))
            self.Debug(f"[LIVE] Columns in CSV: {df.columns}")

            # Apply the filter conditions directly on the DataFrame
            self.df = df.groupby('Symbol').filter(lambda x: len(x) >= 10 and
                                                        (x['Beat_EPS'].mean() >= config.BEAT_EPS_PERCENTAGE if config.USE_BEAT_EPS else True) and
                                                        (x['Beat_Revenue'].mean() >= config.BEAT_REVENUE_PERCENTAGE if config.USE_BEAT_REVENUE else True) and
                                                        (x['Raised_EPS_Guidance'].sum() > 0 or x['Raised_REVENUE_Guidance'].sum() > 0).mean() >= config.GUIDANCE_RAISED_PERCENTAGE and
                                                        (x['Price_Change'] >= 0.01).mean() >= 0.5 and
                                                        (x['Price_Change'] >= 0.01) and
                                                        (x['EPS_Surprise_Earnings'] >= config.EPS_SURPRISE_PERCENTAGE) and
                                                        (x['Revenue_Surprise_Earnings'] >= config.REVENUE_SURPRISE_PERCENTAGE))

            # Get unique symbols
            self.symbol_list = self.df['Symbol'].unique().tolist()
            self.Debug(f"[LIVE] After filter, symbol_list = {self.symbol_list}")

        # --------------------------------------------------------------------
        # BACKTEST MODE
        # --------------------------------------------------------------------
        else:
            x = self.Download("https://docs.google.com/spreadsheets/d/1A2oY8FA9197O9LpZWtae5QdAC0jrIT8s9N5fzsUJg4Y/export?format=csv")
            df = pd.read_csv(StringIO(x))

            self.Debug(f"### DEBUG ### CSV columns: {df.columns.tolist()}")
            self.Debug(f"### DEBUG ### First few rows:\n{df.head()}")

            # Ensure numeric columns are truly numeric
            numeric_columns = [
                'Price_Change',
                'Beat_EPS',
                'Beat_Revenue',
                'EPS_Surprise_Earnings',
                'Revenue_Surprise_Earnings'
            ]
            for col in numeric_columns:
                df[col] = pd.to_numeric(df[col], errors='coerce')

            # 1) Condition: Beat_EPS
            if config.USE_BEAT_EPS:
                cond_beat_eps = df.groupby('Symbol')['Beat_EPS'].transform(lambda x: x.sum() / len(x)) >= config.BEAT_EPS_PERCENTAGE
            else:
                cond_beat_eps = pd.Series([True]*len(df), index=df.index)
            self.Debug(f"### DEBUG ### cond_beat_eps passing rows: {cond_beat_eps.sum()} / {len(cond_beat_eps)}")

            # 2) Condition: Beat_Revenue
            if config.USE_BEAT_REVENUE:
                cond_beat_revenue = df.groupby('Symbol')['Beat_Revenue'].transform(lambda x: x.sum() / len(x)) >= config.BEAT_REVENUE_PERCENTAGE
            else:
                cond_beat_revenue = pd.Series([True]*len(df), index=df.index)
            self.Debug(f"### DEBUG ### cond_beat_revenue passing rows: {cond_beat_revenue.sum()} / {len(cond_beat_revenue)}")

            # 3) Condition: Guidance
            cond_guidance = (
                (df['Raised_EPS_Guidance'] == 1).groupby(df['Symbol']).transform('any')
                | (df['Raised_REVENUE_Guidance'] == 1).groupby(df['Symbol']).transform('any')
            )
            self.Debug(f"### DEBUG ### cond_guidance passing rows: {cond_guidance.sum()} / {len(cond_guidance)}")

            # 4) Condition: Price_Change ratio >= 0.52
            cond_price_change_ratio = df.groupby('Symbol')['Price_Change'] \
                .transform(lambda x: (x >= 0.01).sum() / len(x)) >= 0.52
            self.Debug(f"### DEBUG ### cond_price_change_ratio passing rows: {cond_price_change_ratio.sum()} / {len(cond_price_change_ratio)}")

            # 5) Condition: EPS_Surprise_Earnings
            cond_eps_surprise = df.groupby('Symbol')['EPS_Surprise_Earnings'].transform('mean') >= config.EPS_SURPRISE_PERCENTAGE
            self.Debug(f"### DEBUG ### cond_eps_surprise passing rows: {cond_eps_surprise.sum()} / {len(cond_eps_surprise)}")

            # 6) Condition: Revenue_Surprise_Earnings
            cond_rev_surprise = df.groupby('Symbol')['Revenue_Surprise_Earnings'].transform('mean') >= config.REVENUE_SURPRISE_PERCENTAGE
            self.Debug(f"### DEBUG ### cond_rev_surprise passing rows: {cond_rev_surprise.sum()} / {len(cond_rev_surprise)}")

            # 7) Condition: row-level Price_Change >= 0.01
            cond_price_change = df['Price_Change'] >= 0.01
            self.Debug(f"### DEBUG ### cond_price_change passing rows: {cond_price_change.sum()} / {len(cond_price_change)}")

            # Combine them all
            combined_mask = (
                cond_beat_eps
                & cond_beat_revenue
                & cond_guidance
                & cond_price_change_ratio
                & cond_eps_surprise
                & cond_rev_surprise
                & cond_price_change
            )

            self.df = df[combined_mask]
            self.Debug(f"### DEBUG ### Combined mask final row count: {len(self.df)}")

            # 3) Symbol list
            self.symbol_list = self.df['Symbol'].unique().tolist()
            self.Debug(f"### DEBUG ### Final symbol_list count: {len(self.symbol_list)} => {self.symbol_list}")


    def CustomSecurityInitializer(self, security):
        security.SetLeverage(4)


    def get_updated_database(self):
        x = self.Download("https://docs.google.com/spreadsheets/d/1KoL8P32yXQZiMbnsDAfQ7F4qT8fL73L5/export?format=csv")
        df = pd.read_csv(StringIO(x))

        # Apply the filter conditions directly on the DataFrame
        self.df = df.groupby('Symbol').filter(lambda x: len(x) >= 10 and
                                                    (x['Beat_EPS'].mean() >= config.BEAT_EPS_PERCENTAGE if config.USE_BEAT_EPS else True) and
                                                    (x['Beat_Revenue'].mean() >= config.BEAT_REVENUE_PERCENTAGE if config.USE_BEAT_REVENUE else True) and
                                                    (x['Raised_EPS_Guidance'].sum() > 0 or x['Raised_REVENUE_Guidance'].sum() > 0).mean() >= config.GUIDANCE_RAISED_PERCENTAGE and
                                                    (x['Price_Change'] >= 0.01).mean() >= 0.5 and
                                                    (x['Price_Change'] >= 0.01) and
                                                    (x['EPS_Surprise_Earnings'] >= config.EPS_SURPRISE_PERCENTAGE) and
                                                    (x['Revenue_Surprise_Earnings'] >= config.REVENUE_SURPRISE_PERCENTAGE))
        # Get unique symbols
        self.symbol_list = self.df['Symbol'].unique().tolist()
        self.Debug(f"[LIVE] get_updated_database => symbol_list now has {len(self.symbol_list)} symbols")


    def get_updated_earnings(self):
        x = self.Download("https://docs.google.com/spreadsheets/d/1KoL8P32yXQZiMbnsDAfQ7F4qT8fL73L5/export?format=csv")
        self.earnings_today_df = pd.read_csv(StringIO(x))

        # Debug: fix the loop to iterate properly
        self.Debug("[LIVE] get_updated_earnings => First few rows from earnings_today_df:")
        for i, row in self.earnings_today_df.head(5).iterrows():
            self.Debug(str(row))


    def OnSecuritiesChanged(self, changes):
        for security in changes.RemovedSecurities:
            symbol = security.Symbol
            if symbol in self.symbol_instances:
                self.symbol_instances.pop(symbol, None)

        for security in changes.AddedSecurities:
            symbol = security.Symbol
            if symbol not in self.symbol_instances:
                self.symbol_instances[symbol] = SymbolData(self, symbol)
                # Debug: Show the new_string or the actual ticker
                self.Debug(f"### OnSecuritiesChanged ### Created SymbolData for {symbol} => new_string={self.symbol_instances[symbol].new_string}")


    def CoarseFilterFunction(self, coarse: List[CoarseFundamental]) -> List[Symbol]:
        filtered = [c.Symbol for c in coarse
                    if c.HasFundamentalData
                    and c.Volume >= 1_000_000
                    and c.Symbol.Value in self.symbol_list]
        if len(filtered) == 0:
            self.Debug("WARNING: CoarseFilter returned 0 symbols. Potential mismatch with symbol_list or volume filter.")
        invested = [i.Key for i in self.Portfolio if i.Value.Invested]
        unique = list(set(invested + filtered))
        return unique


    def FineFilterFunction(self, fine: List[FineFundamental]) -> List[Symbol]:
        filtered = [f.Symbol for f in fine if f.MarketCap >= 5_000_000_000]
        if len(filtered) == 0:
            self.Debug("WARNING: FineFilter returned 0 symbols. Potential mismatch with MarketCap filter.")
        invested = [i.Key for i in self.Portfolio if i.Value.Invested]
        unique = list(set(invested + filtered))
        return unique


    def OnData(self, data: Slice):
        # ----------------------------
        # BACKTEST MODE OnData
        # ----------------------------
        if self.mode == "BACKTEST":
            if self.Portfolio.Invested:
                for symbol, symbolData in self.symbol_instances.items():
                    if self.percent_exit_type == "ATR":
                        self.move_pre_entry = 1 + (symbolData.natr.Current.Value/100)
                        self.profit_percentage = 1 + (symbolData.natr.Current.Value/100)
                        self.stop_percentage   = 1 - (symbolData.natr.Current.Value/100)
                        self.trail_percentage  = 1 + (symbolData.natr.Current.Value/100)
                    elif self.percent_exit_type == "STANDARD_DEVIATION":
                        self.move_pre_entry = 1 + (symbolData.stdev_percent)
                        self.profit_percentage = 1 + (symbolData.stdev_percent)
                        self.stop_percentage   = 1 - (symbolData.stdev_percent)
                        self.trail_percentage  = 1 + (symbolData.stdev_percent)

                    if self.Portfolio[symbol].Invested:
                        # profit
                        if (self.Securities[symbol].Price
                            > (self.Portfolio[symbol].AveragePrice * self.profit_percentage)
                            and not symbolData.sold_half
                            and not symbolData.open_profit_order):
                            self.LimitOrder(symbol, -round(self.Portfolio[symbol].Quantity/2), 0, tag="PROFIT")
                            symbolData.sold_half = True
                            symbolData.trail_start = self.Securities[symbol].Price
                            symbolData.open_profit_order = True

                        # stop
                        elif (self.Securities[symbol].Price
                              < (self.Portfolio[symbol].AveragePrice * self.stop_percentage)
                              and not symbolData.open_stop_order):
                            self.LimitOrder(symbol, -self.Portfolio[symbol].Quantity, 0, tag="STOP")
                            symbolData.sold_half = False
                            symbolData.trail_start = None
                            symbolData.open_stop_order = True
                        else:
                            # trailing
                            if symbolData.trail_start is not None and self.Securities[symbol].Price > symbolData.trail_start:
                                symbolData.trail_start = self.Securities[symbol].Price
                            if (symbolData.trail_start is not None
                                and self.Securities[symbol].Price * self.trail_percentage <= symbolData.trail_start
                                and not symbolData.open_trail_order):
                                self.LimitOrder(symbol, -self.Portfolio[symbol].Quantity, 0, tag="TRAIL STOP")
                                symbolData.sold_half = False
                                symbolData.trail_start = None
                                symbolData.open_trail_order = True
                    else:
                        # reset
                        symbolData.open_profit_order = False
                        symbolData.open_trail_order  = False
                        symbolData.open_stop_order   = False
                        symbolData.open_entry_order  = False

            # At 16:00, cancel orders, reset ten_am_price
            if self.Time.hour == 16 and self.Time.minute == 0:
                self.Transactions.CancelOpenOrders()
                for symbol, symbolData in self.symbol_instances.items():
                    symbolData.ten_am_price = self.Securities[symbol].Price
                    symbolData.open_entry_order = False

            # After 16:00, check for new trades
            if self.Time.hour == 16 and self.Time.minute >= 1:
                formatted_date = self.Time.strftime("%Y-%m-%d")

                # -- Debug Check: does formatted_date exist in df["Date_Earnings"]?
                if formatted_date not in self.df["Date_Earnings"].unique():
                    self.Debug(f"MISMATCH WARNING: {formatted_date} not found in df['Date_Earnings'].unique() => {self.df['Date_Earnings'].unique()}")

                # -- Debug Check: does 'amc' exist in Time_Earnings?
                unique_times = self.df["Time_Earnings"].unique().tolist()
                if "amc" not in unique_times:
                    self.Debug(f"MISMATCH WARNING: 'amc' not found in df['Time_Earnings'].unique() => {unique_times}")

                symbols = self.df.loc[
                    (self.df["Date_Earnings"] == formatted_date) &
                    (self.df["Time_Earnings"] == "amc"),
                    "Symbol"
                ]

                if len(symbols) > 0:
                    # Debug: check for symbol mismatch
                    for csv_symbol in symbols:
                        # Do we have any SymbolData with new_string = csv_symbol?
                        if csv_symbol not in [sd.new_string for sd in self.symbol_instances.values()]:
                            self.Debug(f"MISMATCH WARNING: CSV symbol '{csv_symbol}' not found among symbolData.new_string "
                                       f"=> {[sd.new_string for sd in self.symbol_instances.values()]}")

                    for symbol, symbolData in self.symbol_instances.items():
                        if not self.Portfolio.Invested and self.Securities[symbol].Price != 0:
                            cash = self.Portfolio.Cash * self.cash_percent
                            quantity = math.floor(cash / self.Securities[symbol].Price)
                            # Check if symbol is in 'symbols'
                            if symbols.isin([symbolData.new_string]).any():
                                if (not self.Portfolio.Invested
                                    and self.Securities[symbol].Price >= symbolData.ten_am_price * self.move_pre_entry
                                    and not symbolData.open_entry_order
                                    and quantity > 1):
                                    self.LimitOrder(symbol, quantity, self.Securities[symbol].Price)
                                    symbolData.open_entry_order = True
                            if symbolData.open_entry_order:
                                break

        # ----------------------------
        # LIVE MODE OnData
        # ----------------------------
        else:
            if self.Portfolio.Invested:
                for symbol, symbolData in self.symbol_instances.items():
                    if self.percent_exit_type == "ATR":
                        self.move_pre_entry = 1 + (symbolData.natr.Current.Value/100)
                        self.profit_percentage = 1 + (symbolData.natr.Current.Value/100)
                        self.stop_percentage   = 1 - (symbolData.natr.Current.Value/100)
                        self.trail_percentage  = 1 + (symbolData.natr.Current.Value/100)
                    elif self.percent_exit_type == "STANDARD_DEVIATION":
                        self.move_pre_entry = 1 + (symbolData.stdev_percent)
                        self.profit_percentage = 1 + (symbolData.stdev_percent)
                        self.stop_percentage   = 1 - (symbolData.stdev_percent)
                        self.trail_percentage  = 1 + (symbolData.stdev_percent)

                    if self.Portfolio[symbol].Invested:
                        if (self.Securities[symbol].Price
                            > (self.Portfolio[symbol].AveragePrice * self.profit_percentage)
                            and not symbolData.sold_half
                            and not symbolData.open_profit_order):
                            self.LimitOrder(symbol, -round(self.Portfolio[symbol].Quantity/2), 0, tag="PROFIT")
                            symbolData.sold_half = True
                            symbolData.trail_start = self.Securities[symbol].Price
                            symbolData.open_profit_order = True

                        elif (self.Securities[symbol].Price
                              < (self.Portfolio[symbol].AveragePrice * self.stop_percentage)
                              and not symbolData.open_stop_order):
                            self.LimitOrder(symbol, -self.Portfolio[symbol].Quantity, 0, tag="STOP")
                            symbolData.sold_half = False
                            symbolData.trail_start = None
                            symbolData.open_stop_order = True
                        else:
                            if symbolData.trail_start is not None and self.Securities[symbol].Price > symbolData.trail_start:
                                symbolData.trail_start = self.Securities[symbol].Price
                            if (symbolData.trail_start is not None
                                and self.Securities[symbol].Price * self.trail_percentage <= symbolData.trail_start
                                and not symbolData.open_trail_order):
                                self.LimitOrder(symbol, -self.Portfolio[symbol].Quantity, 0, tag="TRAIL STOP")
                                symbolData.sold_half = False
                                symbolData.trail_start = None
                                symbolData.open_trail_order = True
                    else:
                        symbolData.open_profit_order = False
                        symbolData.open_trail_order  = False
                        symbolData.open_stop_order   = False
                        symbolData.open_entry_order  = False

            if self.Time.hour == 16 and self.Time.minute == 0:
                for symbol, symbolData in self.symbol_instances.items():
                    symbolData.ten_am_price = self.Securities[symbol].Price

            if self.Time.hour == 16 and self.Time.minute >= 1:
                formatted_date = self.Time.strftime("%Y-%m-%d")

                # Quick mismatch debug
                if formatted_date not in self.earnings_today_df["Date"].unique():
                    self.Debug(f"[LIVE] MISMATCH: {formatted_date} not in earnings_today_df['Date'].unique() => "
                               f"{self.earnings_today_df['Date'].unique()}")

                # Example logic (depends on your config)
                if config.USE_BEAT_EPS and config.USE_BEAT_REVENUE:
                    symbols = self.earnings_today_df.loc[
                        (self.earnings_today_df["Date"] == formatted_date)
                        & (self.earnings_today_df["Actual"] > self.earnings_today_df["ActEstimate"])
                        & (self.earnings_today_df["RevActual"] > self.earnings_today_df["ActRevEst"])
                        & (self.earnings_today_df["Guidance"] == "pos"),
                        "Symbol"
                    ]
                elif config.USE_BEAT_EPS:
                    symbols = self.earnings_today_df.loc[
                        (self.earnings_today_df["Date"] == formatted_date)
                        & (self.earnings_today_df["Actual"] > self.earnings_today_df["ActEstimate"])
                        & (self.earnings_today_df["Guidance"] == "pos"),
                        "Symbol"
                    ]
                elif config.USE_BEAT_REVENUE:
                    symbols = self.earnings_today_df.loc[
                        (self.earnings_today_df["Date"] == formatted_date)
                        & (self.earnings_today_df["RevActual"] > self.earnings_today_df["ActRevEst"])
                        & (self.earnings_today_df["Guidance"] == "pos"),
                        "Symbol"
                    ]
                else:
                    symbols = self.earnings_today_df.loc[
                        (self.earnings_today_df["Date"] == formatted_date)
                        & (self.earnings_today_df["Guidance"] == "pos"),
                        "Symbol"
                    ]

                if len(symbols) > 0:
                    # Debug check for symbol mismatch
                    for csv_symbol in symbols:
                        if csv_symbol not in [sd.new_string for sd in self.symbol_instances.values()]:
                            self.Debug(f"[LIVE] MISMATCH WARNING: CSV symbol '{csv_symbol}' "
                                       f"not found among symbolData.new_string => {[sd.new_string for sd in self.symbol_instances.values()]}")

                    for symbol, symbolData in self.symbol_instances.items():
                        cash = self.Portfolio.Cash * self.cash_percent
                        quantity = math.floor(cash / self.Securities[symbol].Price)

                        if symbols.isin([symbolData.new_string]).any():
                            if (not self.Portfolio[symbol].Invested
                                and self.Securities[symbol].Price >= symbolData.ten_am_price * self.move_pre_entry
                                and not symbolData.open_entry_order
                                and quantity > 1):
                                self.LimitOrder(symbol, quantity, self.Securities[symbol].Price)
                                symbolData.open_entry_order = True
#region imports
from AlgorithmImports import *
#endregion





class SymbolData():



    def __init__(self, algo, symbol):
        self.symbol = symbol
        self.algo = algo
        self.new_string = str(symbol).split(" ")[0]

        self.natr = NormalizedAverageTrueRange(14)
        self.standard_deviation = StandardDeviation(14)
        self.stdev_percent = None

        self.bar_consolidator = TradeBarConsolidator(timedelta(hours=1))
        self.algo.SubscriptionManager.AddConsolidator(symbol, self.bar_consolidator)
        self.bar_consolidator.DataConsolidated += self.receive_bars


        history = self.algo.History[TradeBar](self.symbol, 1000, Resolution.Minute)

        for bar in history:
            self.bar_consolidator.Update(bar)

        self.ten_am_price = None

        self.algo = algo
        self.sold_half = False
        self.trail_start = None
        self.open_entry_order = False
        self.open_profit_order = False
        self.open_trail_order = False
        self.open_stop_order = False
    


    def receive_bars(self, sender, bar):
        self.natr.Update(bar)
        self.standard_deviation.Update(IndicatorDataPoint(bar.EndTime, bar.Close))

        if self.standard_deviation.IsReady:
            self.stdev_percent = self.standard_deviation.Current.Value / bar.Close