Skip to content

Instantly share code, notes, and snippets.

@eromoe
Forked from Burntt/demo_purgedkfoldcv.ipynb
Created March 10, 2023 06:16
Show Gist options
  • Save eromoe/ef30bc4670f7f5b66f4ad6fb282a64eb to your computer and use it in GitHub Desktop.
Save eromoe/ef30bc4670f7f5b66f4ad6fb282a64eb to your computer and use it in GitHub Desktop.
Demo_PurgedKFoldCV.ipynb
Display the source blob
Display the rendered blob
Raw
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"name": "Demo_PurgedKFoldCV.ipynb",
"provenance": [],
"collapsed_sections": [],
"authorship_tag": "ABX9TyMOK1b4162zSa6YVa3alK99",
"include_colab_link": true
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"language_info": {
"name": "python"
}
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"<a href=\"https://colab.research.google.com/gist/Burntt/f26e5414205542207949aeb9e9cc1ddb/demo_purgedkfoldcv.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
"cell_type": "markdown",
"source": [
"# The Combinatorial Purged Cross-Validation method: indexing example on crypto\n",
"\n",
"*By Berend Gort*\n",
"\n",
"www.medium.com/@CoderBurnt\n",
"\n",
"www.linkedin.com/in/berendgort/\n",
"\n",
"www.twitter.com/CoderBurnt\n",
"\n",
"\n"
],
"metadata": {
"id": "KFmER7NGbpgZ"
}
},
{
"cell_type": "markdown",
"source": [
"### Packages"
],
"metadata": {
"id": "RXozZT8vckj-"
}
},
{
"cell_type": "code",
"source": [
"# Install required packages\n",
"\n",
"%cd /\n",
"!git clone https://github.com/AI4Finance-Foundation/FinRL-Meta\n",
"%cd /FinRL-Meta/\n",
"!pip install git+https://github.com/AI4Finance-LLC/ElegantRL.git\n",
"!pip install git+https://github.com/AI4Finance-LLC/FinRL-Library.git\n",
"!pip install gputil\n",
"!pip install trading_calendars\n",
"!pip install fracdiff\n",
"!pip install timeseriescv\n",
"\n",
"#install TA-lib (technical analysis)\n",
"!wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz \n",
"!tar xvzf ta-lib-0.4.0-src.tar.gz\n",
"import os\n",
"os.chdir('ta-lib') \n",
"!./configure --prefix=/usr\n",
"!make \n",
"!make install\n",
"os.chdir('../')\n",
"!pip install TA-Lib\n",
"!pip install python-binance"
],
"metadata": {
"id": "RQGyziIBbmpk"
},
"execution_count": 1,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"### Imports"
],
"metadata": {
"id": "OA9PXZ2Ccpnd"
}
},
{
"cell_type": "code",
"source": [
"# Other imports\n",
"\n",
"import scipy as sp\n",
"import math\n",
"import pandas as pd\n",
"import requests\n",
"import json\n",
"import matplotlib.dates as mdates\n",
"import numpy as np\n",
"import pickle\n",
"import shutil\n",
"import pandas as pd\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"import itertools as itt\n",
"import numbers\n",
"import datetime\n",
"\n",
"\n",
"import torch\n",
"import torch.nn as nn\n",
"import torch.nn.functional as F\n",
"import torch.optim as optim\n",
"import seaborn as sns\n",
"\n",
"from datetime import datetime, timedelta\n",
"from talib.abstract import *\n",
"from binance.client import Client\n",
"from pandas.testing import assert_frame_equal\n",
"from sklearn import metrics\n",
"from sklearn.metrics import classification_report\n",
"from sklearn.model_selection import train_test_split\n",
"\n",
"from sklearn.preprocessing import MinMaxScaler \n",
"from torch.utils.data import Dataset, DataLoader, WeightedRandomSampler\n",
"from IPython.display import display, HTML\n",
"\n",
"from itertools import combinations\n",
"from abc import abstractmethod\n",
"from typing import Iterable, Tuple, List\n",
"\n",
"#from google.colab import files"
],
"metadata": {
"id": "FRaRl37NbOpo"
},
"execution_count": 2,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Plot settings\n",
"\n",
"SCALE_FACTOR = 1\n",
"\n",
"plt.style.use('seaborn')\n",
"plt.rcParams['figure.figsize'] = [5 * SCALE_FACTOR, 2 * SCALE_FACTOR]\n",
"plt.rcParams['figure.dpi'] = 300 * SCALE_FACTOR\n",
"plt.rcParams['font.size'] = 5 * SCALE_FACTOR\n",
"plt.rcParams['axes.labelsize'] = 5 * SCALE_FACTOR\n",
"plt.rcParams['axes.titlesize'] = 6 * SCALE_FACTOR\n",
"plt.rcParams['xtick.labelsize'] = 4 * SCALE_FACTOR\n",
"plt.rcParams['ytick.labelsize'] = 4 * SCALE_FACTOR\n",
"plt.rcParams['font.family'] = 'serif'"
],
"metadata": {
"id": "vywKmrJ0bQFJ"
},
"execution_count": 3,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"## This requires Binance API keys\n",
"\n",
"Video of how to get them easily:\n",
"\n",
"https://www.youtube.com/watch?v=qg-oboAY8rM"
],
"metadata": {
"id": "dzCD3zmCcrOL"
}
},
{
"cell_type": "code",
"source": [
"# Set your Binance data API keys!\n",
"\n",
"if not 'API_KEY_Binance' in locals():\n",
" print('Please enter your main API key:')\n",
" API_KEY_Binance = input()\n",
"\n",
" print('Please enter your secret API key:')\n",
" API_SECRET_Binance = input()"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "Aml-mqY5bRbL",
"outputId": "155047f4-15aa-4d59-a044-8dd4aa0daf81"
},
"execution_count": 4,
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Please enter your main API key:\n",
"qJHiV64YMnIAxA1nQFJOqJf8I9ZHaSfex44EMwLARiWHDGarV9vnvGRJ6na3K6Dp\n",
"Please enter your secret API key:\n",
"HS3c4TjhLEmMA4U7vGPS6poADyOX32V57jfkqwKOL6cwSz3Ikld6YHxQXQDO51t8\n"
]
}
]
},
{
"cell_type": "code",
"source": [
"def get_features_for_each_coin(tic_df):\n",
" tic_df['rsi'] = RSI(tic_df['close'], timeperiod=14)\n",
" tic_df['macd'], tic_df['macd_signal'], tic_df['macd_hist'] = MACD(tic_df['close'], fastperiod=12,\n",
" slowperiod=26, signalperiod=9)\n",
" tic_df['cci'] = CCI(tic_df['high'], tic_df['low'], tic_df['close'], timeperiod=14)\n",
" tic_df['dx'] = DX(tic_df['high'], tic_df['low'], tic_df['close'], timeperiod=14)\n",
" return tic_df"
],
"metadata": {
"id": "hkcnv_DcbUmb"
},
"execution_count": 5,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"## Get data "
],
"metadata": {
"id": "dJP3-VGIdEwv"
}
},
{
"cell_type": "code",
"source": [
"class BinanceProcessor():\n",
" def __init__(self, api_key_binance, api_secret_binance):\n",
" self.binance_api_key = api_key_binance # Enter your own API-key here\n",
" self.binance_api_secret = api_secret_binance # Enter your own API-secret here\n",
" self.binance_client = Client(api_key=api_key_binance, api_secret=api_secret_binance)\n",
"\n",
" def run(self, ticker_list, start_date, end_date, time_interval, technical_indicator_list, if_vix):\n",
" data = self.download_data(ticker_list, start_date, end_date, time_interval)\n",
" data = self.clean_data(data)\n",
" data = self.add_technical_indicator(data, technical_indicator_list)\n",
" data.index = data['time']\n",
"\n",
" if if_vix:\n",
" data = self.add_vix(data)\n",
"\n",
" price_array, tech_array, turbulence_array, time_array = self.df_to_array(data, if_vix)\n",
"\n",
" tech_nan_positions = np.isnan(tech_array)\n",
" tech_array[tech_nan_positions] = 0\n",
"\n",
" return data\n",
"\n",
" # main functions\n",
" def download_data(self, ticker_list, start_date, end_date,\n",
" time_interval):\n",
"\n",
" self.start_time = start_date\n",
" self.end_time = end_date\n",
" self.interval = time_interval\n",
" self.ticker_list = ticker_list\n",
"\n",
" final_df = pd.DataFrame()\n",
" for i in ticker_list:\n",
" hist_data = self.get_binance_bars(self.start_time, self.end_time, self.interval, symbol=i)\n",
" df = hist_data.iloc[:-1]\n",
" df = df.dropna()\n",
" df['tic'] = i\n",
" final_df = final_df.append(df)\n",
"\n",
" return final_df\n",
"\n",
" def clean_data(self, df):\n",
" df = df.dropna()\n",
"\n",
" return df\n",
"\n",
" def add_technical_indicator(self, df, tech_indicator_list):\n",
" # print('Adding self-defined technical indicators is NOT supported yet.')\n",
" # print('Use default: MACD, RSI, CCI, DX.')\n",
" self.tech_indicator_list = ['open', 'high', 'low', 'close', 'volume',\n",
" 'macd', 'macd_signal', 'macd_hist',\n",
" 'rsi', 'cci', 'dx']\n",
"\n",
" final_df = pd.DataFrame()\n",
" for i in df.tic.unique():\n",
"\n",
" # use massive function in previous cell\n",
" coin_df = df[df.tic == i].copy()\n",
" coin_df = get_features_for_each_coin(coin_df)\n",
"\n",
" # Append constructed tic_df\n",
" final_df = final_df.append(coin_df)\n",
" return final_df\n",
"\n",
" def add_turbulence(self, df):\n",
" print('Turbulence not supported yet. Return original DataFrame.')\n",
"\n",
" return df\n",
"\n",
" def add_vix(self, df):\n",
" print('VIX is not applicable for cryptocurrencies. Return original DataFrame')\n",
"\n",
" return df\n",
"\n",
" def df_to_array(self, df, if_vix):\n",
" unique_ticker = df.tic.unique()\n",
" if_first_time = True\n",
" for tic in unique_ticker:\n",
" if if_first_time:\n",
" price_array = df[df.tic == tic][['close']].values\n",
" tech_array = df[df.tic == tic][self.tech_indicator_list].values\n",
" if_first_time = False\n",
" else:\n",
" price_array = np.hstack([price_array, df[df.tic == tic][['close']].values])\n",
" tech_array = np.hstack([tech_array, df[df.tic == tic][self.tech_indicator_list].values])\n",
"\n",
" time_array = df[df.tic == self.ticker_list[0]]['time'].values\n",
"\n",
" assert price_array.shape[0] == tech_array.shape[0]\n",
"\n",
" return price_array, tech_array, np.array([]), time_array# \n",
"\n",
" # helper functions\n",
" def stringify_dates(self, date: datetime):\n",
" return str(int(date.timestamp() * 1000))\n",
"\n",
" def get_binance_bars(self, start_date, end_date, kline_size, symbol):\n",
" data_df = pd.DataFrame()\n",
" klines = self.binance_client.get_historical_klines(symbol, kline_size, start_date, end_date)\n",
" data = pd.DataFrame(klines,\n",
" columns=['timestamp', 'open', 'high', 'low', 'close', 'volume', 'close_time', 'quote_av',\n",
" 'trades', 'tb_base_av', 'tb_quote_av', 'ignore'])\n",
" data = data.drop(labels=['close_time', 'quote_av', 'trades', 'tb_base_av', 'tb_quote_av', 'ignore'], axis=1)\n",
" if len(data_df) > 0:\n",
" temp_df = pd.DataFrame(data)\n",
" data_df = data_df.append(temp_df)\n",
" else:\n",
" data_df = data\n",
"\n",
" data_df = data_df.apply(pd.to_numeric, errors='coerce')\n",
" data_df['time'] = [datetime.fromtimestamp(x / 1000.0) for x in data_df.timestamp]\n",
" data.drop(labels=[\"timestamp\"], axis=1)\n",
" data_df.index = [x for x in range(len(data_df))]\n",
"\n",
" return data_df\n"
],
"metadata": {
"id": "br8spZ1wbXYy"
},
"execution_count": 6,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"## Set constants"
],
"metadata": {
"id": "kZyTFb60blRh"
}
},
{
"cell_type": "code",
"source": [
"# Set constants:\n",
"\n",
"ticker_list = ['BTCUSDT'\n",
" ]\n",
"\n",
"\n",
"time_interval = '1d'\n",
"\n",
"# Care format\n",
"start_date = '2015-01-01 00:00:00'\n",
"end_date = '2020-01-01 00:00:00'\n",
"\n",
"\n",
"technical_indicator_list = ['open',\n",
" 'high',\n",
" 'low',\n",
" 'close',\n",
" 'volume',\n",
" 'macd',\n",
" 'macd_signal',\n",
" 'macd_hist',\n",
" 'rsi',\n",
" 'cci',\n",
" 'dx'\n",
" ]\n",
"\n",
"if_vix = False"
],
"metadata": {
"id": "wE4-QHXYbYQW"
},
"execution_count": 7,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Process data using unified data processor\n",
"\n",
"DP = BinanceProcessor(API_KEY_Binance, API_SECRET_Binance)\n",
"data_ohlcv = DP.run(ticker_list,\n",
" start_date,\n",
" end_date,\n",
" time_interval,\n",
" technical_indicator_list,\n",
" if_vix)"
],
"metadata": {
"id": "ot2C0ItubZQD"
},
"execution_count": 8,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Drop unecessary columns\n",
"\n",
"if 'timestamp' in data_ohlcv:\n",
" data_ohlcv.drop('timestamp', inplace=True, axis=1)\n",
"\n",
"if 'time' in data_ohlcv:\n",
" data_ohlcv.drop('time', inplace=True, axis=1)\n",
"\n",
"data_ohlcv.head(3)"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 174
},
"id": "yl1W84f1baW_",
"outputId": "3e43695d-d13d-47e5-9e75-f0f725d33930"
},
"execution_count": 9,
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": [
" open high low close volume tic rsi \\\n",
"time \n",
"2017-08-17 4261.48 4485.39 4200.74 4285.08 795.150377 BTCUSDT NaN \n",
"2017-08-18 4285.08 4371.52 3938.77 4108.37 1199.888264 BTCUSDT NaN \n",
"2017-08-19 4108.37 4184.69 3850.00 4139.98 381.309763 BTCUSDT NaN \n",
"\n",
" macd macd_signal macd_hist cci dx \n",
"time \n",
"2017-08-17 NaN NaN NaN NaN NaN \n",
"2017-08-18 NaN NaN NaN NaN NaN \n",
"2017-08-19 NaN NaN NaN NaN NaN "
],
"text/html": [
"\n",
" <div id=\"df-8667c1ad-9cb5-4761-8c1f-49d8fd700028\">\n",
" <div class=\"colab-df-container\">\n",
" <div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>open</th>\n",
" <th>high</th>\n",
" <th>low</th>\n",
" <th>close</th>\n",
" <th>volume</th>\n",
" <th>tic</th>\n",
" <th>rsi</th>\n",
" <th>macd</th>\n",
" <th>macd_signal</th>\n",
" <th>macd_hist</th>\n",
" <th>cci</th>\n",
" <th>dx</th>\n",
" </tr>\n",
" <tr>\n",
" <th>time</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>2017-08-17</th>\n",
" <td>4261.48</td>\n",
" <td>4485.39</td>\n",
" <td>4200.74</td>\n",
" <td>4285.08</td>\n",
" <td>795.150377</td>\n",
" <td>BTCUSDT</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2017-08-18</th>\n",
" <td>4285.08</td>\n",
" <td>4371.52</td>\n",
" <td>3938.77</td>\n",
" <td>4108.37</td>\n",
" <td>1199.888264</td>\n",
" <td>BTCUSDT</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2017-08-19</th>\n",
" <td>4108.37</td>\n",
" <td>4184.69</td>\n",
" <td>3850.00</td>\n",
" <td>4139.98</td>\n",
" <td>381.309763</td>\n",
" <td>BTCUSDT</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>\n",
" <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-8667c1ad-9cb5-4761-8c1f-49d8fd700028')\"\n",
" title=\"Convert this dataframe to an interactive table.\"\n",
" style=\"display:none;\">\n",
" \n",
" <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
" width=\"24px\">\n",
" <path d=\"M0 0h24v24H0V0z\" fill=\"none\"/>\n",
" <path d=\"M18.56 5.44l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94zm-11 1L8.5 8.5l.94-2.06 2.06-.94-2.06-.94L8.5 2.5l-.94 2.06-2.06.94zm10 10l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94z\"/><path d=\"M17.41 7.96l-1.37-1.37c-.4-.4-.92-.59-1.43-.59-.52 0-1.04.2-1.43.59L10.3 9.45l-7.72 7.72c-.78.78-.78 2.05 0 2.83L4 21.41c.39.39.9.59 1.41.59.51 0 1.02-.2 1.41-.59l7.78-7.78 2.81-2.81c.8-.78.8-2.07 0-2.86zM5.41 20L4 18.59l7.72-7.72 1.47 1.35L5.41 20z\"/>\n",
" </svg>\n",
" </button>\n",
" \n",
" <style>\n",
" .colab-df-container {\n",
" display:flex;\n",
" flex-wrap:wrap;\n",
" gap: 12px;\n",
" }\n",
"\n",
" .colab-df-convert {\n",
" background-color: #E8F0FE;\n",
" border: none;\n",
" border-radius: 50%;\n",
" cursor: pointer;\n",
" display: none;\n",
" fill: #1967D2;\n",
" height: 32px;\n",
" padding: 0 0 0 0;\n",
" width: 32px;\n",
" }\n",
"\n",
" .colab-df-convert:hover {\n",
" background-color: #E2EBFA;\n",
" box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
" fill: #174EA6;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert {\n",
" background-color: #3B4455;\n",
" fill: #D2E3FC;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert:hover {\n",
" background-color: #434B5C;\n",
" box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
" filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
" fill: #FFFFFF;\n",
" }\n",
" </style>\n",
"\n",
" <script>\n",
" const buttonEl =\n",
" document.querySelector('#df-8667c1ad-9cb5-4761-8c1f-49d8fd700028 button.colab-df-convert');\n",
" buttonEl.style.display =\n",
" google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
"\n",
" async function convertToInteractive(key) {\n",
" const element = document.querySelector('#df-8667c1ad-9cb5-4761-8c1f-49d8fd700028');\n",
" const dataTable =\n",
" await google.colab.kernel.invokeFunction('convertToInteractive',\n",
" [key], {});\n",
" if (!dataTable) return;\n",
"\n",
" const docLinkHtml = 'Like what you see? Visit the ' +\n",
" '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
" + ' to learn more about interactive tables.';\n",
" element.innerHTML = '';\n",
" dataTable['output_type'] = 'display_data';\n",
" await google.colab.output.renderOutput(dataTable, element);\n",
" const docLink = document.createElement('div');\n",
" docLink.innerHTML = docLinkHtml;\n",
" element.appendChild(docLink);\n",
" }\n",
" </script>\n",
" </div>\n",
" </div>\n",
" "
]
},
"metadata": {},
"execution_count": 9
}
]
},
{
"cell_type": "markdown",
"source": [
"# Triple barrier method\n",
"\n",
"I made a previous medium article about this, if you want to understand it please refer to my previous medium article.\n",
"\n",
"However, it is not required to understand PurgedKFoldCV, you can skip on to the next section: PurgedKFoldCV =)\n",
"\n",
"https://medium.com/coinmonks/crypto-feature-importance-for-deep-reinforcement-learning-38416616c2a36-8416616c2a36\n"
],
"metadata": {
"id": "Jr7mFKxRdbUH"
}
},
{
"cell_type": "code",
"source": [
"# IMPORTANT: Make sure that pd.Timedelta() is according to the time_interval to get the volatility for that time interval\n",
"\n",
"if time_interval == '5m':\n",
" Delta = pd.Timedelta(minutes=5)\n",
"elif time_interval == '1h':\n",
" Delta = pd.Timedelta(hours=1)\n",
"elif time_interval == '1d':\n",
" Delta = pd.Timedelta(days=1)\n",
"else:\n",
" raise ValueError('Timeframe not supported yet, please manually add!')"
],
"metadata": {
"id": "TPMJCMx3dwjw"
},
"execution_count": 10,
"outputs": []
},
{
"cell_type": "code",
"source": [
"def get_vol(prices, span=100, delta=Delta):\n",
"\n",
" # 1. compute returns of the form p[t]/p[t-1] - 1\n",
" # 1.1 find the timestamps of p[t-1] values\n",
" df0 = prices.index.searchsorted(prices.index - delta)\n",
" df0 = df0[df0 > 0]\n",
"\n",
" # 1.2 align timestamps of p[t-1] to timestamps of p[t]\n",
" df0 = pd.Series(prices.index[df0-1], \n",
" index=prices.index[prices.shape[0]-df0.shape[0] : ])\n",
" \n",
" # 1.3 get values by timestamps, then compute returns\n",
" df0 = prices.loc[df0.index] / prices.loc[df0.values].values - 1\n",
"\n",
" # 2. estimate rolling standard deviation\n",
" df0 = df0.ewm(span=span).std()\n",
" \n",
" return df0"
],
"metadata": {
"id": "hqqNxQFIdxBX"
},
"execution_count": 11,
"outputs": []
},
{
"cell_type": "code",
"source": [
"data_ohlcv = data_ohlcv.assign(volatility=get_vol(data_ohlcv.close)).dropna()"
],
"metadata": {
"id": "XhpZGD7VdzRL"
},
"execution_count": 12,
"outputs": []
},
{
"cell_type": "code",
"source": [
"def get_barriers():\n",
"\n",
" #create a container\n",
" barriers = pd.DataFrame(columns=['datapoints_passed', \n",
" 'price', 'vert_barrier', \\\n",
" 'top_barrier', 'bottom_barrier'], \\\n",
" index = daily_volatility.index)\n",
" \n",
" for datapoint, vol in daily_volatility.iteritems():\n",
"\n",
" datapoints_passed = len(daily_volatility.loc \\\n",
" [daily_volatility.index[0] : datapoint])\n",
" \n",
" #set the vertical barrier \n",
" if (datapoints_passed + t_final < len(daily_volatility.index) \\\n",
" and t_final != 0):\n",
" vert_barrier = daily_volatility.index[\n",
" datapoints_passed + t_final]\n",
" else:\n",
" vert_barrier = np.nan\n",
" \n",
" #set the top barrier\n",
" if upper_lower_multipliers[0] > 0:\n",
" top_barrier = prices.loc[datapoint] + prices.loc[datapoint] * \\\n",
" upper_lower_multipliers[0] * vol\n",
" else:\n",
" #set it to NaNs\n",
" top_barrier = pd.Series(index=prices.index)\n",
"\n",
" #set the bottom barrier\n",
" if upper_lower_multipliers[1] > 0:\n",
" bottom_barrier = prices.loc[datapoint] - prices.loc[datapoint] * \\\n",
" upper_lower_multipliers[1] * vol\n",
" else: \n",
" #set it to NaNs\n",
" bottom_barrier = pd.Series(index=prices.index)\n",
" \n",
" barriers.loc[datapoint, ['datapoints_passed', 'price', 'vert_barrier','top_barrier', 'bottom_barrier']] = \\\n",
" datapoints_passed, prices.loc[datapoint], vert_barrier, top_barrier, bottom_barrier\n",
"\n",
" return barriers"
],
"metadata": {
"id": "ZjaI2GDrd0Nr"
},
"execution_count": 13,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Set barrier parameters\n",
"\n",
"daily_volatility = data_ohlcv['volatility']\n",
"t_final = 10\n",
"upper_lower_multipliers = [2, 2]\n",
"price = data_ohlcv['close']\n",
"prices = price[daily_volatility.index]"
],
"metadata": {
"id": "SKWhNCGBd0o4"
},
"execution_count": 14,
"outputs": []
},
{
"cell_type": "code",
"source": [
"barriers = get_barriers()\n",
"barriers.head(5)"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 237
},
"id": "DgqsdrjPd2BQ",
"outputId": "c78b2685-a763-43ee-b527-e7f92b00acce"
},
"execution_count": 15,
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": [
" datapoints_passed price vert_barrier top_barrier \\\n",
"time \n",
"2017-09-19 1 3910.04 2017-09-30 00:00:00 4530.750516 \n",
"2017-09-20 2 3900.0 2017-10-01 00:00:00 4508.118671 \n",
"2017-09-21 3 3609.99 2017-10-02 00:00:00 4171.469472 \n",
"2017-09-22 4 3595.87 2017-10-03 00:00:00 4153.372755 \n",
"2017-09-23 5 3780.0 2017-10-04 00:00:00 4360.233001 \n",
"\n",
" bottom_barrier \n",
"time \n",
"2017-09-19 3289.329484 \n",
"2017-09-20 3291.881329 \n",
"2017-09-21 3048.510528 \n",
"2017-09-22 3038.367245 \n",
"2017-09-23 3199.766999 "
],
"text/html": [
"\n",
" <div id=\"df-b1a930d1-a5ec-418e-8712-b33cc24f0307\">\n",
" <div class=\"colab-df-container\">\n",
" <div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>datapoints_passed</th>\n",
" <th>price</th>\n",
" <th>vert_barrier</th>\n",
" <th>top_barrier</th>\n",
" <th>bottom_barrier</th>\n",
" </tr>\n",
" <tr>\n",
" <th>time</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>2017-09-19</th>\n",
" <td>1</td>\n",
" <td>3910.04</td>\n",
" <td>2017-09-30 00:00:00</td>\n",
" <td>4530.750516</td>\n",
" <td>3289.329484</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2017-09-20</th>\n",
" <td>2</td>\n",
" <td>3900.0</td>\n",
" <td>2017-10-01 00:00:00</td>\n",
" <td>4508.118671</td>\n",
" <td>3291.881329</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2017-09-21</th>\n",
" <td>3</td>\n",
" <td>3609.99</td>\n",
" <td>2017-10-02 00:00:00</td>\n",
" <td>4171.469472</td>\n",
" <td>3048.510528</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2017-09-22</th>\n",
" <td>4</td>\n",
" <td>3595.87</td>\n",
" <td>2017-10-03 00:00:00</td>\n",
" <td>4153.372755</td>\n",
" <td>3038.367245</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2017-09-23</th>\n",
" <td>5</td>\n",
" <td>3780.0</td>\n",
" <td>2017-10-04 00:00:00</td>\n",
" <td>4360.233001</td>\n",
" <td>3199.766999</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>\n",
" <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-b1a930d1-a5ec-418e-8712-b33cc24f0307')\"\n",
" title=\"Convert this dataframe to an interactive table.\"\n",
" style=\"display:none;\">\n",
" \n",
" <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
" width=\"24px\">\n",
" <path d=\"M0 0h24v24H0V0z\" fill=\"none\"/>\n",
" <path d=\"M18.56 5.44l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94zm-11 1L8.5 8.5l.94-2.06 2.06-.94-2.06-.94L8.5 2.5l-.94 2.06-2.06.94zm10 10l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94z\"/><path d=\"M17.41 7.96l-1.37-1.37c-.4-.4-.92-.59-1.43-.59-.52 0-1.04.2-1.43.59L10.3 9.45l-7.72 7.72c-.78.78-.78 2.05 0 2.83L4 21.41c.39.39.9.59 1.41.59.51 0 1.02-.2 1.41-.59l7.78-7.78 2.81-2.81c.8-.78.8-2.07 0-2.86zM5.41 20L4 18.59l7.72-7.72 1.47 1.35L5.41 20z\"/>\n",
" </svg>\n",
" </button>\n",
" \n",
" <style>\n",
" .colab-df-container {\n",
" display:flex;\n",
" flex-wrap:wrap;\n",
" gap: 12px;\n",
" }\n",
"\n",
" .colab-df-convert {\n",
" background-color: #E8F0FE;\n",
" border: none;\n",
" border-radius: 50%;\n",
" cursor: pointer;\n",
" display: none;\n",
" fill: #1967D2;\n",
" height: 32px;\n",
" padding: 0 0 0 0;\n",
" width: 32px;\n",
" }\n",
"\n",
" .colab-df-convert:hover {\n",
" background-color: #E2EBFA;\n",
" box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
" fill: #174EA6;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert {\n",
" background-color: #3B4455;\n",
" fill: #D2E3FC;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert:hover {\n",
" background-color: #434B5C;\n",
" box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
" filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
" fill: #FFFFFF;\n",
" }\n",
" </style>\n",
"\n",
" <script>\n",
" const buttonEl =\n",
" document.querySelector('#df-b1a930d1-a5ec-418e-8712-b33cc24f0307 button.colab-df-convert');\n",
" buttonEl.style.display =\n",
" google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
"\n",
" async function convertToInteractive(key) {\n",
" const element = document.querySelector('#df-b1a930d1-a5ec-418e-8712-b33cc24f0307');\n",
" const dataTable =\n",
" await google.colab.kernel.invokeFunction('convertToInteractive',\n",
" [key], {});\n",
" if (!dataTable) return;\n",
"\n",
" const docLinkHtml = 'Like what you see? Visit the ' +\n",
" '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
" + ' to learn more about interactive tables.';\n",
" element.innerHTML = '';\n",
" dataTable['output_type'] = 'display_data';\n",
" await google.colab.output.renderOutput(dataTable, element);\n",
" const docLink = document.createElement('div');\n",
" docLink.innerHTML = docLinkHtml;\n",
" element.appendChild(docLink);\n",
" }\n",
" </script>\n",
" </div>\n",
" </div>\n",
" "
]
},
"metadata": {},
"execution_count": 15
}
]
},
{
"cell_type": "code",
"source": [
"def get_labels():\n",
" barriers[\"label_barrier\"] = None\n",
" for i in range(len(barriers.index)):\n",
" start = barriers.index[i]\n",
" end = barriers.vert_barrier[i]\n",
" if pd.notna(end):\n",
"\n",
" # assign the initial and final price\n",
" price_initial = barriers.price[start]\n",
" price_final = barriers.price[end]\n",
"\n",
" # assign the top and bottom barriers\n",
" top_barrier = barriers.top_barrier[i]\n",
" bottom_barrier = barriers.bottom_barrier[i]\n",
"\n",
" #set the profit taking and stop loss conditons\n",
" condition_pt = (barriers.price[start: end] >= \\\n",
" top_barrier).any()\n",
" condition_sl = (barriers.price[start: end] <= \\\n",
" bottom_barrier).any()\n",
"\n",
" #assign the labels\n",
" if condition_pt: \n",
" barriers['label_barrier'][i] = 2\n",
" elif condition_sl: \n",
" barriers['label_barrier'][i] = 0 \n",
" else: \n",
" barriers['label_barrier'][i] = 1\n",
" return"
],
"metadata": {
"id": "3o1h3zZ8d3Ob"
},
"execution_count": 16,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Use function to produce barriers\n",
"\n",
"get_labels()\n",
"barriers\n",
"\n",
"# Merge the barriers with the main dataset and drop the last t_final + 1 barriers (as they are too close to the end)\n",
"\n",
"data_ohlcv = data_ohlcv.merge(barriers[['vert_barrier', 'top_barrier', 'bottom_barrier', 'label_barrier']], left_on='time', right_on='time')\n",
"data_ohlcv.drop(data_ohlcv.tail(t_final + 1).index,inplace = True)\n",
"data_ohlcv = data_ohlcv.drop(['vert_barrier', 'top_barrier', 'bottom_barrier','tic'], axis = 1)\n",
"data_ohlcv.head(5)"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 237
},
"id": "GE-s2pGHd4YV",
"outputId": "f256fe81-5f6a-481b-97fc-e77ea5ae169b"
},
"execution_count": 17,
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": [
" open high low close volume rsi \\\n",
"time \n",
"2017-09-19 4060.00 4089.97 3830.91 3910.04 902.332129 46.049881 \n",
"2017-09-20 3910.04 4046.08 3820.00 3900.00 720.935076 45.861376 \n",
"2017-09-21 3889.99 3910.00 3567.00 3609.99 1001.654084 40.681139 \n",
"2017-09-22 3592.84 3750.00 3505.55 3595.87 838.966425 40.441621 \n",
"2017-09-23 3595.88 3817.19 3542.91 3780.00 752.792791 44.990039 \n",
"\n",
" macd macd_signal macd_hist cci dx \\\n",
"time \n",
"2017-09-19 -117.398092 -51.421239 -65.976853 -19.131593 38.032614 \n",
"2017-09-20 -114.436313 -64.024254 -50.412060 -15.986404 38.307447 \n",
"2017-09-21 -133.946415 -78.008686 -55.937729 -63.756903 44.459188 \n",
"2017-09-22 -148.832034 -92.173355 -56.658678 -71.873305 45.871014 \n",
"2017-09-23 -144.110031 -102.560690 -41.549340 -35.179194 41.631687 \n",
"\n",
" volatility label_barrier \n",
"time \n",
"2017-09-19 0.079374 1 \n",
"2017-09-20 0.077964 1 \n",
"2017-09-21 0.077767 2 \n",
"2017-09-22 0.077520 2 \n",
"2017-09-23 0.076750 2 "
],
"text/html": [
"\n",
" <div id=\"df-44360e0f-930e-454d-8e78-b85753806e08\">\n",
" <div class=\"colab-df-container\">\n",
" <div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>open</th>\n",
" <th>high</th>\n",
" <th>low</th>\n",
" <th>close</th>\n",
" <th>volume</th>\n",
" <th>rsi</th>\n",
" <th>macd</th>\n",
" <th>macd_signal</th>\n",
" <th>macd_hist</th>\n",
" <th>cci</th>\n",
" <th>dx</th>\n",
" <th>volatility</th>\n",
" <th>label_barrier</th>\n",
" </tr>\n",
" <tr>\n",
" <th>time</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>2017-09-19</th>\n",
" <td>4060.00</td>\n",
" <td>4089.97</td>\n",
" <td>3830.91</td>\n",
" <td>3910.04</td>\n",
" <td>902.332129</td>\n",
" <td>46.049881</td>\n",
" <td>-117.398092</td>\n",
" <td>-51.421239</td>\n",
" <td>-65.976853</td>\n",
" <td>-19.131593</td>\n",
" <td>38.032614</td>\n",
" <td>0.079374</td>\n",
" <td>1</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2017-09-20</th>\n",
" <td>3910.04</td>\n",
" <td>4046.08</td>\n",
" <td>3820.00</td>\n",
" <td>3900.00</td>\n",
" <td>720.935076</td>\n",
" <td>45.861376</td>\n",
" <td>-114.436313</td>\n",
" <td>-64.024254</td>\n",
" <td>-50.412060</td>\n",
" <td>-15.986404</td>\n",
" <td>38.307447</td>\n",
" <td>0.077964</td>\n",
" <td>1</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2017-09-21</th>\n",
" <td>3889.99</td>\n",
" <td>3910.00</td>\n",
" <td>3567.00</td>\n",
" <td>3609.99</td>\n",
" <td>1001.654084</td>\n",
" <td>40.681139</td>\n",
" <td>-133.946415</td>\n",
" <td>-78.008686</td>\n",
" <td>-55.937729</td>\n",
" <td>-63.756903</td>\n",
" <td>44.459188</td>\n",
" <td>0.077767</td>\n",
" <td>2</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2017-09-22</th>\n",
" <td>3592.84</td>\n",
" <td>3750.00</td>\n",
" <td>3505.55</td>\n",
" <td>3595.87</td>\n",
" <td>838.966425</td>\n",
" <td>40.441621</td>\n",
" <td>-148.832034</td>\n",
" <td>-92.173355</td>\n",
" <td>-56.658678</td>\n",
" <td>-71.873305</td>\n",
" <td>45.871014</td>\n",
" <td>0.077520</td>\n",
" <td>2</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2017-09-23</th>\n",
" <td>3595.88</td>\n",
" <td>3817.19</td>\n",
" <td>3542.91</td>\n",
" <td>3780.00</td>\n",
" <td>752.792791</td>\n",
" <td>44.990039</td>\n",
" <td>-144.110031</td>\n",
" <td>-102.560690</td>\n",
" <td>-41.549340</td>\n",
" <td>-35.179194</td>\n",
" <td>41.631687</td>\n",
" <td>0.076750</td>\n",
" <td>2</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>\n",
" <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-44360e0f-930e-454d-8e78-b85753806e08')\"\n",
" title=\"Convert this dataframe to an interactive table.\"\n",
" style=\"display:none;\">\n",
" \n",
" <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
" width=\"24px\">\n",
" <path d=\"M0 0h24v24H0V0z\" fill=\"none\"/>\n",
" <path d=\"M18.56 5.44l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94zm-11 1L8.5 8.5l.94-2.06 2.06-.94-2.06-.94L8.5 2.5l-.94 2.06-2.06.94zm10 10l.94 2.06.94-2.06 2.06-.94-2.06-.94-.94-2.06-.94 2.06-2.06.94z\"/><path d=\"M17.41 7.96l-1.37-1.37c-.4-.4-.92-.59-1.43-.59-.52 0-1.04.2-1.43.59L10.3 9.45l-7.72 7.72c-.78.78-.78 2.05 0 2.83L4 21.41c.39.39.9.59 1.41.59.51 0 1.02-.2 1.41-.59l7.78-7.78 2.81-2.81c.8-.78.8-2.07 0-2.86zM5.41 20L4 18.59l7.72-7.72 1.47 1.35L5.41 20z\"/>\n",
" </svg>\n",
" </button>\n",
" \n",
" <style>\n",
" .colab-df-container {\n",
" display:flex;\n",
" flex-wrap:wrap;\n",
" gap: 12px;\n",
" }\n",
"\n",
" .colab-df-convert {\n",
" background-color: #E8F0FE;\n",
" border: none;\n",
" border-radius: 50%;\n",
" cursor: pointer;\n",
" display: none;\n",
" fill: #1967D2;\n",
" height: 32px;\n",
" padding: 0 0 0 0;\n",
" width: 32px;\n",
" }\n",
"\n",
" .colab-df-convert:hover {\n",
" background-color: #E2EBFA;\n",
" box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
" fill: #174EA6;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert {\n",
" background-color: #3B4455;\n",
" fill: #D2E3FC;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert:hover {\n",
" background-color: #434B5C;\n",
" box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
" filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
" fill: #FFFFFF;\n",
" }\n",
" </style>\n",
"\n",
" <script>\n",
" const buttonEl =\n",
" document.querySelector('#df-44360e0f-930e-454d-8e78-b85753806e08 button.colab-df-convert');\n",
" buttonEl.style.display =\n",
" google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
"\n",
" async function convertToInteractive(key) {\n",
" const element = document.querySelector('#df-44360e0f-930e-454d-8e78-b85753806e08');\n",
" const dataTable =\n",
" await google.colab.kernel.invokeFunction('convertToInteractive',\n",
" [key], {});\n",
" if (!dataTable) return;\n",
"\n",
" const docLinkHtml = 'Like what you see? Visit the ' +\n",
" '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
" + ' to learn more about interactive tables.';\n",
" element.innerHTML = '';\n",
" dataTable['output_type'] = 'display_data';\n",
" await google.colab.output.renderOutput(dataTable, element);\n",
" const docLink = document.createElement('div');\n",
" docLink.innerHTML = docLinkHtml;\n",
" element.appendChild(docLink);\n",
" }\n",
" </script>\n",
" </div>\n",
" </div>\n",
" "
]
},
"metadata": {},
"execution_count": 17
}
]
},
{
"cell_type": "markdown",
"source": [
"# Combinatorial PurgedKFoldCV"
],
"metadata": {
"id": "PNNUXedJd4s5"
}
},
{
"cell_type": "code",
"source": [
"class BaseTimeSeriesCrossValidator:\n",
" \"\"\"\n",
" Abstract class for time series cross-validation.\n",
" Time series cross-validation requires each sample has a prediction time pred_time, at which the features are used to\n",
" predict the response, and an evaluation time eval_time, at which the response is known and the error can be\n",
" computed. Importantly, it means that unlike in standard sklearn cross-validation, the samples X, response y,\n",
" pred_times and eval_times must all be pandas dataframe/series having the same index. It is also assumed that the\n",
" samples are time-ordered with respect to the prediction time (i.e. pred_times is non-decreasing).\n",
" Parameters\n",
" ----------\n",
" n_splits : int, default=10\n",
" Number of folds. Must be at least 2.\n",
" \"\"\"\n",
" def __init__(self, n_splits=10):\n",
" if not isinstance(n_splits, numbers.Integral):\n",
" raise ValueError(f\"The number of folds must be of Integral type. {n_splits} of type {type(n_splits)}\"\n",
" f\" was passed.\")\n",
" n_splits = int(n_splits)\n",
" if n_splits <= 1:\n",
" raise ValueError(f\"K-fold cross-validation requires at least one train/test split by setting n_splits = 2 \"\n",
" f\"or more, got n_splits = {n_splits}.\")\n",
" self.n_splits = n_splits\n",
" self.pred_times = None\n",
" self.eval_times = None\n",
" self.indices = None\n",
"\n",
" @abstractmethod\n",
" def split(self, X: pd.DataFrame, y: pd.Series = None,\n",
" pred_times: pd.Series = None, eval_times: pd.Series = None):\n",
" if not isinstance(X, pd.DataFrame) and not isinstance(X, pd.Series):\n",
" raise ValueError('X should be a pandas DataFrame/Series.')\n",
" if not isinstance(y, pd.Series) and y is not None:\n",
" raise ValueError('y should be a pandas Series.')\n",
" if not isinstance(pred_times, pd.Series):\n",
" raise ValueError('pred_times should be a pandas Series.')\n",
" if not isinstance(eval_times, pd.Series):\n",
" raise ValueError('eval_times should be a pandas Series.')\n",
" if y is not None and (X.index == y.index).sum() != len(y):\n",
" raise ValueError('X and y must have the same index')\n",
" if (X.index == pred_times.index).sum() != len(pred_times):\n",
" raise ValueError('X and pred_times must have the same index')\n",
" if (X.index == eval_times.index).sum() != len(eval_times):\n",
" raise ValueError('X and eval_times must have the same index')\n",
"\n",
" if not pred_times.equals(pred_times.sort_values()):\n",
" raise ValueError('pred_times should be sorted')\n",
" if not eval_times.equals(eval_times.sort_values()):\n",
" raise ValueError('eval_times should be sorted')\n",
"\n",
" self.pred_times = pred_times\n",
" self.eval_times = eval_times\n",
" self.indices = np.arange(X.shape[0])\n",
"\n",
"class CombPurgedKFoldCVLocal(BaseTimeSeriesCrossValidator):\n",
" \"\"\"\n",
" Purged and embargoed combinatorial cross-validation\n",
" As described in Advances in financial machine learning, Marcos Lopez de Prado, 2018.\n",
" The samples are decomposed into n_splits folds containing equal numbers of samples, without shuffling. In each cross\n",
" validation round, n_test_splits folds are used as the test set, while the other folds are used as the train set.\n",
" There are as many rounds as n_test_splits folds among the n_splits folds.\n",
" Each sample should be tagged with a prediction time pred_time and an evaluation time eval_time. The split is such\n",
" that the intervals [pred_times, eval_times] associated to samples in the train and test set do not overlap. (The\n",
" overlapping samples are dropped.) In addition, an \"embargo\" period is defined, giving the minimal time between an\n",
" evaluation time in the test set and a prediction time in the training set. This is to avoid, in the presence of\n",
" temporal correlation, a contamination of the test set by the train set.\n",
" Parameters\n",
" ----------\n",
" n_splits : int, default=10\n",
" Number of folds. Must be at least 2.\n",
" n_test_splits : int, default=2\n",
" Number of folds used in the test set. Must be at least 1.\n",
" embargo_td : pd.Timedelta, default=0\n",
" Embargo period (see explanations above).\n",
" \"\"\"\n",
" def __init__(self, n_splits=10, n_test_splits=2, embargo_td=pd.Timedelta(minutes=0)):\n",
" super().__init__(n_splits)\n",
" if not isinstance(n_test_splits, numbers.Integral):\n",
" raise ValueError(f\"The number of test folds must be of Integral type. {n_test_splits} of type \"\n",
" f\"{type(n_test_splits)} was passed.\")\n",
" n_test_splits = int(n_test_splits)\n",
" if n_test_splits <= 0 or n_test_splits > self.n_splits - 1:\n",
" raise ValueError(f\"K-fold cross-validation requires at least one train/test split by setting \"\n",
" f\"n_test_splits between 1 and n_splits - 1, got n_test_splits = {n_test_splits}.\")\n",
" self.n_test_splits = n_test_splits\n",
" if not isinstance(embargo_td, pd.Timedelta):\n",
" raise ValueError(f\"The embargo time should be of type Pandas Timedelta. {embargo_td} of type \"\n",
" f\"{type(embargo_td)} was passed.\")\n",
" if embargo_td < pd.Timedelta(minutes=0):\n",
" raise ValueError(f\"The embargo time should be positive, got embargo = {embargo_td}.\")\n",
" self.embargo_td = embargo_td\n",
"\n",
" def split(self, X: pd.DataFrame, y: pd.Series = None,\n",
" pred_times: pd.Series = None, eval_times: pd.Series = None) -> Iterable[Tuple[np.ndarray, np.ndarray]]:\n",
" \"\"\"\n",
" Yield the indices of the train and test sets.\n",
" Although the samples are passed in the form of a pandas dataframe, the indices returned are position indices,\n",
" not labels.\n",
" Parameters\n",
" ----------\n",
" X : pd.DataFrame, shape (n_samples, n_features), required\n",
" Samples. Only used to extract n_samples.\n",
" y : pd.Series, not used, inherited from _BaseKFold\n",
" pred_times : pd.Series, shape (n_samples,), required\n",
" Times at which predictions are made. pred_times.index has to coincide with X.index.\n",
" eval_times : pd.Series, shape (n_samples,), required\n",
" Times at which the response becomes available and the error can be computed. eval_times.index has to\n",
" coincide with X.index.\n",
" Returnst\n",
" -------\n",
" train_indices: np.ndarray\n",
" A numpy array containing all the indices in the train set.\n",
" test_indices : np.ndarray\n",
" A numpy array containing all the indices in the test set.\n",
" \"\"\"\n",
" super().split(X, y, pred_times, eval_times)\n",
"\n",
" # Fold boundaries\n",
" fold_bounds = [(fold[0], fold[-1] + 1) for fold in np.array_split(self.indices, self.n_splits)]\n",
" # List of all combinations of n_test_splits folds selected to become test sets\n",
" selected_fold_bounds = list(itt.combinations(fold_bounds, self.n_test_splits))\n",
" # In order for the first round to have its whole test set at the end of the dataset\n",
" selected_fold_bounds.reverse()\n",
"\n",
" for fold_bound_list in selected_fold_bounds:\n",
" # Computes the bounds of the test set, and the corresponding indices\n",
" test_fold_bounds, test_indices = self.compute_test_set(fold_bound_list)\n",
" # Computes the train set indices\n",
" train_indices = self.compute_train_set(test_fold_bounds, test_indices)\n",
"\n",
" yield train_indices, test_indices\n",
"\n",
" def compute_train_set(self, test_fold_bounds: List[Tuple[int, int]], test_indices: np.ndarray) -> np.ndarray:\n",
" \"\"\"\n",
" Compute the position indices of samples in the train set.\n",
" Parameters\n",
" ----------\n",
" test_fold_bounds : List of tuples of position indices\n",
" Each tuple records the bounds of a block of indices in the test set.\n",
" test_indices : np.ndarray\n",
" A numpy array containing all the indices in the test set.\n",
" Returns\n",
" -------\n",
" train_indices: np.ndarray\n",
" A numpy array containing all the indices in the train set.\n",
" \"\"\"\n",
" # As a first approximation, the train set is the complement of the test set\n",
" train_indices = np.setdiff1d(self.indices, test_indices)\n",
" # But we now have to purge and embargo\n",
" for test_fold_start, test_fold_end in test_fold_bounds:\n",
" # Purge\n",
" train_indices = purge(self, train_indices, test_fold_start, test_fold_end)\n",
" # Embargo\n",
" train_indices = embargo(self, train_indices, test_indices, test_fold_end)\n",
" return train_indices\n",
"\n",
" def compute_test_set(self, fold_bound_list: List[Tuple[int, int]]) -> Tuple[List[Tuple[int, int]], np.ndarray]:\n",
" \"\"\"\n",
" Compute the indices of the samples in the test set.\n",
" Parameterst\n",
" ----------\n",
" fold_bound_list: List of tuples of position indices\n",
" Each tuple records the bounds of the folds belonging to the test set.\n",
" Returns\n",
" -------\n",
" test_fold_bounds: List of tuples of position indices\n",
" Like fold_bound_list, but witest_fold_boundsth the neighboring folds in the test set merged.\n",
" test_indices: np.ndarray\n",
" A numpy array containing the test indices.\n",
" \"\"\"\n",
" test_indices = np.empty(0)\n",
" test_fold_bounds = []\n",
" for fold_start, fold_end in fold_bound_list:\n",
" # Records the boundaries of the current test split\n",
" if not test_fold_bounds or fold_start != test_fold_bounds[-1][-1]:\n",
" test_fold_bounds.append((fold_start, fold_end))\n",
" # If the current test split is contiguous to the previous one, simply updates the endpoint\n",
" elif fold_start == test_fold_bounds[-1][-1]:\n",
" test_fold_bounds[-1] = (test_fold_bounds[-1][0], fold_end)\n",
" test_indices = np.union1d(test_indices, self.indices[fold_start:fold_end]).astype(int)\n",
" return test_fold_bounds, test_indices\n",
"\n",
"\n",
"def compute_fold_bounds(cv: BaseTimeSeriesCrossValidator, split_by_time: bool) -> List[int]:\n",
" \"\"\"\n",
" Compute a list containing the fold (left) boundaries.\n",
" Parameters\n",
" ----------\n",
" cv: BaseTimeSeriesCrossValidator\n",
" Cross-validation object for which the bounds need to be computed.\n",
" split_by_time: bool\n",
" If False, the folds contain an (approximately) equal number of samples. If True, the folds span identical\n",
" time intervals.\n",
" \"\"\"\n",
" if split_by_time:\n",
" full_time_span = cv.pred_times.max() - cv.pred_times.min()\n",
" fold_time_span = full_time_span / cv.n_splits\n",
" fold_bounds_times = [cv.pred_times.iloc[0] + fold_time_span * n for n in range(cv.n_splits)]\n",
" return cv.pred_times.searchsorted(fold_bounds_times)\n",
" else:\n",
" return [fold[0] for fold in np.array_split(cv.indices, cv.n_splits)]\n",
"\n",
"\n",
"def embargo(cv: BaseTimeSeriesCrossValidator, train_indices: np.ndarray,\n",
" test_indices: np.ndarray, test_fold_end: int) -> np.ndarray:\n",
" \"\"\"\n",
" Apply the embargo procedure to part of the train set.\n",
" This amounts to dropping the train set samples whose prediction time occurs within self.embargo_dt of the test\n",
" set sample evaluation times. This method applies the embargo only to the part of the training set immediately\n",
" following the end of the test set determined by test_fold_end.\n",
" Parameters\n",
" -------mestamps of p[t-1] values\n",
" df0 = prices.inde---\n",
" cv: Cross-validation class\n",
" Needs to have the attributes cv.pred_times, cv.eval_times, cv.embargo_dt and cv.indices.\n",
" train_indices: np.ndarray\n",
" A numpy array containing all the indices of the samples currently included in the train set.\n",
" test_indices : np.ndarray\n",
" A numpy array containing all the indices of the samples in the test set.\n",
" test_fold_end : int\n",
" Index corresponding to the end of a test set block.\n",
" Returns\n",
" -------\n",
" train_indices: np.ndarray\n",
" The same array, with the indices subject to embargo removed.\n",
" \"\"\"\n",
" if not hasattr(cv, 'embargo_td'):\n",
" raise ValueError(\"The passed cross-validation object should have a member cv.embargo_td defining the embargo\"\n",
" \"time.\")\n",
" last_test_eval_time = cv.eval_times.iloc[test_indices[test_indices <= test_fold_end]].max()\n",
" min_train_index = len(cv.pred_times[cv.pred_times <= last_test_eval_time + cv.embargo_td])\n",
" if min_train_index < cv.indices.shape[0]:\n",
" allowed_indices = np.concatenate((cv.indices[:test_fold_end], cv.indices[min_train_index:]))\n",
" train_indices = np.intersect1d(train_indices, allowed_indices)\n",
" return train_indices\n",
"\n",
"\n",
"def purge(cv: BaseTimeSeriesCrossValidator, train_indices: np.ndarray,\n",
" test_fold_start: int, test_fold_end: int) -> np.ndarray:\n",
" \"\"\"data_ohlcv\n",
" Purge part of the train set.\n",
" Given a left boundary index test_fold_start of the test set, this method removes from the train set all the\n",
" samples whose evaluation time is posterior to the prediction time of the first test sample after the boundary.\n",
" Parameters\n",
" ----------combinatorial purged k fold\n",
" cv: Cross-validation class\n",
" Needs to have the attributes cv.pred_times, cv.eval_times and cv.indices.\n",
" train_indices: np.ndarray\n",
" A numpy array containing all the indices of the samples currently included in the train set.\n",
" test_fold_start : int\n",
" Index corresponding to the start of a test set block.\n",
" test_fold_end : int\n",
" Index corresponding to the end of the same test set block.\n",
" Returns\n",
" -------\n",
" train_indices: np.ndarray\n",
" A numpy array containing the train indices purged at test_fold_start.\n",
" \"\"\"\n",
" time_test_fold_start = cv.pred_times.iloc[test_fold_start]\n",
" # The train indices before the start of the test fold, purged.\n",
" train_indices_1 = np.intersect1d(train_indices, cv.indices[cv.eval_times < time_test_fold_start])\n",
" # The train indices after the end of the test fold.\n",
" train_indices_2 = np.intersect1d(train_indices, cv.indices[test_fold_end:])\n",
" return np.concatenate((train_indices_1, train_indices_2))"
],
"metadata": {
"id": "6--viSo7i0ql"
},
"execution_count": 31,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"### The generator function for the unique paths"
],
"metadata": {
"id": "VI8JXiu1eotV"
}
},
{
"cell_type": "code",
"source": [
"def back_test_paths_generator(t_span, n, k, prediction_times, evaluation_times, verbose=True):\n",
" # split data into N groups, with N << T\n",
" # this will assign each index position to a group position\n",
" group_num = np.arange(t_span) // (t_span // n)\n",
" group_num[group_num == n] = n-1\n",
" \n",
" # generate the combinations \n",
" test_groups = np.array(list(itt.combinations(np.arange(n), k))).reshape(-1, k)\n",
" C_nk = len(test_groups)\n",
" n_paths = C_nk * k // n \n",
" \n",
" \n",
" if verbose:\n",
" print('n_sim:', C_nk)\n",
" print('n_paths:', n_paths)\n",
" \n",
" # is_test is a T x C(n, k) array where each column is a logical array \n",
" # indicating which observation in in the test set\n",
" is_test_group = np.full((n, C_nk), fill_value=False)\n",
" is_test = np.full((t_span, C_nk), fill_value=False)\n",
" \n",
" # assign test folds for each of the C(n, k) simulations\n",
" for k, pair in enumerate(test_groups):\n",
" i, j = pair\n",
" is_test_group[[i, j], k] = True\n",
" \n",
" # assigning the test folds\n",
" mask = (group_num == i) | (group_num == j)\n",
" is_test[mask, k] = True\n",
" \n",
" # for each path, connect the folds from different simulations to form a backtest path\n",
" # the fold coordinates are: the fold number, and the simulation index e.g. simulation 0, fold 0 etc\n",
" path_folds = np.full((n, n_paths), fill_value=np.nan)\n",
" \n",
" for i in range(n_paths):\n",
" for j in range(n):\n",
" s_idx = is_test_group[j, :].argmax().astype(int)\n",
" path_folds[j, i] = s_idx\n",
" is_test_group[j, s_idx] = False\n",
" cv.split(X, y, pred_times=prediction_times, eval_times=evaluation_times)\n",
" \n",
" # finally, for each path we indicate which simulation we're building the path from and the time indices\n",
" paths = np.full((t_span, n_paths), fill_value= np.nan)\n",
" \n",
" for p in range(n_paths):\n",
" for i in range(n):\n",
" mask = (group_num == i)\n",
" paths[mask, p] = int(path_folds[i, p])\n",
" # paths = paths_# .astype(int)\n",
"\n",
" return (is_test, paths, path_folds) "
],
"metadata": {
"id": "MJ9RUhkSeL_j"
},
"execution_count": 53,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"### The plotting function for the Combinatorial PurgedKFold\n",
"\n",
"Made this based on https://scikit-learn.org/stable/auto_examples/model_selection/plot_cv_indices.html"
],
"metadata": {
"id": "cMr24XD2er1a"
}
},
{
"cell_type": "code",
"source": [
"cmap_data = plt.cm.Paired\n",
"cmap_cv = plt.cm.coolwarm\n",
"\n",
"def plot_cv_indices(cv, X, y, group, ax, n_paths, k, paths, lw=5):\n",
" \"\"\"Create a sample plot for indices of a cross-validation object.\"\"\"\n",
"\n",
" # generate the combinations\n",
" N = n_paths + 1\n",
" test_groups = np.array(list(itt.combinations(np.arange(N), k))).reshape(-1, k)\n",
" n_splits = len(test_groups)\n",
"\n",
" # Generate the training/testing visualizations for each CV split\n",
" for ii, (tr, tt) in enumerate(cv.split(X, y, pred_times=prediction_times, eval_times=evaluation_times)):\n",
"\n",
" # print('fold', ii, '\\n')\n",
" # print(tr, '\\n')\n",
" # print(tt, '\\n')\n",
"\n",
" # Fill in indices with the training/test groups\n",
" indices = np.array([np.nan] * len(X))\n",
" indices[tt] = 1\n",
" indices[tr] = 0\n",
" indices[np.isnan(indices)] = 2\n",
"\n",
" # Visualize the results\n",
" ax.scatter(\n",
" [ii + 0.5] * len(indices),\n",
" range(len(indices)),\n",
" c=[indices],\n",
" marker=\"_\",\n",
" lw=lw,\n",
" cmap=cmap_cv,\n",
" vmin=-0.2,\n",
" vmax=1.2\n",
" )\n",
"\n",
" # Plot the data classes and groups at the end\n",
" ax.scatter(\n",
" [ii + 1.5] * len(X), \n",
" range(len(X)), \n",
" c=y, \n",
" marker=\"_\", \n",
" lw=lw, \n",
" cmap=cmap_data\n",
" )\n",
"\n",
" ax.scatter(\n",
" [ii + 2.5] * len(X), \n",
" range(len(X)), \n",
" c=group, \n",
" marker=\"_\", \n",
" lw=lw, \n",
" cmap=cmap_data\n",
" )\n",
"\n",
" # Formatting\n",
" xlabelz = list(range(n_splits, 0 , -1))\n",
" xlabelz = ['S' + str(x) for x in xlabelz]\n",
" xticklabels = xlabelz + [\"class\", \"group\"]\n",
"\n",
" ax.set(\n",
" xticks=np.arange(n_splits + 2) + 0.45,\n",
" xticklabels=xticklabels,\n",
" ylabel=\"Sample index\",\n",
" xlabel=\"CV iteration\",\n",
" xlim=[n_splits + 2.2, -0.2],\n",
" ylim=[0, X.shape[0]],\n",
" )\n",
" ax.set_title(\"{}\".format(type(cv).__name__), fontsize=5)\n",
" ax.xaxis.tick_top()\n",
"\n",
" return ax"
],
"metadata": {
"id": "8Qco6-mmeRaH"
},
"execution_count": 42,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"### Just setting the constants + timeseriescv installation"
],
"metadata": {
"id": "SLBYAi72exrd"
}
},
{
"cell_type": "code",
"source": [
"data = data_ohlcv\n",
"\n",
"data_index = data.index\n",
"\n",
"# Train data\n",
"X = data.drop(['label_barrier'], axis = 1)\n",
"X.drop(X.tail(t_final).index,inplace = True)\n",
"\n",
"# Test data\n",
"y = data[['label_barrier']]\n",
"y.reindex(data_index)\n",
"y = y[:-t_final]\n",
"y = y.squeeze()\n",
"\n",
"# prediction and evalution times\n",
"t1_ = data.index\n",
"\n",
"# recall that we are holding our position for 10 days\n",
"# normally t1 is important is there events such as stop losses, or take profit events\n",
"# Recall t_final from before! This is the maximum of a box!!\n",
"\n",
"# prediction time is moment of observationxticklabels\n",
"prediction_times = pd.Series(t1_[:-t_final], index = X.index)\n",
"\n",
"# evaluation time is moment of evaluation event\n",
"evaluation_times = pd.Series(t1_[t_final:], index = X.index)"
],
"metadata": {
"id": "NMRv7V_9eSWs"
},
"execution_count": 43,
"outputs": []
},
{
"cell_type": "code",
"source": [
"num_paths = 5\n",
"k = 2\n",
"N = num_paths + 1\n",
"embargo_td = Delta * t_final * 2\n",
"cv = CombPurgedKFoldCVLocal(n_splits=N, n_test_splits=k, embargo_td=embargo_td)\n",
"\n",
"# Compute backtest paths\n",
"_, paths, _= back_test_paths_generator(X.shape[0], N, k, prediction_times, evaluation_times)\n",
"\n",
"# Plot PurgedKFold split\n",
"groups = list(range(X.shape[0]))\n",
"fig, ax = plt.subplots()\n",
"plot_cv_indices(cv, X, y, groups, ax, num_paths, k, paths)\n",
"plt.gca().invert_yaxis()"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 661
},
"id": "piFfU3SBeUaK",
"outputId": "a7cd3b77-3e6f-455d-f746-d52eafcd8d9b"
},
"execution_count": 45,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"n_sim: 15\n",
"n_paths: 5\n"
]
},
{
"output_type": "display_data",
"data": {
"text/plain": [
"<Figure size 1500x600 with 1 Axes>"
],
"image/png": "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pUuXKjw8XJs2bZKvr/0wOyEhQbNmzdLatWt1/Phx+fr6qkqVKurQoYNCQkJyVCNjtjExMbr//vvVtm1bPffcc1kuv3v3bu3YsUP58uXTs88+e8N1V6xYUUOGDNHDDz/s8Hj79u21b98+RUREaPDgwfLz88v02kWLFqldu3by8fHJ0fbcyNatWzVnzhzt379fCQkJKlOmjJo1a6bXX389y/0hNjZWX375pb7//nudOXNG99xzj8qUKaN69eopNDRUFotFkmQYhtauXav169frwIEDOnPmjPLnz69q1arptddeo8EKAICbceYkAAC4K2zZskWSnGoueXt7a8iQIapQoYLtseHDh+vtt99WgwYN9P333+uXX35Rr1699OWXX6pTp04Ol7p+9913ioyMlCQlJSXp3XffVYcOHbR582aNGjVKGzZsUJ8+fbR9+3bt2LFD06dP1/r161W+fHl9+OGH2rx5c7Zj27Fjh2bMmKHRo0fr119/1RdffKHDhw+rY8eO+uOPP3KUyZUrV2yvadCggaT0BqB13sWMrPMGZjXf4bPPPuvw3OrVq7V27Vp98skn+umnnxwaY0lJSerevbtmzZqlzp07a/v27Vq2bJmKFi1qa3AGBwcrMjLSoTG5cOFCdenSRcnJyVq4cKF27dql999/X6tXr9ZLL72k8+fPO4xpz5496tixo6KiojRt2jT9+uuvGj16tL788sssb8KSlWvXrql///7aunWrvvnmm9tqTGbn8uXL6tixo2bMmKHevXvrl19+0YYNGxQcHKy33377hjc1ut712W7btk2LFy+Wn5+fPvzwwyxfY31fWCwWBQQE3LRGr169bGdKWj333HPKnz+/YmJisrzJz9GjR7V7927bJeC5ITw8XL169VK5cuW0YsUK/fbbb3r33XcVERGhF154wXaDGqvo6Gi98MILWrVqld577z39/vvvWrZsmZo1a6aZM2c6nC2dlJSkQYMG6eLFiwoPD9dvv/2mpUuXqkKFCurVq9cNzxAFAAC5j+YkAAC4Kxw9elSSVKZMmRy/dvHixVq6dKmee+45vfnmmypWrJgCAgLUpk0bvf322zpw4IA++uijLF977tw5tW/fXvXq1VOhQoXUsmVLNW7cWAcOHNC0adM0dOhQlSxZUqVLl7Y155YuXZrtWPbt26eJEyeqUqVK8vf3V3BwsMaPH6+rV6/q3XfflWEYN92ea9eu6Y8//lDfvn0VGxur2rVrq2PHjjnOJTt//fWXJk6cqAceeECFChVSjx499NRTT0mSZs+erT179ig0NFQ9e/ZU4cKFVaRIEfXq1UsPPfRQluuLiorSqFGjVLBgQU2ePFkVK1aUv7+/mjRpouHDh+vEiRO2Mzyl9DPf3n33XSUkJGj8+PGqV6+e/P39VblyZf373/92qombkJCgbt266cCBA1qwYIEeeOCBHGUwfPhwBQYGOnxl1Wj86KOPdPDgQQ0bNkzPPfecAgICVLx4cQ0cOFCtW7fW0qVLtWTJEqdq3ijbe+65J8vXWM/wvZX3hVWhQoXUokULSVlf2r1o0SLVr18/20vGc2r79u2aOnWqHnnkEY0aNUr333+/8uXLpyeeeEJjx47VqVOn9M477zi85u2339bp06c1YcIENWnSRPny5dN9992nN954Q88//7zDsl5eXgoKCtK///1vWSwW5cuXz/aPgypVquiTTz5RampqrmwLAAC4OZqTAADgrhAXFydJypcvX45faz2TMKtLbNu0aSMfHx+tXLlSsbGxmZ739vbOdBmotdFVp04dh8crVaokyd4wykqTJk1UpEgRh8eCg4NVqlQpHT58WHv27MnydcuWLbM1yWrVqqWuXbsqLi5Ob731lubNm3dLuWSnVatWDpfvVq1a1TZfobXRltVlxtc3iay++eYbJScnq3Xr1ipcuLDDcy1atJC/v7++++4729mr1puzlCpVSvXq1XNYvmjRomrSpMlNt+HChQvas2ePTp8+rXXr1t10+etldbfu6+/SHRsbq1WrVsnHxyfLS6rbtm0rSZozZ45TNW8lW2tmt/vzt54V+dNPP+nvv/+2PZ6cnKzly5fn6o1wrO/HrLapcePGKlGihHbu3KkDBw5Ikv7880/t2rVLJUuWVMOGDTO9plOnTnrmmWds3/v7+2v58uUqXry4w3JeXl4KDAzUhQsXbnjzHwAAkLtoTgIAgLuCtal17dq1HL3u6tWrtku0szqzr1ChQrr//vuVmpqqffv2ZXq+aNGimebgK1iwoCSpZMmSmdZlrZmdsmXLZvn4gw8+KEm2hsz1QkNDHRple/bs0bJly/T666/L398/23q34vq5Oq3i4uL0119/SbI3YjPKbtusDdeqVatmes7X11elSpVScnKyQzMquxo3qpNRmTJlbJepjxo1SgsWLLjpa3Jq3759Sk1NVenSpW37REbW/e1///vfTe8cf6vZ3ur74nr16tVThQoVlJaW5nCm58aNG5WWlubQ/Ltd1v0hq/ejl5eXbft3797tsHx2+0OdOnUyTVcQFRWl4cOHq0WLFqpZs6atsR8RESFJunTpUu5sDAAAuCmakwAA4K5gbd6dOXMmR6/LOJdkdnPyWRtLWTUs8ufPn+26b/RcdrIbg/Xxy5cv53iduS3jzYEysp69mt0yWTXoJPs2ffDBB5kulQ4MDNTJkyclSTExMZLsP7PsxpFdnev1799f/fv3l2EY+uCDD7Ro0SKnXpeddu3aOczJaN1fbvYzNQzDYT/Myq1me6vvi+t5eXnphRdekJR+Bqd1eoFFixYpJCQkVxvg1v3hZu9H63I3W/56v/32m9q2bastW7Zo6NCh+vnnn21N/dDQUEnpN0oCAADuQXMSAADcFZ588klJ0t69e286L2NycrLOnTunK1euOFxGnN3Za/Hx8ZKU6XJrV8huDNbHs5tbMCe8vLyyfS4hIeGW12s9MzS79VhzvJ51m8aPH5/pUumMX61bt3ZYPruxZlcnK2FhYRowYIAMw9B77713w/lAc8q6v9zsZ+rl5ZXpcvbr3Wq21veFM2dnpqWl6dy5c7p48WKWz4eGhsrHx0enTp3S9u3bdfr0aW3bti1XL+mW7D/fm70frcvdbPnr/ec//1FSUpL69Omjp59++qbZAwAA16I5CQAA7gotW7bUgw8+qDNnzmjbtm03XPbLL7/U448/ro0bNyogIEAWi0WSdPjw4UzLxsXF6e+//5aPj0+muxi7wvV3Ibay3vCnWrVqt13DekZnVs2cs2fP3vJ6CxUqZJtvM6s5+06fPp3l62rXri1JtjMkr3f+/Hn98MMPtvFaM7Bm4myd7PTt21cDBw6UYRgaMWKEli9fnqPXZ6dGjRry8fHRmTNnHM58tDpy5Igk5+6kfavZ1qlTRw0bNlRiYqJWrVp1wxorV67U448/rvnz52f5/H333afHH39cUvoZk0uWLFGtWrX08MMP33C9zhg9erTtZju1atWSlPX70TAM28/dOqerdf/Jbn+IjIzU7NmzbZe2W/ezrG6CdLuXvwMAgJyjOQkAAO4Kvr6+mjRpkgICAjR69Ohsz/6KiorSnDlzFBgYqDZt2kiSunbtKklZNqVWrFih1NRUPfvssypWrJjrNuD/+/HHHzNdur1jxw6dO3dOFovF1ri5HRUrVpS3t3emZk5CQoJ+/PHH21q39cYpq1evzvTcihUrsnxNx44d5efnp4iICKWkpGR6fvz48Ro9erStqVq7dm09/PDD+ueff7Rjxw6HZS9evKgffvghx+N+4403NHjwYKWlpWn48OHZjjUnihUrpmeffVapqalauXJlpueXLVsmSerSpYtT67uVbKX0m/eULFlSkydPVnR0dJbLnDt3Tp9++qlKliypV199Ndt1WS/t/v7777Vw4ULbmG7X//73P9ul59b3Y0RERKazoH/44QedP39ejz76qG2O0mrVqunRRx/VuXPntH379kzrnjRpkiIiImz7j3XOVOtcs1ZJSUlO3ekdAADkLpqTAADgrhEUFKQ5c+YoKSlJ7du3V0REhGJiYpSUlKTo6GjNmTNHXbp00f33369Zs2bZ7jj94osvKiQkRGvWrNHkyZMVGxurhIQErVq1Sh9//LECAwM1cuRIt2xDmTJlNGjQIB07dkxJSUnauXOnhg0bpoCAAI0ZM+aGl2Q7q0iRInryySd1+PBhzZo1S5cvX9bx48f19ttv3/aZmd26dVOdOnW0bNkyzZgxQ1euXNHly5c1Y8aMbG8yUrlyZb3//vs6efKkevfurT///FMJCQmKjo7WRx99pPXr12vUqFHy9k4/dPXy8tK//vUvBQQEaNiwYdq5c6eSkpJ07NgxDRo0yKkb4mSld+/eGjJkiNLS0jRs2LAsm4A5NXLkSAUGBmrChAlauXKlEhISFBsbqylTpmjt2rVq27at05dF3yhb61mYWSldurTmz5+vEiVKqEOHDvr666/1999/KykpSWfOnNHSpUvVsWNHpaWlaf78+Q6XkF+vadOmKlasmJKSkhQfH2+71D4r8fHxunz5sm3+xqtXr+ry5ctZfmVsSjdq1EhvvPGG9uzZo5EjR9rGunXrVo0YMUJlypTRhAkTHGqNHz9epUuX1jvvvKMff/xRiYmJ+vvvvzVu3Dht27bNdjd5SXrttdfk5eWladOmae3atYqLi1N0dLTeeeedHJ91CwAAbp+XcbNJmQAAAO4w165d08KFC7Vhwwb973//s80tabFY1KJFC7344ovKly+fw2sMw1BERIS+/fZbRUZGKiUlRRUrVlTLli3VvXt3h8tu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\n"
},
"metadata": {}
}
]
},
{
"cell_type": "markdown",
"source": [
"# Paths example"
],
"metadata": {
"id": "zcf2cHw1jE3O"
}
},
{
"cell_type": "code",
"source": [
"def back_test_paths_generator(t_span, n, k, prediction_times, evaluation_times, verbose=True):\n",
" # split data into N groups, with N << T\n",
" # this will assign each index position to a group position\n",
" group_num = np.arange(t_span) // (t_span // n)\n",
" group_num[group_num == n] = n-1\n",
" \n",
" # generate the combinations \n",
" test_groups = np.array(list(itt.combinations(np.arange(n), k))).reshape(-1, k)\n",
" C_nk = len(test_groups)\n",
" n_paths = C_nk * k // n \n",
" \n",
" print(n_paths)\n",
" \n",
" if verbose:\n",
" print('n_sim:', C_nk)\n",
" print('n_paths:', n_paths)\n",
" \n",
" # is_test is a T x C(n, k) array where each column is a logical array \n",
" # indicating which observation in in the test set\n",
" is_test_group = np.full((n, C_nk), fill_value=False)\n",
" is_test = np.full((t_span, C_nk), fill_value=False)\n",
" \n",
" # assign test folds for each of the C(n, k) simulations\n",
" for k, pair in enumerate(test_groups):\n",
" i, j = pair\n",
" is_test_group[[i, j], k] = True\n",
" \n",
" # assigning the test folds\n",
" mask = (group_num == i) | (group_num == j)\n",
" is_test[mask, k] = True\n",
" \n",
" # for each path, connect the folds from different simulations to form a backtest path\n",
" # the fold coordinates are: the fold number, and the simulation index e.g. simulation 0, fold 0 etc\n",
" path_folds = np.full((n, n_paths), fill_value=np.nan)\n",
" \n",
" for i in range(n_paths):\n",
" for j in range(n):\n",
" s_idx = is_test_group[j, :].argmax().astype(int)\n",
" path_folds[j, i] = s_idx\n",
" is_test_group[j, s_idx] = False\n",
" cv.split(X, y, pred_times=prediction_times, eval_times=evaluation_times)\n",
" \n",
" # finally, for each path we indicate which simulation we're building the path from and the time indices\n",
" paths = np.full((t_span, n_paths), fill_value= np.nan)\n",
" \n",
" for p in range(n_paths):\n",
" for i in range(n):\n",
" mask = (group_num == i)\n",
" paths[mask, p] = int(path_folds[i, p])\n",
" # paths = paths_# .astype(int)\n",
"\n",
" return (is_test, paths, path_folds) "
],
"metadata": {
"id": "8OLHBM1gpVlr"
},
"execution_count": 54,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Compute backtest paths\n",
"_, paths, _= back_test_paths_generator(30, 6, k, prediction_times, evaluation_times)\n",
"paths + 1"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "ZGEkk6rzjHIM",
"outputId": "4b6bfbd9-2804-434d-8975-f77b31a84885"
},
"execution_count": 62,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"5\n",
"n_sim: 15\n",
"n_paths: 5\n"
]
},
{
"output_type": "execute_result",
"data": {
"text/plain": [
"array([[ 1., 2., 3., 4., 5.],\n",
" [ 1., 2., 3., 4., 5.],\n",
" [ 1., 2., 3., 4., 5.],\n",
" [ 1., 2., 3., 4., 5.],\n",
" [ 1., 2., 3., 4., 5.],\n",
" [ 1., 6., 7., 8., 9.],\n",
" [ 1., 6., 7., 8., 9.],\n",
" [ 1., 6., 7., 8., 9.],\n",
" [ 1., 6., 7., 8., 9.],\n",
" [ 1., 6., 7., 8., 9.],\n",
" [ 2., 6., 10., 11., 12.],\n",
" [ 2., 6., 10., 11., 12.],\n",
" [ 2., 6., 10., 11., 12.],\n",
" [ 2., 6., 10., 11., 12.],\n",
" [ 2., 6., 10., 11., 12.],\n",
" [ 3., 7., 10., 13., 14.],\n",
" [ 3., 7., 10., 13., 14.],\n",
" [ 3., 7., 10., 13., 14.],\n",
" [ 3., 7., 10., 13., 14.],\n",
" [ 3., 7., 10., 13., 14.],\n",
" [ 4., 8., 11., 13., 15.],\n",
" [ 4., 8., 11., 13., 15.],\n",
" [ 4., 8., 11., 13., 15.],\n",
" [ 4., 8., 11., 13., 15.],\n",
" [ 4., 8., 11., 13., 15.],\n",
" [ 5., 9., 12., 14., 15.],\n",
" [ 5., 9., 12., 14., 15.],\n",
" [ 5., 9., 12., 14., 15.],\n",
" [ 5., 9., 12., 14., 15.],\n",
" [ 5., 9., 12., 14., 15.]])"
]
},
"metadata": {},
"execution_count": 62
}
]
},
{
"cell_type": "code",
"source": [
"paths.shape"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "UKN6a8JDoWZb",
"outputId": "9dc553f5-5130-40bf-eca9-0176bdc8c8e2"
},
"execution_count": 57,
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": [
"(20, 5)"
]
},
"metadata": {},
"execution_count": 57
}
]
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment