Overall Statistics |
Total Orders 83 Average Win 12.24% Average Loss -5.46% Compounding Annual Return 19.554% Drawdown 32.700% Expectancy 0.818 Start Equity 10000 End Equity 29066.76 Net Profit 190.668% Sharpe Ratio 0.567 Sortino Ratio 0.454 Probabilistic Sharpe Ratio 11.181% Loss Rate 44% Win Rate 56% Profit-Loss Ratio 2.24 Alpha 0.115 Beta 0.19 Annual Standard Deviation 0.238 Annual Variance 0.057 Information Ratio 0.11 Tracking Error 0.271 Treynor Ratio 0.713 Total Fees $724.40 Estimated Strategy Capacity $270000000.00 Lowest Capacity Asset MARA VSI9G9W3OAXX Portfolio Turnover 1.17% |
from AlgorithmImports import * from sklearn.svm import SVC from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score import numpy as np import pandas as pd # ------------------------------ # 1) Custom Models: 0.1% Fee & 0 Slippage # ------------------------------ class CustomFeeModel(FeeModel): """ Applies a 0.1% transaction fee on each trade (open/close). """ def GetOrderFee(self, security, order): orderValue = security.Price * abs(order.Quantity) fee = 0.001 * orderValue # 0.1% of trade notional return fee class CustomSlippageModel: """ Sets slippage to 0. """ def GetSlippageApproximation(self, security, order): return 0 class MLTradingAlgorithm(QCAlgorithm): def Initialize(self): # 1. Algorithm Parameters self.SetStartDate(2019, 1, 1) # Start date self.SetEndDate(2024, 12, 31) # End date self.SetCash(10000) # Increased capital for demonstration # 2. Add MSTR Equity with custom fee & slippage self.symbol = self.AddEquity("MARA", Resolution.Daily).Symbol self.Securities[self.symbol].SetFeeModel(CustomFeeModel()) self.Securities[self.symbol].SetSlippageModel(CustomSlippageModel()) # 3. RollingWindow to Store 200 Days of TradeBar Data self.data = RollingWindow[TradeBar](200) # 4. Warm-Up Period self.SetWarmUp(200) # 5. Initialize SVM Model # probability=True so we can get class probabilities self.model = SVC(probability=True, random_state=42) self.training_count = 0 self.is_model_trained = False # Tracks if the model is trained # 6. Partial Allocation (e.g., 20% of total capital) self.allocation_fraction = 0.3 # 7. Add SPY for Benchmark self.spySymbol = self.AddEquity("SPY", Resolution.Daily).Symbol self.Securities[self.spySymbol].SetFeeModel(CustomFeeModel()) self.Securities[self.spySymbol].SetSlippageModel(CustomSlippageModel()) self.SetBenchmark(self.spySymbol) # For measuring SPY buy-and-hold returns: self.spyPriceStart = None # will set once we see first price # 8. Track initial capital to measure strategy returns self.initialCapital = None # 9. Schedule Model 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): # Set initial capital & SPY start price (only once) if self.initialCapital is None: self.initialCapital = self.Portfolio.TotalPortfolioValue if self.spyPriceStart is None and data.ContainsKey(self.spySymbol): bar = data[self.spySymbol] if bar and bar.Close > 0: self.spyPriceStart = bar.Close # Ensure MSTR data is available if not data.ContainsKey(self.symbol): return trade_bar = data[self.symbol] if trade_bar is None: return # Add TradeBar to Rolling Window self.data.Add(trade_bar) # Check if RollingWindow is Ready if not self.data.IsReady or self.data.Count < 200: return # Ensure Model is Fitted Before Using It if not self.is_model_trained: self.Debug("Model is not trained yet. Skipping prediction.") return # Extract Features for Prediction df = self.GetFeatureDataFrame() if df is None or len(df) < 1: return latest_features = df.iloc[-1, :-1].values.reshape(1, -1) # Make Predictions using Probability Threshold try: # predict_proba returns [prob_class0, prob_class1] prob_class = self.model.predict_proba(latest_features)[0][1] prediction = 1 if prob_class > 0.5 else 0 except Exception as e: self.Debug(f"Error: Model prediction failed. {e}") return # Trading Logic holdings = self.Portfolio[self.symbol].Quantity # Buy if prediction == 1 and not currently invested if prediction == 1 and holdings <= 0: # Check if we have enough buying power before placing orders if self.Portfolio.GetBuyingPower(self.symbol, OrderDirection.Buy) > 0: self.SetHoldings(self.symbol, self.allocation_fraction) else: self.Debug("Insufficient buying power to execute buy order") # Sell if prediction == 0 and currently invested elif prediction == 0 and holdings > 0: self.Liquidate(self.symbol) def TrainModel(self): # Prepare Training Data df = self.GetFeatureDataFrame() if df is None or len(df) < 50: # Require enough data to train self.Debug("Insufficient data for training.") return # Split Data (chronological, no shuffle) X = df.iloc[:, :-1] # Features y = df.iloc[:, -1] # Target (0 or 1) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, shuffle=False, random_state=42 ) # Train SVM Model self.model.fit(X_train, y_train) self.is_model_trained = True # Evaluate Model Performance y_train_prob = self.model.predict_proba(X_train)[:, 1] y_train_pred_binary = [1 if val > 0.5 else 0 for val in y_train_prob] train_accuracy = accuracy_score(y_train, y_train_pred_binary) y_test_prob = self.model.predict_proba(X_test)[:, 1] y_test_pred_binary = [1 if val > 0.5 else 0 for val in y_test_prob] test_accuracy = accuracy_score(y_test, y_test_pred_binary) 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): # Wait until we have 200 data points in the rolling window if self.data.Count < 200: return None # Convert rolling window data (TradeBars) to a DataFrame # RollingWindow has newest items at index 0, so we need to reverse close_prices = [self.data[i].Close for i in range(self.data.Count - 1, -1, -1)] df = pd.DataFrame(close_prices, columns=["Close"]) # Feature Engineering df["SMA_10"] = df["Close"].rolling(window=10).mean() df["SMA_50"] = df["Close"].rolling(window=50).mean() # RSI Calculation with safe division delta = df["Close"].diff() gain = (delta.where(delta > 0, 0)).rolling(14).mean() loss = (-delta.where(delta < 0, 0)).rolling(14).mean() # Avoid division by zero rs = gain / loss.replace(0, 1e-10) df["RSI"] = 100 - (100 / (1 + rs)) # MACD Calculation df["MACD"] = df["Close"].ewm(span=12, adjust=False).mean() - df["Close"].ewm(span=26, adjust=False).mean() df["MACD_Signal"] = df["MACD"].ewm(span=9, adjust=False).mean() # Historical Volatility (HV_30) df["HV_30"] = df["Close"].pct_change().rolling(window=30).std() * np.sqrt(252) # Define Target: 1 if next day's Close > today's Close, else 0 df["Target"] = (df["Close"].shift(-1) > df["Close"]).astype(int) # Remove rows with NaN values from rolling calculations df.dropna(inplace=True) return df def OnOrderEvent(self, orderEvent): """ Triggered every time an order is filled. Prints a performance conclusion vs. SPY buy-and-hold. """ if orderEvent.Status == OrderStatus.Filled: # Safety checks for SPY reference if self.spyPriceStart is None or self.spyPriceStart == 0: return # Not enough data yet to compare # Strategy % Return strategyReturn = (self.Portfolio.TotalPortfolioValue / self.initialCapital - 1) * 100.0 # SPY Buy-and-Hold % Return (comparing current price vs. starting price) spyPriceNow = self.Securities[self.spySymbol].Price spyReturn = (spyPriceNow / self.spyPriceStart - 1) * 100.0 if strategyReturn > spyReturn: conclusion = "Strategy is beating SPY" elif strategyReturn < spyReturn: conclusion = "SPY is beating the Strategy" else: conclusion = "Strategy and SPY are at the same return" self.Debug(f"[Order Filled] Strategy Return: {strategyReturn:.2f}%, " f"SPY B/H Return: {spyReturn:.2f}%. {conclusion}")