Overall Statistics
Total Trades
1593
Average Win
0.01%
Average Loss
-0.04%
Compounding Annual Return
0.875%
Drawdown
0.700%
Expectancy
0.108
Net Profit
3.461%
Sharpe Ratio
0.862
Probabilistic Sharpe Ratio
36.502%
Loss Rate
15%
Win Rate
85%
Profit-Loss Ratio
0.30
Alpha
0.006
Beta
0.003
Annual Standard Deviation
0.007
Annual Variance
0
Information Ratio
-0.719
Tracking Error
0.173
Treynor Ratio
2.141
Total Fees
$945.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
MSFT 31S6IM58E6LZA|MSFT R735QTJ8XC9X
from arch import arch_model
import numpy as np
import pandas as pd
from sklearn.model_selection import KFold
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
from sklearn.metrics import mean_squared_error
import pickle

class KNNVolModel(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2018, 1, 1)  # Set Start Date
        #self.SetEndDate(2018, 3, 1)  # Set Start Date
        self.SetCash(1000000)  # Set Strategy Cash
        # serializedModel = bytes( self.ObjectStore.ReadBytes("VolModel") )
        # self.volModel = pickle.loads(serializedModel)
        self.symbolDataBySymbol = {}
        
        self.tickers = ["MSFT", "WFC", "HD"]
        
        self.spy = self.AddEquity("SPY", Resolution.Minute).Symbol
        
        for ticker in self.tickers:
            self.AddEquity(ticker, Resolution.Minute)
        
        self.Schedule.On(self.DateRules.EveryDay("SPY"),
                 self.TimeRules.AfterMarketOpen(self.spy, 2),
                 self.EveryDayAfterMarketOpen)
                 
        self.AutomaticIndicatorWarmup = True
                 
        self.slice = None
        
        self.lastYear = self.Time.year
        
    def CheckYear(self):
        if self.lastYear == self.Time.year:
            return
        self.lastYear = self.Time.year
        for symbol, symbolData in self.symbolDataBySymbol.items():
            symbolData.TrainModel()

    def OnData(self, slice):
        self.slice = slice
        self.CheckYear()
        
        for symbol, symbolData in self.symbolDataBySymbol.items():
            symbolData.CheckExercise()
        
    def EveryDayAfterMarketOpen(self):
        for symbol, symbolData in self.symbolDataBySymbol.items():
            symbolData.EveryDay()
        
    def OnSecuritiesChanged(self, changes):
        for security in changes.AddedSecurities:
            if security.Symbol.SecurityType != SecurityType.Equity: continue
            symbol = security.Symbol
            if symbol == self.spy: continue
            option = self.AddOption(symbol)
            option.SetFilter(self.UniverseFunc)
            atr = self.ATR(symbol, 14, MovingAverageType.Simple, Resolution.Daily)
            history = self.History(symbol, 100, Resolution.Daily)
            history = history.loc[symbol]
            symbolData = SymbolData(symbol, option, atr, history, self)
            self.Debug(f"Created symbolData for {symbol.Value}")
            self.symbolDataBySymbol[symbol] = symbolData
            
    def UniverseFunc(self, universe):
        return universe.IncludeWeeklys().Strikes(-15, 15).Expiration(timedelta(10), timedelta(30))
                
