{ "cells": [ { "cell_type": "markdown", "source": [ "# \"Beating the market with the simple possible predictive metric.\"\n", "\n", "This is an implementation of algorithm from https://www.reddit.com/r/algotrading/comments/mtp8b5/beating_the_market_with_the_simple_possible/ using [universal-portfolios](https://github.com/Marigold/universal-portfolios) package.\n", "\n", "Note that this is just a demonstration how to use the package and replicate the results, it's not meant as a real analysis and it's likely wrong." ], "metadata": {} }, { "cell_type": "code", "execution_count": 1, "source": [ "# some init stuff\n", "%matplotlib inline\n", "%load_ext autoreload\n", "%autoreload 2\n", "%config InlineBackend.figure_format = 'svg'\n", "\n", "import numpy as np\n", "import pandas as pd\n", "import seaborn as sns\n", "import datetime as dt\n", "import matplotlib.pyplot as plt\n", "\n", "sns.set_context(\"notebook\")\n", "plt.rcParams[\"figure.figsize\"] = (16, 8)" ], "outputs": [], "metadata": {} }, { "cell_type": "markdown", "source": [ "# Get data" ], "metadata": {} }, { "cell_type": "code", "execution_count": 2, "source": [ "# nasdaq 100 as of 2021-08-18\n", "nasdaq100 = [\n", "\"AAPL\",\n", "\"MSFT\",\n", "\"AMZN\",\n", "\"GOOG\",\n", "\"FB\",\n", "\"TSLA\",\n", "\"NVDA\",\n", "\"PYPL\",\n", "\"ADBE\",\n", "\"CMCSA\",\n", "\"CSCO\",\n", "\"NFLX\",\n", "\"PEP\",\n", "\"INTC\",\n", "\"COST\",\n", "\"AVGO\",\n", "\"TMUS\",\n", "\"TXN\",\n", "\"QCOM\",\n", "\"MRNA\",\n", "\"HON\",\n", "\"CHTR\",\n", "\"INTU\",\n", "\"SBUX\",\n", "\"AMGN\",\n", "\"AMD\",\n", "\"ISRG\",\n", "\"AMAT\",\n", "\"GILD\",\n", "\"ADP\",\n", "\"MDLZ\",\n", "\"MELI\",\n", "\"BKNG\",\n", "\"LRCX\",\n", "\"ZM\",\n", "\"MU\",\n", "\"CSX\",\n", "\"ILMN\",\n", "\"FISV\",\n", "\"ADSK\",\n", "\"REGN\",\n", "\"ATVI\",\n", "\"ASML\",\n", "\"ADI\",\n", "\"IDXX\",\n", "\"NXPI\",\n", "\"DOCU\",\n", "\"ALGN\",\n", "\"BIIB\",\n", "\"JD\",\n", "\"MNST\",\n", "\"VRTX\",\n", "\"EBAY\",\n", "\"KLAC\",\n", "\"DXCM\",\n", "\"KDP\",\n", "\"LULU\",\n", "\"MRVL\",\n", "\"EXC\",\n", "\"KHC\",\n", "\"TEAM\",\n", "\"AEP\",\n", "\"SNPS\",\n", "\"WDAY\",\n", "\"ROST\",\n", "\"WBA\",\n", "\"MAR\",\n", "\"PAYX\",\n", "\"ORLY\",\n", "\"CDNS\",\n", "\"CTAS\",\n", "\"CTSH\",\n", "\"EA\",\n", "\"MCHP\",\n", "\"XEL\",\n", "\"BIDU\",\n", "\"MTCH\",\n", "\"XLNX\",\n", "\"CPRT\",\n", "\"FAST\",\n", "\"VRSK\",\n", "\"ANSS\",\n", "\"PTON\",\n", "\"PDD\",\n", "\"SWKS\",\n", "\"SGEN\",\n", "\"OKTA\",\n", "\"PCAR\",\n", "\"CDW\",\n", "\"MXIM\",\n", "\"NTES\",\n", "\"SIRI\",\n", "\"CERN\",\n", "\"VRSN\",\n", "\"SPLK\",\n", "\"DLTR\",\n", "\"INCY\",\n", "\"CHKP\",\n", "\"TCOM\",\n", "\"FOX\",\n", "]\n", "\n", "import pandas_datareader.data as web\n", "S = []\n", "# loading data in chunks from yahoo is more robust than loading everything at once\n", "for i, chunk in enumerate(np.array_split(nasdaq100, 10)):\n", " print(f'Loading chunk {i}')\n", " # 2010-01-01 is arbitrary\n", " S.append(web.DataReader(chunk, 'yahoo', start='2010-01-01', end='2021-08-17')['Adj Close'])\n", " \n", "S = pd.concat(S, axis=1)" ], "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Loading chunk 0\n", "Loading chunk 1\n", "Loading chunk 2\n", "Loading chunk 3\n", "Loading chunk 4\n", "Loading chunk 5\n", "Loading chunk 6\n", "Loading chunk 7\n", "Loading chunk 8\n", "Loading chunk 9\n" ] } ], "metadata": {} }, { "cell_type": "markdown", "source": [ "# Construct weights for momentum & reversal strategies\n", "\n", "Momentum strategy goes long stock with the highest yesterday return, reversal goes long stock with the lowest yesterday return.\n", "\n", "Construct weight matrices for CRP \"algorithm\" that just allocates portfolio based on given weights." ], "metadata": {} }, { "cell_type": "code", "execution_count": 11, "source": [ "from universal.algos import CRP\n", "\n", "# find best performing and worst performing stock\n", "R = S / S.shift(1)\n", "highest_return_symbol = R.idxmax(axis=1).shift(1)\n", "lower_return_symbol = R.idxmin(axis=1).shift(1)\n", "\n", "# construct weights\n", "W_mom = S * 0\n", "for col in R.columns:\n", " W_mom.loc[highest_return_symbol == col, col] = 1\n", "\n", "W_rev = S * 0\n", "for col in R.columns:\n", " W_rev.loc[lower_return_symbol == col, col] = 1" ], "outputs": [], "metadata": {} }, { "cell_type": "code", "execution_count": 12, "source": [ "# keep graphs simple\n", "plot_kwargs = {\n", " \"logy\": True,\n", " \"assets\": False,\n", " \"weights\": False,\n", " \"ucrp\": True,\n", "}\n", "\n", "algo = CRP(W_mom)\n", "result = algo.run(S)\n", "print(result.summary())\n", "result.plot(**plot_kwargs, title='Momentum strategy')\n", "r_mom = result.r_log" ], "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Summary:\n", " Profit factor: 0.97\n", " Sharpe ratio: 0.06 ± 0.29\n", " Ulcer index: 0.05\n", " Information ratio (wrt UCRP): -0.49\n", " UCRP sharpe: 1.36 ± 0.41\n", " Appraisal ratio (wrt UCRP): -0.48 ± 0.29\n", " Beta / Alpha: 0.98 / -22.686%\n", " Annualized return: 3.12%\n", " Annualized volatility: 50.97%\n", " Longest drawdown: 2484 days\n", " Max drawdown: 89.01%\n", " Winning days: 49.0%\n", " Annual turnover: 466.4\n", " \n" ] }, { "output_type": "display_data", "data": { "image/svg+xml": "\n\n\n\n \n \n \n \n 2021-08-19T10:21:05.980623\n image/svg+xml\n \n \n Matplotlib v3.3.4, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" } } ], "metadata": {} }, { "cell_type": "code", "execution_count": 13, "source": [ "algo = CRP(W_rev)\n", "result = algo.run(S)\n", "print(result.summary())\n", "result.plot(**plot_kwargs, title='Reversal strategy')\n", "r_rev = result.r_log" ], "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Summary:\n", " Profit factor: 1.04\n", " Sharpe ratio: 0.45 ± 0.31\n", " Ulcer index: 0.59\n", " Information ratio (wrt UCRP): -0.04\n", " UCRP sharpe: 1.36 ± 0.41\n", " Appraisal ratio (wrt UCRP): -0.12 ± 0.29\n", " Beta / Alpha: 1.15 / -5.719%\n", " Annualized return: 24.41%\n", " Annualized volatility: 53.75%\n", " Longest drawdown: 1651 days\n", " Max drawdown: 79.64%\n", " Winning days: 51.4%\n", " Annual turnover: 495.8\n", " \n" ] }, { "output_type": "display_data", "data": { "image/svg+xml": "\n\n\n\n \n \n \n \n 2021-08-19T10:21:08.205296\n image/svg+xml\n \n \n Matplotlib v3.3.4, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" } } ], "metadata": {} }, { "cell_type": "markdown", "source": [ "# Combined strategy" ], "metadata": {} }, { "cell_type": "code", "execution_count": 17, "source": [ "# get recent performance, e.g. last 22 days\n", "PERIOD = 22\n", "performance = pd.DataFrame({\n", " 'mom': r_mom.rolling(PERIOD).mean(),\n", " 'rev': r_rev.rolling(PERIOD).mean(),\n", "})\n", "\n", "# shift performance by 1 day to avoid lookahead bias\n", "performance = performance.shift(1)\n", "\n", "# plot performance\n", "performance.plot()" ], "outputs": [ { "output_type": "execute_result", "data": { "text/plain": [ "" ] }, "metadata": {}, "execution_count": 17 }, { "output_type": "display_data", "data": { "image/svg+xml": "\n\n\n\n \n \n \n \n 2021-08-19T10:22:04.755929\n image/svg+xml\n \n \n Matplotlib v3.3.4, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" } } ], "metadata": {} }, { "cell_type": "code", "execution_count": 18, "source": [ "# construct new weight matrix from momentum and reversal\n", "mom_better = (performance.mom > performance.rev)\n", "rev_better = (performance.rev > performance.mom)\n", "W = W_mom.mul(mom_better, axis=0) + W_rev.mul(rev_better, axis=0)\n", "\n", "print(f'Momentum is better {mom_better.mean():.2%} of time')\n", "print(f'Reversal is better {rev_better.mean():.2%} of time')\n", "\n", "# fill the rest with uniform weights so that it's comparable to UCRP\n", "W.iloc[:PERIOD, :] = 1./100" ], "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Momentum is better 47.95% of time\n", "Reversal is better 51.30% of time\n" ] } ], "metadata": {} }, { "cell_type": "code", "execution_count": 19, "source": [ "# final combined strategy\n", "algo = CRP(W)\n", "result = algo.run(S)\n", "print(result.summary())\n", "result.plot(logy=True, assets=False, weights=False, ucrp=True, title='Combined strategy')" ], "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Summary:\n", " Profit factor: 1.06\n", " Sharpe ratio: 0.56 ± 0.32\n", " Ulcer index: 0.78\n", " Information ratio (wrt UCRP): 0.08\n", " UCRP sharpe: 1.36 ± 0.41\n", " Appraisal ratio (wrt UCRP): 0.01 ± 0.29\n", " Beta / Alpha: 1.14 / 0.258%\n", " Annualized return: 30.05%\n", " Annualized volatility: 53.40%\n", " Longest drawdown: 1234 days\n", " Max drawdown: 77.98%\n", " Winning days: 50.4%\n", " Annual turnover: 477.2\n", " \n" ] }, { "output_type": "execute_result", "data": { "text/plain": [ "[]" ] }, "metadata": {}, "execution_count": 19 }, { "output_type": "display_data", "data": { "image/svg+xml": "\n\n\n\n \n \n \n \n 2021-08-19T10:22:07.298471\n image/svg+xml\n \n \n Matplotlib v3.3.4, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" } } ], "metadata": {} }, { "cell_type": "markdown", "source": [ "# Summary\n", "\n", "Beating nasdaq in the last 10 years is IMHO nearly impossible. The post on reddit was published 4 months ago and goes approx 3 years into history. That corresponds to the right part of equity that goes straight up (still, its sharpe ratio is lower than that of Nasdaq 100). Period from 2010 to 2016 is disappointing. Has the strategy started working because of the rise of retail trading? Or is it simply data mining that doesn't work out-of-sample?\n", "\n", "Note that the above analysis is without fees which could be high because of high annual turnover." ], "metadata": {} } ], "metadata": { "interpreter": { "hash": "7612c6f8650389e88863a049f9a2cb6646507872268d41b62d879a8162923304" }, "kernelspec": { "name": "python3", "display_name": "Python 3.7.9 64-bit ('.venv': poetry)" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.9" } }, "nbformat": 4, "nbformat_minor": 5 }