| Overall Statistics |
|
Total Trades 55 Average Win 0.25% Average Loss -0.51% Compounding Annual Return 5.441% Drawdown 5.900% Expectancy 0.050 Net Profit 30.378% Sharpe Ratio 1.032 Probabilistic Sharpe Ratio 50.932% Loss Rate 29% Win Rate 71% Profit-Loss Ratio 0.48 Alpha 0.047 Beta -0.009 Annual Standard Deviation 0.044 Annual Variance 0.002 Information Ratio -0.481 Tracking Error 0.178 Treynor Ratio -4.964 Total Fees $64.42 |
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2020 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from Model import Model
class OptimizedUncoupledAutosequencers(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 10, 1) # Set Start Date
self.SetEndDate(2020, 10, 1)
self.SetCash(100000) # Set Strategy Cash
tickers = ['VTI', 'AGG', 'DBC', 'VIXY']
for ticker in tickers:
self.AddEquity(ticker, Resolution.Daily)
n_periods = 51
self.data = RollingWindow[Slice](n_periods)
self.Train(self.DateRules.MonthStart('VTI'), self.TimeRules.Midnight, self.Rebalance)
self.model = None
self.SetWarmup(n_periods)
self.prev_day = -1
def OnData(self, data):
# this prevents duplicate bars that sometimes occurs because of SetWarmUp
if self.prev_day != self.Time.day:
self.data.Add(data)
self.prev_day = self.Time.day
def Rebalance(self):
if not self.data.IsReady:
return
try:
# since RollingWindow is recent at top, we need to reverse it
data = self.PandasConverter.GetDataFrame(self.data).iloc[::-1]
except:
return
# turn the closing prices for each equity into columns
data = data['close'].unstack(level=0)
# sometimes we are missing a row of data
if len(data) < self.data.Count:
return
tickers = [symbol.split(' ')[0] for symbol in data.columns]
if self.model is None:
self.model = Model()
allocations = self.model.get_allocations(data)
self.Log(f'Portfolio Allocations: {allocations}')
for ticker, allocation in zip(tickers, allocations):
self.SetHoldings(ticker, allocation)import numpy as np
# setting the seed allows for reproducible results
np.random.seed(123)
import tensorflow as tf
from tensorflow.keras.layers import LSTM, Flatten, Dense
from tensorflow.keras.models import Sequential
import tensorflow.keras.backend as K
class Model:
def __init__(self):
self.data = None
self.model = None
def __build_model(self, input_shape, outputs):
'''
Builds and returns the Deep Neural Network that will compute the allocation ratios
that optimize the Sharpe Ratio of the portfolio
inputs: input_shape - tuple of the input shape, outputs - the number of assets
returns: a Deep Neural Network model
'''
model = Sequential([
LSTM(64, input_shape=input_shape),
Flatten(),
Dense(outputs, activation='softmax')
])
def sharpe_loss(_, y_pred):
# make all time-series start at 1
data = tf.divide(self.data, self.data[0])
# value of the portfolio after allocations applied
portfolio_values = tf.reduce_sum(tf.multiply(data, y_pred), axis=1)
portfolio_returns = (portfolio_values[1:] - portfolio_values[:-1]) / portfolio_values[:-1] # % change formula
sharpe = K.mean(portfolio_returns) / K.std(portfolio_returns)
# since we want to maximize Sharpe, while gradient descent minimizes the loss,
# we can negate Sharpe (the min of a negated function is its max)
return -sharpe
model.compile(loss=sharpe_loss, optimizer='adam')
return model
def get_allocations(self, data):
'''
Computes and returns the allocation ratios that optimize the Sharpe over the given data
input: data - DataFrame of historical closing prices of various assets
return: the allocations ratios for each of the given assets
'''
# data with returns
data_w_ret = np.concatenate([ data.values[1:], data.pct_change().values[1:] ], axis=1)
data = data.iloc[1:]
self.data = tf.cast(tf.constant(data), float)
if self.model is None:
self.model = self.__build_model(data_w_ret.shape, len(data.columns))
fit_predict_data = data_w_ret[np.newaxis,:]
self.model.fit(fit_predict_data, np.zeros((1, len(data.columns))), epochs=20, shuffle=False)
return self.model.predict(fit_predict_data)[0]