Overall Statistics
Total Trades
1469
Average Win
0.83%
Average Loss
-0.85%
Compounding Annual Return
6.173%
Drawdown
15.900%
Expectancy
0.153
Net Profit
143.357%
Sharpe Ratio
0.574
Probabilistic Sharpe Ratio
2.267%
Loss Rate
42%
Win Rate
58%
Profit-Loss Ratio
0.98
Alpha
0.029
Beta
0.221
Annual Standard Deviation
0.079
Annual Variance
0.006
Information Ratio
-0.201
Tracking Error
0.149
Treynor Ratio
0.206
Total Fees
$8184.08
Estimated Strategy Capacity
$580000000.00
Lowest Capacity Asset
SPY R735QTJ8XC9X
"""
ML Technical Algorithm for SPY with random signal generator and Kelly sizing

@email: info@beawai.com
@creation date: 25/11/2022
"""

from AlgorithmImports import *

import sklearn
import numpy as np
import pandas as pd
pd.set_option('mode.use_inf_as_na', True)
from sklearn.dummy import DummyClassifier
from sklearn.model_selection import train_test_split


class E2E(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2008, 1, 1)
        self.SetEndDate(2022, 11, 1)
        self.lookback = self.GetParameter("lookback", 21)

        self.resolution = Resolution.Daily
        self.ticker = "SPY"
        self.AddEquity(self.ticker, self.resolution)
        self.model = None
        self.kelly_size = 0

        every_day = self.DateRules.EveryDay(self.ticker)
        self.Train(every_day, self.TimeRules.At(0, 0), self.train)
        self.Schedule.On(every_day,
                         self.TimeRules.AfterMarketOpen(self.ticker, 0),
                         self.trade)

    def train(self):
        """ Train model and calculate kelly position daily """
        if self.model is None: self.model = DummyClassifier(strategy="uniform")  # Random binary generator
        features, returns = self.get_data(252)  # Use last year of data for training
        target = returns >= 0  # Up/Down binary target
        model_temp = sklearn.base.clone(self.model)
        x_train, x_test, y_train, y_test, r_train, r_test = \
            train_test_split(features, target, returns, train_size=0.5, shuffle=False)
        model_temp.fit(x_train, y_train)
        y_pred = model_temp.predict(x_test)
        self.kelly_size = kelly_size(y_test, y_pred, r_test)  # Calculate kelly position on test data
        self.kelly_size = np.clip(self.kelly_size, 0, 1)  # Applies fractional kelly and clips between 0 and 1
        self.model.fit(features, target)
        self.Debug(f"{self.Time} Training - Kelly: {self.kelly_size:.1%}\n")
        self.Plot("ML", "Score", self.kelly_size)

    def trade(self):
        """ Trades based on prediction at market open """
        if self.model is None: return  # Don't trade until the model is trained
        self.Transactions.CancelOpenOrders()
        x_pred = self.get_data(self.lookback, include_y=False)
        if len(x_pred) == 0: return

        y_pred = self.model.predict(x_pred)[0]
        position = y_pred * self.kelly_size  # Sizing based on Kelly and individual probabilty
        self.Plot("ML", "Prediction", y_pred.mean())
        self.Debug(f"{self.Time} Trading\tPos: {position:.1%}")
        self.SetHoldings(self.ticker, position)

    def get_data(self, datapoints, include_y=True):
        """ Calculate features and target data """
        data = self.History([self.ticker], datapoints, self.resolution)
        features = data.eval("close/open - 1").to_frame("returns")
        x = pd.concat([features.shift(s) for s in range(self.lookback)],
                      axis=1).dropna()  # Sequence of last "lookback" returns
        if include_y:
            y = features["returns"].shift(-1).reindex_like(x).dropna()
            return x.loc[y.index], y
        else:
            return x


def kelly_size(y_true, y_pred, returns):
    """ Calculate Kelly position based on the prediction accuracy """
    trades = y_pred!=0
    wins = y_true[trades]==y_pred[trades]
    win_rate = wins.mean()
    loss_rate = 1-win_rate
    avg_win = abs(returns[trades][wins].mean())
    avg_loss = abs(returns[trades][~wins].mean())
    return win_rate/avg_loss - loss_rate/avg_win