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
-0.658
Tracking Error
0.175
Treynor Ratio
0
Total Fees
$0.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
Portfolio Turnover
0%
# region imports
from AlgorithmImports import *
import pandas as pd
from collections import defaultdict
import io
# endregion


class ComprehensiveEarningsDateComparisonWithTime(QCAlgorithm): # Renamed class slightly

    def Initialize(self):
        # --- Critical Settings ---
        self.object_store_key = "filteredEarnings.csv"
        # Save results under a new key to avoid overwriting previous results
        self.results_save_key = "earnings_comparison_details_with_time.csv"

        # --- Use explicit dates based on your CSV range ---
        self.SetStartDate(2020, 1, 17) # Slightly before first CSV date
        self.SetEndDate(2024, 12, 25)   # Slightly after last CSV date

        self.SetCash(100000)

        # --- Data Structures ---
        # ---> CHANGE 2: Modify data structure to store {date: time_str} mapping <---
        # Structure: { symbol: { datetime.date : "BMO" / "AMC" } }
        self.csv_earnings_by_symbol = defaultdict(dict)
        self.csv_unique_symbols = set()
        self.comparison_summary = {"matches": 0, "mismatches": 0, "qc_events_for_csv_symbols": 0}
        self.detailed_results = []
        self.processed_qc_dates = set()

        # --- CSV Loading ---
        if not self.ObjectStore.ContainsKey(self.object_store_key):
            self.Error(f"'{self.object_store_key}' not found in Object Store.")
            self.Quit()
            return

        try:
            file_path = self.ObjectStore.GetFilePath(self.object_store_key)
            # Load all three columns now
            earnings_data_df = pd.read_csv(file_path, header=None, names=["symbol", "earnings_date", "earnings_time_str"])

            # Parse date correctly, keeping only the date part
            earnings_data_df["date_only"] = pd.to_datetime(earnings_data_df["earnings_date"].str.slice(0, 19), format="%Y-%m-%d %H:%M:%S").dt.date

            # ---> CHANGE 2 (cont.): Populate the modified data structure <---
            processed_count = 0
            for index, row in earnings_data_df.iterrows():
                symbol_str = row["symbol"]
                earnings_dt = row["date_only"]
                time_str_raw = row["earnings_time_str"]

                # Map CSV time string to standardized BMO/AMC
                if "Before market open" in time_str_raw:
                    time_std = "BMO"
                elif "After market close" in time_str_raw:
                    time_std = "AMC"
                else:
                    # Log a warning if unexpected value found in CSV time column
                    self.Debug(f"Warning: Unexpected time string '{time_str_raw}' in CSV for {symbol_str} on {earnings_dt}. Skipping this entry.")
                    continue # Skip rows with unexpected time strings

                # Store date -> time mapping for the symbol
                self.csv_earnings_by_symbol[symbol_str][earnings_dt] = time_std
                self.csv_unique_symbols.add(symbol_str)
                processed_count += 1

            self.Log(f"Loaded and processed {processed_count} earnings records (with valid BMO/AMC) from CSV for {len(self.csv_unique_symbols)} unique symbols.")
            del earnings_data_df # Free up memory

        except Exception as e:
            self.Error(f"Error processing CSV file: {e}")
            self.Quit()
            return
        # --- End CSV Loading ---

        # --- Universe Setup ---
        self.UniverseSettings.Resolution = Resolution.Daily
        # Assuming EODHDUpcomingEarnings provides the ReportTime attribute
        self.AddUniverse(EODHDUpcomingEarnings, self.SelectionFilter)
        # --- End Universe Setup ---

    # Removed get_csv_date_range as per user request

    def SelectionFilter(self, earnings: List[EODHDUpcomingEarnings]) -> List[Symbol]:
        """
        Compares QC earnings dates AND times against CSV data and stores detailed results.
        """
        for earning_event in earnings:
            symbol_str = earning_event.Symbol.Value
            qc_report_date = earning_event.ReportDate.date()
            # ---> CHANGE 3: Get QC Report Time and map it <---
            qc_report_time_enum = earning_event.ReportTime

            if qc_report_time_enum == 0:
                qc_time_std = "BMO"
            elif qc_report_time_enum == 1:
                qc_time_std = "AMC"
            # Add handling for other potential QC times if necessary (e.g., DURING_MARKET)
            # elif qc_report_time_enum == ReportTime.DURING_MARKET_HOURS:
            #     qc_time_std = "DURING" # Decide how to handle this - treat as mismatch?
            else:
                qc_time_std = "UNKNOWN"
                # Only log UNKNOWN once per symbol/date to avoid spamming
                if (symbol_str, qc_report_date, "QC_Time_Unknown") not in self.processed_qc_dates:
                     self.Debug(f"Warning: Unknown/Unhandled QC ReportTime '{qc_report_time_enum}' for {symbol_str} on {qc_report_date}")
                     self.processed_qc_dates.add((symbol_str, qc_report_date, "QC_Time_Unknown"))


            # Use a combined key including time for processing check if needed, but date check is likely sufficient
            event_key = (symbol_str, qc_report_date) # Sticking to date key for processed check

            # Process only relevant symbols & new events
            if symbol_str in self.csv_unique_symbols and event_key not in self.processed_qc_dates:
                self.comparison_summary["qc_events_for_csv_symbols"] += 1
                self.processed_qc_dates.add(event_key)

                # ---> CHANGE 3 (cont.): Perform combined date and time check <---
                csv_date_time_map = self.csv_earnings_by_symbol.get(symbol_str, {})
                csv_expected_time_std = csv_date_time_map.get(qc_report_date, "N/A") # Get expected time if date exists

                match_status = False # Default to mismatch
                if qc_report_date in csv_date_time_map:
                    # Date matches, now check if time matches
                    if csv_expected_time_std == qc_time_std:
                        match_status = True
                    # Else: Date matches but time doesn't -> remains mismatch
                # Else: Date doesn't match -> remains mismatch

                # Store detailed results, including time info
                result_detail = {
                    "Symbol": symbol_str,
                    "QC_Report_Date": qc_report_date,
                    "QC_Report_Time": qc_time_std, # Add QC mapped time
                    "CSV_Expected_Time": csv_expected_time_std, # Add CSV mapped time for this date
                    "Match_Status": match_status
                    # Removed CSV_Dates_For_Symbol_List to keep results simpler,
                    # can be added back if needed for debugging mismatches later
                }
                self.detailed_results.append(result_detail)

                # Update summary counts
                if match_status:
                    self.comparison_summary["matches"] += 1
                else:
                    self.comparison_summary["mismatches"] += 1
                    # Optional: Debug log for mismatches showing times
                    # self.Debug(f"MISMATCH: {symbol_str} {qc_report_date} QC:{qc_time_std} CSV:{csv_expected_time_std}")


        return [] # No need to subscribe to equity data

    def OnEndOfAlgorithm(self):
        """
        Log summary statistics and save detailed results (including time) to Object Store.
        """
        self.Log("--- End of Algorithm: Earnings Date & Time Comparison Summary ---") # Updated title
        total_compared = self.comparison_summary["qc_events_for_csv_symbols"]
        matches = self.comparison_summary["matches"]
        mismatches = self.comparison_summary["mismatches"]

        self.Log(f"Backtest Range: {self.StartDate.date()} to {self.EndDate.date()}")
        self.Log(f"Total QC Earning Events Checked (for symbols in CSV): {total_compared}")
        self.Log(f"Matching Dates & Times Found: {matches}") # Updated label
        self.Log(f"Mismatched Dates or Times Found: {mismatches}") # Updated label

        if total_compared > 0:
            accuracy = (matches / total_compared) * 100
            self.Log(f"Accuracy (Matching Date & Time / Total Checked): {accuracy:.2f}%") # Updated label
        else:
            self.Log("No QC earning events found for symbols listed in the CSV during the backtest period.")

        # --- Save detailed results to Object Store ---
        if self.detailed_results:
            self.Log(f"Preparing to save {len(self.detailed_results)} detailed comparison results...")
            try:
                results_df = pd.DataFrame(self.detailed_results)

                # Convert date objects to strings for reliable CSV storage
                # No complex list columns added this time, simpler conversion.
                results_df['QC_Report_Date'] = results_df['QC_Report_Date'].astype(str)

                # ---> CHANGE 4: Ensure new columns QC_Report_Time, CSV_Expected_Time are saved (automatic with DataFrame) <---
                save_path = self.ObjectStore.Save(self.results_save_key, results_df.to_csv(index=False))

                if save_path:
                    self.Log(f"Detailed comparison results successfully saved to Object Store: {save_path}")
                else:
                     self.Error("Failed to save detailed results to Object Store. Save method returned empty path.")

            except Exception as e:
                self.Error(f"Error saving detailed results to Object Store: {e}")
        else:
            self.Log("No detailed results were generated to save.")
        # --- End Save ---