{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "In this example, we will use a simple Dual Moving Average Crossover (DMAC, not to be confused with MACD) indicator as our entry and exit strategy. We will start with analyzing a single fast and slow window combination, and then move on to some advanced analysis such as building 2D heatmaps to compare multiple combinations and 3D cubes to see how they perform against time. Finally, we will explore how our DMAC strategy compares to holding Bitcoin and trading randomly." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import vectorbt as vbt" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "Collapsed": "false" }, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "from datetime import datetime, timedelta\n", "import pytz\n", "from dateutil.parser import parse\n", "import ipywidgets as widgets\n", "from copy import deepcopy\n", "from tqdm import tqdm\n", "import imageio\n", "from IPython import display\n", "import plotly.graph_objects as go\n", "import itertools\n", "import dateparser\n", "import gc" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "Collapsed": "false" }, "outputs": [], "source": [ "# Enter your parameters here\n", "seed = 42\n", "symbol = 'BTC-USD'\n", "metric = 'total_return'\n", "\n", "start_date = datetime(2018, 1, 1, tzinfo=pytz.utc) # time period for analysis, must be timezone-aware\n", "end_date = datetime(2020, 1, 1, tzinfo=pytz.utc)\n", "time_buffer = timedelta(days=100) # buffer before to pre-calculate SMA/EMA, best to set to max window\n", "freq = '1D'\n", "\n", "vbt.settings.portfolio['init_cash'] = 100. # 100$\n", "vbt.settings.portfolio['fees'] = 0.0025 # 0.25%\n", "vbt.settings.portfolio['slippage'] = 0.0025 # 0.25%" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(831, 5)\n", "Index(['Open', 'High', 'Low', 'Close', 'Volume'], dtype='object')\n" ] } ], "source": [ "# Download data with time buffer\n", "cols = ['Open', 'High', 'Low', 'Close', 'Volume']\n", "ohlcv_wbuf = vbt.YFData.download(symbol, start=start_date-time_buffer, end=end_date).get(cols)\n", "\n", "ohlcv_wbuf = ohlcv_wbuf.astype(np.float64)\n", " \n", "print(ohlcv_wbuf.shape)\n", "print(ohlcv_wbuf.columns)" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(731, 5)\n" ] } ], "source": [ "# Create a copy of data without time buffer\n", "wobuf_mask = (ohlcv_wbuf.index >= start_date) & (ohlcv_wbuf.index <= end_date) # mask without buffer\n", "\n", "ohlcv = ohlcv_wbuf.loc[wobuf_mask, :]\n", "\n", "print(ohlcv.shape)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "5k10k15k20kJan 2018Jul 2018Jan 2019Jul 2019Jan 2020010B20B30B40BOHLCVolume" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Plot the OHLC data\n", "ohlcv_wbuf.vbt.ohlcv.plot().show_svg() \n", "# remove show_svg() to display interactive chart!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Single window combination\n", "\n", "Perform a single test to see how our DMAC strategy compares to the hold strategy." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "fast_window = 30\n", "slow_window = 80" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(831,)\n", "(831,)\n" ] } ], "source": [ "# Pre-calculate running windows on data with time buffer\n", "fast_ma = vbt.MA.run(ohlcv_wbuf['Open'], fast_window)\n", "slow_ma = vbt.MA.run(ohlcv_wbuf['Open'], slow_window)\n", "\n", "print(fast_ma.ma.shape)\n", "print(slow_ma.ma.shape)" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(731,)\n", "(731,)\n" ] } ], "source": [ "# Remove time buffer\n", "fast_ma = fast_ma[wobuf_mask]\n", "slow_ma = slow_ma[wobuf_mask]\n", "\n", "# there should be no nans after removing time buffer\n", "assert(~fast_ma.ma.isnull().any()) \n", "assert(~slow_ma.ma.isnull().any())\n", "\n", "print(fast_ma.ma.shape)\n", "print(slow_ma.ma.shape)" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "# Generate crossover signals\n", "dmac_entries = fast_ma.ma_crossed_above(slow_ma)\n", "dmac_exits = fast_ma.ma_crossed_below(slow_ma)" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "Jan 2018Apr 2018Jul 2018Oct 2018Jan 2019Apr 2019Jul 2019Oct 2019Jan 20205k10k15kPriceFast MASlow MAEntryExit" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fig = ohlcv['Open'].vbt.plot(trace_kwargs=dict(name='Price'))\n", "fig = fast_ma.ma.vbt.plot(trace_kwargs=dict(name='Fast MA'), fig=fig)\n", "fig = slow_ma.ma.vbt.plot(trace_kwargs=dict(name='Slow MA'), fig=fig)\n", "fig = dmac_entries.vbt.signals.plot_as_entry_markers(ohlcv['Open'], fig=fig)\n", "fig = dmac_exits.vbt.signals.plot_as_exit_markers(ohlcv['Open'], fig=fig)\n", "\n", "fig.show_svg()" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Start 2018-01-01 00:00:00+00:00\n", "End 2020-01-01 00:00:00+00:00\n", "Period 731 days 00:00:00\n", "Total 3\n", "Rate [%] 0.410397\n", "Total Overlapping 0\n", "Overlapping Rate [%] 0.0\n", "First Index 2018-05-11 00:00:00+00:00\n", "Last Index 2019-03-03 00:00:00+00:00\n", "Norm Avg Index [-1, 1] -0.297717\n", "Distance -> Other: Min 25 days 00:00:00\n", "Distance -> Other: Max 184 days 00:00:00\n", "Distance -> Other: Mean 78 days 08:00:00\n", "Distance -> Other: Std 91 days 12:16:23.545375334\n", "Total Partitions 3\n", "Partition Rate [%] 100.0\n", "Partition Length: Min 1 days 00:00:00\n", "Partition Length: Max 1 days 00:00:00\n", "Partition Length: Mean 1 days 00:00:00\n", "Partition Length: Std 0 days 00:00:00\n", "Partition Distance: Min 83 days 00:00:00\n", "Partition Distance: Max 213 days 00:00:00\n", "Partition Distance: Mean 148 days 00:00:00\n", "Partition Distance: Std 91 days 22:10:23.366287302\n", "dtype: object\n" ] } ], "source": [ "# Signal stats\n", "print(dmac_entries.vbt.signals.stats(settings=dict(other=dmac_exits)))" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "Jan 2018Apr 2018Jul 2018Oct 2018Jan 2019Apr 2019Jul 2019Oct 2019Jan 2020falsetrueEntriesExits" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Plot signals\n", "fig = dmac_entries.vbt.signals.plot(trace_kwargs=dict(name='Entries'))\n", "dmac_exits.vbt.signals.plot(trace_kwargs=dict(name='Exits'), fig=fig).show_svg()" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Start 2018-01-01 00:00:00+00:00\n", "End 2020-01-01 00:00:00+00:00\n", "Period 731 days 00:00:00\n", "Start Value 100.0\n", "End Value 227.256402\n", "Total Return [%] 127.256402\n", "Benchmark Return [%] -47.27928\n", "Max Gross Exposure [%] 100.0\n", "Total Fees Paid 1.68223\n", "Max Drawdown [%] 29.194035\n", "Max Drawdown Duration 323 days 00:00:00\n", "Total Trades 3\n", "Total Closed Trades 3\n", "Total Open Trades 0\n", "Open Trade PnL 0.0\n", "Win Rate [%] 33.333333\n", "Best Trade [%] 173.824516\n", "Worst Trade [%] -10.494557\n", "Avg Winning Trade [%] 173.824516\n", "Avg Losing Trade [%] -8.834053\n", "Avg Winning Trade Duration 184 days 00:00:00\n", "Avg Losing Trade Duration 25 days 12:00:00\n", "Profit Factor 8.541137\n", "Expectancy 42.418801\n", "Sharpe Ratio 1.177409\n", "Calmar Ratio 1.735479\n", "Omega Ratio 1.37092\n", "Sortino Ratio 1.869101\n", "dtype: object\n" ] } ], "source": [ "# Build partfolio, which internally calculates the equity curve\n", "\n", "# Volume is set to np.inf by default to buy/sell everything\n", "# You don't have to pass freq here because our data is already perfectly time-indexed\n", "dmac_pf = vbt.Portfolio.from_signals(ohlcv['Close'], dmac_entries, dmac_exits)\n", "\n", "# Print stats\n", "print(dmac_pf.stats())" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " id col size entry_idx entry_price entry_fees exit_idx \\\n", "0 0 0 0.011787 130 8462.593960 0.249377 155 \n", "1 1 0 0.011773 213 7586.067777 0.223271 239 \n", "2 2 0 0.021499 426 3856.793721 0.207294 610 \n", "\n", " exit_price exit_fees pnl return direction status parent_id \n", "0 7614.675366 0.224390 -10.468386 -0.104946 0 1 0 \n", "1 7078.539086 0.208333 -6.406577 -0.071735 0 1 1 \n", "2 10596.981189 0.569565 144.131365 1.738245 0 1 2 \n" ] }, { "data": { "image/svg+xml": [ "Jan 2018Apr 2018Jul 2018Oct 2018Jan 2019Apr 2019Jul 2019Oct 2019Jan 20205k10k15kCloseEntryExit - ProfitExit - Loss" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Plot trades\n", "print(dmac_pf.trades.records)\n", "dmac_pf.trades.plot().show_svg()" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "Collapsed": "false" }, "outputs": [], "source": [ "# Now build portfolio for a \"Hold\" strategy\n", "# Here we buy once at the beginning and sell at the end\n", "hold_entries = pd.Series.vbt.signals.empty_like(dmac_entries)\n", "hold_entries.iloc[0] = True\n", "hold_exits = pd.Series.vbt.signals.empty_like(hold_entries)\n", "hold_exits.iloc[-1] = True\n", "hold_pf = vbt.Portfolio.from_signals(ohlcv['Close'], hold_entries, hold_exits)" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "Jan 2018Apr 2018Jul 2018Oct 2018Jan 2019Apr 2019Jul 2019Oct 2019Jan 202050100150200250Value (DMAC)Value (Hold)" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Equity\n", "fig = dmac_pf.value().vbt.plot(trace_kwargs=dict(name='Value (DMAC)'))\n", "hold_pf.value().vbt.plot(trace_kwargs=dict(name='Value (Hold)'), fig=fig).show_svg()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we will implement an interactive window slider to easily compare windows by their performance." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "min_window = 2\n", "max_window = 100" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "", "version_major": 2, "version_minor": 0 }, "text/plain": [ "VBox(children=(HBox(children=(Label(value='Fast and slow window:'), IntRangeSlider(value=(30, 80), layout=Layo…" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "perf_metrics = ['total_return', 'positions.win_rate', 'positions.expectancy', 'max_drawdown']\n", "perf_metric_names = ['Total return', 'Win rate', 'Expectancy', 'Max drawdown']\n", "\n", "windows_slider = widgets.IntRangeSlider(\n", " value=[fast_window, slow_window],\n", " min=min_window,\n", " max=max_window,\n", " step=1,\n", " layout=dict(width='500px'),\n", " continuous_update=True\n", ")\n", "dmac_fig = None\n", "dmac_img = widgets.Image(\n", " format='png',\n", " width=vbt.settings['plotting']['layout']['width'],\n", " height=vbt.settings['plotting']['layout']['height']\n", ")\n", "metrics_html = widgets.HTML()\n", "\n", "def on_value_change(value):\n", " global dmac_fig\n", " \n", " # Calculate portfolio\n", " fast_window, slow_window = value['new']\n", " fast_ma = vbt.MA.run(ohlcv_wbuf['Open'], fast_window)\n", " slow_ma = vbt.MA.run(ohlcv_wbuf['Open'], slow_window)\n", " fast_ma = fast_ma[wobuf_mask]\n", " slow_ma = slow_ma[wobuf_mask]\n", " dmac_entries = fast_ma.ma_crossed_above(slow_ma)\n", " dmac_exits = fast_ma.ma_crossed_below(slow_ma)\n", " dmac_pf = vbt.Portfolio.from_signals(ohlcv['Close'], dmac_entries, dmac_exits)\n", "\n", " # Update figure\n", " if dmac_fig is None:\n", " dmac_fig = ohlcv['Open'].vbt.plot(trace_kwargs=dict(name='Price'))\n", " fast_ma.ma.vbt.plot(trace_kwargs=dict(name='Fast MA'), fig=dmac_fig)\n", " slow_ma.ma.vbt.plot(trace_kwargs=dict(name='Slow MA'), fig=dmac_fig)\n", " dmac_entries.vbt.signals.plot_as_entry_markers(ohlcv['Open'], fig=dmac_fig)\n", " dmac_exits.vbt.signals.plot_as_exit_markers(ohlcv['Open'], fig=dmac_fig)\n", " else:\n", " with dmac_fig.batch_update():\n", " dmac_fig.data[1].y = fast_ma.ma\n", " dmac_fig.data[2].y = slow_ma.ma\n", " dmac_fig.data[3].x = ohlcv['Open'].index[dmac_entries]\n", " dmac_fig.data[3].y = ohlcv['Open'][dmac_entries]\n", " dmac_fig.data[4].x = ohlcv['Open'].index[dmac_exits]\n", " dmac_fig.data[4].y = ohlcv['Open'][dmac_exits]\n", " dmac_img.value = dmac_fig.to_image(format=\"png\")\n", " \n", " # Update metrics table\n", " sr = pd.Series([dmac_pf.deep_getattr(m) for m in perf_metrics], \n", " index=perf_metric_names, name='Performance')\n", " metrics_html.value = sr.to_frame().style.set_properties(**{'text-align': 'right'}).render()\n", " \n", "windows_slider.observe(on_value_change, names='value')\n", "on_value_change({'new': windows_slider.value})\n", "\n", "dashboard = widgets.VBox([\n", " widgets.HBox([widgets.Label('Fast and slow window:'), windows_slider]),\n", " dmac_img,\n", " metrics_html\n", "])\n", "dashboard" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "dashboard.close() # after using, release memory and notebook metadata" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1660" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "gc.collect()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Multiple window combinations\n", "\n", "Calculate the performance of each window combination in a vectorized way and display the results as a heatmap." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(831, 4851)\n", "(831, 4851)\n", "Int64Index([ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,\n", " ...\n", " 96, 96, 96, 96, 97, 97, 97, 98, 98, 99],\n", " dtype='int64', name='fast_ma_window', length=4851)\n", "Int64Index([ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,\n", " ...\n", " 97, 98, 99, 100, 98, 99, 100, 99, 100, 100],\n", " dtype='int64', name='slow_ma_window', length=4851)\n" ] } ], "source": [ "# Pre-calculate running windows on data with time buffer\n", "fast_ma, slow_ma = vbt.MA.run_combs(\n", " ohlcv_wbuf['Open'], np.arange(min_window, max_window+1), \n", " r=2, short_names=['fast_ma', 'slow_ma'])\n", "\n", "print(fast_ma.ma.shape)\n", "print(slow_ma.ma.shape)\n", "print(fast_ma.ma.columns)\n", "print(slow_ma.ma.columns)" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(731, 4851)\n", "(731, 4851)\n" ] } ], "source": [ "# Remove time buffer\n", "fast_ma = fast_ma[wobuf_mask]\n", "slow_ma = slow_ma[wobuf_mask]\n", "\n", "print(fast_ma.ma.shape)\n", "print(slow_ma.ma.shape)" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "MultiIndex([( 2, 3),\n", " ( 2, 4),\n", " ( 2, 5),\n", " ( 2, 6),\n", " ( 2, 7),\n", " ( 2, 8),\n", " ( 2, 9),\n", " ( 2, 10),\n", " ( 2, 11),\n", " ( 2, 12),\n", " ...\n", " (96, 97),\n", " (96, 98),\n", " (96, 99),\n", " (96, 100),\n", " (97, 98),\n", " (97, 99),\n", " (97, 100),\n", " (98, 99),\n", " (98, 100),\n", " (99, 100)],\n", " names=['fast_ma_window', 'slow_ma_window'], length=4851)\n" ] } ], "source": [ "# We perform the same steps, but now we have 4851 columns instead of 1\n", "# Each column corresponds to a pair of fast and slow windows\n", "# Generate crossover signals\n", "dmac_entries = fast_ma.ma_crossed_above(slow_ma)\n", "dmac_exits = fast_ma.ma_crossed_below(slow_ma)\n", "\n", "print(dmac_entries.columns) # the same for dmac_exits" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "# Build portfolio\n", "dmac_pf = vbt.Portfolio.from_signals(ohlcv['Close'], dmac_entries, dmac_exits)" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(4851,)\n", "MultiIndex([( 2, 3),\n", " ( 2, 4),\n", " ( 2, 5),\n", " ( 2, 6),\n", " ( 2, 7),\n", " ( 2, 8),\n", " ( 2, 9),\n", " ( 2, 10),\n", " ( 2, 11),\n", " ( 2, 12),\n", " ...\n", " (96, 97),\n", " (96, 98),\n", " (96, 99),\n", " (96, 100),\n", " (97, 98),\n", " (97, 99),\n", " (97, 100),\n", " (98, 99),\n", " (98, 100),\n", " (99, 100)],\n", " names=['fast_ma_window', 'slow_ma_window'], length=4851)\n" ] } ], "source": [ "# Calculate performance of each window combination\n", "dmac_perf = dmac_pf.deep_getattr(metric)\n", "\n", "print(dmac_perf.shape)\n", "print(dmac_perf.index)" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(38, 96)" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dmac_perf.idxmax() # your optimal window combination" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(99, 99)\n" ] } ], "source": [ "# Convert this array into a matrix of shape (99, 99): 99 fast windows x 99 slow windows\n", "dmac_perf_matrix = dmac_perf.vbt.unstack_to_df(symmetric=True, \n", " index_levels='fast_ma_window', column_levels='slow_ma_window')\n", "\n", "print(dmac_perf_matrix.shape)" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "20406080100102030405060708090100−0.500.511.52Slow windowFast window" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "dmac_perf_matrix.vbt.heatmap(\n", " xaxis_title='Slow window', \n", " yaxis_title='Fast window').show_svg()\n", "# remove show_svg() for interactivity" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we will implement an interactive date range slider to easily compare heatmaps over time." ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [], "source": [ "def dmac_pf_from_date_range(from_date, to_date):\n", " # Portfolio from MA crossover, filtered by time range\n", " range_mask = (ohlcv.index >= from_date) & (ohlcv.index <= to_date)\n", " range_fast_ma = fast_ma[range_mask] # use our variables defined above\n", " range_slow_ma = slow_ma[range_mask]\n", " dmac_entries = range_fast_ma.ma_crossed_above(range_slow_ma)\n", " dmac_exits = range_fast_ma.ma_crossed_below(range_slow_ma)\n", " dmac_pf = vbt.Portfolio.from_signals(ohlcv.loc[range_mask, 'Close'], dmac_entries, dmac_exits)\n", " return dmac_pf" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [], "source": [ "def rand_pf_from_date_range(from_date, to_date):\n", " # Portfolio from random strategy, filtered by time range\n", " range_mask = (ohlcv.index >= from_date) & (ohlcv.index <= to_date)\n", " range_fast_ma = fast_ma[range_mask] # use our variables defined above\n", " range_slow_ma = slow_ma[range_mask]\n", " dmac_entries = range_fast_ma.ma_crossed_above(range_slow_ma)\n", " dmac_exits = range_fast_ma.ma_crossed_below(range_slow_ma)\n", " rand_entries = dmac_entries.vbt.signals.shuffle(seed=seed) # same number of signals as in dmac\n", " rand_exits = rand_entries.vbt.signals.generate_random_exits(seed=seed)\n", " rand_pf = vbt.Portfolio.from_signals(ohlcv.loc[range_mask, 'Close'], rand_entries, rand_exits)\n", " return rand_pf" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [], "source": [ "def hold_pf_from_date_range(from_date, to_date):\n", " # Portfolio from holding strategy, filtered by time range\n", " range_mask = (ohlcv.index >= from_date) & (ohlcv.index <= to_date)\n", " hold_entries = pd.Series.vbt.signals.empty(range_mask.sum(), index=ohlcv[range_mask].index)\n", " hold_entries.iloc[0] = True\n", " hold_exits = pd.Series.vbt.signals.empty_like(hold_entries)\n", " hold_exits.iloc[-1] = True\n", " hold_pf = vbt.Portfolio.from_signals(ohlcv.loc[range_mask, 'Close'], hold_entries, hold_exits)\n", " return hold_pf" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "", "version_major": 2, "version_minor": 0 }, "text/plain": [ "VBox(children=(FigureWidget({\n", " 'data': [{'close': array([13657.20019531, 14982.09960938, 15201. , ..…" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# TimeSeries (OHLC)\n", "ts_fig = ohlcv.vbt.ohlcv.plot(\n", " title=symbol, \n", " show_volume=False,\n", " annotations=[dict(\n", " align='left',\n", " showarrow=False,\n", " xref='paper',\n", " yref='paper',\n", " x=0.5,\n", " y=0.9,\n", " font=dict(size=14),\n", " bordercolor='black',\n", " borderwidth=1,\n", " bgcolor='white'\n", " )],\n", " width=700, \n", " height=250)\n", "\n", "# Histogram (DMAC vs Random)\n", "histogram = vbt.plotting.Histogram(\n", " trace_names=['Random strategy', 'DMAC strategy'],\n", " title='%s distribution' % metric,\n", " xaxis_tickformat='%',\n", " annotations=[dict(\n", " y=0, \n", " xref='x', \n", " yref='paper', \n", " showarrow=True, \n", " arrowcolor=\"black\",\n", " arrowsize=1,\n", " arrowwidth=1,\n", " arrowhead=1,\n", " xanchor='left', \n", " text='Hold', \n", " textangle=0,\n", " font=dict(size=14),\n", " bordercolor='black',\n", " borderwidth=1,\n", " bgcolor='white',\n", " ax=0,\n", " ay=-50,\n", " )],\n", " width=700,\n", " height=250\n", ")\n", "\n", "# Heatmap (DMAC vs Holding)\n", "heatmap = vbt.plotting.Heatmap(\n", " x_labels=np.arange(min_window, max_window+1),\n", " y_labels=np.arange(min_window, max_window+1),\n", " trace_kwargs=dict(\n", " colorbar=dict(\n", " tickformat='%', \n", " ticks=\"outside\"\n", " ), \n", " colorscale='RdBu'),\n", " title='%s by window' % metric,\n", " width=650,\n", " height=420\n", ")\n", "\n", "dmac_perf_matrix = None\n", "rand_perf_matrix = None\n", "hold_value = None\n", "\n", "def update_heatmap_colorscale(perf_matrix):\n", " # Update heatmap colorscale based on performance matrix\n", " with heatmap.fig.batch_update():\n", " heatmap.fig.data[0].zmid = hold_value\n", " heatmap.fig.data[0].colorbar.tickvals = [\n", " np.nanmin(perf_matrix), \n", " hold_value, \n", " np.nanmax(perf_matrix)\n", " ]\n", " heatmap.fig.data[0].colorbar.ticktext = [\n", " 'Min: {:.0%}'.format(np.nanmin(perf_matrix)).ljust(12), \n", " 'Hold: {:.0%}'.format(hold_value).ljust(12), \n", " 'Max: {:.0%}'.format(np.nanmax(perf_matrix)).ljust(12)\n", " ]\n", " \n", "def update_histogram(dmac_perf_matrix, rand_perf_matrix, hold_value):\n", " # Update histogram figure\n", " with histogram.fig.batch_update():\n", " histogram.update(\n", " np.asarray([\n", " rand_perf_matrix.values.flatten(),\n", " dmac_perf_matrix.values.flatten()\n", " ]).transpose()\n", " )\n", " histogram.fig.layout.annotations[0].x = hold_value\n", "\n", "def update_figs(from_date, to_date):\n", " global dmac_perf_matrix, rand_perf_matrix, hold_value # needed for on_heatmap_change\n", " \n", " # Build portfolios\n", " dmac_pf = dmac_pf_from_date_range(from_date, to_date)\n", " rand_pf = rand_pf_from_date_range(from_date, to_date)\n", " hold_pf = hold_pf_from_date_range(from_date, to_date)\n", "\n", " # Calculate performance\n", " dmac_perf_matrix = dmac_pf.deep_getattr(metric)\n", " dmac_perf_matrix = dmac_perf_matrix.vbt.unstack_to_df(\n", " symmetric=True, index_levels='fast_ma_window', column_levels='slow_ma_window')\n", " rand_perf_matrix = rand_pf.deep_getattr(metric)\n", " rand_perf_matrix = rand_perf_matrix.vbt.unstack_to_df(\n", " symmetric=True, index_levels='fast_ma_window', column_levels='slow_ma_window')\n", " hold_value = hold_pf.deep_getattr(metric)\n", "\n", " # Update figures\n", " update_histogram(dmac_perf_matrix, rand_perf_matrix, hold_value)\n", " with ts_fig.batch_update():\n", " ts_fig.update_xaxes(range=(from_date, to_date))\n", " ts_fig.layout.annotations[0].text = 'Hold: %.f%%' % (hold_value * 100)\n", " with heatmap.fig.batch_update():\n", " heatmap.update(dmac_perf_matrix)\n", " update_heatmap_colorscale(dmac_perf_matrix.values)\n", "\n", "def on_ts_change(layout, x_range):\n", " global dmac_perf_matrix, rand_perf_matrix, hold_value # needed for on_heatmap_change\n", " \n", " if isinstance(x_range[0], str) and isinstance(x_range[1], str):\n", " update_figs(x_range[0], x_range[1])\n", "\n", "ts_fig.layout.on_change(on_ts_change, 'xaxis.range')\n", "\n", "def on_heatmap_change(layout, x_range, y_range):\n", " if dmac_perf_matrix is not None:\n", " x_mask = (dmac_perf_matrix.columns >= x_range[0]) & (dmac_perf_matrix.columns <= x_range[1])\n", " y_mask = (dmac_perf_matrix.index >= y_range[0]) & (dmac_perf_matrix.index <= y_range[1])\n", " if x_mask.any() and y_mask.any():\n", " # Update widgets\n", " sub_dmac_perf_matrix = dmac_perf_matrix.loc[y_mask, x_mask] # y_mask is index, x_mask is columns\n", " sub_rand_perf_matrix = rand_perf_matrix.loc[y_mask, x_mask]\n", " update_histogram(sub_dmac_perf_matrix, sub_rand_perf_matrix, hold_value)\n", " update_heatmap_colorscale(sub_dmac_perf_matrix.values)\n", " \n", "heatmap.fig.layout.on_change(on_heatmap_change, 'xaxis.range', 'yaxis.range')\n", "\n", "dashboard = widgets.VBox([\n", " ts_fig,\n", " histogram.fig,\n", " heatmap.fig\n", "])\n", "dashboard" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [], "source": [ "dashboard.close() # after using, release memory and notebook metadata" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Animate the whole thing as a GIF." ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "6477421947b84b1f9c0fb5210e48fef2", "version_major": 2, "version_minor": 0 }, "text/plain": [ " 0%| | 0/92 [00:00" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "def display_gif(fn):\n", " from IPython.display import Image, display\n", " \n", " with open(fn,'rb') as f:\n", " display(Image(data=f.read(), format='png'))\n", " \n", "display_gif(gif_fname)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Strategy comparison\n", "\n", "To compare multiple strategies, we need to find out their performance over multiple time ranges. To do this, we will roll a fixed date range over price, and calculate the performance matrix (think of the heatmap we played with above) for each time range. We will then stack those matrices together to form a cube. You can then easily reduce this cube along the time range axis to compare window combinations or along the both combination axes to compare strategies. The more time ranges you include, the more statistically significant are your findings, but also the more memory the calculation will occupy.\n", "\n", "This is similar to a GIF animation (stacks heatmaps into a cube) but in raw form and a fully vectorized way!" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [], "source": [ "ts_window = timedelta(days=365)\n", "ts_window_n = 50 # split the whole period into 50 (overlapping) time ranges" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(465, 50)\n", "Int64Index([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,\n", " 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,\n", " 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49],\n", " dtype='int64', name='split_idx')\n" ] } ], "source": [ "# Roll a window over price\n", "# Creates a matrix with columns being time series of length ts_window\n", "# We need buffer here, otherwise first elements will be NaNs\n", "open_roll_wbuf, split_indexes = ohlcv_wbuf['Open'].vbt.range_split(\n", " range_len=(ts_window + time_buffer).days, n=ts_window_n)\n", "close_roll_wbuf, _ = ohlcv_wbuf['Close'].vbt.range_split(\n", " range_len=(ts_window + time_buffer).days, n=ts_window_n)\n", "\n", "print(open_roll_wbuf.shape)\n", "print(open_roll_wbuf.columns)" ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(465, 242550)\n", "(465, 242550)\n", "MultiIndex([( 2, 0),\n", " ( 2, 1),\n", " ( 2, 2),\n", " ( 2, 3),\n", " ( 2, 4),\n", " ( 2, 5),\n", " ( 2, 6),\n", " ( 2, 7),\n", " ( 2, 8),\n", " ( 2, 9),\n", " ...\n", " (99, 40),\n", " (99, 41),\n", " (99, 42),\n", " (99, 43),\n", " (99, 44),\n", " (99, 45),\n", " (99, 46),\n", " (99, 47),\n", " (99, 48),\n", " (99, 49)],\n", " names=['fast_ma_window', 'split_idx'], length=242550)\n", "MultiIndex([( 3, 0),\n", " ( 3, 1),\n", " ( 3, 2),\n", " ( 3, 3),\n", " ( 3, 4),\n", " ( 3, 5),\n", " ( 3, 6),\n", " ( 3, 7),\n", " ( 3, 8),\n", " ( 3, 9),\n", " ...\n", " (100, 40),\n", " (100, 41),\n", " (100, 42),\n", " (100, 43),\n", " (100, 44),\n", " (100, 45),\n", " (100, 46),\n", " (100, 47),\n", " (100, 48),\n", " (100, 49)],\n", " names=['slow_ma_window', 'split_idx'], length=242550)\n" ] } ], "source": [ "# This will calculate moving averages for all date ranges and window combinations\n", "fast_ma_roll, slow_ma_roll = vbt.MA.run_combs(\n", " open_roll_wbuf, np.arange(min_window, max_window+1), \n", " r=2, short_names=['fast_ma', 'slow_ma'])\n", "\n", "print(fast_ma_roll.ma.shape)\n", "print(slow_ma_roll.ma.shape)\n", "print(fast_ma_roll.ma.columns)\n", "print(slow_ma_roll.ma.columns)" ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(365, 50)\n", "(365, 242550)\n", "(365, 242550)\n" ] } ], "source": [ "# Remove time buffer\n", "close_roll = close_roll_wbuf.iloc[time_buffer.days:]\n", "fast_ma_roll = fast_ma_roll.iloc[time_buffer.days:]\n", "slow_ma_roll = slow_ma_roll.iloc[time_buffer.days:]\n", "\n", "print(close_roll.shape)\n", "print(fast_ma_roll.ma.shape)\n", "print(slow_ma_roll.ma.shape)" ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(365, 242550)\n", "(365, 242550)\n", "MultiIndex([( 2, 3, 0),\n", " ( 2, 3, 1),\n", " ( 2, 3, 2),\n", " ( 2, 3, 3),\n", " ( 2, 3, 4),\n", " ( 2, 3, 5),\n", " ( 2, 3, 6),\n", " ( 2, 3, 7),\n", " ( 2, 3, 8),\n", " ( 2, 3, 9),\n", " ...\n", " (99, 100, 40),\n", " (99, 100, 41),\n", " (99, 100, 42),\n", " (99, 100, 43),\n", " (99, 100, 44),\n", " (99, 100, 45),\n", " (99, 100, 46),\n", " (99, 100, 47),\n", " (99, 100, 48),\n", " (99, 100, 49)],\n", " names=['fast_ma_window', 'slow_ma_window', 'split_idx'], length=242550)\n" ] } ], "source": [ "# Generate crossover signals\n", "dmac_entries_roll = fast_ma_roll.ma_crossed_above(slow_ma_roll)\n", "dmac_exits_roll = fast_ma_roll.ma_crossed_below(slow_ma_roll)\n", "\n", "print(dmac_entries_roll.shape)\n", "print(dmac_exits_roll.shape)\n", "print(dmac_entries_roll.columns)" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(242550,)\n", "MultiIndex([( 2, 3, 0),\n", " ( 2, 3, 1),\n", " ( 2, 3, 2),\n", " ( 2, 3, 3),\n", " ( 2, 3, 4),\n", " ( 2, 3, 5),\n", " ( 2, 3, 6),\n", " ( 2, 3, 7),\n", " ( 2, 3, 8),\n", " ( 2, 3, 9),\n", " ...\n", " (99, 100, 40),\n", " (99, 100, 41),\n", " (99, 100, 42),\n", " (99, 100, 43),\n", " (99, 100, 44),\n", " (99, 100, 45),\n", " (99, 100, 46),\n", " (99, 100, 47),\n", " (99, 100, 48),\n", " (99, 100, 49)],\n", " names=['fast_ma_window', 'slow_ma_window', 'split_idx'], length=242550)\n" ] } ], "source": [ "# Calculate the performance of the DMAC Strategy applied on rolled price\n", "# We need to specify freq here since our dataframes are not more indexed by time\n", "dmac_roll_pf = vbt.Portfolio.from_signals(close_roll, dmac_entries_roll, dmac_exits_roll, freq=freq)\n", "\n", "dmac_roll_perf = dmac_roll_pf.deep_getattr(metric)\n", "\n", "print(dmac_roll_perf.shape)\n", "print(dmac_roll_perf.index)" ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(98, 98, 50)\n" ] } ], "source": [ "# Unstack this array into a cube\n", "dmac_perf_cube = dmac_roll_perf.vbt.unstack_to_array(\n", " levels=('fast_ma_window', 'slow_ma_window', 'split_idx'))\n", "\n", "print(dmac_perf_cube.shape)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Apply your reducer on the cube." ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/Users/olegpolakow/miniconda3/lib/python3.7/site-packages/ipykernel_launcher.py:4: RuntimeWarning:\n", "\n", "Mean of empty slice\n", "\n" ] }, { "data": { "image/svg+xml": [ "20406080100102030405060708090100−1−0.500.51Slow windowFast window" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# For example, get mean performance for each window combination over all date ranges\n", "heatmap_index = dmac_roll_perf.index.levels[0]\n", "heatmap_columns = dmac_roll_perf.index.levels[1]\n", "heatmap_df = pd.DataFrame(np.nanmean(dmac_perf_cube, axis=2), index=heatmap_index, columns=heatmap_columns)\n", "heatmap_df = heatmap_df.vbt.make_symmetric()\n", "\n", "heatmap_df.vbt.heatmap(\n", " xaxis_title='Slow window', \n", " yaxis_title='Fast window', \n", " trace_kwargs=dict(zmid=0, colorscale='RdBu')).show_svg()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This image alone tells much more than the net profit heatmaps we plotted above!" ] }, { "cell_type": "code", "execution_count": 46, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "8 20 71 75\n" ] }, { "data": { "image/svg+xml": [ "−0.500.511.5205101520(10, 22)(73, 77)" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Or for example, compare a pair of window combinations using a histogram\n", "window_comb1 = (10, 22)\n", "window_comb2 = (73, 77)\n", "\n", "# Get index of each window in strat_cube\n", "fast1_idx = np.where(heatmap_df.index == window_comb1[0])[0][0]\n", "slow1_idx = np.where(heatmap_df.columns == window_comb1[1])[0][0]\n", "fast2_idx = np.where(heatmap_df.index == window_comb2[0])[0][0]\n", "slow2_idx = np.where(heatmap_df.columns == window_comb2[1])[0][0]\n", "\n", "print(fast1_idx, slow1_idx, fast2_idx, slow2_idx)\n", "\n", "dmac_comb1_perf = dmac_perf_cube[fast1_idx, slow1_idx, :]\n", "dmac_comb2_perf = dmac_perf_cube[fast2_idx, slow2_idx, :]\n", "\n", "pd.DataFrame({str(window_comb1): dmac_comb1_perf, str(window_comb2): dmac_comb2_perf}).vbt.histplot().show_svg()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now the fun part.\n", "\n", "Is our DMAC strategy really better than a simple random or hold strategy? To make any conclusions, we need to perform multiple tests over multiple time ranges (similar to how we compared window combinations earlier). And to make this experiment fair, all strategies must have the same number of tests. Moreover, the random strategy must have the same number of entry and exit signals as in our DMAC strategy." ] }, { "cell_type": "code", "execution_count": 47, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(242550,)\n", "MultiIndex([( 2, 3, 0),\n", " ( 2, 3, 1),\n", " ( 2, 3, 2),\n", " ( 2, 3, 3),\n", " ( 2, 3, 4),\n", " ( 2, 3, 5),\n", " ( 2, 3, 6),\n", " ( 2, 3, 7),\n", " ( 2, 3, 8),\n", " ( 2, 3, 9),\n", " ...\n", " (99, 100, 40),\n", " (99, 100, 41),\n", " (99, 100, 42),\n", " (99, 100, 43),\n", " (99, 100, 44),\n", " (99, 100, 45),\n", " (99, 100, 46),\n", " (99, 100, 47),\n", " (99, 100, 48),\n", " (99, 100, 49)],\n", " names=['fast_ma_window', 'slow_ma_window', 'split_idx'], length=242550)\n" ] } ], "source": [ "# Hold strategy applied on rolled price\n", "# Entry at the beginning, no exit\n", "hold_entries_roll = pd.DataFrame.vbt.signals.empty_like(dmac_entries_roll)\n", "hold_entries_roll.iloc[0] = True\n", "hold_exits_roll = pd.DataFrame.vbt.signals.empty_like(hold_entries_roll)\n", "hold_exits_roll.iloc[-1] = True\n", "\n", "hold_roll_pf = vbt.Portfolio.from_signals(close_roll, hold_entries_roll, hold_exits_roll, freq=freq)\n", "\n", "hold_roll_perf = hold_roll_pf.deep_getattr(metric)\n", "\n", "print(hold_roll_perf.shape)\n", "print(hold_roll_perf.index)" ] }, { "cell_type": "code", "execution_count": 48, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(242550,)\n", "MultiIndex([( 2, 3, 0),\n", " ( 2, 3, 1),\n", " ( 2, 3, 2),\n", " ( 2, 3, 3),\n", " ( 2, 3, 4),\n", " ( 2, 3, 5),\n", " ( 2, 3, 6),\n", " ( 2, 3, 7),\n", " ( 2, 3, 8),\n", " ( 2, 3, 9),\n", " ...\n", " (99, 100, 40),\n", " (99, 100, 41),\n", " (99, 100, 42),\n", " (99, 100, 43),\n", " (99, 100, 44),\n", " (99, 100, 45),\n", " (99, 100, 46),\n", " (99, 100, 47),\n", " (99, 100, 48),\n", " (99, 100, 49)],\n", " names=['fast_ma_window', 'slow_ma_window', 'split_idx'], length=242550)\n" ] } ], "source": [ "# Random strategy applied on rolled price\n", "# Shuffle entries to get random entry signals of the same shape and cardinality\n", "rand_entries_roll = dmac_entries_roll.vbt.signals.shuffle(seed=seed)\n", "\n", "# Must have the same number of signals\n", "pd.testing.assert_series_equal(rand_entries_roll.sum(axis=0), dmac_entries_roll.sum(axis=0))\n", "\n", "# If we generate exits the same way as entries, we will get multiple exits between entries\n", "# This is the correct way to generate ONLY ONE exit between two entries\n", "rand_exits_roll = rand_entries_roll.vbt.signals.generate_random_exits(seed=seed)\n", "\n", "rand_roll_pf = vbt.Portfolio.from_signals(close_roll, rand_entries_roll, rand_exits_roll, freq=freq)\n", "\n", "rand_roll_perf = rand_roll_pf.deep_getattr(metric)\n", "\n", "print(rand_roll_perf.shape)\n", "print(rand_roll_perf.index)" ] }, { "cell_type": "code", "execution_count": 49, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(0.6553703828556747, 0.12563595094523475, 0.09655358843403498)" ] }, "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dmac_roll_perf.mean(), hold_roll_perf.mean(), rand_roll_perf.mean()" ] }, { "cell_type": "code", "execution_count": 50, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "−0.500.511.522.53050k100k150k200k250kRandom StrategyHold StrategyDMAC Strategytotal_returnCumulative # of tests" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "pd.DataFrame({\n", " 'Random Strategy': rand_roll_perf, \n", " 'Hold Strategy': hold_roll_perf,\n", " 'DMAC Strategy': dmac_roll_perf, \n", "}).vbt.histplot(\n", " xaxis_title=metric,\n", " yaxis_title='Cumulative # of tests',\n", " trace_kwargs=dict(cumulative_enabled=True)).show_svg()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The lower the curve, the better.\n", "\n", "To get more insights, we should compare them over time." ] }, { "cell_type": "code", "execution_count": 51, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "010203040−0.500.511.5Random strategyHold strategyDMAC strategySplit indexMean total_return" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "pd.DataFrame({\n", " 'Random strategy': rand_roll_perf.groupby('split_idx').mean(), \n", " 'Hold strategy': hold_roll_perf.groupby('split_idx').mean(),\n", " 'DMAC strategy': dmac_roll_perf.groupby('split_idx').mean()\n", "}).vbt.plot(\n", " xaxis_title='Split index',\n", " yaxis_title='Mean %s' % metric).show_svg()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can go further and plot the above graph for each window combination." ] }, { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "", "version_major": 2, "version_minor": 0 }, "text/plain": [ "VBox(children=(HBox(children=(Label(value='Fast and slow window:'), IntRangeSlider(value=(30, 80), layout=Layo…" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "windows_slider = widgets.IntRangeSlider(\n", " value=(fast_window, slow_window),\n", " min=min_window,\n", " max=max_window,\n", " step=1,\n", " layout=dict(width='500px'),\n", " continuous_update=True\n", ")\n", "\n", "scatter = vbt.plotting.Scatter(\n", " trace_names=['Random strategy', 'Hold strategy', 'DMAC strategy'],\n", " x_labels=dmac_roll_perf.index.get_level_values('split_idx').unique(),\n", " xaxis_title='Split index',\n", " yaxis_title='Mean %s' % metric\n", ")\n", "scatter_img = widgets.Image(\n", " format='png',\n", " width=scatter.fig.layout.width,\n", " height=scatter.fig.layout.height\n", ")\n", "\n", "def on_value_change(value):\n", " fast_window, slow_window = value['new']\n", " \n", " # Build boolean mask that acts like a filter\n", " fast_windows = dmac_roll_perf.index.get_level_values('fast_ma_window')\n", " slow_windows = dmac_roll_perf.index.get_level_values('slow_ma_window')\n", " mask = (fast_windows == fast_window) & (slow_windows == slow_window)\n", " \n", " # Return raw data\n", " new_data = np.asarray([\n", " rand_roll_perf[mask].groupby('split_idx').mean(),\n", " hold_roll_perf[mask].groupby('split_idx').mean(),\n", " dmac_roll_perf[mask].groupby('split_idx').mean()\n", " ]).transpose()\n", "\n", " # Update figures\n", " scatter.update(new_data)\n", " scatter_img.value = scatter.fig.to_image(format=\"png\")\n", " \n", "windows_slider.observe(on_value_change, names='value')\n", "on_value_change({'new': windows_slider.value}) # default range\n", "\n", "dashboard = widgets.VBox([\n", " widgets.HBox([\n", " widgets.Label('Fast and slow window:'), \n", " windows_slider\n", " ]), \n", " scatter_img\n", "])\n", "dashboard" ] }, { "cell_type": "code", "execution_count": 53, "metadata": {}, "outputs": [], "source": [ "dashboard.close() # after using, release memory and notebook metadata" ] }, { "cell_type": "code", "execution_count": 54, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1405" ] }, "execution_count": 54, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import gc\n", "\n", "gc.collect()" ] }, { "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" }, "widgets": { "application/vnd.jupyter.widget-state+json": { "state": {}, "version_major": 2, "version_minor": 0 } } }, "nbformat": 4, "nbformat_minor": 4 }