Created with Highcharts 12.1.2EquityJan 2019Jan…Jul 2019Jan 2020Jul 2020Jan 2021Jul 2021Jan 2022Jul 2022Jan 2023Jul 2023Jan 2024Jul 2024Jan 20250100k200k-100-500010120100M200M02.5M5M405060
Overall Statistics
Total Orders
196
Average Win
14.57%
Average Loss
-8.06%
Compounding Annual Return
47.945%
Drawdown
73.600%
Expectancy
0.547
Start Equity
10000
End Equity
103703.10
Net Profit
937.031%
Sharpe Ratio
0.905
Sortino Ratio
0.835
Probabilistic Sharpe Ratio
24.920%
Loss Rate
45%
Win Rate
55%
Profit-Loss Ratio
1.81
Alpha
0.397
Beta
0.862
Annual Standard Deviation
0.537
Annual Variance
0.289
Information Ratio
0.738
Tracking Error
0.518
Treynor Ratio
0.564
Total Fees
$629.60
Estimated Strategy Capacity
$340000000.00
Lowest Capacity Asset
MSTR RBGP9S2961YD
Portfolio Turnover
8.97%
from AlgorithmImports import *
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import numpy as np
import pandas as pd

class MLTradingAlgorithm(QCAlgorithm):
    
    def Initialize(self):
        # 1. Setup Algorithm Parameters
        self.SetStartDate(2019, 1, 1)  # Start date
        self.SetEndDate(2024, 12, 31)  # End date
        self.SetCash(10000)            # Initial capital

        # 2. Add Equity (MSTR)
        self.symbol = self.AddEquity("MSTR", Resolution.Daily).Symbol

        # 3. Rolling Window for 200 Days of TradeBar Data
        self.data = RollingWindow[TradeBar](200)  # Store last 200 bars

        # 4. Warm-Up Period to ensure rolling window is filled
        self.SetWarmUp(200)

        # 5. Initialize Machine Learning Model
        self.model = RandomForestClassifier(n_estimators=100, random_state=42)
        self.training_count = 0
        self.is_model_trained = False  # Flag to check if model is trained

        # 6. Schedule Training Every Monday at 10:00 AM
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday), 
                         self.TimeRules.At(10, 0), 
                         self.TrainModel)
    
    def OnData(self, data):
        # 7. Check data existence
        if not data.ContainsKey(self.symbol):
            return
        trade_bar = data[self.symbol]
        if trade_bar is None:
            return
        
        # 8. Add TradeBar data to rolling window
        self.data.Add(trade_bar)

        # 9. Ensure rolling window is ready
        if not self.data.IsReady or self.data.Count < 200:
            return
        
        # Check if the model is trained
        if not self.is_model_trained:
            self.Debug("Model not trained yet. Skipping prediction.")
            return

        # 10. Prepare the latest feature row
        df = self.GetFeatureDataFrame()
        if df is None or len(df) == 0:
            return
        
        # Extract the most recent feature row (X)
        latest_features = df.iloc[-1, :-1].values.reshape(1, -1)

        # 11. Predict (try-catch in case model fails)
        try:
            prediction = self.model.predict(latest_features)[0]  # 1 = Buy, 0 = Sell
        except:
            self.Debug("Error during prediction.")
            return
        
        # 12. Execute Trading Logic
        holdings = self.Portfolio[self.symbol].Quantity
        
        # If prediction is Buy (1), and we don't currently hold shares, go long
        if prediction == 1 and holdings <= 0:
            self.SetHoldings(self.symbol, 1.0)
        # If prediction is Sell (0), and we do hold shares, liquidate
        elif prediction == 0 and holdings > 0:
            self.Liquidate(self.symbol)

    def TrainModel(self):
        # Prepare features for training
        df = self.GetFeatureDataFrame()
        if df is None or len(df) < 50:
            self.Debug("Insufficient data for training.")
            return
        
        # Split into X (features) and y (target)
        X = df.iloc[:, :-1]
        y = df.iloc[:, -1]
        
        # 80/20 time-based split
        X_train, X_test, y_train, y_test = train_test_split(
            X, y, test_size=0.2, shuffle=False, random_state=42
        )

        # Fit the model
        self.model.fit(X_train, y_train)
        self.is_model_trained = True

        # Evaluate model accuracy
        y_train_pred = self.model.predict(X_train)
        train_accuracy = accuracy_score(y_train, y_train_pred)

        y_test_pred = self.model.predict(X_test)
        test_accuracy = accuracy_score(y_test, y_test_pred)

        self.training_count += 1
        self.Debug(f"Training #{self.training_count}: "
                   f"Train Accuracy: {train_accuracy:.2%}, "
                   f"Test Accuracy: {test_accuracy:.2%}")

    def GetFeatureDataFrame(self):
        """
        Convert rolling window data to DataFrame and compute:
          - Bollinger Bands (20-day)
          - Historical Volatility (HV_30)
          - Target variable (1 if next day's Close > today's Close, else 0)
        """
        if self.data.Count < 200:
            return None
        
        # Extract close prices
        close_prices = [bar.Close for bar in self.data]
        df = pd.DataFrame(close_prices, columns=["Close"])
        
        # -----------------------------------------
        # 1) Bollinger Bands (20-day)
        # -----------------------------------------
        period = 20
        df["BB_mid"] = df["Close"].rolling(period).mean()
        df["BB_std"] = df["Close"].rolling(period).std()
        df["BB_upper"] = df["BB_mid"] + 2 * df["BB_std"]
        df["BB_lower"] = df["BB_mid"] - 2 * df["BB_std"]
        
        # -----------------------------------------
        # 2) Historical Volatility (HV_30)
        #    annualized = std dev of daily returns * sqrt(252)
        # -----------------------------------------
        df["daily_returns"] = df["Close"].pct_change()
        df["HV_30"] = df["daily_returns"].rolling(window=30).std() * np.sqrt(252)
        
        # -----------------------------------------
        # 3) Target variable
        #    1 = next day close > current day close
        #    0 = otherwise
        # -----------------------------------------
        df["Target"] = (df["Close"].shift(-1) > df["Close"]).astype(int)
        
        # Clean up
        df.dropna(inplace=True)
        
        # Drop any columns we don't want in the features
        # daily_returns is intermediate; keep HV_30, Bollinger bands
        df.drop(columns=["daily_returns"], inplace=True)
        
        return df