Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds Deep Learning Portfolio Optimization #338

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,15 @@
],
'description' => "Analyzes the news releases of drug manufacturers and places intraday trades for the stocks with positive news.",
'tags'=>'Equities, NLP, News Sentiment, Drug Manufacturers, Tiingo, Intraday'
],
[
'name' => 'Deep Learning Portfolio Optimization',
'link' => 'strategy-library/deep-learning-portfolio-optimization',
'sources' => [
'arXiv' => 'https://arxiv.org/abs/1812.04199'
],
'description' => "Analyzes the news releases of drug manufacturers and places intraday trades for the stocks with positive news.",
'tags'=>'Equities, NLP, News Sentiment, Drug Manufacturers, Tiingo, Intraday'
]
];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<p>
In this tutorial, we apply Deep Learning to optimize a portfolio of various ETFs.
</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<p><a href="https://en.wikipedia.org/wiki/Portfolio_optimization">Portfolio Optimization</a> is the process of choosing asset allocation ratios to select the best portfolio according to some objective. For example, for investors looking to minimize the risk of their portfolio, an investor can set out to minimize a risk metric, such as <a href="https://www.investopedia.com/investing/beta-know-risk/#:~:text=Beta%20is%20a%20measure%20of,has%20a%20beta%20above%201.0.">Beta</a> or <a href="https://www.investopedia.com/terms/v/var.asp">Value at Risk</a>. In our <a href="https://www.quantconnect.com/tutorials/strategy-library/optimal-pairs-trading">Optimal Pairs Trading algorithm</a>, our objective was to maximize the fit of a portfolio of GLD and SLV to an Ornstein-Uhlenbeck process, which we measured using Maximum Likelihood Estimation. The strategy we discuss today will seek to maximize the <a href="https://www.investopedia.com/terms/s/sharperatio.asp">Sharpe Ratio</a> in an attempt to achieve a portfolio that achieves desirable risk-adjusted returns. However, it should be noted to investors that since the metrics are obtained through a fit to past data, there is no guarantee that the portfolio will behave in a similar fashion in the future. This can be partially addressed by choosing metrics and a combination of assets that lead to strong autocorrelation in the metric values, however, we leave this as a suggestion for future research.&nbsp;</p>
<p>Moving on, there are a few ways to determine the optimal asset allocations. The simplest way is to all possible combinations of asset allocations ratios discretized to values between 0 and 1. To illustrate, say we discretize allocation ratios using an increment of .01, and say we are testing allocation ratios for two assets, A1 and A2, we&rsquo;d first test 1.00 for A1 and 0.00 for A2, and compute the objective, then we&rsquo;d test 0.99 for A1 and .44 for A2, and compute the objective, and repeat this method until the objective values for all combinations are calculated. Then, we can choose the allocation ratios that lead to the most optimal objective value. However, this iterative method can be quite slow, and the runtime grows exponentially with each newly added asset. Thus, several alternative methods have been developed to tackle optimization, such as Newton and Quasi-Newton optimization algorithms. In our strategy, we leverage a Deep Neural Network with Adam Optimizer to determine the asset allocations for optimal Sharpe.</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<p>
Let’s start by importing the necessary packages. We use Keras to build the Model, and we require NumPy for a few manipulations of our data, so our imports are the following:
</p>

<div class="section-example-container">
<pre class="python">
import numpy as np
np.random.seed(1)
import tensorflow as tf
from tensorflow.keras.layers import LSTM, Flatten, Dense
from tensorflow.keras.models import Sequential
import tensorflow.keras.backend as K
</pre>
</div>

<p><strong>np.random.seed(1)</strong> isn&rsquo;t actually an import, however, we call it early, even before the imports, so that our results are reproducible.</p>

<p>We initialize our <strong>Model</strong> class with the following:</p>

<div class="section-example-container">
<pre class="python">
class Model:
def __init__(self):
self.data = None
self.model = None
</pre>
</div>

<p><strong>self.model </strong>is how we will store our Keras model, and <strong>self.data</strong> will be explained later, though for now think of it as a matrix of data of our asset prices stored in a TensorFlow <strong>Tensor </strong>object.</p>

<p>Now inside our <strong>Model</strong>&rsquo;s <strong>__build_model</strong> function, we first build the Keras Neural Network:</p>

<div class="section-example-container">
<pre class="python">
def __build_model(self, input_shape, outputs):
model = Sequential([
LSTM(64, input_shape=input_shape),
Flatten(),
Dense(outputs, activation='softmax')
])
</pre>
</div>

<p>The outputs of our Neural Network are the allocations ratios of the assets. Our first layer is an LSTM as LSTMs work well with financial data. Since our input is not flat, we then need to flatten the neurons in the next layer with a Flatten layer. With our output layer, a standard Dense layer, we use Softmax activation so that the allocation ratios add up to 1.</p>
<p>Then, since we are optimizing the Sharpe Ratio, we need to create a custom loss function that computes the Sharpe Ratio:</p>

