1679 lines
376 KiB
Plaintext
1679 lines
376 KiB
Plaintext
|
|
{
|
||
|
|
"cells": [
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"In this example, we will compare development of a pairs trading strategy using backtrader and vectorbt."
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 1,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"import numpy as np\n",
|
||
|
|
"import pandas as pd\n",
|
||
|
|
"import datetime\n",
|
||
|
|
"import collections\n",
|
||
|
|
"import math\n",
|
||
|
|
"import pytz"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 2,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"import scipy.stats as st\n",
|
||
|
|
"\n",
|
||
|
|
"SYMBOL1 = 'PEP'\n",
|
||
|
|
"SYMBOL2 = 'KO'\n",
|
||
|
|
"FROMDATE = datetime.datetime(2017, 1, 1, tzinfo=pytz.utc)\n",
|
||
|
|
"TODATE = datetime.datetime(2019, 1, 1, tzinfo=pytz.utc)\n",
|
||
|
|
"PERIOD = 100\n",
|
||
|
|
"CASH = 100000\n",
|
||
|
|
"COMMPERC = 0.005 # 0.5%\n",
|
||
|
|
"ORDER_PCT1 = 0.1\n",
|
||
|
|
"ORDER_PCT2 = 0.1\n",
|
||
|
|
"UPPER = st.norm.ppf(1 - 0.05 / 2)\n",
|
||
|
|
"LOWER = -st.norm.ppf(1 - 0.05 / 2)\n",
|
||
|
|
"MODE = 'OLS' # OLS, log_return"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"## Data"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 3,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"import vectorbt as vbt\n",
|
||
|
|
"\n",
|
||
|
|
"start_date = FROMDATE.replace(tzinfo=pytz.utc)\n",
|
||
|
|
"end_date = TODATE.replace(tzinfo=pytz.utc)\n",
|
||
|
|
"data = vbt.YFData.download([SYMBOL1, SYMBOL2], start=start_date, end=end_date)\n",
|
||
|
|
"data = data.loc[(data.wrapper.index >= start_date) & (data.wrapper.index < end_date)]"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 4,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
" Open High Low Close \\\n",
|
||
|
|
"Date \n",
|
||
|
|
"2017-01-03 00:00:00+00:00 91.831129 91.962386 91.192316 91.577354 \n",
|
||
|
|
"2018-12-31 00:00:00+00:00 102.775161 103.249160 101.604091 102.682220 \n",
|
||
|
|
"\n",
|
||
|
|
" Volume Dividends Stock Splits \n",
|
||
|
|
"Date \n",
|
||
|
|
"2017-01-03 00:00:00+00:00 3741200 0.0 0 \n",
|
||
|
|
"2018-12-31 00:00:00+00:00 5019100 0.0 0 \n",
|
||
|
|
" Open High Low Close \\\n",
|
||
|
|
"Date \n",
|
||
|
|
"2017-01-03 00:00:00+00:00 35.801111 36.068542 35.611321 36.059914 \n",
|
||
|
|
"2018-12-31 00:00:00+00:00 43.810820 43.856946 43.321878 43.681664 \n",
|
||
|
|
"\n",
|
||
|
|
" Volume Dividends Stock Splits \n",
|
||
|
|
"Date \n",
|
||
|
|
"2017-01-03 00:00:00+00:00 14711000 0.0 0 \n",
|
||
|
|
"2018-12-31 00:00:00+00:00 10576300 0.0 0 \n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"print(data.data[SYMBOL1].iloc[[0, -1]])\n",
|
||
|
|
"print(data.data[SYMBOL2].iloc[[0, -1]])"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"## backtrader"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"Adapted version of https://github.com/mementum/backtrader/blob/master/contrib/samples/pair-trading/pair-trading.py"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 5,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"import backtrader as bt\n",
|
||
|
|
"import backtrader.feeds as btfeeds\n",
|
||
|
|
"import backtrader.indicators as btind\n",
|
||
|
|
"\n",
|
||
|
|
"class CommInfoFloat(bt.CommInfoBase):\n",
|
||
|
|
" \"\"\"Commission schema that keeps size as float.\"\"\"\n",
|
||
|
|
" params = (\n",
|
||
|
|
" ('stocklike', True),\n",
|
||
|
|
" ('commtype', bt.CommInfoBase.COMM_PERC),\n",
|
||
|
|
" ('percabs', True),\n",
|
||
|
|
" )\n",
|
||
|
|
" \n",
|
||
|
|
" def getsize(self, price, cash):\n",
|
||
|
|
" if not self._stocklike:\n",
|
||
|
|
" return self.p.leverage * (cash / self.get_margin(price))\n",
|
||
|
|
"\n",
|
||
|
|
" return self.p.leverage * (cash / price)\n",
|
||
|
|
" \n",
|
||
|
|
"class OLSSlopeIntercept(btind.PeriodN):\n",
|
||
|
|
" \"\"\"Calculates a linear regression using OLS.\"\"\"\n",
|
||
|
|
" _mindatas = 2 # ensure at least 2 data feeds are passed\n",
|
||
|
|
"\n",
|
||
|
|
" packages = (\n",
|
||
|
|
" ('pandas', 'pd'),\n",
|
||
|
|
" ('statsmodels.api', 'sm'),\n",
|
||
|
|
" )\n",
|
||
|
|
" lines = ('slope', 'intercept',)\n",
|
||
|
|
" params = (\n",
|
||
|
|
" ('period', 10),\n",
|
||
|
|
" )\n",
|
||
|
|
"\n",
|
||
|
|
" def next(self):\n",
|
||
|
|
" p0 = pd.Series(self.data0.get(size=self.p.period))\n",
|
||
|
|
" p1 = pd.Series(self.data1.get(size=self.p.period))\n",
|
||
|
|
" p1 = sm.add_constant(p1)\n",
|
||
|
|
" intercept, slope = sm.OLS(p0, p1).fit().params\n",
|
||
|
|
"\n",
|
||
|
|
" self.lines.slope[0] = slope\n",
|
||
|
|
" self.lines.intercept[0] = intercept\n",
|
||
|
|
" \n",
|
||
|
|
" \n",
|
||
|
|
"class Log(btind.Indicator):\n",
|
||
|
|
" \"\"\"Calculates log.\"\"\"\n",
|
||
|
|
" lines = ('log',)\n",
|
||
|
|
" \n",
|
||
|
|
" def next(self):\n",
|
||
|
|
" self.l.log[0] = math.log(self.data[0])\n",
|
||
|
|
"\n",
|
||
|
|
"\n",
|
||
|
|
"class OLSSpread(btind.PeriodN):\n",
|
||
|
|
" \"\"\"Calculates the z-score of the OLS spread.\"\"\"\n",
|
||
|
|
" _mindatas = 2 # ensure at least 2 data feeds are passed\n",
|
||
|
|
" lines = ('spread', 'spread_mean', 'spread_std', 'zscore',)\n",
|
||
|
|
" params = (('period', 10),)\n",
|
||
|
|
"\n",
|
||
|
|
" def __init__(self):\n",
|
||
|
|
" data0_log = Log(self.data0)\n",
|
||
|
|
" data1_log = Log(self.data1)\n",
|
||
|
|
" slint = OLSSlopeIntercept(data0_log, data1_log, period=self.p.period)\n",
|
||
|
|
"\n",
|
||
|
|
" spread = data0_log - (slint.slope * data1_log + slint.intercept)\n",
|
||
|
|
" self.l.spread = spread\n",
|
||
|
|
"\n",
|
||
|
|
" self.l.spread_mean = bt.ind.SMA(spread, period=self.p.period)\n",
|
||
|
|
" self.l.spread_std = bt.ind.StdDev(spread, period=self.p.period)\n",
|
||
|
|
" self.l.zscore = (spread - self.l.spread_mean) / self.l.spread_std\n",
|
||
|
|
" \n",
|
||
|
|
"class LogReturns(btind.PeriodN):\n",
|
||
|
|
" \"\"\"Calculates the log returns.\"\"\"\n",
|
||
|
|
" lines = ('logret',)\n",
|
||
|
|
" params = (('period', 1),)\n",
|
||
|
|
" \n",
|
||
|
|
" def __init__(self):\n",
|
||
|
|
" self.addminperiod(self.p.period + 1)\n",
|
||
|
|
" \n",
|
||
|
|
" def next(self):\n",
|
||
|
|
" self.l.logret[0] = math.log(self.data[0] / self.data[-self.p.period])\n",
|
||
|
|
" \n",
|
||
|
|
"class LogReturnSpread(btind.PeriodN):\n",
|
||
|
|
" \"\"\"Calculates the spread of the log returns.\"\"\"\n",
|
||
|
|
" _mindatas = 2 # ensure at least 2 data feeds are passed\n",
|
||
|
|
" lines = ('logret0', 'logret1', 'spread', 'spread_mean', 'spread_std', 'zscore',)\n",
|
||
|
|
" params = (('period', 10),)\n",
|
||
|
|
"\n",
|
||
|
|
" def __init__(self):\n",
|
||
|
|
" self.l.logret0 = LogReturns(self.data0, period=1)\n",
|
||
|
|
" self.l.logret1 = LogReturns(self.data1, period=1)\n",
|
||
|
|
" self.l.spread = self.l.logret0 - self.l.logret1\n",
|
||
|
|
" self.l.spread_mean = bt.ind.SMA(self.l.spread, period=self.p.period)\n",
|
||
|
|
" self.l.spread_std = bt.ind.StdDev(self.l.spread, period=self.p.period)\n",
|
||
|
|
" self.l.zscore = (self.l.spread - self.l.spread_mean) / self.l.spread_std\n",
|
||
|
|
"\n",
|
||
|
|
"class PairTradingStrategy(bt.Strategy):\n",
|
||
|
|
" \"\"\"Basic pair trading strategy.\"\"\"\n",
|
||
|
|
" params = dict(\n",
|
||
|
|
" period=PERIOD,\n",
|
||
|
|
" order_pct1=ORDER_PCT1,\n",
|
||
|
|
" order_pct2=ORDER_PCT2,\n",
|
||
|
|
" printout=True,\n",
|
||
|
|
" upper=UPPER,\n",
|
||
|
|
" lower=LOWER,\n",
|
||
|
|
" mode=MODE\n",
|
||
|
|
" )\n",
|
||
|
|
"\n",
|
||
|
|
" def log(self, txt, dt=None):\n",
|
||
|
|
" if self.p.printout:\n",
|
||
|
|
" dt = dt or self.data.datetime[0]\n",
|
||
|
|
" dt = bt.num2date(dt)\n",
|
||
|
|
" print('%s, %s' % (dt.isoformat(), txt))\n",
|
||
|
|
"\n",
|
||
|
|
" def notify_order(self, order):\n",
|
||
|
|
" if order.status in [bt.Order.Submitted, bt.Order.Accepted]:\n",
|
||
|
|
" return # Await further notifications\n",
|
||
|
|
"\n",
|
||
|
|
" if order.status == order.Completed:\n",
|
||
|
|
" if order.isbuy():\n",
|
||
|
|
" buytxt = 'BUY COMPLETE {}, size = {:.2f}, price = {:.2f}'.format(\n",
|
||
|
|
" order.data._name, order.executed.size, order.executed.price)\n",
|
||
|
|
" self.log(buytxt, order.executed.dt)\n",
|
||
|
|
" else:\n",
|
||
|
|
" selltxt = 'SELL COMPLETE {}, size = {:.2f}, price = {:.2f}'.format(\n",
|
||
|
|
" order.data._name, order.executed.size, order.executed.price)\n",
|
||
|
|
" self.log(selltxt, order.executed.dt)\n",
|
||
|
|
"\n",
|
||
|
|
" elif order.status in [order.Expired, order.Canceled, order.Margin]:\n",
|
||
|
|
" self.log('%s ,' % order.Status[order.status])\n",
|
||
|
|
" pass # Simply log\n",
|
||
|
|
"\n",
|
||
|
|
" # Allow new orders\n",
|
||
|
|
" self.orderid = None\n",
|
||
|
|
"\n",
|
||
|
|
" def __init__(self):\n",
|
||
|
|
" # To control operation entries\n",
|
||
|
|
" self.orderid = None\n",
|
||
|
|
" self.order_pct1 = self.p.order_pct1\n",
|
||
|
|
" self.order_pct2 = self.p.order_pct2\n",
|
||
|
|
" self.upper = self.p.upper\n",
|
||
|
|
" self.lower = self.p.lower\n",
|
||
|
|
" self.status = 0\n",
|
||
|
|
" \n",
|
||
|
|
" # Signals performed with PD.OLS :\n",
|
||
|
|
" if self.p.mode == 'log_return':\n",
|
||
|
|
" self.transform = LogReturnSpread(self.data0, self.data1, period=self.p.period)\n",
|
||
|
|
" elif self.p.mode == 'OLS':\n",
|
||
|
|
" self.transform = OLSSpread(self.data0, self.data1, period=self.p.period)\n",
|
||
|
|
" else:\n",
|
||
|
|
" raise ValueError(\"Unknown mode\")\n",
|
||
|
|
" self.spread = self.transform.spread\n",
|
||
|
|
" self.zscore = self.transform.zscore\n",
|
||
|
|
" \n",
|
||
|
|
" # For tracking\n",
|
||
|
|
" self.spread_sr = pd.Series(dtype=float, name='spread')\n",
|
||
|
|
" self.zscore_sr = pd.Series(dtype=float, name='zscore')\n",
|
||
|
|
" self.short_signal_sr = pd.Series(dtype=bool, name='short_signals')\n",
|
||
|
|
" self.long_signal_sr = pd.Series(dtype=bool, name='long_signals')\n",
|
||
|
|
"\n",
|
||
|
|
" def next(self):\n",
|
||
|
|
" if self.orderid:\n",
|
||
|
|
" return # if an order is active, no new orders are allowed\n",
|
||
|
|
" \n",
|
||
|
|
" self.spread_sr[self.data0.datetime.datetime()] = self.spread[0]\n",
|
||
|
|
" self.zscore_sr[self.data0.datetime.datetime()] = self.zscore[0]\n",
|
||
|
|
" self.short_signal_sr[self.data0.datetime.datetime()] = False\n",
|
||
|
|
" self.long_signal_sr[self.data0.datetime.datetime()] = False\n",
|
||
|
|
"\n",
|
||
|
|
" if self.zscore[0] > self.upper and self.status != 1:\n",
|
||
|
|
" # Check conditions for shorting the spread & place the order\n",
|
||
|
|
" self.short_signal_sr[self.data0.datetime.datetime()] = True\n",
|
||
|
|
"\n",
|
||
|
|
" # Placing the order\n",
|
||
|
|
" self.log('SELL CREATE {}, price = {:.2f}, target pct = {:.2%}'.format(\n",
|
||
|
|
" self.data0._name, self.data0.close[0], -self.order_pct1))\n",
|
||
|
|
" self.order_target_percent(data=self.data0, target=-self.order_pct1)\n",
|
||
|
|
" self.log('BUY CREATE {}, price = {:.2f}, target pct = {:.2%}'.format(\n",
|
||
|
|
" self.data1._name, self.data1.close[0], self.order_pct2))\n",
|
||
|
|
" self.order_target_percent(data=self.data1, target=self.order_pct2)\n",
|
||
|
|
"\n",
|
||
|
|
" self.status = 1\n",
|
||
|
|
"\n",
|
||
|
|
" elif self.zscore[0] < self.lower and self.status != 2:\n",
|
||
|
|
" # Check conditions for longing the spread & place the order\n",
|
||
|
|
" self.long_signal_sr[self.data0.datetime.datetime()] = True\n",
|
||
|
|
"\n",
|
||
|
|
" # Place the order\n",
|
||
|
|
" self.log('SELL CREATE {}, price = {:.2f}, target pct = {:.2%}'.format(\n",
|
||
|
|
" self.data1._name, self.data1.close[0], -self.order_pct2))\n",
|
||
|
|
" self.order_target_percent(data=self.data1, target=-self.order_pct2)\n",
|
||
|
|
" self.log('BUY CREATE {}, price = {:.2f}, target pct = {:.2%}'.format(\n",
|
||
|
|
" self.data0._name, self.data0.close[0], self.order_pct1))\n",
|
||
|
|
" self.order_target_percent(data=self.data0, target=self.order_pct1)\n",
|
||
|
|
" \n",
|
||
|
|
" self.status = 2\n",
|
||
|
|
"\n",
|
||
|
|
" def stop(self):\n",
|
||
|
|
" if self.p.printout:\n",
|
||
|
|
" print('==================================================')\n",
|
||
|
|
" print('Starting Value - %.2f' % self.broker.startingcash)\n",
|
||
|
|
" print('Ending Value - %.2f' % self.broker.getvalue())\n",
|
||
|
|
" print('==================================================')"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 6,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"class DataAnalyzer(bt.analyzers.Analyzer):\n",
|
||
|
|
" \"\"\"Analyzer to extract OHLCV.\"\"\"\n",
|
||
|
|
" def create_analysis(self):\n",
|
||
|
|
" self.rets0 = {}\n",
|
||
|
|
" self.rets1 = {}\n",
|
||
|
|
"\n",
|
||
|
|
" def next(self):\n",
|
||
|
|
" self.rets0[self.strategy.datetime.datetime()] = [\n",
|
||
|
|
" self.data0.open[0],\n",
|
||
|
|
" self.data0.high[0],\n",
|
||
|
|
" self.data0.low[0],\n",
|
||
|
|
" self.data0.close[0],\n",
|
||
|
|
" self.data0.volume[0]\n",
|
||
|
|
" ]\n",
|
||
|
|
" self.rets1[self.strategy.datetime.datetime()] = [\n",
|
||
|
|
" self.data1.open[0],\n",
|
||
|
|
" self.data1.high[0],\n",
|
||
|
|
" self.data1.low[0],\n",
|
||
|
|
" self.data1.close[0],\n",
|
||
|
|
" self.data1.volume[0]\n",
|
||
|
|
" ]\n",
|
||
|
|
"\n",
|
||
|
|
" def get_analysis(self):\n",
|
||
|
|
" return self.rets0, self.rets1\n",
|
||
|
|
"\n",
|
||
|
|
"class CashValueAnalyzer(bt.analyzers.Analyzer):\n",
|
||
|
|
" \"\"\"Analyzer to extract cash and value.\"\"\"\n",
|
||
|
|
" def create_analysis(self):\n",
|
||
|
|
" self.rets = {}\n",
|
||
|
|
"\n",
|
||
|
|
" def notify_cashvalue(self, cash, value):\n",
|
||
|
|
" self.rets[self.strategy.datetime.datetime()] = (cash, value)\n",
|
||
|
|
"\n",
|
||
|
|
" def get_analysis(self):\n",
|
||
|
|
" return self.rets\n",
|
||
|
|
" \n",
|
||
|
|
"class OrderAnalyzer(bt.analyzers.Analyzer):\n",
|
||
|
|
" \"\"\"Analyzer to extract order price, size, value, and paid commission.\"\"\"\n",
|
||
|
|
" def create_analysis(self):\n",
|
||
|
|
" self.rets0 = {}\n",
|
||
|
|
" self.rets1 = {}\n",
|
||
|
|
"\n",
|
||
|
|
" def notify_order(self, order):\n",
|
||
|
|
" if order.status == order.Completed:\n",
|
||
|
|
" if order.data._name == SYMBOL1:\n",
|
||
|
|
" rets = self.rets0\n",
|
||
|
|
" else:\n",
|
||
|
|
" rets = self.rets1\n",
|
||
|
|
" rets[self.strategy.datetime.datetime()] = (\n",
|
||
|
|
" order.executed.price,\n",
|
||
|
|
" order.executed.size,\n",
|
||
|
|
" -order.executed.size * order.executed.price,\n",
|
||
|
|
" order.executed.comm\n",
|
||
|
|
" )\n",
|
||
|
|
"\n",
|
||
|
|
" def get_analysis(self):\n",
|
||
|
|
" return self.rets0, self.rets1"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 7,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"def prepare_cerebro(data0, data1, use_analyzers=True, **params):\n",
|
||
|
|
" # Create a cerebro\n",
|
||
|
|
" cerebro = bt.Cerebro()\n",
|
||
|
|
"\n",
|
||
|
|
" # Add the 1st data to cerebro\n",
|
||
|
|
" cerebro.adddata(data0)\n",
|
||
|
|
"\n",
|
||
|
|
" # Add the 2nd data to cerebro\n",
|
||
|
|
" cerebro.adddata(data1)\n",
|
||
|
|
"\n",
|
||
|
|
" # Add the strategy\n",
|
||
|
|
" cerebro.addstrategy(PairTradingStrategy, **params)\n",
|
||
|
|
"\n",
|
||
|
|
" # Add the commission - only stocks like a for each operation\n",
|
||
|
|
" cerebro.broker.setcash(CASH)\n",
|
||
|
|
"\n",
|
||
|
|
" # Add the commission - only stocks like a for each operation\n",
|
||
|
|
" comminfo = CommInfoFloat(commission=COMMPERC)\n",
|
||
|
|
" cerebro.broker.addcommissioninfo(comminfo)\n",
|
||
|
|
"\n",
|
||
|
|
" if use_analyzers:\n",
|
||
|
|
" # Add analyzers \n",
|
||
|
|
" cerebro.addanalyzer(DataAnalyzer)\n",
|
||
|
|
" cerebro.addanalyzer(CashValueAnalyzer)\n",
|
||
|
|
" cerebro.addanalyzer(OrderAnalyzer)\n",
|
||
|
|
" \n",
|
||
|
|
" return cerebro"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 8,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"class PandasData(btfeeds.PandasData):\n",
|
||
|
|
" params = (\n",
|
||
|
|
" # Possible values for datetime (must always be present)\n",
|
||
|
|
" # None : datetime is the \"index\" in the Pandas Dataframe\n",
|
||
|
|
" # -1 : autodetect position or case-wise equal name\n",
|
||
|
|
" # >= 0 : numeric index to the colum in the pandas dataframe\n",
|
||
|
|
" # string : column name (as index) in the pandas dataframe\n",
|
||
|
|
" ('datetime', None),\n",
|
||
|
|
"\n",
|
||
|
|
" ('open', 'Open'),\n",
|
||
|
|
" ('high', 'High'),\n",
|
||
|
|
" ('low', 'Low'),\n",
|
||
|
|
" ('close', 'Close'),\n",
|
||
|
|
" ('volume', 'Volume'),\n",
|
||
|
|
" ('openinterest', None),\n",
|
||
|
|
" )"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 9,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stderr",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"/Users/olegpolakow/miniconda3/lib/python3.7/site-packages/statsmodels/tsa/tsatools.py:142: FutureWarning: In a future version of pandas all arguments of concat except for the argument 'objs' will be keyword-only\n",
|
||
|
|
" x = pd.concat(x[::order], 1)\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"2017-11-14T00:00:00, SELL CREATE PEP, price = 103.41, target pct = -10.00%\n",
|
||
|
|
"2017-11-14T00:00:00, BUY CREATE KO, price = 41.95, target pct = 10.00%\n",
|
||
|
|
"2017-11-15T00:00:00, SELL COMPLETE PEP, size = -96.70, price = 103.32\n",
|
||
|
|
"2017-11-15T00:00:00, BUY COMPLETE KO, size = 238.39, price = 41.85\n",
|
||
|
|
"2018-04-11T00:00:00, SELL CREATE KO, price = 39.57, target pct = -10.00%\n",
|
||
|
|
"2018-04-11T00:00:00, BUY CREATE PEP, price = 98.49, target pct = 10.00%\n",
|
||
|
|
"2018-04-12T00:00:00, SELL COMPLETE KO, size = -490.65, price = 39.65\n",
|
||
|
|
"2018-04-12T00:00:00, BUY COMPLETE PEP, size = 198.06, price = 98.74\n",
|
||
|
|
"2018-08-31T00:00:00, SELL CREATE PEP, price = 102.44, target pct = -10.00%\n",
|
||
|
|
"2018-08-31T00:00:00, BUY CREATE KO, price = 40.45, target pct = 10.00%\n",
|
||
|
|
"2018-09-04T00:00:00, SELL COMPLETE PEP, size = -198.78, price = 102.26\n",
|
||
|
|
"2018-09-04T00:00:00, BUY COMPLETE KO, size = 498.98, price = 40.48\n",
|
||
|
|
"2018-10-02T00:00:00, SELL CREATE KO, price = 42.57, target pct = -10.00%\n",
|
||
|
|
"2018-10-02T00:00:00, BUY CREATE PEP, price = 100.25, target pct = 10.00%\n",
|
||
|
|
"2018-10-03T00:00:00, SELL COMPLETE KO, size = -482.28, price = 42.52\n",
|
||
|
|
"2018-10-03T00:00:00, BUY COMPLETE PEP, size = 197.45, price = 100.70\n",
|
||
|
|
"2018-11-30T00:00:00, SELL CREATE PEP, price = 112.44, target pct = -10.00%\n",
|
||
|
|
"2018-11-30T00:00:00, BUY CREATE KO, price = 46.50, target pct = 10.00%\n",
|
||
|
|
"2018-12-03T00:00:00, SELL COMPLETE PEP, size = -189.20, price = 111.09\n",
|
||
|
|
"2018-12-03T00:00:00, BUY COMPLETE KO, size = 451.21, price = 46.01\n",
|
||
|
|
"==================================================\n",
|
||
|
|
"Starting Value - 100000.00\n",
|
||
|
|
"Ending Value - 100284.08\n",
|
||
|
|
"==================================================\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# Create the 1st data\n",
|
||
|
|
"data0 = PandasData(dataname=data.data[SYMBOL1], name=SYMBOL1)\n",
|
||
|
|
"\n",
|
||
|
|
"# Create the 2nd data\n",
|
||
|
|
"data1 = PandasData(dataname=data.data[SYMBOL2], name=SYMBOL2)\n",
|
||
|
|
"\n",
|
||
|
|
"# Prepare a cerebro\n",
|
||
|
|
"cerebro = prepare_cerebro(data0, data1)\n",
|
||
|
|
"\n",
|
||
|
|
"# And run it\n",
|
||
|
|
"bt_strategy = cerebro.run()[0]"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 81,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"data": {
|
||
|
|
"image/png": "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
|
||
|
|
"text/plain": [
|
||
|
|
"<Figure size 1300x800 with 7 Axes>"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"metadata": {},
|
||
|
|
"output_type": "display_data"
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"data": {
|
||
|
|
"text/plain": [
|
||
|
|
"[[<Figure size 1300x800 with 7 Axes>]]"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"execution_count": 81,
|
||
|
|
"metadata": {},
|
||
|
|
"output_type": "execute_result"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"%matplotlib inline\n",
|
||
|
|
"import matplotlib.pyplot as plt\n",
|
||
|
|
"\n",
|
||
|
|
"plt.rcParams[\"figure.figsize\"] = (13, 8)\n",
|
||
|
|
"cerebro.plot(iplot=False)"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 10,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"(502, 5)\n",
|
||
|
|
"(502, 5)\n",
|
||
|
|
" open high low close volume\n",
|
||
|
|
"2017-01-03 91.831129 91.962386 91.192316 91.577354 3741200.0\n",
|
||
|
|
"2018-12-31 102.775161 103.249160 101.604091 102.682220 5019100.0\n",
|
||
|
|
" open high low close volume\n",
|
||
|
|
"2017-01-03 35.801111 36.068542 35.611321 36.059914 14711000.0\n",
|
||
|
|
"2018-12-31 43.810820 43.856946 43.321878 43.681664 10576300.0\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# Extract OHLCV\n",
|
||
|
|
"bt_s1_rets, bt_s2_rets = bt_strategy.analyzers.dataanalyzer.get_analysis()\n",
|
||
|
|
"data_cols = ['open', 'high', 'low', 'close', 'volume']\n",
|
||
|
|
"bt_s1_ohlcv = pd.DataFrame.from_dict(bt_s1_rets, orient='index', columns=data_cols)\n",
|
||
|
|
"bt_s2_ohlcv = pd.DataFrame.from_dict(bt_s2_rets, orient='index', columns=data_cols)\n",
|
||
|
|
"\n",
|
||
|
|
"print(bt_s1_ohlcv.shape)\n",
|
||
|
|
"print(bt_s2_ohlcv.shape)\n",
|
||
|
|
"\n",
|
||
|
|
"print(bt_s1_ohlcv.iloc[[0, -1]])\n",
|
||
|
|
"print(bt_s2_ohlcv.iloc[[0, -1]])"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 11,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"try:\n",
|
||
|
|
" np.testing.assert_allclose(bt_s1_ohlcv.values, data.data[SYMBOL1].iloc[:, :5].values)\n",
|
||
|
|
" np.testing.assert_allclose(bt_s2_ohlcv.values, data.data[SYMBOL2].iloc[:, :5].values)\n",
|
||
|
|
"except AssertionError as e:\n",
|
||
|
|
" print(e)"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 12,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"2017-01-03 100000.000000\n",
|
||
|
|
"2018-12-31 100020.682139\n",
|
||
|
|
"Name: cash, dtype: float64\n",
|
||
|
|
"2017-01-03 100000.000000\n",
|
||
|
|
"2018-12-31 100284.081822\n",
|
||
|
|
"Name: value, dtype: float64\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# Extract cash and value series\n",
|
||
|
|
"bt_cashvalue_rets = bt_strategy.analyzers.cashvalueanalyzer.get_analysis()\n",
|
||
|
|
"bt_cashvalue_df = pd.DataFrame.from_dict(bt_cashvalue_rets, orient='index', columns=['cash', 'value'])\n",
|
||
|
|
"bt_cash = bt_cashvalue_df['cash']\n",
|
||
|
|
"bt_value = bt_cashvalue_df['value']\n",
|
||
|
|
"\n",
|
||
|
|
"print(bt_cash.iloc[[0, -1]])\n",
|
||
|
|
"print(bt_value.iloc[[0, -1]])"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 13,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
" order_price order_size order_value order_comm\n",
|
||
|
|
"2017-11-15 103.316229 -96.698241 9990.497637 49.952488\n",
|
||
|
|
"2018-12-03 111.094282 -189.202087 21019.269920 105.096350\n",
|
||
|
|
" order_price order_size order_value order_comm\n",
|
||
|
|
"2017-11-15 41.851497 238.385955 -9976.809115 49.884046\n",
|
||
|
|
"2018-12-03 46.006429 451.211817 -20758.644494 103.793222\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# Extract order info\n",
|
||
|
|
"bt_s1_order_rets, bt_s2_order_rets = bt_strategy.analyzers.orderanalyzer.get_analysis()\n",
|
||
|
|
"order_cols = ['order_price', 'order_size', 'order_value', 'order_comm']\n",
|
||
|
|
"bt_s1_orders = pd.DataFrame.from_dict(bt_s1_order_rets, orient='index', columns=order_cols)\n",
|
||
|
|
"bt_s2_orders = pd.DataFrame.from_dict(bt_s2_order_rets, orient='index', columns=order_cols)\n",
|
||
|
|
"\n",
|
||
|
|
"print(bt_s1_orders.iloc[[0, -1]])\n",
|
||
|
|
"print(bt_s2_orders.iloc[[0, -1]])"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 14,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"2017-10-16 -0.009388\n",
|
||
|
|
"2018-12-31 -0.021157\n",
|
||
|
|
"Name: spread, dtype: float64\n",
|
||
|
|
"2017-10-16 0.242706\n",
|
||
|
|
"2018-12-31 -0.307914\n",
|
||
|
|
"Name: zscore, dtype: float64\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# Extract spread and z-score\n",
|
||
|
|
"bt_spread = bt_strategy.spread_sr\n",
|
||
|
|
"bt_zscore = bt_strategy.zscore_sr\n",
|
||
|
|
"\n",
|
||
|
|
"print(bt_spread.iloc[[0, -1]])\n",
|
||
|
|
"print(bt_zscore.iloc[[0, -1]])"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 15,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"2017-11-14 True\n",
|
||
|
|
"2018-08-31 True\n",
|
||
|
|
"2018-11-30 True\n",
|
||
|
|
"Name: short_signals, dtype: bool\n",
|
||
|
|
"2018-04-11 True\n",
|
||
|
|
"2018-10-02 True\n",
|
||
|
|
"Name: long_signals, dtype: bool\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# Extract signals\n",
|
||
|
|
"bt_short_signals = bt_strategy.short_signal_sr\n",
|
||
|
|
"bt_long_signals = bt_strategy.long_signal_sr\n",
|
||
|
|
"\n",
|
||
|
|
"print(bt_short_signals[bt_short_signals])\n",
|
||
|
|
"print(bt_long_signals[bt_long_signals])"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 14,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"1.07 s ± 16.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# How fast is bt?\n",
|
||
|
|
"cerebro = prepare_cerebro(data0, data1, use_analyzers=False, printout=False)\n",
|
||
|
|
"\n",
|
||
|
|
"%timeit cerebro.run(preload=False)"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"## vectorbt"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"### Using Portfolio.from_orders"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 16,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"from numba import njit\n",
|
||
|
|
"\n",
|
||
|
|
"@njit\n",
|
||
|
|
"def rolling_logret_zscore_nb(a, b, period):\n",
|
||
|
|
" \"\"\"Calculate the log return spread.\"\"\"\n",
|
||
|
|
" spread = np.full_like(a, np.nan, dtype=np.float64)\n",
|
||
|
|
" spread[1:] = np.log(a[1:] / a[:-1]) - np.log(b[1:] / b[:-1])\n",
|
||
|
|
" zscore = np.full_like(a, np.nan, dtype=np.float64)\n",
|
||
|
|
" for i in range(a.shape[0]):\n",
|
||
|
|
" from_i = max(0, i + 1 - period)\n",
|
||
|
|
" to_i = i + 1\n",
|
||
|
|
" if i < period - 1:\n",
|
||
|
|
" continue\n",
|
||
|
|
" spread_mean = np.mean(spread[from_i:to_i])\n",
|
||
|
|
" spread_std = np.std(spread[from_i:to_i])\n",
|
||
|
|
" zscore[i] = (spread[i] - spread_mean) / spread_std\n",
|
||
|
|
" return spread, zscore\n",
|
||
|
|
"\n",
|
||
|
|
"@njit\n",
|
||
|
|
"def ols_spread_nb(a, b):\n",
|
||
|
|
" \"\"\"Calculate the OLS spread.\"\"\"\n",
|
||
|
|
" a = np.log(a)\n",
|
||
|
|
" b = np.log(b)\n",
|
||
|
|
" _b = np.vstack((b, np.ones(len(b)))).T\n",
|
||
|
|
" slope, intercept = np.dot(np.linalg.inv(np.dot(_b.T, _b)), np.dot(_b.T, a))\n",
|
||
|
|
" spread = a - (slope * b + intercept)\n",
|
||
|
|
" return spread[-1]\n",
|
||
|
|
" \n",
|
||
|
|
"@njit\n",
|
||
|
|
"def rolling_ols_zscore_nb(a, b, period):\n",
|
||
|
|
" \"\"\"Calculate the z-score of the rolling OLS spread.\"\"\"\n",
|
||
|
|
" spread = np.full_like(a, np.nan, dtype=np.float64)\n",
|
||
|
|
" zscore = np.full_like(a, np.nan, dtype=np.float64)\n",
|
||
|
|
" for i in range(a.shape[0]):\n",
|
||
|
|
" from_i = max(0, i + 1 - period)\n",
|
||
|
|
" to_i = i + 1\n",
|
||
|
|
" if i < period - 1:\n",
|
||
|
|
" continue\n",
|
||
|
|
" spread[i] = ols_spread_nb(a[from_i:to_i], b[from_i:to_i])\n",
|
||
|
|
" spread_mean = np.mean(spread[from_i:to_i])\n",
|
||
|
|
" spread_std = np.std(spread[from_i:to_i])\n",
|
||
|
|
" zscore[i] = (spread[i] - spread_mean) / spread_std\n",
|
||
|
|
" return spread, zscore"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 17,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"# Calculate OLS z-score using Numba for a nice speedup\n",
|
||
|
|
"if MODE == 'OLS':\n",
|
||
|
|
" vbt_spread, vbt_zscore = rolling_ols_zscore_nb(\n",
|
||
|
|
" bt_s1_ohlcv['close'].values, \n",
|
||
|
|
" bt_s2_ohlcv['close'].values, \n",
|
||
|
|
" PERIOD\n",
|
||
|
|
" )\n",
|
||
|
|
"elif MODE == 'log_return':\n",
|
||
|
|
" vbt_spread, vbt_zscore = rolling_logret_zscore_nb(\n",
|
||
|
|
" bt_s1_ohlcv['close'].values, \n",
|
||
|
|
" bt_s2_ohlcv['close'].values, \n",
|
||
|
|
" PERIOD\n",
|
||
|
|
" )\n",
|
||
|
|
"else:\n",
|
||
|
|
" raise ValueError(\"Unknown mode\")\n",
|
||
|
|
"vbt_spread = pd.Series(vbt_spread, index=bt_s1_ohlcv.index, name='spread')\n",
|
||
|
|
"vbt_zscore = pd.Series(vbt_zscore, index=bt_s1_ohlcv.index, name='zscore')"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 18,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"# Assert equality of bt and vbt z-score arrays\n",
|
||
|
|
"pd.testing.assert_series_equal(bt_spread, vbt_spread[bt_spread.index])\n",
|
||
|
|
"pd.testing.assert_series_equal(bt_zscore, vbt_zscore[bt_zscore.index])"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 19,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"# Generate short and long spread signals\n",
|
||
|
|
"vbt_short_signals = (vbt_zscore > UPPER).rename('short_signals')\n",
|
||
|
|
"vbt_long_signals = (vbt_zscore < LOWER).rename('long_signals')"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 20,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"data": {
|
||
|
|
"image/svg+xml": [
|
||
|
|
"<svg class=\"main-svg\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"700\" height=\"500\" style=\"\" viewBox=\"0 0 700 500\"><rect x=\"0\" y=\"0\" width=\"700\" height=\"500\" style=\"fill: rgb(255, 255, 255); fill-opacity: 1;\"/><defs id=\"defs-043d55\"><g class=\"clips\"><clipPath id=\"clip043d55xyplot\" class=\"plotclip\"><rect width=\"620\" height=\"199.975\"/></clipPath><clipPath id=\"clip043d55x2y2plot\" class=\"plotclip\"><rect width=\"620\" height=\"199.975\"/></clipPath><clipPath class=\"axesclip\" id=\"clip043d55x\"><rect x=\"50\" y=\"0\" width=\"620\" height=\"500\"/></clipPath><clipPath class=\"axesclip\" id=\"clip043d55y\"><rect x=\"0\" y=\"49\" width=\"700\" height=\"199.975\"/></clipPath><clipPath class=\"axesclip\" id=\"clip043d55xy\"><rect x=\"50\" y=\"49\" width=\"620\" height=\"199.975\"/></clipPath><clipPath class=\"axesclip\" id=\"clip043d55y2\"><rect x=\"0\" y=\"270.025\" width=\"700\" height=\"199.975\"/></clipPath><clipPath class=\"axesclip\" id=\"clip043d55xy2\"><rect x=\"50\" y=\"270.025\" width=\"620\" height=\"199.975\"/></clipPath><clipPath class=\"axesclip\" id=\"clip043d55x2\"><rect x=\"50\" y=\"0\" width=\"620\" height=\"500\"/></clipPath><clipPath class=\"axesclip\" id=\"clip043d55x2y\"><rect x=\"50\" y=\"49\" width=\"620\" height=\"199.975\"/></clipPath><clipPath class=\"axesclip\" id=\"clip043d55x2y2\"><rect x=\"50\" y=\"270.025\" width=\"620\" height=\"199.975\"/></clipPath></g><g class=\"gradients\"/><g class=\"patterns\"/></defs><g class=\"bglayer\"><rect class=\"bg\" x=\"50\" y=\"49\" width=\"620\" height=\"199.975\" style=\"fill: rgb(229, 236, 246); fill-opacity: 1; stroke-width: 0;\"/><rect class=\"bg\" x=\"50\" y=\"270.025\" width=\"620\" height=\"199.975\" style=\"fill: rgb(229, 236, 246); fill-opacity: 1; stroke-width: 0;\"/></g><g class=\"layer-below\"><g class=\"imagelayer\"/><g class=\"shapelayer\"><path data-index=\"0\" fill-rule=\"evenodd\" d=\"M50,307.025H670V418.44499999999994H50Z\" clip-path=\"url(#clip043d55y2)\" style=\"opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(128, 128, 128); fill-opacity: 1; stroke-width: 0px;\"/></g></g><g class=\"cartesianlayer\"><g class=\"subplot xy\"><g class=\"layer-subplot\"><g class=\"shapelayer\"/><g class=\"imagelayer\"/></g><g class=\"gridlayer\"><g class=\"x\"><path class=\"xgrid crisp\" transform=\"translate(123.86,0)\" d=\"M0,49v199.975\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"xgrid crisp\" transform=\"translate(200.24,0)\" d=\"M0,49v199.975\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"xgrid crisp\" transform=\"translate(277.45,0)\" d=\"M0,49v199.975\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"xgrid crisp\" transform=\"translate(354.67,0)\" d=\"M0,49v199.975\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"xgrid crisp\" transform=\"translate(430.21,0)\" d=\"M0,49v199.975\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"xgrid crisp\" transform=\"translate(506.58,0)\" d=\"M0,49v199.975\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"xgrid crisp\" transform=\"translate(583.8,0)\" d=\"M0,49v199.975\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"xgrid crisp\" transform=\"translate(661.01,0)\" d=\"M0,49v199.975\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/></g><g class=\"y\"><path class=\"ygrid crisp\" transform=\"translate(0,198.3)\" d=\"M50,0h620\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"ygrid crisp\" transform=\"translate(0,57.59)\" d=\"M50,0h620\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/></g></g><g class=\"zerolinelayer\"><path class=\"yzl zl crisp\" transform=\"translate(0,127.95)\" d=\"M50,0h620\" style=\"stroke: rgb(255
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"metadata": {},
|
||
|
|
"output_type": "display_data"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"vbt_short_signals, vbt_long_signals = pd.Series.vbt.signals.clean(\n",
|
||
|
|
" vbt_short_signals, vbt_long_signals, entry_first=False, broadcast_kwargs=dict(columns_from='keep'))\n",
|
||
|
|
"\n",
|
||
|
|
"def plot_spread_and_zscore(spread, zscore):\n",
|
||
|
|
" fig = vbt.make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.05)\n",
|
||
|
|
" spread.vbt.plot(add_trace_kwargs=dict(row=1, col=1), fig=fig)\n",
|
||
|
|
" zscore.vbt.plot(add_trace_kwargs=dict(row=2, col=1), fig=fig)\n",
|
||
|
|
" vbt_short_signals.vbt.signals.plot_as_exit_markers(zscore, add_trace_kwargs=dict(row=2, col=1), fig=fig)\n",
|
||
|
|
" vbt_long_signals.vbt.signals.plot_as_entry_markers(zscore, add_trace_kwargs=dict(row=2, col=1), fig=fig)\n",
|
||
|
|
" fig.update_layout(height=500)\n",
|
||
|
|
" fig.add_shape(\n",
|
||
|
|
" type=\"rect\",\n",
|
||
|
|
" xref='paper',\n",
|
||
|
|
" yref='y2',\n",
|
||
|
|
" x0=0,\n",
|
||
|
|
" y0=UPPER,\n",
|
||
|
|
" x1=1,\n",
|
||
|
|
" y1=LOWER,\n",
|
||
|
|
" fillcolor=\"gray\",\n",
|
||
|
|
" opacity=0.2,\n",
|
||
|
|
" layer=\"below\",\n",
|
||
|
|
" line_width=0,\n",
|
||
|
|
" )\n",
|
||
|
|
" return fig\n",
|
||
|
|
" \n",
|
||
|
|
"plot_spread_and_zscore(vbt_spread, vbt_zscore).show_svg()"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 21,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"# Assert equality of bt and vbt signal arrays\n",
|
||
|
|
"pd.testing.assert_series_equal(\n",
|
||
|
|
" bt_short_signals[bt_short_signals], \n",
|
||
|
|
" vbt_short_signals[vbt_short_signals]\n",
|
||
|
|
")\n",
|
||
|
|
"pd.testing.assert_series_equal(\n",
|
||
|
|
" bt_long_signals[bt_long_signals], \n",
|
||
|
|
" vbt_long_signals[vbt_long_signals]\n",
|
||
|
|
")"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 22,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"symbol PEP KO\n",
|
||
|
|
"2017-11-15 -0.1 0.1\n",
|
||
|
|
"2018-04-12 0.1 -0.1\n",
|
||
|
|
"2018-09-04 -0.1 0.1\n",
|
||
|
|
"2018-10-03 0.1 -0.1\n",
|
||
|
|
"2018-12-03 -0.1 0.1\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# Build percentage order size\n",
|
||
|
|
"symbol_cols = pd.Index([SYMBOL1, SYMBOL2], name='symbol')\n",
|
||
|
|
"vbt_order_size = pd.DataFrame(index=bt_s1_ohlcv.index, columns=symbol_cols)\n",
|
||
|
|
"vbt_order_size[SYMBOL1] = np.nan\n",
|
||
|
|
"vbt_order_size[SYMBOL2] = np.nan\n",
|
||
|
|
"vbt_order_size.loc[vbt_short_signals, SYMBOL1] = -ORDER_PCT1\n",
|
||
|
|
"vbt_order_size.loc[vbt_long_signals, SYMBOL1] = ORDER_PCT1\n",
|
||
|
|
"vbt_order_size.loc[vbt_short_signals, SYMBOL2] = ORDER_PCT2\n",
|
||
|
|
"vbt_order_size.loc[vbt_long_signals, SYMBOL2] = -ORDER_PCT2\n",
|
||
|
|
"\n",
|
||
|
|
"# Execute at the next bar\n",
|
||
|
|
"vbt_order_size = vbt_order_size.vbt.fshift(1)\n",
|
||
|
|
"\n",
|
||
|
|
"print(vbt_order_size[~vbt_order_size.isnull().any(axis=1)])"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 23,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"# Simulate the portfolio\n",
|
||
|
|
"vbt_close_price = pd.concat((bt_s1_ohlcv['close'], bt_s2_ohlcv['close']), axis=1, keys=symbol_cols)\n",
|
||
|
|
"vbt_open_price = pd.concat((bt_s1_ohlcv['open'], bt_s2_ohlcv['open']), axis=1, keys=symbol_cols)\n",
|
||
|
|
"\n",
|
||
|
|
"def simulate_from_orders():\n",
|
||
|
|
" \"\"\"Simulate using `Portfolio.from_orders`.\"\"\"\n",
|
||
|
|
" return vbt.Portfolio.from_orders(\n",
|
||
|
|
" vbt_close_price, # current close as reference price\n",
|
||
|
|
" size=vbt_order_size, \n",
|
||
|
|
" price=vbt_open_price, # current open as execution price\n",
|
||
|
|
" size_type='targetpercent', \n",
|
||
|
|
" val_price=vbt_close_price.vbt.fshift(1), # previous close as group valuation price\n",
|
||
|
|
" init_cash=CASH,\n",
|
||
|
|
" fees=COMMPERC,\n",
|
||
|
|
" cash_sharing=True, # share capital between assets in the same group\n",
|
||
|
|
" group_by=True, # all columns belong to the same group\n",
|
||
|
|
" call_seq='auto', # sell before buying\n",
|
||
|
|
" freq='d' # index frequency for annualization\n",
|
||
|
|
" )\n",
|
||
|
|
"\n",
|
||
|
|
"vbt_pf = simulate_from_orders()"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 24,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
" Order Id Column Timestamp Size Price Fees Side\n",
|
||
|
|
"0 0 PEP 2017-11-15 96.698241 103.316229 49.952488 Sell\n",
|
||
|
|
"1 1 KO 2017-11-15 238.385955 41.851497 49.884046 Buy\n",
|
||
|
|
"2 2 KO 2018-04-12 490.647327 39.652521 97.277017 Sell\n",
|
||
|
|
"3 3 PEP 2018-04-12 198.056953 98.739352 97.780076 Buy\n",
|
||
|
|
"4 4 PEP 2018-09-04 198.781680 102.263047 101.640101 Sell\n",
|
||
|
|
"5 5 KO 2018-09-04 498.975917 40.477607 100.986755 Buy\n",
|
||
|
|
"6 6 KO 2018-10-03 482.284086 42.524342 102.544068 Sell\n",
|
||
|
|
"7 7 PEP 2018-10-03 197.454665 100.702245 99.420640 Buy\n",
|
||
|
|
"8 8 PEP 2018-12-03 189.202087 111.094282 105.096350 Sell\n",
|
||
|
|
"9 9 KO 2018-12-03 451.211817 46.006429 103.793222 Buy\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"print(vbt_pf.orders.records_readable)"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 25,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"# Proof that both bt and vbt produce the same result\n",
|
||
|
|
"pd.testing.assert_series_equal(bt_cash, vbt_pf.cash().rename('cash'))\n",
|
||
|
|
"pd.testing.assert_series_equal(bt_value, vbt_pf.value().rename('value'))"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 26,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"Start 2017-01-03 00:00:00\n",
|
||
|
|
"End 2018-12-31 00:00:00\n",
|
||
|
|
"Period 502 days 00:00:00\n",
|
||
|
|
"Start Value 100000.0\n",
|
||
|
|
"End Value 100284.081822\n",
|
||
|
|
"Total Return [%] 0.284082\n",
|
||
|
|
"Benchmark Return [%] 16.631282\n",
|
||
|
|
"Max Gross Exposure [%] 0.915879\n",
|
||
|
|
"Total Fees Paid 908.374763\n",
|
||
|
|
"Max Drawdown [%] 1.030291\n",
|
||
|
|
"Max Drawdown Duration 168 days 00:00:00\n",
|
||
|
|
"Total Trades 10\n",
|
||
|
|
"Total Closed Trades 8\n",
|
||
|
|
"Total Open Trades 2\n",
|
||
|
|
"Open Trade PnL 149.652774\n",
|
||
|
|
"Win Rate [%] 62.5\n",
|
||
|
|
"Best Trade [%] 9.267971\n",
|
||
|
|
"Worst Trade [%] -9.229397\n",
|
||
|
|
"Avg Winning Trade [%] 3.967201\n",
|
||
|
|
"Avg Losing Trade [%] -6.182852\n",
|
||
|
|
"Avg Winning Trade Duration 56 days 19:12:00\n",
|
||
|
|
"Avg Losing Trade Duration 80 days 16:00:00\n",
|
||
|
|
"Profit Factor 1.072464\n",
|
||
|
|
"Expectancy 16.803631\n",
|
||
|
|
"Sharpe Ratio 0.185035\n",
|
||
|
|
"Calmar Ratio 0.200403\n",
|
||
|
|
"Omega Ratio 1.036058\n",
|
||
|
|
"Sortino Ratio 0.266979\n",
|
||
|
|
"Name: group, dtype: object\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"print(vbt_pf.stats())"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 27,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"data": {
|
||
|
|
"image/svg+xml": [
|
||
|
|
"<svg class=\"main-svg\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"750\" height=\"670\" style=\"\" viewBox=\"0 0 750 670\"><rect x=\"0\" y=\"0\" width=\"750\" height=\"670\" style=\"fill: rgb(255, 255, 255); fill-opacity: 1;\"/><defs id=\"defs-86638d\"><g class=\"clips\"><clipPath id=\"clip86638dxyplot\" class=\"plotclip\"><rect width=\"659\" height=\"256.2313432835821\"/></clipPath><clipPath id=\"clip86638dx2y2plot\" class=\"plotclip\"><rect width=\"659\" height=\"256.2313432835821\"/></clipPath><clipPath class=\"axesclip\" id=\"clip86638dx\"><rect x=\"61\" y=\"0\" width=\"659\" height=\"670\"/></clipPath><clipPath class=\"axesclip\" id=\"clip86638dy\"><rect x=\"0\" y=\"73\" width=\"750\" height=\"256.2313432835821\"/></clipPath><clipPath class=\"axesclip\" id=\"clip86638dxy\"><rect x=\"61\" y=\"73\" width=\"659\" height=\"256.2313432835821\"/></clipPath><clipPath class=\"axesclip\" id=\"clip86638dy2\"><rect x=\"0\" y=\"361.7686567164179\" width=\"750\" height=\"256.2313432835821\"/></clipPath><clipPath class=\"axesclip\" id=\"clip86638dxy2\"><rect x=\"61\" y=\"361.7686567164179\" width=\"659\" height=\"256.2313432835821\"/></clipPath><clipPath class=\"axesclip\" id=\"clip86638dx2\"><rect x=\"61\" y=\"0\" width=\"659\" height=\"670\"/></clipPath><clipPath class=\"axesclip\" id=\"clip86638dx2y\"><rect x=\"61\" y=\"73\" width=\"659\" height=\"256.2313432835821\"/></clipPath><clipPath class=\"axesclip\" id=\"clip86638dx2y2\"><rect x=\"61\" y=\"361.7686567164179\" width=\"659\" height=\"256.2313432835821\"/></clipPath></g><g class=\"gradients\"/><g class=\"patterns\"/></defs><g class=\"bglayer\"><rect class=\"bg\" x=\"61\" y=\"73\" width=\"659\" height=\"256.2313432835821\" style=\"fill: rgb(229, 236, 246); fill-opacity: 1; stroke-width: 0;\"/><rect class=\"bg\" x=\"61\" y=\"361.7686567164179\" width=\"659\" height=\"256.2313432835821\" style=\"fill: rgb(229, 236, 246); fill-opacity: 1; stroke-width: 0;\"/></g><g class=\"layer-below\"><g class=\"imagelayer\"/><g class=\"shapelayer\"/></g><g class=\"cartesianlayer\"><g class=\"subplot xy\"><g class=\"layer-subplot\"><g class=\"shapelayer\"/><g class=\"imagelayer\"/></g><g class=\"gridlayer\"><g class=\"x\"><path class=\"xgrid crisp\" transform=\"translate(139.19,0)\" d=\"M0,73v256.2313432835821\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"xgrid crisp\" transform=\"translate(220.04,0)\" d=\"M0,73v256.2313432835821\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"xgrid crisp\" transform=\"translate(301.78,0)\" d=\"M0,73v256.2313432835821\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"xgrid crisp\" transform=\"translate(383.52,0)\" d=\"M0,73v256.2313432835821\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"xgrid crisp\" transform=\"translate(463.48,0)\" d=\"M0,73v256.2313432835821\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"xgrid crisp\" transform=\"translate(544.3399999999999,0)\" d=\"M0,73v256.2313432835821\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"xgrid crisp\" transform=\"translate(626.08,0)\" d=\"M0,73v256.2313432835821\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"xgrid crisp\" transform=\"translate(707.82,0)\" d=\"M0,73v256.2313432835821\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/></g><g class=\"y\"><path class=\"ygrid crisp\" transform=\"translate(0,290.35)\" d=\"M61,0h659\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"ygrid crisp\" transform=\"translate(0,244.78)\" d=\"M61,0h659\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"ygrid crisp\" transform=\"translate(0,199.2)\" d=\"M61,0h659\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><pa
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"metadata": {},
|
||
|
|
"output_type": "display_data"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# Plot portfolio\n",
|
||
|
|
"from functools import partial\n",
|
||
|
|
"\n",
|
||
|
|
"def plot_orders(portfolio, column=None, add_trace_kwargs=None, fig=None):\n",
|
||
|
|
" portfolio.orders.plot(column=column, add_trace_kwargs=add_trace_kwargs, fig=fig)\n",
|
||
|
|
"\n",
|
||
|
|
"vbt_pf.plot(subplots=[\n",
|
||
|
|
" ('symbol1_orders', dict(\n",
|
||
|
|
" title=f\"Orders ({SYMBOL1})\",\n",
|
||
|
|
" yaxis_title=\"Price\",\n",
|
||
|
|
" check_is_not_grouped=False,\n",
|
||
|
|
" plot_func=partial(plot_orders, column=SYMBOL1),\n",
|
||
|
|
" pass_column=False\n",
|
||
|
|
" )),\n",
|
||
|
|
" ('symbol2_orders', dict(\n",
|
||
|
|
" title=f\"Orders ({SYMBOL2})\",\n",
|
||
|
|
" yaxis_title=\"Price\",\n",
|
||
|
|
" check_is_not_grouped=False,\n",
|
||
|
|
" plot_func=partial(plot_orders, column=SYMBOL2),\n",
|
||
|
|
" pass_column=False\n",
|
||
|
|
" ))\n",
|
||
|
|
"]).show_svg()"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 29,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"3.72 ms ± 15.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# How fast is vbt?\n",
|
||
|
|
"%timeit simulate_from_orders()"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"While Portfolio.from_orders is a very convenient and optimized function for simulating portfolios, it requires some prior steps to produce the size array. In the example above, we needed to manually run the calculation of the spread z-score, generate the signals from the z-score, build the size array from the signals, and make sure that all arrays are perfectly aligned. All these steps must be repeated and adapted accordingly once there is more than one hyperparameter combination to test.\n",
|
||
|
|
"\n",
|
||
|
|
"Nevertheless, dividing the pipeline into clearly separated backtesting steps helps us to analyze each step thoroughly and actually does wonders for strategy development and debugging."
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"### Using Portfolio.from_order_func"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"Portfolio.from_order_func follows a different (self-contained) approach where as much steps as possible should be defined in the simulation function itself. It sequentially processes timestamps one by one and executes orders based on the logic the user defined rather than parses this logic from some arrays. While this makes order execution less transparent as you cannot analyze each piece of data on the fly anymore (sadly, no pandas and plotting within Numba), it has one big advantage over other vectorized methods: event-driven order processing. This gives best flexibility (you can write any logic), security (less probability of exposing yourself to a look-ahead bias among other biases), and performance (you're traversing the data only once). This method is the most similar one compared to backtrader."
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 32,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"from vectorbt.portfolio import nb as portfolio_nb\n",
|
||
|
|
"from vectorbt.base.reshape_fns import flex_select_auto_nb\n",
|
||
|
|
"from vectorbt.portfolio.enums import SizeType, Direction\n",
|
||
|
|
"from collections import namedtuple\n",
|
||
|
|
"\n",
|
||
|
|
"Memory = namedtuple(\"Memory\", ('spread', 'zscore', 'status'))\n",
|
||
|
|
"Params = namedtuple(\"Params\", ('period', 'upper', 'lower', 'order_pct1', 'order_pct2'))\n",
|
||
|
|
"\n",
|
||
|
|
"@njit\n",
|
||
|
|
"def pre_group_func_nb(c, _period, _upper, _lower, _order_pct1, _order_pct2):\n",
|
||
|
|
" \"\"\"Prepare the current group (= pair of columns).\"\"\"\n",
|
||
|
|
" assert c.group_len == 2\n",
|
||
|
|
" \n",
|
||
|
|
" # In contrast to bt, we don't have a class instance that we could use to store arrays,\n",
|
||
|
|
" # so let's create a namedtuple acting as a container for our arrays\n",
|
||
|
|
" # ( you could also pass each array as a standalone object, but a single object is more convenient)\n",
|
||
|
|
" spread = np.full(c.target_shape[0], np.nan, dtype=np.float64)\n",
|
||
|
|
" zscore = np.full(c.target_shape[0], np.nan, dtype=np.float64)\n",
|
||
|
|
" \n",
|
||
|
|
" # Note that namedtuples aren't mutable, you can't simply assign a value,\n",
|
||
|
|
" # thus make status variable an array of one element for an easy assignment\n",
|
||
|
|
" status = np.full(1, 0, dtype=np.int64)\n",
|
||
|
|
" memory = Memory(spread, zscore, status)\n",
|
||
|
|
" \n",
|
||
|
|
" # Treat each param as an array with value per group, and select the combination of params for this group\n",
|
||
|
|
" period = flex_select_auto_nb(np.asarray(_period), 0, c.group, True)\n",
|
||
|
|
" upper = flex_select_auto_nb(np.asarray(_upper), 0, c.group, True)\n",
|
||
|
|
" lower = flex_select_auto_nb(np.asarray(_lower), 0, c.group, True)\n",
|
||
|
|
" order_pct1 = flex_select_auto_nb(np.asarray(_order_pct1), 0, c.group, True)\n",
|
||
|
|
" order_pct2 = flex_select_auto_nb(np.asarray(_order_pct2), 0, c.group, True)\n",
|
||
|
|
" \n",
|
||
|
|
" # Put all params into a container (again, this is optional)\n",
|
||
|
|
" params = Params(period, upper, lower, order_pct1, order_pct2)\n",
|
||
|
|
" \n",
|
||
|
|
" # Create an array that will store our two target percentages used by order_func_nb\n",
|
||
|
|
" # we do it here instead of in pre_segment_func_nb to initialize the array once, instead of in each row\n",
|
||
|
|
" size = np.empty(c.group_len, dtype=np.float64)\n",
|
||
|
|
" \n",
|
||
|
|
" # The returned tuple is passed as arguments to the function below\n",
|
||
|
|
" return (memory, params, size)\n",
|
||
|
|
" \n",
|
||
|
|
"\n",
|
||
|
|
"@njit\n",
|
||
|
|
"def pre_segment_func_nb(c, memory, params, size, mode):\n",
|
||
|
|
" \"\"\"Prepare the current segment (= row within group).\"\"\"\n",
|
||
|
|
" \n",
|
||
|
|
" # We want to perform calculations once we reach full window size\n",
|
||
|
|
" if c.i < params.period - 1:\n",
|
||
|
|
" size[0] = np.nan # size of nan means no order\n",
|
||
|
|
" size[1] = np.nan\n",
|
||
|
|
" return (size,)\n",
|
||
|
|
" \n",
|
||
|
|
" # z-core is calculated using a window (=period) of spread values\n",
|
||
|
|
" # This window can be specified as a slice\n",
|
||
|
|
" window_slice = slice(max(0, c.i + 1 - params.period), c.i + 1)\n",
|
||
|
|
" \n",
|
||
|
|
" # Here comes the same as in rolling_ols_zscore_nb\n",
|
||
|
|
" if mode == 'OLS':\n",
|
||
|
|
" a = c.close[window_slice, c.from_col]\n",
|
||
|
|
" b = c.close[window_slice, c.from_col + 1]\n",
|
||
|
|
" memory.spread[c.i] = ols_spread_nb(a, b)\n",
|
||
|
|
" elif mode == 'log_return':\n",
|
||
|
|
" logret_a = np.log(c.close[c.i, c.from_col] / c.close[c.i - 1, c.from_col])\n",
|
||
|
|
" logret_b = np.log(c.close[c.i, c.from_col + 1] / c.close[c.i - 1, c.from_col + 1])\n",
|
||
|
|
" memory.spread[c.i] = logret_a - logret_b\n",
|
||
|
|
" else:\n",
|
||
|
|
" raise ValueError(\"Unknown mode\")\n",
|
||
|
|
" spread_mean = np.mean(memory.spread[window_slice])\n",
|
||
|
|
" spread_std = np.std(memory.spread[window_slice])\n",
|
||
|
|
" memory.zscore[c.i] = (memory.spread[c.i] - spread_mean) / spread_std\n",
|
||
|
|
" \n",
|
||
|
|
" # Check if any bound is crossed\n",
|
||
|
|
" # Since zscore is calculated using close, use zscore of the previous step\n",
|
||
|
|
" # This way we are executing signals defined at the previous bar\n",
|
||
|
|
" # Same logic as in PairTradingStrategy\n",
|
||
|
|
" if memory.zscore[c.i - 1] > params.upper and memory.status[0] != 1:\n",
|
||
|
|
" size[0] = -params.order_pct1\n",
|
||
|
|
" size[1] = params.order_pct2\n",
|
||
|
|
" \n",
|
||
|
|
" # Here we specify the order of execution\n",
|
||
|
|
" # call_seq_now defines order for the current group (2 elements)\n",
|
||
|
|
" c.call_seq_now[0] = 0\n",
|
||
|
|
" c.call_seq_now[1] = 1\n",
|
||
|
|
" memory.status[0] = 1\n",
|
||
|
|
" elif memory.zscore[c.i - 1] < params.lower and memory.status[0] != 2:\n",
|
||
|
|
" size[0] = params.order_pct1\n",
|
||
|
|
" size[1] = -params.order_pct2\n",
|
||
|
|
" c.call_seq_now[0] = 1 # execute the second order first to release funds early\n",
|
||
|
|
" c.call_seq_now[1] = 0\n",
|
||
|
|
" memory.status[0] = 2\n",
|
||
|
|
" else:\n",
|
||
|
|
" size[0] = np.nan\n",
|
||
|
|
" size[1] = np.nan\n",
|
||
|
|
" \n",
|
||
|
|
" # Group value is converted to shares using previous close, just like in bt\n",
|
||
|
|
" # Note that last_val_price contains valuation price of all columns, not just the current pair\n",
|
||
|
|
" c.last_val_price[c.from_col] = c.close[c.i - 1, c.from_col]\n",
|
||
|
|
" c.last_val_price[c.from_col + 1] = c.close[c.i - 1, c.from_col + 1]\n",
|
||
|
|
" \n",
|
||
|
|
" return (size,)\n",
|
||
|
|
"\n",
|
||
|
|
"@njit\n",
|
||
|
|
"def order_func_nb(c, size, price, commperc):\n",
|
||
|
|
" \"\"\"Place an order (= element within group and row).\"\"\"\n",
|
||
|
|
" \n",
|
||
|
|
" # Get column index within group (if group starts at column 58 and current column is 59, \n",
|
||
|
|
" # the column within group is 1, which can be used to get size)\n",
|
||
|
|
" group_col = c.col - c.from_col\n",
|
||
|
|
" return portfolio_nb.order_nb(\n",
|
||
|
|
" size=size[group_col], \n",
|
||
|
|
" price=price[c.i, c.col],\n",
|
||
|
|
" size_type=SizeType.TargetPercent,\n",
|
||
|
|
" fees=commperc\n",
|
||
|
|
" )"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 33,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"def simulate_from_order_func():\n",
|
||
|
|
" \"\"\"Simulate using `Portfolio.from_order_func`.\"\"\"\n",
|
||
|
|
" return vbt.Portfolio.from_order_func(\n",
|
||
|
|
" vbt_close_price,\n",
|
||
|
|
" order_func_nb, \n",
|
||
|
|
" vbt_open_price.values, COMMPERC, # *args for order_func_nb\n",
|
||
|
|
" pre_group_func_nb=pre_group_func_nb, \n",
|
||
|
|
" pre_group_args=(PERIOD, UPPER, LOWER, ORDER_PCT1, ORDER_PCT2),\n",
|
||
|
|
" pre_segment_func_nb=pre_segment_func_nb, \n",
|
||
|
|
" pre_segment_args=(MODE,),\n",
|
||
|
|
" fill_pos_record=False, # a bit faster\n",
|
||
|
|
" init_cash=CASH,\n",
|
||
|
|
" cash_sharing=True, \n",
|
||
|
|
" group_by=True,\n",
|
||
|
|
" freq='d'\n",
|
||
|
|
" )\n",
|
||
|
|
"\n",
|
||
|
|
"vbt_pf2 = simulate_from_order_func()"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 34,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
" Order Id Column Timestamp Size Price Fees Side\n",
|
||
|
|
"0 0 PEP 2017-11-15 96.698241 103.316229 49.952488 Sell\n",
|
||
|
|
"1 1 KO 2017-11-15 238.385955 41.851497 49.884046 Buy\n",
|
||
|
|
"2 2 KO 2018-04-12 490.647327 39.652521 97.277017 Sell\n",
|
||
|
|
"3 3 PEP 2018-04-12 198.056953 98.739352 97.780076 Buy\n",
|
||
|
|
"4 4 PEP 2018-09-04 198.781680 102.263047 101.640101 Sell\n",
|
||
|
|
"5 5 KO 2018-09-04 498.975917 40.477607 100.986755 Buy\n",
|
||
|
|
"6 6 KO 2018-10-03 482.284086 42.524342 102.544068 Sell\n",
|
||
|
|
"7 7 PEP 2018-10-03 197.454665 100.702245 99.420640 Buy\n",
|
||
|
|
"8 8 PEP 2018-12-03 189.202087 111.094282 105.096350 Sell\n",
|
||
|
|
"9 9 KO 2018-12-03 451.211817 46.006429 103.793222 Buy\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"print(vbt_pf2.orders.records_readable)"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 35,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"# Proof that both bt and vbt produce the same result\n",
|
||
|
|
"pd.testing.assert_series_equal(bt_cash, vbt_pf2.cash().rename('cash'))\n",
|
||
|
|
"pd.testing.assert_series_equal(bt_value, vbt_pf2.value().rename('value'))"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 36,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"4.4 ms ± 17.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# How fast is vbt?\n",
|
||
|
|
"%timeit simulate_from_order_func()"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"### Numba paradise (or hell?) - fastest"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 38,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"def simulate_nb_from_order_func():\n",
|
||
|
|
" \"\"\"Simulate using `simulate_nb`.\"\"\"\n",
|
||
|
|
" # iterate over 502 rows and 2 columns, each element is a potential order\n",
|
||
|
|
" target_shape = vbt_close_price.shape\n",
|
||
|
|
" \n",
|
||
|
|
" # number of columns in the group - exactly two\n",
|
||
|
|
" group_lens = np.array([2])\n",
|
||
|
|
" \n",
|
||
|
|
" # build default call sequence (orders are executed from the left to the right column)\n",
|
||
|
|
" call_seq = portfolio_nb.build_call_seq(target_shape, group_lens)\n",
|
||
|
|
" \n",
|
||
|
|
" # initial cash per group\n",
|
||
|
|
" init_cash = np.array([CASH], dtype=np.float64)\n",
|
||
|
|
" \n",
|
||
|
|
" order_records, log_records = portfolio_nb.simulate_nb(\n",
|
||
|
|
" target_shape=target_shape, \n",
|
||
|
|
" group_lens=group_lens,\n",
|
||
|
|
" init_cash=init_cash,\n",
|
||
|
|
" cash_sharing=True,\n",
|
||
|
|
" call_seq=call_seq, \n",
|
||
|
|
" segment_mask=np.full(target_shape, True), # used for disabling some segments\n",
|
||
|
|
" pre_group_func_nb=pre_group_func_nb, \n",
|
||
|
|
" pre_group_args=(PERIOD, UPPER, LOWER, ORDER_PCT1, ORDER_PCT2),\n",
|
||
|
|
" pre_segment_func_nb=pre_segment_func_nb, \n",
|
||
|
|
" pre_segment_args=(MODE,),\n",
|
||
|
|
" order_func_nb=order_func_nb, \n",
|
||
|
|
" order_args=(vbt_open_price.values, COMMPERC),\n",
|
||
|
|
" close=vbt_close_price.values, # used for target percentage, but we override the valuation price\n",
|
||
|
|
" fill_pos_record=False\n",
|
||
|
|
" )\n",
|
||
|
|
" \n",
|
||
|
|
" return target_shape, group_lens, call_seq, init_cash, order_records, log_records\n",
|
||
|
|
"\n",
|
||
|
|
"target_shape, group_lens, call_seq, init_cash, order_records, log_records = simulate_nb_from_order_func()"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 39,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
" Order Id Column Timestamp Size Price Fees Side\n",
|
||
|
|
"0 0 PEP 2017-11-15 96.698241 103.316229 49.952488 Sell\n",
|
||
|
|
"1 1 KO 2017-11-15 238.385955 41.851497 49.884046 Buy\n",
|
||
|
|
"2 2 KO 2018-04-12 490.647327 39.652521 97.277017 Sell\n",
|
||
|
|
"3 3 PEP 2018-04-12 198.056953 98.739352 97.780076 Buy\n",
|
||
|
|
"4 4 PEP 2018-09-04 198.781680 102.263047 101.640101 Sell\n",
|
||
|
|
"5 5 KO 2018-09-04 498.975917 40.477607 100.986755 Buy\n",
|
||
|
|
"6 6 KO 2018-10-03 482.284086 42.524342 102.544068 Sell\n",
|
||
|
|
"7 7 PEP 2018-10-03 197.454665 100.702245 99.420640 Buy\n",
|
||
|
|
"8 8 PEP 2018-12-03 189.202087 111.094282 105.096350 Sell\n",
|
||
|
|
"9 9 KO 2018-12-03 451.211817 46.006429 103.793222 Buy\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# Print order records in a readable format\n",
|
||
|
|
"print(vbt.Orders(vbt_close_price.vbt.wrapper, order_records, vbt_close_price).records_readable)"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 40,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"# Proof that both bt and vbt produce the same cash\n",
|
||
|
|
"from vectorbt.records import nb as records_nb\n",
|
||
|
|
"\n",
|
||
|
|
"col_map = records_nb.col_map_nb(order_records['col'], target_shape[1])\n",
|
||
|
|
"cash_flow = portfolio_nb.cash_flow_nb(target_shape, order_records, col_map, False)\n",
|
||
|
|
"cash_flow_grouped = portfolio_nb.cash_flow_grouped_nb(cash_flow, group_lens)\n",
|
||
|
|
"cash_grouped = portfolio_nb.cash_grouped_nb(target_shape, cash_flow_grouped, group_lens, init_cash)\n",
|
||
|
|
"\n",
|
||
|
|
"pd.testing.assert_series_equal(bt_cash, bt_cash.vbt.wrapper.wrap(cash_grouped))"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 42,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"# Proof that both bt and vbt produce the same value\n",
|
||
|
|
"asset_flow = portfolio_nb.asset_flow_nb(target_shape, order_records, col_map, Direction.Both)\n",
|
||
|
|
"assets = portfolio_nb.assets_nb(asset_flow)\n",
|
||
|
|
"asset_value = portfolio_nb.asset_value_nb(vbt_close_price.values, assets)\n",
|
||
|
|
"asset_value_grouped = portfolio_nb.asset_value_grouped_nb(asset_value, group_lens)\n",
|
||
|
|
"value = portfolio_nb.value_nb(cash_grouped, asset_value_grouped)\n",
|
||
|
|
"\n",
|
||
|
|
"pd.testing.assert_series_equal(bt_value, bt_value.vbt.wrapper.wrap(value))"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 43,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"Start 2017-01-03 00:00:00\n",
|
||
|
|
"End 2018-12-31 00:00:00\n",
|
||
|
|
"Period 502 days 00:00:00\n",
|
||
|
|
"Start Value 100000.0\n",
|
||
|
|
"End Value 100284.081822\n",
|
||
|
|
"Total Return [%] 0.284082\n",
|
||
|
|
"Benchmark Return [%] 16.631282\n",
|
||
|
|
"Max Gross Exposure [%] 0.915879\n",
|
||
|
|
"Total Fees Paid 908.374763\n",
|
||
|
|
"Max Drawdown [%] 1.030291\n",
|
||
|
|
"Max Drawdown Duration 168 days 00:00:00\n",
|
||
|
|
"Total Trades 10\n",
|
||
|
|
"Total Closed Trades 8\n",
|
||
|
|
"Total Open Trades 2\n",
|
||
|
|
"Open Trade PnL 149.652774\n",
|
||
|
|
"Win Rate [%] 62.5\n",
|
||
|
|
"Best Trade [%] 9.267971\n",
|
||
|
|
"Worst Trade [%] -9.229397\n",
|
||
|
|
"Avg Winning Trade [%] 3.967201\n",
|
||
|
|
"Avg Losing Trade [%] -6.182852\n",
|
||
|
|
"Avg Winning Trade Duration 56 days 19:12:00\n",
|
||
|
|
"Avg Losing Trade Duration 80 days 16:00:00\n",
|
||
|
|
"Profit Factor 1.072464\n",
|
||
|
|
"Expectancy 16.803631\n",
|
||
|
|
"Sharpe Ratio 0.185035\n",
|
||
|
|
"Calmar Ratio 0.200403\n",
|
||
|
|
"Omega Ratio 1.036058\n",
|
||
|
|
"Sortino Ratio 0.266979\n",
|
||
|
|
"Name: group, dtype: object\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# To produce more complex metrics such as stats, it's advisable to use Portfolio,\n",
|
||
|
|
"# which can be easily constructed from the arguments and outputs of simulate_nb\n",
|
||
|
|
"vbt_pf3 = vbt.Portfolio(\n",
|
||
|
|
" wrapper=vbt_close_price.vbt(freq='d', group_by=True).wrapper, \n",
|
||
|
|
" close=vbt_close_price, \n",
|
||
|
|
" order_records=order_records, \n",
|
||
|
|
" log_records=log_records, \n",
|
||
|
|
" init_cash=init_cash,\n",
|
||
|
|
" cash_sharing=True, \n",
|
||
|
|
" call_seq=call_seq\n",
|
||
|
|
")\n",
|
||
|
|
"\n",
|
||
|
|
"print(vbt_pf3.stats())"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 44,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"2.3 ms ± 9.23 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# How fast is vbt?\n",
|
||
|
|
"%timeit simulate_nb_from_order_func()"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"As you can see, writing Numba isn't straightforward and requires at least intermediate knowledge of NumPy. That's why Portfolio.from_orders and other class methods based on arrays are usually a good starting point."
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"### Multiple parameters"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"Now, why waste all energy to port a strategy to vectorbt? Right, for hyperparameter optimization.\n",
|
||
|
|
"\n",
|
||
|
|
"*The example below is just for demo purposes, usually brute-forcing many combinations on a single data sample easily leads to overfitting.*"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 45,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"periods = np.arange(10, 105, 5)\n",
|
||
|
|
"uppers = np.arange(1.5, 2.2, 0.1)\n",
|
||
|
|
"lowers = -1 * np.arange(1.5, 2.2, 0.1)"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 46,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"def simulate_mult_from_order_func(periods, uppers, lowers):\n",
|
||
|
|
" \"\"\"Simulate multiple parameter combinations using `Portfolio.from_order_func`.\"\"\"\n",
|
||
|
|
" # Build param grid\n",
|
||
|
|
" param_product = vbt.utils.params.create_param_product([periods, uppers, lowers])\n",
|
||
|
|
" param_tuples = list(zip(*param_product))\n",
|
||
|
|
" param_columns = pd.MultiIndex.from_tuples(param_tuples, names=['period', 'upper', 'lower'])\n",
|
||
|
|
" \n",
|
||
|
|
" # We need two price columns per param combination\n",
|
||
|
|
" vbt_close_price_mult = vbt_close_price.vbt.tile(len(param_columns), keys=param_columns)\n",
|
||
|
|
" vbt_open_price_mult = vbt_open_price.vbt.tile(len(param_columns), keys=param_columns)\n",
|
||
|
|
" \n",
|
||
|
|
" return vbt.Portfolio.from_order_func(\n",
|
||
|
|
" vbt_close_price_mult,\n",
|
||
|
|
" order_func_nb, \n",
|
||
|
|
" vbt_open_price_mult.values, COMMPERC, # *args for order_func_nb\n",
|
||
|
|
" pre_group_func_nb=pre_group_func_nb, \n",
|
||
|
|
" pre_group_args=(\n",
|
||
|
|
" np.array(param_product[0]), \n",
|
||
|
|
" np.array(param_product[1]), \n",
|
||
|
|
" np.array(param_product[2]), \n",
|
||
|
|
" ORDER_PCT1, \n",
|
||
|
|
" ORDER_PCT2\n",
|
||
|
|
" ),\n",
|
||
|
|
" pre_segment_func_nb=pre_segment_func_nb, \n",
|
||
|
|
" pre_segment_args=(MODE,),\n",
|
||
|
|
" fill_pos_record=False,\n",
|
||
|
|
" init_cash=CASH,\n",
|
||
|
|
" cash_sharing=True, \n",
|
||
|
|
" group_by=param_columns.names,\n",
|
||
|
|
" freq='d'\n",
|
||
|
|
" )\n",
|
||
|
|
"\n",
|
||
|
|
"vbt_pf_mult = simulate_mult_from_order_func(periods, uppers, lowers)"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 47,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"period upper lower\n",
|
||
|
|
"10 1.6 -1.5 -0.068466\n",
|
||
|
|
" 1.5 -1.5 -0.067897\n",
|
||
|
|
" 1.8 -1.5 -0.066885\n",
|
||
|
|
" 1.5 -1.6 -0.065260\n",
|
||
|
|
" 1.7 -1.5 -0.065036\n",
|
||
|
|
" ... \n",
|
||
|
|
"100 2.0 -2.2 0.003538\n",
|
||
|
|
" 1.9 -2.2 0.003538\n",
|
||
|
|
" 2.1 -2.2 0.003538\n",
|
||
|
|
" 1.8 -2.2 0.003538\n",
|
||
|
|
" 2.2 -2.2 0.003538\n",
|
||
|
|
"Name: total_return, Length: 1216, dtype: float64\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"data": {
|
||
|
|
"image/svg+xml": [
|
||
|
|
"<svg class=\"main-svg\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"700\" height=\"350\" style=\"\" viewBox=\"0 0 700 350\"><rect x=\"0\" y=\"0\" width=\"700\" height=\"350\" style=\"fill: rgb(255, 255, 255); fill-opacity: 1;\"/><defs id=\"defs-8b96f9\"><g class=\"clips\"><clipPath id=\"clip8b96f9xyplot\" class=\"plotclip\"><rect width=\"634\" height=\"274\"/></clipPath><clipPath class=\"axesclip\" id=\"clip8b96f9x\"><rect x=\"36\" y=\"0\" width=\"634\" height=\"350\"/></clipPath><clipPath class=\"axesclip\" id=\"clip8b96f9y\"><rect x=\"0\" y=\"46\" width=\"700\" height=\"274\"/></clipPath><clipPath class=\"axesclip\" id=\"clip8b96f9xy\"><rect x=\"36\" y=\"46\" width=\"634\" height=\"274\"/></clipPath></g><g class=\"gradients\"/><g class=\"patterns\"/></defs><g class=\"bglayer\"><rect class=\"bg\" x=\"36\" y=\"46\" width=\"634\" height=\"274\" style=\"fill: rgb(229, 236, 246); fill-opacity: 1; stroke-width: 0;\"/></g><g class=\"layer-below\"><g class=\"imagelayer\"/><g class=\"shapelayer\"/></g><g class=\"cartesianlayer\"><g class=\"subplot xy\"><g class=\"layer-subplot\"><g class=\"shapelayer\"/><g class=\"imagelayer\"/></g><g class=\"gridlayer\"><g class=\"x\"/><g class=\"y\"><path class=\"ygrid crisp\" transform=\"translate(0,273.1)\" d=\"M36,0h634\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"ygrid crisp\" transform=\"translate(0,226.2)\" d=\"M36,0h634\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"ygrid crisp\" transform=\"translate(0,179.3)\" d=\"M36,0h634\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"ygrid crisp\" transform=\"translate(0,132.4)\" d=\"M36,0h634\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/><path class=\"ygrid crisp\" transform=\"translate(0,85.5)\" d=\"M36,0h634\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;\"/></g></g><g class=\"zerolinelayer\"><path class=\"yzl zl crisp\" transform=\"translate(0,320)\" d=\"M36,0h634\" style=\"stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 2px;\"/></g><path class=\"xlines-below\"/><path class=\"ylines-below\"/><g class=\"overlines-below\"/><g class=\"xaxislayer-below\"/><g class=\"yaxislayer-below\"/><g class=\"overaxes-below\"/><g class=\"plot\" transform=\"translate(36,46)\" clip-path=\"url(#clip8b96f9xyplot)\"><g class=\"barlayer mlayer\"><g class=\"trace bars\" shape-rendering=\"crispEdges\" style=\"opacity: 1;\"><g class=\"points\"><g class=\"point\"><path d=\"M0,274V271.65H17.14V274Z\" style=\"vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;\"/></g><g class=\"point\"><path d=\"M17.14,274V269.31H34.27V274Z\" style=\"vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;\"/></g><g class=\"point\"><path d=\"M34.27,274V266.96H51.41V274Z\" style=\"vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;\"/></g><g class=\"point\"><path d=\"M51.41,274V264.62H68.54V274Z\" style=\"vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;\"/></g><g class=\"point\"><path d=\"M68.54,274V269.31H85.68V274Z\" style=\"vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;\"/></g><g class=\"point\"><path d=\"M85.68,274V271.65H102.81V274Z\" style=\"vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;\"/></g><g class=\"point\"><path d=\"M102.81,274V259.93H119.95V274Z\" style=\"vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;\"/></g><g class=\"point\"><path d=\"M119.95,274V259.93H137.08V274Z\" style=\"vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;\"/></g><g class=\"point\"><path
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"metadata": {},
|
||
|
|
"output_type": "display_data"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"print(vbt_pf_mult.total_return().sort_values())\n",
|
||
|
|
"\n",
|
||
|
|
"vbt_pf_mult.total_return().vbt.histplot().show_svg()"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 48,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"2.15 s ± 15.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# How fast is vbt?\n",
|
||
|
|
"%timeit simulate_mult_from_order_func(periods, uppers, lowers)"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"Even though the strategy is profitable on paper, the majority of hyperparameter combinations yield a loss, so finding a proper \"slice\" of hyperparameters is just a question of luck when relying on a single backtest. Thanks to vectorbt, we can do thousands of tests in seconds to validate our strategy - the same would last hours using conventional libraries."
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": null,
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": []
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"metadata": {
|
||
|
|
"kernelspec": {
|
||
|
|
"display_name": "Python 3 (ipykernel)",
|
||
|
|
"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.3"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"nbformat": 4,
|
||
|
|
"nbformat_minor": 4
|
||
|
|
}
|