Overall Statistics |
Total Orders 607 Average Win 0.91% Average Loss -0.95% Compounding Annual Return 7.489% Drawdown 23.700% Expectancy 0.159 Start Equity 100000 End Equity 153921.40 Net Profit 53.921% Sharpe Ratio 0.272 Sortino Ratio 0.277 Probabilistic Sharpe Ratio 6.656% Loss Rate 41% Win Rate 59% Profit-Loss Ratio 0.96 Alpha 0.032 Beta -0.004 Annual Standard Deviation 0.115 Annual Variance 0.013 Information Ratio -0.36 Tracking Error 0.201 Treynor Ratio -8.631 Total Fees $2526.52 Estimated Strategy Capacity $1000000000.00 Lowest Capacity Asset MSTR RBGP9S2961YD Portfolio Turnover 5.44% |
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) self.SetEndDate(2024, 12, 31) self.SetCash(100000) # Increase to 100k for more comfortable partial allocation # 2. Configurable partial allocation (20% of portfolio by default) self.allocation = 0.20 # 3. Add Equity self.symbol = self.AddEquity("MSTR", Resolution.Daily).Symbol # 4. Rolling Window for 200 Days self.data = RollingWindow[TradeBar](200) self.SetWarmUp(200) # 5. Random Forest Model self.model = RandomForestClassifier(n_estimators=100, random_state=42) self.training_count = 0 self.is_model_trained = False # 6. Schedule Weekly Retraining self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday), self.TimeRules.At(10, 0), self.TrainModel) # 7. Trailing Stop Tracking self.highestPrice = 0 self.trailStopTicket = None self.trailing_stop_pct = 1 # Example: 5% trailing stop below highest price def OnData(self, data): # Check for valid data if not data.ContainsKey(self.symbol): return trade_bar = data[self.symbol] if trade_bar is None: return # Update rolling window self.data.Add(trade_bar) if not self.data.IsReady or self.data.Count < 200: return # Skip if model not trained if not self.is_model_trained: return # Build features for the latest bar df = self.GetFeatureDataFrame() if df is None or len(df) == 0: return latest_features = df.iloc[-1, :-1].values.reshape(1, -1) try: prediction = self.model.predict(latest_features)[0] # 1 = Buy, 0 = Sell except: return holdings = self.Portfolio[self.symbol].Quantity # -- Trading Logic -- # If prediction = 1 (bullish), we want to go long (to 'allocation') if not already if prediction == 1 and holdings <= 0: # Liquidate any short holdings just in case if holdings < 0: self.Liquidate(self.symbol) self.ResetTrailingStop() # Go long with partial allocation self.SetHoldings(self.symbol, self.allocation) # Reset trailing stop logic for a new position self.highestPrice = trade_bar.Close self.PlaceOrUpdateTrailingStop() # If prediction = 0 (bearish), we want to close any existing position elif prediction == 0 and holdings > 0: self.Liquidate(self.symbol) self.ResetTrailingStop() # -- Update Trailing Stop if holding a long position -- if holdings > 0: # If price made a new high, adjust the trailing stop upward if trade_bar.Close > self.highestPrice: self.highestPrice = trade_bar.Close self.PlaceOrUpdateTrailingStop() def TrainModel(self): # Prepare training data df = self.GetFeatureDataFrame() if df is None or len(df) < 50: self.Debug("Insufficient data for training.") return X = df.iloc[:, :-1] y = df.iloc[:, -1] # 80/20 split (time-based) 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 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_pred, y_test) 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): """ Build features: Bollinger Bands + Historical Volatility, target = next-day close up/down """ if self.data.Count < 200: return None close_prices = [bar.Close for bar in self.data] df = pd.DataFrame(close_prices, columns=["Close"]) # 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"] # Historical Volatility (30-day) df["daily_returns"] = df["Close"].pct_change() df["HV_30"] = df["daily_returns"].rolling(window=30).std() * np.sqrt(252) # Target df["Target"] = (df["Close"].shift(-1) > df["Close"]).astype(int) # Drop NaN df.dropna(inplace=True) # Drop intermediate columns not used as features df.drop(columns=["daily_returns"], inplace=True) return df # ---------------------------------------------------------------- # Trailing Stop Logic # ---------------------------------------------------------------- def PlaceOrUpdateTrailingStop(self): """ Places a stop-market order ticket if we don't have one, or updates the stop price if we do. """ quantity = self.Portfolio[self.symbol].Quantity if quantity <= 0: return # Trailing stop is 5% below highestPrice, for example newStopPrice = self.highestPrice * self.trailing_stop_pct if not self.trailStopTicket or self.trailStopTicket.Status in [OrderStatus.Filled, OrderStatus.Canceled, OrderStatus.Invalid]: # Create a new stop-market ticket (negative quantity = sell) self.trailStopTicket = self.StopMarketOrder(self.symbol, -quantity, newStopPrice) else: # Update existing stop ticket updateFields = UpdateOrderFields() updateFields.StopPrice = newStopPrice self.trailStopTicket.Update(updateFields) def ResetTrailingStop(self): """ Resets tracking variables and cancels any active stop ticket when we exit or reverse position. """ self.highestPrice = 0 if self.trailStopTicket is not None: # Cancel the trailing stop ticket if it is open if self.trailStopTicket.Status not in [OrderStatus.Filled, OrderStatus.Canceled, OrderStatus.Invalid]: self.trailStopTicket.Cancel("Exiting position or reversing trade.") self.trailStopTicket = None