<div class="section-example-container">
<pre class="python">
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
</pre>
</div>

<p>This uses the values of <strong>y_pred</strong> as the coefficients for the allocations for the assets, weighs the time-series data accordingly to the allocation, and computes the portfolio values. Then, we calculate the Sharpe Ratio from the portfolio values. We negate the Sharpe value before returning it because gradient descent minimizes the loss, so by minimizing the negative of our function, we get the maximum of our function. Note that we do not use the first argument, the &ldquo;true y values&rdquo; parameter, because there are no &ldquo;true y values&rdquo;, as we use the architecture of the Deep Neural Network for everything except prediction.</p>

<p>Finally, we need to compile our Neural Network using our custom defined loss along with the Adam solver:</p>

<div class="section-example-container">
<pre class="python">
model.compile(loss=sharpe_loss, optimizer='adam')
return model
</pre>
</div>

<p>Then, we need to get the ratios computed from this model when we feed in data, and we put this functionality inside the <strong>get_allocations</strong> method, where <strong>data</strong> is a <strong>pandas DataFrame</strong> of closing prices for the different assets:</p>

<div class="section-example-container">
<pre class="python">
def get_allocations(self, data):
</pre>
</div>

<p>The features for the model are the original closing price time-series for each of the assets, as well as the daily returns computed from this data:</p>

<div class="section-example-container">
<pre class="python">
data_w_ret = np.concatenate([ data.values[1:], data.pct_change().values[1:] ], axis=1)
</pre>
</div>

<p>Because computing the returns causes the first row to be NaNs, so we skip the first row for the returns data, and to make the data even in shape, we need to skip the first row for the original data as well.</p>

<p>Then, we also need to save the closing prices to <strong>self.data</strong> so we can use it to compute the Sharpe Ratio in the custom loss function we defined earlier:</p>

<div class="section-example-container">
<pre class="python">
data = data.iloc[1:]
self.data = tf.cast(tf.constant(data), float)
</pre>
</div>

<p>To remain consistent with before, we remove the first row of the data. When we store this data inside <strong>self.data</strong>, we need to convert the data into a Tensorflow Tensor and cast it to the standard <strong>float</strong> so it is compatible with the matrix operations we perform inside the Sharpe loss functions.</p>
<p>Then, to call the function we created earlier to build and compile our model, we use:</p>

<div class="section-example-container">
<pre class="python">
if self.model is None:
self.model = self.__build_model(data_w_ret.shape, len(data.columns))
</pre>
</div>

<p>We delay the building and compiling of our Neural Network model to make the logic for determining the model parameters, the input shape and the number of outputs, cleaner as we can determine these parameters from the data directly.&nbsp;</p>
<p>Then to compute and return the allocation ratios, we use the following:</p>

<div class="section-example-container">
<pre class="python">
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]
</pre>
</div>

<p>Note that we pass in zeros for the &ldquo;true y values&rdquo;. As long as the size of a row matches the size of the outputs, the values we pass in for this don&rsquo;t matter, because as we explained earlier, we don&rsquo;t use these values in our custom loss function.</p>
<p><br />The rest of the algorithm is quite simple. We pass in the DataFrame of the past 51 closing prices for the different assets and use <strong>SetHoldings(</strong><strong><em>asset symbol, allocation</em></strong><strong>)</strong> on each asset using the allocation computed using our <strong>Model</strong>&rsquo;s <strong>get_allocations</strong> method.</p>

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<div class="qc-embed-frame" style="display: inline-block; position: relative; width: 100%; min-height: 100px; min-width: 300px;">
<div class="qc-embed-dummy" style="padding-top: 56.25%;"></div>
<div class="qc-embed-element" style="position: absolute; top: 0; bottom: 0; left: 0; right: 0;">
<iframe class="qc-embed-backtest" height="100%" width="100%" style="border: 1px solid #ccc; padding: 0; margin: 0;" src="https://www.quantconnect.com/terminal/processCache?request=embedded_backtest_4ebbe01bfea8c5ae6f98fcda38a50b1c.html"></iframe>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<p>
We benchmark the performance of this algorithm against the S&P500 index, which we track using SPY. Over the five years from October 2015 to October 2020, the algorithm achieved a Sharpe Ratio of 1.032, while SPY achieved a Sharpe Ratio of 0.769 over the same period. Furthermore, our algorithm had a relatively low drawdown during the drawdown in March.
</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ol>
<li>
Zihao Zhang, Stefan Zohren, &amp; Stephen Roberts. (2020). Deep Learning for Portfolio Optimisation. <a href="https://arxiv.org/pdf/2005.13665.pdf">Online Copy</a>.
</li>
</ol>