Overall Statistics
Total Trades
10
Average Win
5.48%
Average Loss
-0.21%
Compounding Annual Return
26.199%
Drawdown
6.800%
Expectancy
19.471
Net Profit
25.557%
Sharpe Ratio
1.795
Probabilistic Sharpe Ratio
79.908%
Loss Rate
25%
Win Rate
75%
Profit-Loss Ratio
26.29
Alpha
0.223
Beta
-0.116
Annual Standard Deviation
0.109
Annual Variance
0.012
Information Ratio
-0.21
Tracking Error
0.167
Treynor Ratio
-1.695
Total Fees
$10.00
# https://stackoverflow.com/questions/38714959/understanding-keras-lstms?rq=1
SEED = 42
import os
import random as rn
import numpy as np
from tensorflow import set_random_seed
os.environ['PYTHONHASHSEED']=str(SEED)
np.random.seed(SEED)
set_random_seed(SEED)
rn.seed(SEED)

import pandas as pd

from sklearn.preprocessing import MinMaxScaler

from keras.models import load_model
from keras.optimizers import Adam
from keras.layers import LSTM
from keras.layers import Dense
from keras.layers import Dropout, BatchNormalization
from keras.layers import LeakyReLU
from keras.models import Sequential

from sklearn.model_selection import train_test_split


from keras import backend as K 
K.clear_session()

LOOKBACK_DAYS = 1200
FEATURE_LEN = 40 #400
FEATURE_DIM = 2
def get_model():
    
    #input_shape = (features_set.shape[1],features_set.shape[2])
    input_shape = (FEATURE_LEN,FEATURE_DIM)
    feature_len = input_shape[0]
    dropout_rate = 0.2
    drop = True
    norm = False
    
    model = Sequential()
    
    model.add(LSTM(units=feature_len, return_sequences=True, input_shape=input_shape))
    if drop:
        model.add(Dropout(rate=dropout_rate))
    if norm:
        model.add(BatchNormalization())
    model.add(LeakyReLU())
    
    model.add(LSTM(units=feature_len, return_sequences=True))
    if drop:
        model.add(Dropout(rate=dropout_rate))
    if norm:
        model.add(BatchNormalization())
    model.add(LeakyReLU())
    
    model.add(LSTM(units=int(feature_len/2), return_sequences=True))
    if drop:
        model.add(Dropout(rate=dropout_rate))
    if norm:
        model.add(BatchNormalization())
    model.add(LeakyReLU())
    
    model.add(LSTM(units=int(feature_len/2)))
    if drop:
        model.add(Dropout(rate=dropout_rate))
    if norm:
        model.add(BatchNormalization())
    model.add(LeakyReLU())
    
    model.add(Dense(units=10))
    if drop:
        model.add(Dropout(rate=dropout_rate))
    if norm:
        model.add(BatchNormalization())
    model.add(LeakyReLU())
    
    model.add(Dense(units=10))
    if drop:
        model.add(Dropout(rate=dropout_rate))
    if norm:
        model.add(BatchNormalization())
    model.add(LeakyReLU())
    
    model.add(Dense(units=1,activation='linear'))
    return model
    
