| Overall Statistics |
|
Total Orders 102 Average Win 11.69% Average Loss -3.00% Compounding Annual Return 38.419% Drawdown 32.900% Expectancy 1.594 Start Equity 10000 End Equity 69752.13 Net Profit 597.521% Sharpe Ratio 1.015 Sortino Ratio 1.333 Probabilistic Sharpe Ratio 47.364% Loss Rate 47% Win Rate 53% Profit-Loss Ratio 3.90 Alpha 0.231 Beta 0.341 Annual Standard Deviation 0.263 Annual Variance 0.069 Information Ratio 0.58 Tracking Error 0.279 Treynor Ratio 0.783 Total Fees $596.16 Estimated Strategy Capacity $130000000.00 Lowest Capacity Asset MARA VSI9G9W3OAXX Portfolio Turnover 0.99% |
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
# ------------------------------
# Custom Models: 0.1% Fee & 0 Slippage
# ------------------------------
class CustomFeeModel:
"""
Applies a 0.1% transaction fee on each trade (open/close).
"""
def GetOrderFee(self, parameters):
orderValue = parameters.Security.Price * abs(parameters.Order.Quantity)
fee = 0.001 * orderValue # 0.1% of trade notional
return OrderFee(CashAmount(fee, "USD"))
class CustomSlippageModel:
"""
Sets slippage to 0.
"""
def GetSlippageApproximation(self, asset, order):
return 0
class MLTradingAlgorithm(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2019, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(10000)
self.symbol = self.AddEquity("MARA", Resolution.Daily).Symbol
self.Securities[self.symbol].SetFeeModel(CustomFeeModel())
self.Securities[self.symbol].SetSlippageModel(CustomSlippageModel())
self.data = RollingWindow[TradeBar](200)
self.SetWarmUp(200)
self.model = SVC(probability=True, random_state=42)
self.training_count = 0
self.is_model_trained = False
self.allocation_fraction = 0.2
self.spySymbol = self.AddEquity("SPY", Resolution.Daily).Symbol
self.Securities[self.spySymbol].SetFeeModel(CustomFeeModel())
self.Securities[self.spySymbol].SetSlippageModel(CustomSlippageModel())
self.SetBenchmark(self.spySymbol)
self.spyPriceStart = None
self.initialCapital = None
# Initialize counters for tracking performance against SPY
self.beat_spy_count = 0
self.total_comparisons = 0
self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday),
self.TimeRules.At(10, 0),
self.TrainModel)
def OnData(self, data):
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
if not data.ContainsKey(self.symbol):
return
trade_bar = data[self.symbol]
if trade_bar is None:
return
self.data.Add(trade_bar)
if not self.data.IsReady or self.data.Count < 200:
return
if not self.is_model_trained:
self.Debug("Model is not trained yet. Skipping prediction.")
return
df = self.GetFeatureDataFrame()
if df is None or len(df) < 1:
return
latest_features = df.iloc[-1, :-1].values.reshape(1, -1)
try:
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
holdings = self.Portfolio[self.symbol].Quantity
if prediction == 1 and holdings <= 0:
self.SetHoldings(self.symbol, self.allocation_fraction)
elif prediction == 0 and holdings > 0:
self.Liquidate(self.symbol)
def TrainModel(self):
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]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, shuffle=False, random_state=42
)
self.model.fit(X_train, y_train)
self.is_model_trained = True
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):
if self.data.Count < 200:
return None
close_prices = [bar.Close for bar in self.data]
df = pd.DataFrame(close_prices, columns=["Close"])
df["SMA_10"] = df["Close"].rolling(window=10).mean()
df["SMA_50"] = df["Close"].rolling(window=50).mean()
delta = df["Close"].diff()
gain = (delta.where(delta > 0, 0)).rolling(14).mean()
loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
rs = gain / loss
df["RSI"] = 100 - (100 / (1 + rs))
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()
df["HV_30"] = df["Close"].pct_change().rolling(window=30).std() * np.sqrt(252)
df["Target"] = (df["Close"].shift(-1) > df["Close"]).astype(int)
df.dropna(inplace=True)
return df
def OnOrderEvent(self, orderEvent):
if orderEvent.Status == OrderStatus.Filled:
if self.spyPriceStart is None or self.spyPriceStart == 0:
return
strategyReturn = (self.Portfolio.TotalPortfolioValue / self.initialCapital - 1) * 100.0
spyPriceNow = self.Securities[self.spySymbol].Price
spyReturn = (spyPriceNow / self.spyPriceStart - 1) * 100.0
# Increment total comparisons and beat count if strategy outperforms SPY
self.total_comparisons += 1
if strategyReturn > spyReturn:
self.beat_spy_count += 1
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}")
def OnEndOfAlgorithm(self):
# Print the final count of times the strategy beat SPY
self.Debug(f"Number of times strategy beat SPY: {self.beat_spy_count} out of {self.total_comparisons}")