Skip to content

Commit

Permalink
Implement main framework
Browse files Browse the repository at this point in the history
  • Loading branch information
jankrepl authored Apr 10, 2020
1 parent ca16940 commit 151e632
Show file tree
Hide file tree
Showing 52 changed files with 4,421 additions and 1,189 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
# deepdow
![final](https://user-images.githubusercontent.com/18519371/79003829-afca6380-7b53-11ea-8322-f05577536957.png)

[![Build Status](https://travis-ci.com/jankrepl/deepdow.svg?branch=master)](https://travis-ci.com/jankrepl/deepdow)
[![codecov](https://codecov.io/gh/jankrepl/deepdow/branch/master/graph/badge.svg)](https://codecov.io/gh/jankrepl/deepdow)
2 changes: 1 addition & 1 deletion deepdow/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Package for using deep learning in portfolio optimization.
"""Package connecting deep learning and portfolio optimization.
Release markers:
X.Y
Expand Down
154 changes: 145 additions & 9 deletions deepdow/benchmarks.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,159 @@
"""Collection of benchmarks."""
from abc import ABC, abstractmethod

import cvxpy as cp
from cvxpylayers.torch import CvxpyLayer
import torch

from .layers import CovarianceMatrix


class Benchmark(ABC):
"""Abstract benchmark class.
The idea is to create some benchmarks that we can use for comparison to our neural networks.
The idea is to create some benchmarks that we can use for comparison to our neural networks. Note that we
assume that benchmarks are not trainable - one can only use them for inference.
"""

@abstractmethod
def __call__(self, X):
"""Prediction of the model."""

def fit(self, *args, **kwargs):
"""Fitting of the model. By default does nothing."""
return self
@property
def hparams(self):
"""Hyperparamters relevant to construction of the model."""
return {}


class MaximumReturn(Benchmark):
"""Markowitz portfolio optimization - maximum return."""

def __init__(self, max_weight=1, n_assets=None, returns_channel=0):
"""Construct.
Parameters
----------
max_weight : float
A number in (0, 1] representing the maximum weight per asset.
n_assets : None or int
If specifed the benchmark will always have to be provided with `n_assets` of assets in the `__call__`.
This way one can achieve major speedups since the optimization problem is canonicalized only once in the
constructor. However, when `n_assets` is None the optimization problem is canonicalized before each
inside of `__call__` which results in overhead but allows for variable number of assets.
returns_channel : int
Which channel in the `X` feature matrix to consider (the 2nd dimension) as returns.
"""
self.max_weight = max_weight
self.n_assets = n_assets
self.return_channel = returns_channel

self.optlayer = self._construct_problem(n_assets, max_weight) if self.n_assets is not None else None

@staticmethod
def _construct_problem(n_assets, max_weight):
"""Construct cvxpylayers problem."""
rets = cp.Parameter(n_assets)
w = cp.Variable(n_assets)

ret = rets @ w
prob = cp.Problem(cp.Maximize(ret), [cp.sum(w) == 1,
w >= 0,
w <= max_weight])

return CvxpyLayer(prob, parameters=[rets], variables=[w])

def __call__(self, X):
"""Predict weights.
Parameters
----------
X : torch.Tensor
Tensor of shape `(n_samples, n_input_channels, lookback, n_assets)`.
Returns
-------
weights : torch.Tensor
Tensor of shape `(n_samples, n_assets)` representing the predicted weights.
"""
n_samples, _, lookback, n_assets = X.shape

# Problem setup
if self.optlayer is not None:
if self.n_assets != n_assets:
raise ValueError('Incorrect number of assets: {}, expected: {}'.format(n_assets, self.n_assets))

optlayer = self.optlayer
else:
optlayer = self._construct_problem(n_assets, self.max_weight)

rets_estimate = X[:, self.return_channel, :, :].mean(dim=1) # (n_samples, n_assets)

return optlayer(rets_estimate)[0]


class MinimumVariance(Benchmark):
"""Markowitz portfolio optimization - minimum variance."""

def __init__(self, max_weight=1, returns_channel=0, n_assets=None):
"""Construct.
Parameters
----------
max_weight : float
A number in (0, 1] representing the maximum weight per asset.
"""
self.n_assets = n_assets
self.return_channel = returns_channel
self.max_weight = max_weight

self.optlayer = self._construct_problem(n_assets, max_weight) if self.n_assets is not None else None

@staticmethod
def _construct_problem(n_assets, max_weight):
"""Construct cvxpylayers problem."""
covmat_sqrt = cp.Parameter((n_assets, n_assets))
w = cp.Variable(n_assets)

risk = cp.sum_squares(covmat_sqrt @ w)
prob = cp.Problem(cp.Minimize(risk), [cp.sum(w) == 1,
w >= 0,
w <= max_weight])

return CvxpyLayer(prob, parameters=[covmat_sqrt], variables=[w])

def __call__(self, X):
"""Predict weights.
Parameters
----------
X : torch.Tensor
Tensor of shape `(n_samples, n_input_channels, lookback, n_assets)`.
Returns
-------
weights : torch.Tensor
Tensor of shape `(n_samples, n_assets)` representing the predicted weights.
"""
n_samples, _, lookback, n_assets = X.shape

# Problem setup
if self.optlayer is not None:
if self.n_assets != n_assets:
raise ValueError('Incorrect number of assets: {}, expected: {}'.format(n_assets, self.n_assets))

optlayer = self.optlayer
else:
optlayer = self._construct_problem(n_assets, self.max_weight)

# problem solver
covmat_sqrt_estimates = CovarianceMatrix(sqrt=True)(X[:, self.return_channel, :, :])

return optlayer(covmat_sqrt_estimates)[0]


class OneOverN(Benchmark):
Expand All @@ -29,7 +165,7 @@ def __call__(self, X):
Parameters
----------
X : torch.Tensor
Tensor of shape `(n_samples, 1, lookback, n_assets)`.
Tensor of shape `(n_samples, n_input_channels, lookback, n_assets)`.
Returns
-------
Expand All @@ -39,7 +175,7 @@ def __call__(self, X):
"""
n_samples, n_channels, lookback, n_assets = X.shape

return torch.ones((n_samples, n_assets)) / n_assets
return torch.ones((n_samples, n_assets), dtype=X.dtype, device=X.device) / n_assets


class Random(Benchmark):
Expand All @@ -51,7 +187,7 @@ def __call__(self, X):
Parameters
----------
X : torch.Tensor
Tensor of shape `(n_samples, 1, lookback, n_assets)`.
Tensor of shape `(n_samples, n_input_channels, lookback, n_assets)`.
Returns
-------
Expand All @@ -61,7 +197,7 @@ def __call__(self, X):
"""
n_samples, n_channels, lookback, n_assets = X.shape

weights_unscaled = torch.rand((n_samples, n_assets))
weights_unscaled = torch.rand((n_samples, n_assets), dtype=X.dtype, device=X.device)
weights_sums = weights_unscaled.sum(dim=1, keepdim=True).repeat(1, n_assets)

return weights_unscaled / weights_sums
Expand Down Expand Up @@ -99,7 +235,7 @@ def __call__(self, X):
if self.asset_ix not in set(range(n_assets)):
raise IndexError('The selected asset index is out of range.')

weights = torch.zeros((n_samples, n_assets))
weights = torch.zeros((n_samples, n_assets), dtype=X.dtype, device=X.device)
weights[:, self.asset_ix] = 1

return weights
Loading

0 comments on commit 151e632

Please sign in to comment.