Created with Highcharts 12.1.2EquityJan 2019Jan…Jul 2019Jan 2020Jul 2020Jan 2021Jul 2021Jan 2022Jul 2022Jan 2023Jul 2023Jan 2024Jul 2024Jan 2025100k110k120k130k-10-5000.050.100.10.2050M100M02M4M
Overall Statistics
Total Orders
212
Average Win
0.50%
Average Loss
-0.40%
Compounding Annual Return
1.889%
Drawdown
5.300%
Expectancy
0.273
Start Equity
110000.00
End Equity
123020.44
Net Profit
11.837%
Sharpe Ratio
-0.232
Sortino Ratio
-0.278
Probabilistic Sharpe Ratio
16.415%
Loss Rate
43%
Win Rate
57%
Profit-Loss Ratio
1.25
Alpha
-0.032
Beta
0.023
Annual Standard Deviation
0.032
Annual Variance
0.001
Information Ratio
-1.74
Tracking Error
0.635
Treynor Ratio
-0.324
Total Fees
$2024.21
Estimated Strategy Capacity
$43000000.00
Lowest Capacity Asset
BTCUSDT 18N
Portfolio Turnover
0.77%
from AlgorithmImports import *
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import numpy as np
import pandas as pd

class BTCUSDTLinearRegressionAlgo(QCAlgorithm):
    
    def Initialize(self):
        # 1. Algorithm Parameters
        self.SetStartDate(2019, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash("USDT", 10000)

        # Optional: Set Binance brokerage model for a cash account
        self.SetBrokerageModel(BrokerageName.Binance, AccountType.Cash)

        # 2. Add BTC/USDT from Binance, Daily Resolution (produces QuoteBars)
        self.symbol = self.AddCrypto("BTCUSDT", Resolution.Daily, Market.Binance).Symbol
        
        # 3. RollingWindow to store the last 200 QuoteBars
        self.data = RollingWindow[QuoteBar](200)
        
        # 4. Warm-up to get 200 bars before starting trading
        self.SetWarmUp(200)

        # 5. Initialize ML Model
        self.model = LinearRegression()
        self.training_count = 0
        self.is_model_trained = False

        # 6. Configurable parameter: fraction of available cash to spend when buying
        #    Adjust this to 0.90 (90%), 0.99 (99%), etc., as you see fit
        self.allocationFraction = 0.50
        
        # 7. Configurable Training Frequency: "Daily" or "4H" or "6H"
        self.trainingFrequency = "4H"  # Train 4 times/day
        self.ConfigureTrainingSchedule()

    def ConfigureTrainingSchedule(self):
        """Schedules the TrainModel() calls according to the desired frequency."""
        if self.trainingFrequency == "Daily":
            # Train once per day at 00:00 UTC
            self.Schedule.On(
                self.DateRules.EveryDay(self.symbol),
                self.TimeRules.At(0, 0),
                self.TrainModel
            )
        elif self.trainingFrequency == "6H":
            # Train every 6 hours: 00:00, 06:00, 12:00, 18:00 (UTC)
            for hour in [0, 6, 12, 18]:
                self.Schedule.On(
                    self.DateRules.EveryDay(self.symbol),
                    self.TimeRules.At(hour, 0),
                    self.TrainModel
                )
        elif self.trainingFrequency == "4H":
            # Train every 4 hours: 00:00, 04:00, 08:00, 12:00, 16:00, 20:00 (UTC)
            for hour in [0, 4, 8, 12, 16, 20]:
                self.Schedule.On(
                    self.DateRules.EveryDay(self.symbol),
                    self.TimeRules.At(hour, 0),
                    self.TrainModel
                )
        else:
            self.Debug(f"Unknown training frequency: {self.trainingFrequency}. Using daily by default.")
            self.Schedule.On(
                self.DateRules.EveryDay(self.symbol),
                self.TimeRules.At(0, 0),
                self.TrainModel
            )

    def OnData(self, data):
        # 8. Ensure we have QuoteBars for the symbol
        if not data.QuoteBars.ContainsKey(self.symbol):
            return
        
        quote_bar = data.QuoteBars[self.symbol]
        if quote_bar is None:
            return
        
        # 9. Add the QuoteBar to our rolling window
        self.data.Add(quote_bar)

        # 10. Wait until rolling window is ready
        if not self.data.IsReady or self.data.Count < 200:
            return

        # 11. Skip predictions if the model is not trained
        if not self.is_model_trained:
            self.Debug("Model not trained yet. Skipping prediction.")
            return

        # 12. Retrieve the latest feature vector
        df = self.GetFeatureDataFrame()
        if df is None or len(df) < 1:
            return
        
        latest_features = df.iloc[-1, :-1].values.reshape(1, -1)
        
        # 13. Prediction: continuous → binary threshold at 0.5
        try:
            prediction_value = self.model.predict(latest_features)[0]
            prediction = 1 if prediction_value > 0.5 else 0
        except Exception as e:
            self.Debug(f"Model prediction failed: {e}")
            return

        # 14. Trading Logic: manually calculate quantity
        holdings = self.Portfolio[self.symbol].Quantity
        current_price = quote_bar.Close  # mid-price from the QuoteBar

        if prediction == 1 and not self.Portfolio.Invested:
            # Buy up to allocationFraction of current USDT
            available_usdt = self.Portfolio.CashBook["USDT"].Amount * self.allocationFraction
            if available_usdt > 0 and current_price > 0:
                quantity_to_buy = available_usdt / current_price
                self.MarketOrder(self.symbol, quantity_to_buy)

        elif prediction == 0 and self.Portfolio.Invested:
            # If we have BTC and the model says sell, liquidate
            self.Liquidate(self.symbol)

    def TrainModel(self):
        # 15. Prepare data for training
        df = self.GetFeatureDataFrame()
        if df is None or len(df) < 50:
            self.Debug("Insufficient data for training.")
            return

        # Separate features (X) and label (y)
        X = df.iloc[:, :-1]
        y = df.iloc[:, -1]
        
        # Chronological split: 80% train, 20% test
        X_train, X_test, y_train, y_test = train_test_split(
            X, y, test_size=0.2, shuffle=False, random_state=42
        )

        # 16. Train the Linear Regression model
        self.model.fit(X_train, y_train)
        self.is_model_trained = True

        # 17. Evaluate performance (threshold regression to binary for accuracy)
        y_train_pred = self.model.predict(X_train)
        y_train_pred_binary = [1 if val > 0.5 else 0 for val in y_train_pred]
        train_accuracy = accuracy_score(y_train, y_train_pred_binary)

        y_test_pred = self.model.predict(X_test)
        y_test_pred_binary = [1 if val > 0.5 else 0 for val in y_test_pred]
        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):
        # Need 200 bars in rolling window
        if self.data.Count < 200:
            return None

        # Extract the midpoint close from QuoteBars
        close_prices = [qb.Close for qb in self.data]
        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(14)
        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))
        
        # MACD & Signal
        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)

        # Binary target: next day's Close > today's => 1, else 0
        df["Target"] = (df["Close"].shift(-1) > df["Close"]).astype(int)
        
        # Remove NaN rows introduced by rolling calculations
        df.dropna(inplace=True)

        return df