Overall Statistics
Total Orders
247
Average Win
1.78%
Average Loss
-2.34%
Compounding Annual Return
77.101%
Drawdown
15.300%
Expectancy
0.619
Start Equity
100000
End Equity
556051.20
Net Profit
456.051%
Sharpe Ratio
4.252
Sortino Ratio
3.531
Probabilistic Sharpe Ratio
100.000%
Loss Rate
8%
Win Rate
92%
Profit-Loss Ratio
0.76
Alpha
0.434
Beta
0.561
Annual Standard Deviation
0.106
Annual Variance
0.011
Information Ratio
4.526
Tracking Error
0.093
Treynor Ratio
0.803
Total Fees
$637.73
Estimated Strategy Capacity
$0
Lowest Capacity Asset
SPY R735QTJ8XC9X
Portfolio Turnover
22.37%
from AlgorithmImports import *
import joblib
import os
import numpy as np
import io # Import the io module

class MLBasedTradingStrategy(QCAlgorithm):
    def Initialize(self):
        # Set a reasonable backtest period for daily resolution and indicator warm-up
        self.SetStartDate(2022, 1, 1)
        # ✅ FIXED: Extend the end date to ensure enough time for warm-up and trading
        self.SetEndDate(2024, 12, 31) # Example: Backtest until end of 2024
        # Or, to run until the latest available data for a backtest:
        # self.SetEndDate(self.Time.date().replace(year=self.Time.date().year - 1)) # Example: Up to last year's end
        # Or simply remove SetEndDate() for backtests to run until current date based on data availability,
        # but be mindful if it hits "current date" before warm-up completes.
        # For reliable backtest duration, a fixed future date is often best.

        self.SetCash(100000)

        # Add the equity subscription for SPY
        self.symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
        
        # Initialize model variables
        self.model = None
        self.model_loaded = False
        self.last_prediction = None # To store the last prediction for debugging or later use

        # === Indicators ===
        self.sma50 = self.SMA(self.symbol, 50)
        self.sma200 = self.SMA(self.symbol, 200)
        self.ema50 = self.EMA(self.symbol, 50)
        self.macd = self.MACD(self.symbol, 12, 26, 9, MovingAverageType.Exponential, Resolution.Daily)
        self.rsi14 = self.RSI(self.symbol, 14)
        self.stoch = self.STO(self.symbol, 14, 3, 3) 
        self.bb = self.BB(self.symbol, 20, 2) 
        self.atr = self.ATR(self.symbol, 14)
        self.obv = self.OBV(self.symbol, Resolution.Daily)
        self.volume_sma20 = self.SMA(self.symbol, 20, Resolution.Daily, Field.Volume)

        # Initialize RollingWindow for CMF calculation
        self.cmf_window = RollingWindow[TradeBar](20)

        # Set a warm-up period to ensure all indicators have enough data before the strategy starts trading
        self.SetWarmUp(self.sma200.Period + 5) # Warm-up for 205 daily bars

        # --- Model Loading ---
        model_name = "rf_trading_model_SPY.pkl" 
        
        if self.ObjectStore.ContainsKey(model_name):
            try:
                model_bytes = self.ObjectStore.ReadBytes(model_name)
                self.model = joblib.load(io.BytesIO(model_bytes)) 
                self.model_loaded = True
                self.Debug(f"Model '{model_name}' loaded successfully from ObjectStore.")
            except Exception as e:
                self.Error(f"Error loading model '{model_name}' from ObjectStore: {e}")
        else:
            self.Error(f"Model file '{model_name}' not found in ObjectStore. Please ensure it's uploaded with this exact name.")

        # Schedule the prediction and trading logic
        # This will run daily 5 minutes after market open, but only AFTER warm-up is finished.
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen(self.symbol, 5), self.PredictAndTradeIfReady)

        self.Debug("Initialization complete.")

    def OnData(self, data):
        # This method is called whenever new data arrives.
        # Add the current TradeBar to the RollingWindow for CMF calculation.
        if data.Bars.ContainsKey(self.symbol):
            self.cmf_window.Add(data.Bars[self.symbol])

    def PredictAndTradeIfReady(self):
        # Added a check to ensure warm-up is complete
        if self.IsWarmingUp:
            return

        self.PredictAndTrade() # Call the actual prediction and trade logic

    def PredictAndTrade(self):
        # 1. Check if the model is loaded
        if not self.model_loaded or self.model is None:
            self.Debug("Model not loaded yet or failed to load. Skipping prediction and trade.")
            return

        # 2. Check if all required indicators and rolling window are ready
        indicators_ready = all([
            self.sma50.IsReady,
            self.sma200.IsReady,
            self.ema50.IsReady,
            self.macd.IsReady,
            self.rsi14.IsReady,
            self.stoch.IsReady,
            self.bb.IsReady,
            self.atr.IsReady,
            self.obv.IsReady,
            self.volume_sma20.IsReady,
            self.cmf_window.IsReady 
        ])

        if not indicators_ready:
            self.Debug("Indicators or CMF window not ready. Skipping prediction.")
            return

        # 3. Get current security data
        security = self.Securities[self.symbol]
        
        if not security.HasData:
            self.Debug("No current data for SPY. Skipping prediction.")
            return

        price = security.Price
        volume = security.Volume

        # 4. Calculate Chaikin Money Flow (CMF)
        cmf = self.CalculateCMF()

        # 5. Prepare features for the model
        features = [
            security.Open,
            security.High,
            security.Low,
            price,
            volume,
            self.sma50.Current.Value,
            self.sma200.Current.Value,
            self.ema50.Current.Value,
            self.macd.Current.Value,
            self.rsi14.Current.Value,
            self.stoch.StochK.Current.Value,
            self.stoch.StochD.Current.Value,
            self.bb.UpperBand.Current.Value,
            self.bb.LowerBand.Current.Value,
            self.atr.Current.Value,
            self.obv.Current.Value,
            cmf,
            0.0  # Market sentiment (Placeholder)
        ]

        features_array = np.array(features).reshape(1, -1)

        try:
            # 6. Make prediction
            prediction = self.model.predict(features_array)[0]
            self.last_prediction = prediction 
            self.Debug(f"Prediction: {prediction}")

            # 7. Execute trades based on prediction
            if prediction == 1 and not self.Portfolio[self.symbol].Invested:
                self.SetHoldings(self.symbol, 1) 
                self.Debug("BUY signal executed: Going long.")
            elif prediction == -1 and self.Portfolio[self.symbol].Invested:
                self.Liquidate(self.symbol) 
                self.Debug("SELL signal executed: Liquidating position.")
            elif prediction == 0:
                self.Debug("HOLD signal: No action taken.")
            
        except Exception as e:
            self.Error(f"Error during prediction or trading logic: {e}")

    def CalculateCMF(self):
        if not self.cmf_window.IsReady:
            return 0.0 

        money_flow_volume = 0.0
        total_volume = 0.0

        for bar in self.cmf_window:
            typical_price = (bar.High + bar.Low + bar.Close) / 3
            
            if (bar.High - bar.Low) != 0: 
                mfm = ((bar.Close - bar.Low) - (bar.High - bar.Close)) / (bar.High - bar.Low)
            else:
                mfm = 0.0
            
            mfv = mfm * bar.Volume
            
            money_flow_volume += mfv
            total_volume += bar.Volume

        return money_flow_volume / total_volume if total_volume > 0 else 0.0