class MyLSTM:
    
    def __init__(self):
        self.model = None
        self.scaler = MinMaxScaler(feature_range = (0, 1))
        self.feature_len = FEATURE_LEN
        self.feature_dim = FEATURE_DIM
        self.lookback_num = LOOKBACK_DAYS
        
    def _transform(self,df,shift=True):
        
        df['SPY_RET'] = np.log(df['SPY']).diff()
        df['HV'] = df['SPY_RET'].rolling(10).std() * np.sqrt(252)
        df['HV_MA'] = df['HV'].rolling(5).mean()
        df['VRP'] = df['VIX'] - df['HV_MA']*100
        df['VV'] = df['VIX']/df['VXV']
        
        if shift:
            df['VRP'] = df['VRP'].shift(1)
            df['VV'] = df['VV'].shift(1)
        
        df = df.dropna()
        
        #if df.shape[0] < LOOKBACK_DAYS:
        #    raise ValueError('No Data! df shape {}'.format(df.shape))
            
        return df

    def ProcessData(self, df):
        
        df = self._transform(df,shift=True)
        data = df[['SPY_RET','VRP','VV']].values
        
        # build dataset
        features_set = []
        labels = []
        stratify = []
        for i in range(self.feature_len, data.shape[0]):
            
            tmp_ret = data[i,0]
            tmp_c = 1.0 if tmp_ret > 0 else 0.0
            #tmp_y = [tmp_c]
            tmp_y = [tmp_ret]
            tmp_x = data[i-self.feature_len:i, 1:]
            
            features_set.append(tmp_x)
            stratify.append(tmp_c)
            # wishful thinking for predicting 1-day direction, but still fun.
            labels.append(tmp_y)
        
        features_set, labels, stratify = np.array(features_set), np.array(labels), np.array(stratify)
        
        if features_set.shape[1] != self.feature_len:
            raise ValueError('Debug {} {} {} {} {}'.format(df.shape,data.shape,features_set.shape, labels.shape, np.unique(labels)))
        
        return features_set, labels, stratify
    
    def CreateModel(self):
        
        lr = 0.001
        mom = 0.9
        # Create Model
        self.model = get_model()
        self.opt = Adam(lr,mom)
        #self.model.compile(optimizer=self.opt,loss='binary_crossentropy',metrics=['acc'])
        self.model.compile(optimizer=self.opt,loss='mse',metrics=['acc'])
        
    def FitModel(self, features_set, labels, stratify):
        
        epochs = 3
        batch_size = 32
        x, y= features_set, labels
        
        X_train, X_test, y_train, y_test = train_test_split(x, y, test_size=0.33, random_state=42,stratify=stratify)
        self.model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size,validation_data=(X_test,y_test))
        
    def PredictFromModel(self, df):
        
        df = self._transform(df,shift=True) # shift set to True to avoid look ahead??
        df = df[['VRP','VV']].values
        test_inputs = df[-1*self.feature_len:,:]
        test_inputs = np.expand_dims(test_inputs,axis=0)
        
        if test_inputs.shape[1] != self.feature_len:
            raise ValueError('Prdict... Debug {}'.format(df.shape))
        
        predictions = self.model.predict(test_inputs)
        return predictions[0][0]
# REF. https://www.quantconnect.com/forum/discussion/6931/from-research-to-production-long-short-term-memory

from MyLSTM import MyLSTM, LOOKBACK_DAYS, FEATURE_LEN
from my_custom_data import CboeVix, CboeVxV

import numpy as np
import pandas as pd

initial_lookback = LOOKBACK_DAYS*30
training_lookback = LOOKBACK_DAYS*10
predict_lookback = FEATURE_LEN*10 # look back seems like needs to be multiplied by a factor depend on number of symbols?

