Overall Statistics
from AlgorithmImports import *
import xgboost as xgb
import pandas as pd
import numpy as np
from datetime import time, timedelta

class IntradayXGBoostES(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2024, 1, 1)
        self.SetEndDate(2024, 2, 1)
        self.SetCash(100000)

        future = self.AddFuture(Futures.Indices.SP500EMini, Resolution.Minute)
        future.SetFilter(timedelta(0), timedelta(180))
        self.future_symbol = future.Symbol

        self.model = None
        self.lookback_days = 30
        self.retrain_every_n_days = 5
        self.bar_buffer = {}
        self.training_data = []
        self.labels = []
        self.last_training_date = None
        self.current_day = None
        self.history = None

        self.SetWarmUp(timedelta(days=30))

        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(9, 0), self.ResetDailyCache)
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(10, 0), self.TradeSignal)
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(12, 0), self.ExitPosition)

    def ResetDailyCache(self):
        self.current_day = self.Time.date()
        self.bar_buffer[self.current_day] = []

    def OnData(self, data):
        if self.IsWarmingUp or self.future_symbol not in data: return
        bar = data[self.future_symbol]
        if self.current_day not in self.bar_buffer:
            self.bar_buffer[self.current_day] = []
        self.bar_buffer[self.current_day].append(bar)

    def TradeSignal(self):
        if self.model is None and self.history is None:
            self.history = self.History(self.future_symbol, timedelta(days=60), Resolution.Minute)
            self.Debug(f"Loaded historical data: {len(self.history)} rows")
            self.TrainModel()
            self.last_training_date = self.Time.date()

        if self.current_day not in self.bar_buffer:
            return

        bars = [b for b in self.bar_buffer[self.current_day] if time(9, 0) <= b.EndTime.time() < time(10, 0)]
        if len(bars) < 30:
            return

        df = pd.DataFrame([{
            'time': b.EndTime,
            'open': b.Open,
            'high': b.High,
            'low': b.Low,
            'close': b.Close,
            'volume': b.Volume
        } for b in bars]).set_index('time')

        vwap = (df['close'] * df['volume']).sum() / df['volume'].sum()
        features = {
            'ret_5m': df['close'].pct_change(5).iloc[-1],
            'ret_10m': df['close'].pct_change(10).iloc[-1],
            'open_close_range': (df['close'].iloc[-1] - df['close'].iloc[0]) / df['close'].iloc[0],
            'vwap': vwap,
            'price_above_vwap': df['close'].iloc[-1] / vwap,
            'vwap_slope': (df['close'][-10:] * df['volume'][-10:]).sum() / df['volume'][-10:].sum()
                          - (df['close'][:10] * df['volume'][:10]).sum() / df['volume'][:10].sum(),
            'vwap_deviation_at_close': df['close'].iloc[-1] - vwap,
            'early_range': df['high'][:6].max() - df['low'][:6].min(),
            'volume_imbalance': df['volume'][-5:].sum() / max(1, df['volume'][:5].sum()),
            'reversal_from_high': df['high'].max() - df['close'].iloc[-1],
            'reversal_from_low': df['close'].iloc[-1] - df['low'].min()
        }

        X_today = pd.DataFrame([features])

        if self.model and (self.Time.date() - self.last_training_date).days >= self.retrain_every_n_days:
            self.TrainModel()
            self.last_training_date = self.Time.date()

        if self.model:
            pred = self.model.predict(X_today)[0]
            self.Debug(f"{self.Time} Prediction: {pred}")
            if pred == 1:
                self.MarketOrder(self.future_symbol, 1)

    def ExitPosition(self):
        self.Liquidate(self.future_symbol)

    def TrainModel(self):
        self.Debug(f"[{self.Time.date()}] Starting training routine...")
        self.training_data = []
        self.labels = []

        df = self.history.copy()
        df = df.sort_index()

        # Handle MultiIndex correctly
        if isinstance(df.index, pd.MultiIndex):
            df.index = df.index.get_level_values(1)

        # ✅ Final, bulletproof datetime extraction
        dates = sorted(set([x.date() for x in df.index]))

        recent_days = dates[-self.lookback_days:]

        for day in recent_days:
            df_day = df[df.index.date == day]
            df_day = df_day.between_time("09:00", "12:00")

            if len(df_day) < 60:
                self.Debug(f"[{day}] Skipped: not enough data")
                continue

            try:
                entry_bar = df_day.between_time("10:00", "10:00").iloc[0]
                exit_bar = df_day.between_time("12:00", "12:00").iloc[0]
            except IndexError:
                self.Debug(f"[{day}] Skipped: missing entry or exit bar")
                continue

            label = 1 if (exit_bar['close'] / entry_bar['open'] - 1) > 0.0 else 0  # relaxed for debug

            df_feat = df_day.between_time("09:00", "09:59")
            if len(df_feat) < 30:
                self.Debug(f"[{day}] Skipped: insufficient pre-market bars")
                continue

            vwap = (df_feat['close'] * df_feat['volume']).sum() / df_feat['volume'].sum()

            feat = {
                'ret_5m': df_feat['close'].pct_change(5).iloc[-1],
                'ret_10m': df_feat['close'].pct_change(10).iloc[-1],
                'open_close_range': (df_feat['close'].iloc[-1] - df_feat['close'].iloc[0]) / df_feat['close'].iloc[0],
                'vwap': vwap,
                'price_above_vwap': df_feat['close'].iloc[-1] / vwap,
                'vwap_slope': (df_feat['close'][-10:] * df_feat['volume'][-10:]).sum() / df_feat['volume'][-10:].sum()
                              - (df_feat['close'][:10] * df_feat['volume'][:10]).sum() / df_feat['volume'][:10].sum(),
                'vwap_deviation_at_close': df_feat['close'].iloc[-1] - vwap,
                'early_range': df_feat['high'][:6].max() - df_feat['low'][:6].min(),
                'volume_imbalance': df_feat['volume'][-5:].sum() / max(1, df_feat['volume'][:5].sum()),
                'reversal_from_high': df_feat['high'].max() - df_feat['close'].iloc[-1],
                'reversal_from_low': df_feat['close'].iloc[-1] - df_feat['low'].min()
            }

            self.training_data.append(feat)
            self.labels.append(label)

        if self.training_data:
            df_train = pd.DataFrame(self.training_data)
            pos = sum(self.labels)
            total = len(self.labels)
            self.Debug(f"[{self.Time.date()}] Training samples: {total}, Positives: {pos} ({pos/total:.1%})")

            self.model = xgb.XGBClassifier(
                n_estimators=100,
                max_depth=3,
                learning_rate=0.05,
                subsample=0.8,
                colsample_bytree=0.8,
                random_state=42,
                use_label_encoder=False,
                eval_metric='logloss'
            )
            self.model.fit(df_train, self.labels)
        else:
            self.Debug(f"[{self.Time.date()}] No valid training data — model not built.")