class SymbolData:
    def __init__(self, symbol, option, atr, history, algo):
        self.Symbol = symbol
        self.Option = option
        self.Atr = atr
        self.history_ = history
        self.algo = algo
        self.previousPred = -1
        self.investedOptions = False
        self.volModel = None
        self.TrainModel()
        
    def volpred(self, returns,h):
        am = arch_model(returns, p=1,o=1, q=1, vol='Garch', dist='skewt')
        res = am.fit(disp='off')
        forecasts=res.forecast(horizon=h, method='simulation')
        return forecasts.variance.iloc[-1]
    
    def EveryDay(self):
        
        df = self.RetrieveVolatilityMetrics()
        
        if self.CheckDataIntegrity(df) == True:
            self.algo.Debug(f"Integrity failed for {self.Symbol.Value}")
            return

        prediction = self.volModel.predict(df)
        self.EnterPositions(prediction, df)
        self.previousPred = prediction
        
    def CheckExercise(self):
        if self.algo.Portfolio[self.Symbol].Quantity != 0:
            #self.algo.Debug(f"Liquidating {self.Symbol.Value}")
            self.algo.Liquidate(self.Symbol)
            self.investedOptions = False
        
    def CheckDataIntegrity(self, df):
        if pd.isnull(df["GKHV"][0]) == True or pd.isnull(df["GARCH"][0]) == True or pd.isnull(df["ATR"][0]) == True:
            #self.algo.Debug(f"NaN value detected {self.Symbol.Value}")
            self.history_ = self.algo.History(self.Symbol, 100, Resolution.Daily)
            self.history_ = self.history_.loc[self.Symbol]
            return True
        return False
        
    def RetrieveVolatilityMetrics(self):
        yesterday = self.algo.History(self.Symbol, 1, Resolution.Daily)
        yesterday = yesterday.loc[self.Symbol]
        self.history_.append(yesterday)
        
        
        self.history_ = self.history_.iloc[1: , :]
        returns = 100 * self.history_["close"].pct_change().dropna()
        
        gkhv = np.sqrt(252/10 * pd.DataFrame.rolling(0.5 * np.log(self.history_.loc[:, 'high'] / self.history_.loc[:, 'low']) ** 2 -
                                                (2 * np.log(2) - 1) *
                                                 np.log(self.history_.loc[:, 'close'] / self.history_.loc[:, 'open']) ** 2, window=10).sum())
                                                 
        self.gkhv = gkhv.pct_change(3).iloc[-1]
        
        df = pd.DataFrame()
        df["GKHV"] = [abs(self.gkhv)]
        df["GARCH"] = [self.volpred(returns, 10).mean()]
        df["ATR"] = [self.Atr.Current.Value]
        
        return df
        
    def CheckOptionPositions(self):
        option_invested = [x.Key for x in self.algo.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option]
        for option in option_invested:
            if self.Symbol == option.ID.Underlying.Symbol:
                self.investedOptions = True
                return
        self.investedOptions = False
        
    def EnterPositions(self, prediction, df):
        
        self.CheckOptionPositions()
        
        if prediction == 1 and self.previousPred == 0:
            self.algo.Liquidate(self.Symbol)
            self.investedOptions = False

        if prediction == 1 and self.investedOptions == False:
            #self.algo.Debug(df)
            self.LongStraddle(self.algo.slice)
            #self.algo.Debug(f"Long {prediction} for {self.Symbol.Value}")
        elif prediction == 0 and self.investedOptions == False:
            #self.algo.Debug(df)
            self.ShortStraddle(self.algo.slice)
            #self.algo.Debug(f"Short {prediction} for {self.Symbol.Value}")
    
    # def CheckProfit(self):
    #     if self.Portfolio.TotalUnrealizedProfit >= (self.Portfolio.TotalUnrealizedProfit):
    
        
    def LongStraddle(self, slice):
        # If there is undelying assets in portfolio at expiration, liquidate the stocks in order to roll into new contracts
        self.CheckExercise()
        
        if self.algo.Time.hour != 0 and self.algo.Time.minute != 0: 
            for i in slice.OptionChains:
                if self.Symbol != i.Value.Underlying.Symbol: continue
                chain = i.Value
                contract_list = [x for x in chain]
                # if there is no optionchain or no contracts in this optionchain, pass the instance
                if (slice.OptionChains.Count == 0) or (len(contract_list) == 0): 
                    return   
    
                # sorted the optionchain by expiration date and choose the furthest date
                expiry = sorted(chain,key = lambda x: x.Expiry)[0].Expiry
                # filter the call and put options from the contracts
                call = [i for i in chain if i.Expiry == expiry and i.Right == 0]
                put = [i for i in chain if i.Expiry == expiry and i.Right == 1]
    
                # sorted the contracts according to their strike prices 
                call_contracts = sorted(call,key = lambda x: x.Strike)    
                put_contracts = sorted(put,key = lambda x: x.Strike)    
                if len(call_contracts) == 0 or len(put_contracts) == 0 : return
                
                atm_call = sorted(call_contracts,key = lambda x: abs(chain.Underlying.Price - x.Strike))[0]
                atm_put = sorted(put_contracts,key = lambda x: abs(chain.Underlying.Price - x.Strike))[0]
                self.algo.Buy(atm_call.Symbol, 2)
                self.algo.Buy(atm_put.Symbol, 2)
                self.investedOptions = True
                return
        self.algo.Debug(f"Failed Long for {self.Symbol.Value}")
                
    def ShortStraddle(self, slice):
        # If there is undelying assets in portfolio at expiration, liquidate the stocks in order to roll into new contracts
        self.CheckExercise()
        
        if self.algo.Time.hour != 0 and self.algo.Time.minute != 0: 
            for i in slice.OptionChains:
                if self.Symbol != i.Value.Underlying.Symbol: continue
                chain = i.Value
                contract_list = [x for x in chain]
                # if there is no optionchain or no contracts in this optionchain, pass the instance
                if (slice.OptionChains.Count == 0) or (len(contract_list) == 0): 
                    return   
    
                # sorted the optionchain by expiration date and choose the furthest date
                expiry = sorted(chain,key = lambda x: x.Expiry)[0].Expiry
                # filter the call and put options from the contracts
                call = [i for i in chain if i.Expiry == expiry and i.Right == 0]
                put = [i for i in chain if i.Expiry == expiry and i.Right == 1]
    
                # sorted the contracts according to their strike prices 
                call_contracts = sorted(call,key = lambda x: x.Strike)    
                put_contracts = sorted(put,key = lambda x: x.Strike)    
                if len(call_contracts) == 0 or len(put_contracts) == 0 : return
                
                if len(call_contracts) < 14:
                    return
                
                atm_call = sorted(call_contracts,key = lambda x: x.Strike)[len(call_contracts)-1]
                atm_put = sorted(put_contracts,key = lambda x: x.Strike)[0]
                self.algo.Sell(atm_call.Symbol, 4)
                self.algo.Sell(atm_put.Symbol, 4)
                #self.algo.Debug(f"{atm_call.Strike} {atm_put.Strike} {self.algo.Securities[self.Symbol].Price}")
                self.investedOptions = True
                return
        self.algo.Debug(f"Failed Short for {self.Symbol.Value}")
            
    def TrainModel(self):
        history = self.algo.History(self.Symbol, 1000, Resolution.Daily)
        history = history.loc[self.Symbol]
        
        atr = AverageTrueRange(14, MovingAverageType.Simple)
        atr_arr = []
        for tuple in history.itertuples():
            bar = TradeBar(tuple.Index, self.Symbol, tuple.open, tuple.high, tuple.low, tuple.close, tuple.volume)
            atr.Update(bar)
            if not atr.IsReady:
                atr_arr.append(np.nan)
                continue
            atr_arr.append(atr.Current.Value)
            
        history["ATR"] = atr_arr
        
        history["pct_change"] = abs(100 * history["close"].pct_change(10).dropna())
        
        Garch = [np.nan for x in range(0, 100)]
        for index in range(100, len(history.index)):
            returns = 100 * history["close"].iloc[index-100:index].pct_change().dropna()
            garchPred = self.volpred(returns, 10)
            Garch.append(garchPred.mean())
        history["Garch_Pred"] = Garch
        
        gkhv = np.sqrt(252/10 * pd.DataFrame.rolling(0.5 * np.log(history.loc[:, 'high'] / history.loc[:, 'low']) ** 2 -
                                                (2 * np.log(2) - 1) *
                                                 np.log(history.loc[:, 'close'] / history.loc[:, 'open']) ** 2, window=10).sum())
        
        history["GKHV"] = abs(gkhv.pct_change(3))# gkhv.iloc[-1]#
        history = history.dropna()
        
        history["pct_delayed"] = history["pct_change"].shift(-10).dropna()
        
        history.loc[abs(history['pct_delayed']) > (history["pct_change"].mean()*1.5), 'Strong_Change'] = 1
        history.loc[abs(history['pct_delayed']) <= (history["pct_change"].mean()*1.5), 'Strong_Change'] = 0
        
        history = history.dropna()
        
        y = np.array(history["Strong_Change"])
        X = history[["GKHV", "Garch_Pred", "ATR"]]
        
        # split_percentage = 0.8
        # split = int(split_percentage*len(history.index))
        
        # X_train = X[:split]
        # y_train = y[:split]
        # X_test = X[split:]
        # y_test = y[split:]
        
        cls = KNeighborsClassifier(n_neighbors=10)
        cls = cls.fit(X, y)

        self.volModel = cls