{
  "cells": [
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "%matplotlib inline"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "\n# Getting started\n\nWelcome to :code:`deepdow`! This tutorial is going to demonstrate all the essential features.\nBefore you continue, make sure to check out `basics` to familiarize yourself with the core ideas\nof :code:`deepdow`. This hands-on tutorial is divided into 4 sections\n\n1. Dataset creation and loading\n2. Network definition\n3. Training\n4. Evaluation and visualization of results\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Preliminaries\nLet us start with importing all important dependencies.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "from deepdow.benchmarks import Benchmark, OneOverN, Random\nfrom deepdow.callbacks import EarlyStoppingCallback\nfrom deepdow.data import InRAMDataset, RigidDataLoader, prepare_standard_scaler, Scale\nfrom deepdow.data.synthetic import sin_single\nfrom deepdow.experiments import Run\nfrom deepdow.layers import SoftmaxAllocator\nfrom deepdow.losses import MeanReturns, SharpeRatio, MaximumDrawdown\nfrom deepdow.visualize import generate_metrics_table, generate_weights_table, plot_metrics, plot_weight_heatmap\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport torch"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "In order to be able to reproduce all results we set both the :code:`numpy` and :code:`torch` seed.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "torch.manual_seed(4)\nnp.random.seed(5)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Dataset creation and loading\nIn this example, we are going to be using a synthetic dataset. Asset returns are going to be\nsine functions where the frequency and phase are randomly selected for each asset. First of\nall let us set all the parameters relevant to data creation.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "n_timesteps, n_assets = 1000, 20\nlookback, gap, horizon = 40, 2, 20\nn_samples = n_timesteps - lookback - horizon - gap + 1"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Additionally, we will use approximately 80% of the data for training and 20% for testing.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "split_ix = int(n_samples * 0.8)\nindices_train = list(range(split_ix))\nindices_test = list(range(split_ix + lookback + horizon, n_samples))\n\nprint('Train range: {}:{}\\nTest range: {}:{}'.format(indices_train[0], indices_train[-1],\n                                                     indices_test[0], indices_test[-1]))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Now we can generate the synthetic asset returns of with shape :code:`(n_timesteps, n_assets)`.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "returns = np.array([sin_single(n_timesteps,\n                               freq=1 / np.random.randint(3, lookback),\n                               amplitude=0.05,\n                               phase=np.random.randint(0, lookback)\n                               ) for _ in range(n_assets)]).T"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "We also add some noise.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "returns += np.random.normal(scale=0.02, size=returns.shape)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "See below the first 100 timesteps of 2 assets.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "plt.plot(returns[:100, [1, 2]])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "To obtain the feature matrix :code:`X` and the target :code:`y` we apply the rolling window\nstrategy.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "X_list, y_list = [], []\n\nfor i in range(lookback, n_timesteps - horizon - gap + 1):\n    X_list.append(returns[i - lookback: i, :])\n    y_list.append(returns[i + gap: i + gap + horizon, :])\n\nX = np.stack(X_list, axis=0)[:, None, ...]\ny = np.stack(y_list, axis=0)[:, None, ...]\n\nprint('X: {}, y: {}'.format(X.shape, y.shape))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "As commonly done in every deep learning application, we want to scale our input features to\nbe approximately centered around 0 and have a standard deviation of 1. In :code:`deepdow` we\ncan achieve this with the :code:`prepare_standard_scaler` function that computes the mean\nand standard deviation of the input (for each channel). Additionally, we do not want to leak\nany information from our test set and therefore we only compute these statistics over the\ntraining set.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "means, stds = prepare_standard_scaler(X, indices=indices_train)\nprint('mean: {}, std: {}'.format(means, stds))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "We can now construct the :code:`InRAMDataset`. By providing the optional :code:`transform` we\nmake sure that when the samples are streamed they are always scaled based on our computed\n(training) statistics. See `inramdataset` for more details.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "dataset = InRAMDataset(X, y, transform=Scale(means, stds))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Using the :code:`dataset` we can now construct two dataloaders\u2014one for training and the other one\nfor testing. For more details see `dataloaders`.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "dataloader_train = RigidDataLoader(dataset,\n                                   indices=indices_train,\n                                   batch_size=32)\n\ndataloader_test = RigidDataLoader(dataset,\n                                  indices=indices_test,\n                                  batch_size=32)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Network definition\nLet us now write a custom network. See `writing_custom_networks`.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "class GreatNet(torch.nn.Module, Benchmark):\n    def __init__(self, n_assets, lookback, p=0.5):\n        super().__init__()\n\n        n_features = n_assets * lookback\n\n        self.dropout_layer = torch.nn.Dropout(p=p)\n        self.dense_layer = torch.nn.Linear(n_features, n_assets, bias=True)\n        self.allocate_layer = SoftmaxAllocator(temperature=None)\n        self.temperature = torch.nn.Parameter(torch.ones(1), requires_grad=True)\n\n    def forward(self, x):\n        \"\"\"Perform forward pass.\n\n        Parameters\n        ----------\n        x : torch.Tensor\n            Of shape (n_samples, 1, lookback, n_assets).\n\n        Returns\n        -------\n        weights : torch.Torch\n            Tensor of shape (n_samples, n_assets).\n\n        \"\"\"\n        n_samples, _, _, _ = x.shape\n        x = x.view(n_samples, -1)  # flatten features\n        x = self.dropout_layer(x)\n        x = self.dense_layer(x)\n\n        temperatures = torch.ones(n_samples).to(device=x.device, dtype=x.dtype) * self.temperature\n        weights = self.allocate_layer(x, temperatures)\n\n        return weights"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "So what is this network doing? First of all, we make an assumption that assets and lookback will\nnever change (the same shape and order at train and at inference time). This assumption\nis justified since we are using :code:`RigidDataLoader`.\nWe can learn :code:`n_assets` linear models that have :code:`n_assets * lookback` features. In\nother words we have a dense layer that takes the flattened feature tensor :code:`x` and returns\na vector of length :code:`n_assets`. Since elements of this vector can range from $-\\infty$\nto $\\infty$ we turn it into an asset allocation via :code:`SoftmaxAllocator`.\nAdditionally, we learn the :code:`temperature` from the data. This will enable us to learn the\noptimal trade-off between an equally weighted allocation (uniform distribution) and\nsingle asset portfolios.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "network = GreatNet(n_assets, lookback)\nprint(network)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "In :code:`torch` networks are either in the **train** or **eval** mode. Since we are using\ndropout it is essential that we set the mode correctly based on what we are trying to do.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "network = network.train()  # it is the default, however, just to make the distinction clear"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Training\nIt is now time to define our loss. Let's say we want to achieve multiple objectives at the same\ntime. We want to minimize the drawdowns, maximize the mean returns and also maximize the Sharpe\nratio. All of these losses are implemented in :code:`deepdow.losses`. To avoid confusion, they\nare always implemented in a way that **the lower the value of the loss the better**. To combine\nmultiple objectives we can simply sum all of the individual losses. Similarly, if we want to\nassign more importance to one of them we can achieve this by multiplying by a constant. To learn\nmore see `losses`.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "loss = MaximumDrawdown() + 2 * MeanReturns() + SharpeRatio()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Note that by default all the losses assume that we input logarithmic returns\n(:code:`input_type='log'`) and that they are in the 0th channel (:code:`returns_channel=0`).\n\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "We now have all the ingredients ready for training of the neural network. :code:`deepdow` implements\na simple wrapper :code:`Run` that implements the training loop and a minimal callback\nframework. For further information see `experiments`.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "run = Run(network,\n          loss,\n          dataloader_train,\n          val_dataloaders={'test': dataloader_test},\n          optimizer=torch.optim.Adam(network.parameters(), amsgrad=True),\n          callbacks=[EarlyStoppingCallback(metric_name='loss',\n                                           dataloader_name='test',\n                                           patience=15)])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "To run the training loop, we use the :code:`launch` where we specify the number of epochs.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "history = run.launch(30)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Evaluation and visualization\nThe :code:`history` object returned by :code:`launch` contains a lot of useful information related\nto training. Specifically, the property :code:`metrics` returns a comprehensive :code:`pd.DataFrame`.\nTo display the average test loss per each epoch we can run following.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "per_epoch_results = history.metrics.groupby(['dataloader', 'metric', 'model', 'epoch'])['value']\n\nprint(per_epoch_results.count())  # double check number of samples each epoch\nprint(per_epoch_results.mean())  # mean loss per epoch"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "per_epoch_results.mean()['test']['loss']['network'].plot()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "To get more insight into what our network predicts we can use the :code:`deepdow.visualize` module.\nBefore we even start further evaluations, let us make sure the network is in eval model.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "network = network.eval()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "To put the performance of our network in context, we also utilize benchmarks. :code:`deepdow`\noffers multiple benchmarks already. Additionally, one can provide custom simple benchmarks or\nsome pre-trained networks.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "benchmarks = {\n    '1overN': OneOverN(),  # each asset has weight 1 / n_assets\n    'random': Random(),  # random allocation that is however close 1OverN\n    'network': network\n}"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "During training, the only mandatory metric/loss was the loss criterion that we tried to minimize.\nNaturally, one might be interested in many other metrics to evaluate the performance. See below\nan example.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "metrics = {\n    'MaxDD': MaximumDrawdown(),\n    'Sharpe': SharpeRatio(),\n    'MeanReturn': MeanReturns()\n}"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Let us now use the above created objects. We first generate a table with all metrics over all\nsamples and for all benchmarks. This is done via :code:`generate_metrics_table`.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "metrics_table = generate_metrics_table(benchmarks,\n                                       dataloader_test,\n                                       metrics)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "And then we plot it with :code:`plot_metrics`.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "plot_metrics(metrics_table)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Each plot represents a different metric. The x-axis represents the timestamps in our\ntest set. The different colors are capturing different models. How is the value of a metric\ncomputed? We assume that the investor predicts the portfolio at time x and buys it. He then\nholds it for :code:`horizon` timesteps. The actual metric is then computed over this time horizon.\n\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Finally, we are also interested in how the allocation/prediction looks like at each time step.\nWe can use the :code:`generate_weights_table` function to create a :code:`pd.DataFrame`.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "weight_table = generate_weights_table(network, dataloader_test)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "We then call the :code:`plot_weight_heatmap` to see a heatmap of weights.\n\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "collapsed": false
      },
      "outputs": [],
      "source": [
        "plot_weight_heatmap(weight_table,\n                    add_sum_column=True,\n                    time_format=None,\n                    time_skips=25)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "The rows represent different timesteps in our test set. The columns are all the assets in our\nuniverse. The values represent the weight in the portfolio. Additionally, we add a sum column\nto show that we are really generating valid allocations.\n\n"
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.7.9"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}