class MultidimensionalHorizontalFlange(QCAlgorithm):

    def Initialize(self):
        # November 12, 2007 vxv +4
        #self.SetStartDate(2014, 12, 1) 
        # ^^^ Algo doesn't really trade. Likely bug, need research to debug
        # PSR: 59%, win loss rate: 100/0 is just wrong... 
        self.SetStartDate(2019, 1, 1) # cherry picked date range...for wow factor. # PSR: 84% , win loss rate: 75/25.
        
        self.SetCash(10000)  # Set Strategy Cash
        self.SetBrokerageModel(AlphaStreamsBrokerageModel())
        self.SetExecution(ImmediateExecutionModel())
        #self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        self.UniverseSettings.Resolution = Resolution.Daily
        #self.SetUniverseSelection(LiquidETFUniverse())
        
        self.short_volatility = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.long_volatility = self.AddEquity('SHY', Resolution.Daily).Symbol
        self.spy = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.vix = self.AddData(CboeVix, "VIX").Symbol
        self.vxv = self.AddData(CboeVxV, "VXV").Symbol
        
        self.SetWarmUp(timedelta(LOOKBACK_DAYS))

        self.models = {
            self.spy:None,
        }
        self.macro_symbols = {
            'Bull':self.spy,
        }
        
        # Use Train() method to avoid runtime error
        self.Train(self.TrainMyModel)
        self.Train(self.DateRules.MonthEnd(), self.TimeRules.At(8,0), self.TrainMyModel)
        
        # Schedule prediction and plotting
        self.AddEquity('SPY')
        self.Schedule.On(self.DateRules.EveryDay(self.spy), self.TimeRules.AfterMarketOpen(self.spy, 5), self.Predict)
        #self.Schedule.On(self.DateRules.EveryDay(self.spy), self.TimeRules.BeforeMarketClose(self.spy, 5), self.ClosePosition)
        self.Schedule.On(self.DateRules.EveryDay(self.spy), self.TimeRules.BeforeMarketClose(self.spy, 5), self.PlotMe)
        
        # Create custom charts
        prediction = Chart('Prediction Plot')
        prediction.AddSeries(Series('Actual Bull', SeriesType.Line, 0))
        prediction.AddSeries(Series('Predicted Bull', SeriesType.Line, 0))
        
        prediction.AddSeries(Series('Actual Bear', SeriesType.Line, 1))
        prediction.AddSeries(Series('Predicted Bear', SeriesType.Line, 1))
        
        self.ready = False
        
    def get_data(self,lookback):
        
        history = self.History([self.spy,self.vix,self.vxv], lookback, Resolution.Daily)
        df = pd.DataFrame()
        #df['date'] = history.loc[self.spy].index
        df['SPY'] = history.loc[self.spy].close
        df['VIX'] = history.loc[self.vix].close
        df['VXV'] = history.loc[self.vxv].close
        
        return df
        
    def TrainMyModel(self):
        qb = self
        
        for key, symbol in self.macro_symbols.items():
            
            if self.models[symbol] is None:
                df = self.get_data(initial_lookback)
            else:
                df = self.get_data(training_lookback)
            self.Log('data {}...train'.format(df.shape))
            
            if df.shape[0] < predict_lookback: # dont really have that much training_lookback, nor initial_lookback
                self.ready = False
                continue
            
            self.Log('shape {}'.format(df.shape))
            
            # Build model layers
            if self.models[symbol] is None:
                # Initialize LSTM class instance
                lstm = MyLSTM()
                # Prepare data
                features_set, labels, stratify = lstm.ProcessData(df)
                # Create model
                lstm.CreateModel()
                # Fit model
                lstm.FitModel(features_set, labels, stratify)
                
                # Add LSTM class to dictionary to store later
                self.models[symbol] = lstm
                
                self.ready = True
                self.Log('training done')
            else:
                lstm = self.models[symbol]
                # uncomment for real test, might take a while or timeout...
                #features_set, labels, stratify = lstm.ProcessData(df)
                #lstm.FitModel(features_set, labels)
                self.Log('no training done')
            

        
    # close position at the day.
    def ClosePosition(self):
        self.Log('closing position')
        self.SetHoldings([PortfolioTarget(self.short_volatility, 0.0)])
        self.SetHoldings([PortfolioTarget(self.long_volatility, 0.0)])
        
    def Predict(self):
        delta = {}
        qb = self
        for key, symbol in self.macro_symbols.items():
            self.Log('predict')
            self.Log('ready is {}'.format(self.ready))
            
            if self.ready is False:
                continue
            
            self.Log('fetch data.')
            # Fetch history
            df = self.get_data(predict_lookback)
            self.Log('data {}...predict'.format(df.shape))
            
            if df.shape[0] < predict_lookback:
                raise ValueError('not enough data {}'.format(df.shape))
                continue
            
            # Fetch LSTM class
            lstm = self.models[symbol]
            # Predict
            predictions = lstm.PredictFromModel(df)
            
            # Grab latest prediction and calculate if predict symbol to go up or down
            delta[key] = predictions
            # Plot prediction
            self.Plot('Prediction Plot', f'Predicted {key}', predictions)
            
            #confidence = np.clip(np.abs(predictions-0.5)/0.10,0,1)
            #insight = Insight.Price(symbol, timedelta(1), InsightDirection.Up if predictions > 0.5 else InsightDirection.Down, confidence)
            
            confidence = np.clip(np.abs(predictions)/0.10,0,1)
            insight = Insight.Price(symbol, timedelta(1), InsightDirection.Up if predictions > 0.0 else InsightDirection.Down, confidence)
            self.EmitInsights(insight)
            
            #if predictions > 0.5:
            if predictions > 0.0:
                self.Log('Long!')
                self.SetHoldings([PortfolioTarget(self.short_volatility, 1.0)])
                self.SetHoldings([PortfolioTarget(self.long_volatility, 0.0)])
            else:
                self.Log('Short!')
                self.SetHoldings([PortfolioTarget(self.short_volatility, 0.0)])
                self.SetHoldings([PortfolioTarget(self.long_volatility, 0.5)])
    

    def PlotMe(self):
        # Plot current price of symbols to match against prediction
        for key, symbol in self.macro_symbols.items():
            up = 1.0 if (self.Securities[symbol].Close-self.Securities[symbol].Open) > 0 else 0.0
            self.Plot('Prediction Plot', f'Actual {key}', up)
            self.Plot('Prediction Plot', f'Actual {key}', self.Securities[symbol].Price)
