Source code for deepdow.benchmarks

"""Collection of benchmarks."""
from abc import ABC, abstractmethod

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

from .layers import CovarianceMatrix


[docs]class Benchmark(ABC): """Abstract benchmark class. 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. """
[docs] @abstractmethod def __call__(self, x): """Prediction of the model."""
@property def hparams(self): """Hyperparamters relevant to construction of the model.""" return {}
[docs]class InverseVolatility(Benchmark): """Allocation only considering volatility of individual assets. Parameters ---------- use_std : bool If True, then we use standard deviation as a measure of volatility. Otherwise variance is used. returns_channel : int Which channel in the `x` feature matrix to consider (the 2nd dimension) as returns. """ def __init__(self, use_std=False, returns_channel=0): self.use_std = use_std self.returns_channel = returns_channel
[docs] def __call__(self, x): """Predict weights. Parameters ---------- x : torch.Tensor Tensor of shape `(n_samples, n_channels, lookback, n_assets)`. Returns ------- weights : torch.Tensor Tensor of shape `(n_samples, n_assets)` representing the predicted weights. """ eps = 1e-6 x_rets = x[:, self.returns_channel, ...] vols = x_rets.std(dim=1) if self.use_std else x_rets.var(dim=1) ivols = 1 / (vols + eps) weights = ivols / ivols.sum(dim=1, keepdim=True) return weights
@property def hparams(self): """Hyperparamters relevant to construction of the model.""" return {'use_std': self.use_std, 'returns_channel': self.returns_channel}
[docs]class MaximumReturn(Benchmark): """Markowitz portfolio optimization - maximum return. 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. Attributes ---------- optlayer : cvxpylayers.torch.CvxpyLayer or None Equal to None if `n_assets` not provided in the constructor. In this case optimization problem is constructed with each forward pass. This allows for variable number of assets but is slower. If `n_assets` provided than constructed once and for all in the constructor. """ def __init__(self, max_weight=1, n_assets=None, returns_channel=0): self.max_weight = max_weight self.n_assets = n_assets self.returns_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])
[docs] def __call__(self, x): """Predict weights. Parameters ---------- x : torch.Tensor Tensor of shape `(n_samples, n_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.returns_channel, :, :].mean(dim=1) # (n_samples, n_assets) return optlayer(rets_estimate)[0]
@property def hparams(self): """Hyperparamters relevant to construction of the model.""" return {'max_weight': self.max_weight, 'returns_channel': self.returns_channel, 'n_assets': self.n_assets}
[docs]class MinimumVariance(Benchmark): """Markowitz portfolio optimization - minimum variance. 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. Attributes ---------- optlayer : cvxpylayers.torch.CvxpyLayer or None Equal to None if `n_assets` not provided in the constructor. In this case optimization problem is constructed with each forward pass. This allows for variable number of assets but is slower. If `n_assets` provided than constructed once and for all in the constructor. """ def __init__(self, max_weight=1, returns_channel=0, n_assets=None): self.n_assets = n_assets self.returns_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])
[docs] def __call__(self, x): """Predict weights. Parameters ---------- x : torch.Tensor Tensor of shape `(n_samples, n_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.returns_channel, :, :]) return optlayer(covmat_sqrt_estimates)[0]
@property def hparams(self): """Hyperparamters relevant to construction of the model.""" return {'max_weight': self.max_weight, 'returns_channel': self.returns_channel, 'n_assets': self.n_assets}
[docs]class OneOverN(Benchmark): """Equally weighted portfolio."""
[docs] def __call__(self, x): """Predict weights. Parameters ---------- x : torch.Tensor Tensor of shape `(n_samples, n_channels, lookback, n_assets)`. Returns ------- weights : torch.Tensor Tensor of shape `(n_samples, n_assets)` representing the predicted weights. """ n_samples, n_channels, lookback, n_assets = x.shape return torch.ones((n_samples, n_assets), dtype=x.dtype, device=x.device) / n_assets
[docs]class Random(Benchmark): """Random allocation for each prediction."""
[docs] def __call__(self, x): """Predict weights. Parameters ---------- x : torch.Tensor Tensor of shape `(n_samples, n_channels, lookback, n_assets)`. Returns ------- weights : torch.Tensor Tensor of shape `(n_samples, n_assets)` representing the predicted weights. """ n_samples, n_channels, lookback, n_assets = x.shape 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
[docs]class Singleton(Benchmark): """Predict a single asset. Parameters ---------- asset_ix : int Index of the asset to predict. """ def __init__(self, asset_ix): self.asset_ix = asset_ix
[docs] def __call__(self, x): """Predict weights. Parameters ---------- x : torch.Tensor Tensor of shape `(n_samples, n_channels, lookback, n_assets)`. Returns ------- weights : torch.Tensor Tensor of shape `(n_samples, n_assets)` representing the predicted weights. """ n_samples, n_channels, lookback, n_assets = x.shape 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), dtype=x.dtype, device=x.device) weights[:, self.asset_ix] = 1 return weights
@property def hparams(self): """Hyperparamters relevant to construction of the model.""" return {'asset_ix': self.asset_ix}