├── README.md ├── .gitignore └── PlanB-quant_investing.ipynb /README.md: -------------------------------------------------------------------------------- 1 | # PlanB Quant Investing Implementation 2 | Python implementation of PlanB Quant Investing article: 3 | 4 | https://planbtc.com/20220807QuantInvesting101.pdf 5 | 6 | https://twitter.com/100trillionUSD/status/1556626501692526597 7 | 8 | ### Before you start... 9 | I strongly recommend using a Python virtual environment with every project you work on to avoid problems with Python and library versions. See [here](https://www.freecodecamp.org/news/how-to-manage-python-dependencies-using-virtual-environments/). 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Personal data 132 | config.yml 133 | data/*-all.csv 134 | data/*-all.csv 135 | data/*.parquet 136 | chromedriver* 137 | tests.ipynb 138 | *.log 139 | -------------------------------------------------------------------------------- /PlanB-quant_investing.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%load_ext autoreload\n", 10 | "%autoreload 2" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "## PlanB Quant Investing 101\n", 18 | "\n", 19 | "Python implementation of PlanB Quant Investing article: https://planbtc.com/20220807QuantInvesting101.pdf\n", 20 | "\n", 21 | "https://twitter.com/100trillionUSD/status/1556626501692526597" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "#import sys\n", 31 | "#!{sys.executable} -m pip install numpy==1.21.5\n", 32 | "#!{sys.executable} -m pip install pandas==1.3.5\n", 33 | "#!{sys.executable} -m pip install plotly==4.14.3\n", 34 | "#!{sys.executable} -m pip install bt==0.2.9\n", 35 | "#!{sys.executable} -m pip install coinmetrics-api-client==2022.7.14.6" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": null, 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "import pandas as pd\n", 45 | "import numpy as np\n", 46 | "import plotly.graph_objects as go\n", 47 | "from plotly.subplots import make_subplots\n", 48 | "from coinmetrics.api_client import CoinMetricsClient\n", 49 | "import bt" 50 | ] 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "metadata": {}, 55 | "source": [ 56 | "### Gather price data" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": null, 62 | "metadata": {}, 63 | "outputs": [], 64 | "source": [ 65 | "# Obtain metrics by specifying the asset, date interval and frequency.\n", 66 | "# All metrics available: https://docs.coinmetrics.io/info/metrics\n", 67 | "client = CoinMetricsClient()\n", 68 | "price = client.get_asset_metrics(\n", 69 | " assets='btc',\n", 70 | " metrics='PriceUSD',\n", 71 | " #start_time='2010-07-10',\n", 72 | " #end_time='2022-08-01',\n", 73 | " frequency='1d'\n", 74 | " )\n", 75 | "price = pd.DataFrame(price)\n", 76 | "price['time'] = pd.to_datetime(price['time'])\n", 77 | "price = price.set_index('time')\n", 78 | "price['PriceUSD'] = pd.to_numeric(price['PriceUSD'])" 79 | ] 80 | }, 81 | { 82 | "cell_type": "markdown", 83 | "metadata": {}, 84 | "source": [ 85 | "### Functions" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": null, 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "def get_planBTC_strategy(rsi_value, buy_first=True):\n", 95 | " \"\"\"\n", 96 | " https://planbtc.com/20220807QuantInvesting101.pdf\n", 97 | " * Data tested from January 2011\n", 98 | "\n", 99 | " BTC monthly closing data.\n", 100 | " IF (RSI was above 90% last six months AND drops below 65%) THEN sell,\n", 101 | " IF (RSI was below 50% last six months AND jumps +2% from the low) THEN buy, \n", 102 | " ELSE hold\n", 103 | " \"\"\"\n", 104 | " rsi_value.columns = ['value']\n", 105 | "\n", 106 | " # strategy params\n", 107 | " months_range = 6\n", 108 | " overbought_value = 90\n", 109 | " overbought_drop_value = 65\n", 110 | " oversold_value = 50\n", 111 | "\n", 112 | " signal = rsi_value.copy()\n", 113 | " long = False\n", 114 | " if buy_first:\n", 115 | " signal.iloc[0] = 1.0\n", 116 | " long = True\n", 117 | "\n", 118 | " for i in range(rsi_value.size):\n", 119 | " rsi = rsi_value.iloc[i]\n", 120 | " max_rsi_last_6_months = rsi_value[i-months_range:i+1].max()\n", 121 | " min_rsi_last_6_months = rsi_value[i-months_range:i+1].min()\n", 122 | " if i < 6:\n", 123 | " max_rsi_last_6_months = rsi_value[:i+1].max()\n", 124 | " min_rsi_last_6_months = rsi_value[:i+1].min()\n", 125 | "\n", 126 | " # SELL\n", 127 | " if (max_rsi_last_6_months >= overbought_value and \n", 128 | " rsi <= overbought_drop_value and \n", 129 | " long):\n", 130 | " signal.iloc[i] = -1.0\n", 131 | " long = False\n", 132 | " \n", 133 | " # BUY\n", 134 | " if (min_rsi_last_6_months <= oversold_value and \n", 135 | " rsi >= min_rsi_last_6_months + 2 and \n", 136 | " not long):\n", 137 | " signal.iloc[i] = 1.0\n", 138 | " long = True\n", 139 | "\n", 140 | " signal[(signal != 1.0) & (signal != -1.0)] = 0.0\n", 141 | "\n", 142 | " return signal\n", 143 | "\n", 144 | "def get_rsi(close_price, period = 14):\n", 145 | " \"\"\"\n", 146 | " Calculate RSI value.\n", 147 | " \"\"\"\n", 148 | " delta = close_price.diff()\n", 149 | "\n", 150 | " up = delta.copy()\n", 151 | " up[up < 0] = 0\n", 152 | " up = pd.Series.ewm(up, alpha=1/period).mean()\n", 153 | "\n", 154 | " down = delta.copy()\n", 155 | " down[down > 0] = 0\n", 156 | " down *= -1\n", 157 | " down = pd.Series.ewm(down, alpha=1/period).mean()\n", 158 | "\n", 159 | " rsi = np.where(up == 0, 0, np.where(down == 0, 100, 100 - (100 / (1 + up / down))))\n", 160 | "\n", 161 | " return np.round(rsi, 2)\n", 162 | "\n", 163 | "def convert_signal_to_weight(signal):\n", 164 | " \"\"\"\n", 165 | " Function to convert signal signals (1, -1) to bt weigh.\n", 166 | " \"\"\"\n", 167 | " return signal.replace(0, np.nan).replace(-1, 0).ffill().replace(np.nan, 0)\n", 168 | "\n", 169 | "def buy_and_hold_strategy(price_data, name='benchmark', _initial_capital=1000000.0):\n", 170 | " # Define the benchmark strategy\n", 171 | " s = bt.Strategy(name,\n", 172 | " [bt.algos.RunOnce(),\n", 173 | " bt.algos.SelectAll(),\n", 174 | " bt.algos.WeighEqually(),\n", 175 | " bt.algos.Rebalance()])\n", 176 | " # Return the backtest\n", 177 | " return bt.Backtest(strategy=s, data=price_data, integer_positions=False, initial_capital=_initial_capital)\n", 178 | "\n", 179 | "def signal_strategy(price_data, signal, name, _initial_capital=1000000.0):\n", 180 | " weight = convert_signal_to_weight(signal.copy())\n", 181 | "\n", 182 | " # the column names must be the same\n", 183 | " price_data.columns = ['value']\n", 184 | " weight.columns = ['value']\n", 185 | "\n", 186 | " s = bt.Strategy(name,\n", 187 | " [bt.algos.WeighTarget(weight), \n", 188 | " bt.algos.Rebalance()])\n", 189 | "\n", 190 | " return bt.Backtest(strategy=s, data=price_data, integer_positions=False, initial_capital=_initial_capital)" 191 | ] 192 | }, 193 | { 194 | "cell_type": "markdown", 195 | "metadata": {}, 196 | "source": [ 197 | "### Run Strategy" 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": null, 203 | "metadata": {}, 204 | "outputs": [], 205 | "source": [ 206 | "# PlanBTC RSI strategy\n", 207 | "\n", 208 | "# Resample data to 1 month\n", 209 | "df_planBTC = price.resample('1M').last()\n", 210 | "# remove incomplete candle\n", 211 | "df_planBTC = df_planBTC[:-1]\n", 212 | "\n", 213 | "df_planBTC['RSI'] = get_rsi(df_planBTC['PriceUSD'])\n", 214 | "df_planBTC = df_planBTC['2011-04-01':]\n", 215 | "df_planBTC.dropna(inplace=True)\n", 216 | "\n", 217 | "initial_value = df_planBTC['PriceUSD'][0]\n", 218 | "df_planBTC['Signal'] = get_planBTC_strategy(df_planBTC['RSI'].copy())\n", 219 | "\n", 220 | "# backtesting\n", 221 | "bt_plan_BTC = signal_strategy(df_planBTC[['PriceUSD']].copy(), df_planBTC[['Signal']].copy(), 'planBTC', initial_value)\n", 222 | "bt_buy_and_hold = buy_and_hold_strategy(df_planBTC[['PriceUSD']].copy(), 'buy_and_hold', initial_value)\n", 223 | "bt_results = bt.run(bt_buy_and_hold, bt_plan_BTC)" 224 | ] 225 | }, 226 | { 227 | "cell_type": "markdown", 228 | "metadata": {}, 229 | "source": [ 230 | "### Plot" 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": null, 236 | "metadata": {}, 237 | "outputs": [], 238 | "source": [ 239 | "# Plot Strategy\n", 240 | "fig = make_subplots(\n", 241 | " rows=2, cols=1, shared_xaxes=True,\n", 242 | " specs=[[{'secondary_y': True}],[{}]],\n", 243 | " vertical_spacing=0.01, \n", 244 | " row_heights=[0.7, 0.3])\n", 245 | "\n", 246 | "# Add traces\n", 247 | "# Price\n", 248 | "fig.add_trace(\n", 249 | " go.Scatter(x=df_planBTC.index, y=df_planBTC['PriceUSD'], name='Price', legendgroup = '1'),\n", 250 | " secondary_y=True)\n", 251 | "# RSI\n", 252 | "fig.add_trace(\n", 253 | " go.Scatter(x=df_planBTC.index, y=df_planBTC['RSI'], name='RSI', mode='markers+lines', line_color='purple', legendgroup = '1'),\n", 254 | " secondary_y=False)\n", 255 | "# RSI range 50-90\n", 256 | "fig.add_hrect(y0=50, y1=90, line_width=0, fillcolor='purple', opacity=0.2, secondary_y=False)\n", 257 | "# Buy signal\n", 258 | "fig.add_trace(go.Scatter(x=df_planBTC[df_planBTC['Signal'] == 1.0].index, \n", 259 | " y=df_planBTC[df_planBTC['Signal'] == 1.0]['PriceUSD'],\n", 260 | " name='Buy',\n", 261 | " legendgroup = '1',\n", 262 | " mode='markers',\n", 263 | " marker=dict(\n", 264 | " size=15, symbol='triangle-up', color='green')),\n", 265 | " secondary_y=True)\n", 266 | "# Sell Signal\n", 267 | "fig.add_trace(go.Scatter(x=df_planBTC[df_planBTC['Signal'] == -1.0].index, \n", 268 | " y=df_planBTC[df_planBTC['Signal'] == -1.0]['PriceUSD'],\n", 269 | " name='Sell',\n", 270 | " legendgroup = '1',\n", 271 | " mode='markers',\n", 272 | " marker=dict(\n", 273 | " size=15, symbol='triangle-down', color='red')),\n", 274 | " secondary_y=True)\n", 275 | "\n", 276 | "# Results\n", 277 | "fig.add_trace(go.Scatter(x=df_planBTC.index,\n", 278 | " y=bt_results._get_series(None).rebase()['planBTC'],\n", 279 | " #y=bt_results.prices['planBTC'],\n", 280 | " line=dict(color='red', width=2),\n", 281 | " name='PlanBTC',\n", 282 | " legendgroup = '2'), row=2, col=1)\n", 283 | "fig.add_trace(go.Scatter(x=df_planBTC.index,\n", 284 | " y=bt_results._get_series(None).rebase()['buy_and_hold'],\n", 285 | " #y=bt_results.prices['buy_and_hold'],\n", 286 | " line=dict(color='gray', width=2),\n", 287 | " name='Buy and Hold',\n", 288 | " legendgroup = '2'), row=2, col=1)\n", 289 | "\n", 290 | "# Add figure title\n", 291 | "fig.update_layout(\n", 292 | " title_text='PlanB@100TrillionUSD PlanBTC.com - Monthly Closing Data',\n", 293 | " width=1200, height=800,\n", 294 | " legend_tracegroupgap = 360,\n", 295 | " hovermode='x unified')\n", 296 | "\n", 297 | "# Set y-axes titles\n", 298 | "fig.update_yaxes(title_text='Price', type='log', secondary_y=True, row=1, col=1)\n", 299 | "fig.update_yaxes(title_text='RSI', secondary_y=False, row=1, col=1)\n", 300 | "fig.update_yaxes(title_text='Total returns - Log Scale', type='log', row=2, col=1)\n", 301 | "\n", 302 | "fig.show()" 303 | ] 304 | }, 305 | { 306 | "cell_type": "code", 307 | "execution_count": null, 308 | "metadata": {}, 309 | "outputs": [], 310 | "source": [ 311 | "bt_results.display()" 312 | ] 313 | }, 314 | { 315 | "cell_type": "code", 316 | "execution_count": null, 317 | "metadata": {}, 318 | "outputs": [], 319 | "source": [ 320 | "bt_results.get_transactions(strategy_name='planBTC')" 321 | ] 322 | } 323 | ], 324 | "metadata": { 325 | "kernelspec": { 326 | "display_name": "Python 3.8.10 ('.venv': venv)", 327 | "language": "python", 328 | "name": "python3" 329 | }, 330 | "language_info": { 331 | "codemirror_mode": { 332 | "name": "ipython", 333 | "version": 3 334 | }, 335 | "file_extension": ".py", 336 | "mimetype": "text/x-python", 337 | "name": "python", 338 | "nbconvert_exporter": "python", 339 | "pygments_lexer": "ipython3", 340 | "version": "3.8.10" 341 | }, 342 | "orig_nbformat": 4, 343 | "vscode": { 344 | "interpreter": { 345 | "hash": "5d4f1688810693ea4ff903235294cdce42f2f8742427020a6ecf8c71220c6ab4" 346 | } 347 | } 348 | }, 349 | "nbformat": 4, 350 | "nbformat_minor": 2 351 | } 352 | --------------------------------------------------------------------------------