# REF. https://www.quantconnect.com/forum/discussion/2657/a-simple-vix-strategy

from QuantConnect.Python import PythonQuandl # quandl data not CLOSE
from QuantConnect.Python import PythonData # custom data
from QuantConnect.Data import SubscriptionDataSource

from datetime import datetime, timedelta
import decimal

class CboeVix(PythonData):
    '''CBOE Vix Download Custom Data Class'''
    def GetSource(self, config, date, isLiveMode):
        url_vix = "http://www.cboe.com/publish/scheduledtask/mktdata/datahouse/vixcurrent.csv"
        return SubscriptionDataSource(url_vix, 
                                      SubscriptionTransportMedium.RemoteFile)
    def Reader(self, config, line, date, isLiveMode):
        if not (line.strip() and line[0].isdigit()): return None
        # New CboeVix object
        index = CboeVix();
        index.Symbol = config.Symbol
        try:
            # Example File Format:
            # Date          VIX Open    VIX High VIX Low    VIX Close
            # 01/02/2004    17.96    18.68     17.54        18.22
            #print line
            data = line.split(',')
            date = data[0].split('/')
            index.Time = datetime(int(date[2]), int(date[0]), int(date[1]))
            index.Value = decimal.Decimal(data[4])
            index["Open"] = float(data[1])
            index["High"] = float(data[2])
            index["Low"] = float(data[3])
            index["Close"] = float(data[4])
        except ValueError:
            # Do nothing
            return None
#       except KeyError, e:
#          print 'I got a KeyError - reason "%s"' % str(e)
        return index


# NB: CboeVxV class ==  CboeVix class, except for the URL
class CboeVxV(PythonData):
    '''CBOE VXV Download Custom Data Class'''
    
    def GetSource(self, config, date, isLiveMode):
        url_vxv = "http://www.cboe.com/publish/scheduledtask/mktdata/datahouse/vix3mdailyprices.csv"
        return SubscriptionDataSource(url_vxv, 
                                      SubscriptionTransportMedium.RemoteFile)
    def Reader(self, config, line, date, isLiveMode):
        if not (line.strip() and line[0].isdigit()): return None
        index = CboeVxV();
        index.Symbol = config.Symbol
        try:
        # Example File Format:
        #                 OPEN    HIGH    LOW        CLOSE
        # 12/04/2007    24.8    25.01    24.15    24.65
            data = line.split(',')
            date = data[0].split('/')
            index.Time = datetime(int(date[2]), int(date[0]), int(date[1]))
            index.Value = decimal.Decimal(data[4])
            index["Open"] = float(data[1])
            index["High"] = float(data[2])
            index["Low"] = float(data[3])
            index["Close"] = float(data[4])
        except ValueError:
                # Do nothing
                return None
        return index

# for using VIX futures settle in calc. ratios like VIX/VIX1
class QuandlFuture(PythonQuandl):
    '''Custom quandl data type for setting customized value column name. 
       Value column is used for the primary trading calculations and charting.'''
    def __init__(self):
        # Define ValueColumnName: cannot be None, Empty or non-existant column name
        # If ValueColumnName is "Close", do not use PythonQuandl, use Quandl:
        # self.AddData[QuandlFuture](self.VIX1, Resolution.Daily)
        self.ValueColumnName = "Settle"