├── .qrignore ├── LICENSE.txt ├── README.md └── intro_moonshot ├── Introduction.ipynb ├── Part1-Historical-Data-Collection.ipynb ├── Part2-Universe-Selection.ipynb ├── Part3-Momentum-Factor-Research.ipynb ├── Part4-Moonshot-Strategy-Code.ipynb ├── Part5-Moonshot-Backtest.ipynb ├── Part6-Moonshot-Parameter-Scans.ipynb ├── Part7-Debug-Moonshot-Strategies.ipynb ├── __init__.py └── umd.py /.qrignore: -------------------------------------------------------------------------------- 1 | README.md 2 | LICENSE.txt 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 10 | 11 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 12 | 13 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 14 | 15 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 16 | 17 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 18 | 19 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 20 | 21 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 22 | 23 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 24 | 25 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 26 | 27 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 28 | 29 | 2. Grant of Copyright License. 30 | 31 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 32 | 33 | 3. Grant of Patent License. 34 | 35 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 36 | 37 | 4. Redistribution. 38 | 39 | You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 40 | 41 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 42 | You must cause any modified files to carry prominent notices stating that You changed the files; and 43 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 44 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 45 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 46 | 47 | 5. Submission of Contributions. 48 | 49 | Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 50 | 51 | 6. Trademarks. 52 | 53 | This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 54 | 55 | 7. Disclaimer of Warranty. 56 | 57 | Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 58 | 59 | 8. Limitation of Liability. 60 | 61 | In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 62 | 63 | 9. Accepting Warranty or Additional Liability. 64 | 65 | While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 66 | 67 | END OF TERMS AND CONDITIONS 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # moonshot-intro 2 | 3 | Introductory tutorial for Moonshot demonstrating data collection, universe selection, and backtesting of an end-of-day momentum strategy. Uses free sample data. 4 | 5 | ## Clone in QuantRocket 6 | 7 | CLI: 8 | 9 | ```shell 10 | quantrocket codeload clone 'moonshot-intro' 11 | ``` 12 | 13 | Python: 14 | 15 | ```python 16 | from quantrocket.codeload import clone 17 | clone("moonshot-intro") 18 | ``` 19 | 20 | ## Browse in GitHub 21 | 22 | Start here: [intro_moonshot/Introduction.ipynb](intro_moonshot/Introduction.ipynb) 23 | 24 | *** 25 | 26 | Find more code in QuantRocket's [Code Library](https://www.quantrocket.com/code/) 27 | -------------------------------------------------------------------------------- /intro_moonshot/Introduction.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\"QuantRocket
\n", 8 | "Disclaimer" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "# Getting Started with Moonshot\n", 16 | "\n", 17 | "This tutorial shows you how to retrieve daily historical data for equities, analyze your data in a research notebook, and backtest an end-of-day cross-sectional momentum strategy using Moonshot.\n", 18 | " \n", 19 | "**Data collection**\n", 20 | "\n", 21 | "* Part 1: [Historical Data Collection](Part1-Historical-Data-Collection.ipynb)\n", 22 | "* Part 2: [Universe Selection](Part2-Universe-Selection.ipynb)\n", 23 | "\n", 24 | "**Research and Backtesting**\n", 25 | "\n", 26 | "*Research*\n", 27 | "\n", 28 | "* Part 3: [Momentum Factor Research](Part3-Momentum-Factor-Research.ipynb)\n", 29 | "\n", 30 | "*Moonshot*\n", 31 | "\n", 32 | "* Part 4: [Moonshot Strategy Code](Part4-Moonshot-Strategy-Code.ipynb)\n", 33 | "* Part 5: [Moonshot Backtest](Part5-Moonshot-Backtest.ipynb)\n", 34 | "* Part 6: [Moonshot Parameter Scans](Part6-Moonshot-Parameter-Scans.ipynb)\n", 35 | "* Part 7: [Debug Moonshot Strategies](Part7-Debug-Moonshot-Strategies.ipynb)" 36 | ] 37 | } 38 | ], 39 | "metadata": { 40 | "kernelspec": { 41 | "display_name": "Python 3.11", 42 | "language": "python", 43 | "name": "python3" 44 | }, 45 | "language_info": { 46 | "codemirror_mode": { 47 | "name": "ipython", 48 | "version": 3 49 | }, 50 | "file_extension": ".py", 51 | "mimetype": "text/x-python", 52 | "name": "python", 53 | "nbconvert_exporter": "python", 54 | "pygments_lexer": "ipython3", 55 | "version": "3.11.0" 56 | } 57 | }, 58 | "nbformat": 4, 59 | "nbformat_minor": 4 60 | } 61 | -------------------------------------------------------------------------------- /intro_moonshot/Part1-Historical-Data-Collection.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\"QuantRocket
\n", 8 | "Disclaimer" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "***\n", 16 | "[Moonshot Intro](Introduction.ipynb) › Part 1: Data Collection\n", 17 | "***" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# Collect US Stock Data\n", 25 | "\n", 26 | "To collect end-of-day sample data for US stocks, create a database and specify `free=True`. We will name the database \"usstock-free-1d\":" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 1, 32 | "metadata": {}, 33 | "outputs": [ 34 | { 35 | "data": { 36 | "text/plain": [ 37 | "{'status': 'successfully created quantrocket.v2.history.usstock-free-1d.sqlite'}" 38 | ] 39 | }, 40 | "execution_count": 1, 41 | "metadata": {}, 42 | "output_type": "execute_result" 43 | } 44 | ], 45 | "source": [ 46 | "from quantrocket.history import create_usstock_db\n", 47 | "create_usstock_db(\"usstock-free-1d\", bar_size=\"1 day\", free=True)" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "This creates an empty database. Next, we need to fill up the database with data. Data collection runs asynchronously (that is, in the background), so it's a good idea to open a terminal for flightlog if you haven't already done so in order to monitor the progress. You can start a flightlog terminal from the Launcher menu or by executing the `%flightlog` magic command in a notebook:" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 2, 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "%flightlog" 64 | ] 65 | }, 66 | { 67 | "cell_type": "markdown", 68 | "metadata": {}, 69 | "source": [ 70 | "Then collect the data:" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": 3, 76 | "metadata": {}, 77 | "outputs": [ 78 | { 79 | "data": { 80 | "text/plain": [ 81 | "{'status': 'the historical data will be collected asynchronously'}" 82 | ] 83 | }, 84 | "execution_count": 3, 85 | "metadata": {}, 86 | "output_type": "execute_result" 87 | } 88 | ], 89 | "source": [ 90 | "from quantrocket.history import collect_history\n", 91 | "collect_history(\"usstock-free-1d\")" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "metadata": {}, 97 | "source": [ 98 | "Use flightlog to monitor the progress:\n", 99 | "\n", 100 | "```\n", 101 | "quantrocket.history: INFO [usstock-free-1d] Collecting FREE history from 2007 to present\n", 102 | "quantrocket.history: INFO [usstock-free-1d] Collecting updated FREE securities listings\n", 103 | "quantrocket.history: INFO [usstock-free-1d] Collecting additional FREE history from 2020-04 to present\n", 104 | "quantrocket.history: INFO [usstock-free-1d] Collected 160 monthly files in quantrocket.v2.history.usstock-free-1d.sqlite\n", 105 | "```" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "metadata": {}, 111 | "source": [ 112 | "***\n", 113 | "\n", 114 | "## *Next Up*\n", 115 | "\n", 116 | "Part 2: [Universe Selection](Part2-Universe-Selection.ipynb)" 117 | ] 118 | } 119 | ], 120 | "metadata": { 121 | "kernelspec": { 122 | "display_name": "Python 3.11", 123 | "language": "python", 124 | "name": "python3" 125 | }, 126 | "language_info": { 127 | "codemirror_mode": { 128 | "name": "ipython", 129 | "version": 3 130 | }, 131 | "file_extension": ".py", 132 | "mimetype": "text/x-python", 133 | "name": "python", 134 | "nbconvert_exporter": "python", 135 | "pygments_lexer": "ipython3", 136 | "version": "3.11.0" 137 | } 138 | }, 139 | "nbformat": 4, 140 | "nbformat_minor": 4 141 | } 142 | -------------------------------------------------------------------------------- /intro_moonshot/Part2-Universe-Selection.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\"QuantRocket
\n", 8 | "Disclaimer" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "***\n", 16 | "[Moonshot Intro](Introduction.ipynb) › Part 2: Universe Selection\n", 17 | "***" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# Define a universe\n", 25 | "\n", 26 | "QuantRocket relies heavily on the concept of universes, which are user-defined groupings of securities. Universes provide a convenient way to refer to and manipulate groups of securities when collecting historical data, running a trading strategy, etc. You can create universes based on exchanges, security types, sectors, liquidity, or any criteria you like. A universe could consist of one or two securities or thousands of securities." 27 | ] 28 | }, 29 | { 30 | "cell_type": "markdown", 31 | "metadata": {}, 32 | "source": [ 33 | "## Query securities\n", 34 | "To create our first universe, we will query securities from the securities master database, pare them down to a subset of securities, then upload the pared down securities to create our universe. Learn more about universes in the [usage guide](https:www.quantrocket.com/docs/#master).\n", 35 | "\n", 36 | "Security listings for our sample stocks were automatically collected during historical data collection. We query the stock listings from the securities master database using the `get_securities` function, which loads them into a pandas DataFrame:" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 1, 42 | "metadata": {}, 43 | "outputs": [ 44 | { 45 | "name": "stdout", 46 | "output_type": "stream", 47 | "text": [ 48 | "loaded 9 securities\n" 49 | ] 50 | }, 51 | { 52 | "data": { 53 | "text/html": [ 54 | "
\n", 55 | "\n", 68 | "\n", 69 | " \n", 70 | " \n", 71 | " \n", 72 | " \n", 73 | " \n", 74 | " \n", 75 | " \n", 76 | " \n", 77 | " \n", 78 | " \n", 79 | " \n", 80 | " \n", 81 | " \n", 82 | " \n", 83 | " \n", 84 | " \n", 85 | " \n", 86 | " \n", 87 | " \n", 88 | " \n", 89 | " \n", 90 | " \n", 91 | " \n", 92 | " \n", 93 | " \n", 94 | " \n", 95 | " \n", 96 | " \n", 97 | " \n", 98 | " \n", 99 | " \n", 100 | " \n", 101 | " \n", 102 | " \n", 103 | " \n", 104 | " \n", 105 | " \n", 106 | " \n", 107 | " \n", 108 | " \n", 109 | " \n", 110 | " \n", 111 | " \n", 112 | " \n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | " \n", 142 | " \n", 143 | " \n", 144 | " \n", 145 | " \n", 146 | " \n", 147 | " \n", 148 | " \n", 149 | " \n", 150 | " \n", 151 | " \n", 152 | " \n", 153 | " \n", 154 | " \n", 155 | " \n", 156 | " \n", 157 | " \n", 158 | " \n", 159 | " \n", 160 | " \n", 161 | " \n", 162 | " \n", 163 | " \n", 164 | " \n", 165 | " \n", 166 | " \n", 167 | " \n", 168 | " \n", 169 | " \n", 170 | " \n", 171 | " \n", 172 | " \n", 173 | " \n", 174 | " \n", 175 | " \n", 176 | " \n", 177 | " \n", 178 | " \n", 179 | " \n", 180 | " \n", 181 | " \n", 182 | " \n", 183 | " \n", 184 | " \n", 185 | " \n", 186 | " \n", 187 | " \n", 188 | " \n", 189 | " \n", 190 | " \n", 191 | " \n", 192 | "
SymbolExchangeCountryCurrencySecTypeEtfTimezoneNamePriceMagnifierMultiplierDelistedDateDelistedLastTradeDateRolloverDate
Sid
FIBBG000B9XRY4AAPLXNASUSUSDSTKFalseAmerica/New_YorkAPPLE INC11FalseNaTNaTNaT
FIBBG000BDTBL9SPYARCXUSUSDSTKTrueAmerica/New_YorkSPDR S&P 500 ETF TRUST11FalseNaTNaTNaT
FIBBG000BFWKC0MONXNYSUSUSDSTKFalseAmerica/New_YorkMONSANTO CO11True2018-06-06NaTNaT
FIBBG000BKZB36HDXNYSUSUSDSTKFalseAmerica/New_YorkHOME DEPOT INC11FalseNaTNaTNaT
FIBBG000BMHYD1JNJXNYSUSUSDSTKFalseAmerica/New_YorkJOHNSON & JOHNSON11FalseNaTNaTNaT
\n", 193 | "
" 194 | ], 195 | "text/plain": [ 196 | " Symbol Exchange Country ... DateDelisted LastTradeDate RolloverDate\n", 197 | "Sid ... \n", 198 | "FIBBG000B9XRY4 AAPL XNAS US ... NaT NaT NaT\n", 199 | "FIBBG000BDTBL9 SPY ARCX US ... NaT NaT NaT\n", 200 | "FIBBG000BFWKC0 MON XNYS US ... 2018-06-06 NaT NaT\n", 201 | "FIBBG000BKZB36 HD XNYS US ... NaT NaT NaT\n", 202 | "FIBBG000BMHYD1 JNJ XNYS US ... NaT NaT NaT\n", 203 | "\n", 204 | "[5 rows x 14 columns]" 205 | ] 206 | }, 207 | "execution_count": 1, 208 | "metadata": {}, 209 | "output_type": "execute_result" 210 | } 211 | ], 212 | "source": [ 213 | "from quantrocket.master import get_securities\n", 214 | "securities = get_securities(vendors=\"usstock\")\n", 215 | "print(f\"loaded {len(securities)} securities\")\n", 216 | "securities.head()" 217 | ] 218 | }, 219 | { 220 | "cell_type": "markdown", 221 | "metadata": {}, 222 | "source": [ 223 | "Note the `Sid` index in the DataFrame: Sid is short for \"security ID\" and is the unique identifier for a particular security or contract. Sids are used throughout QuantRocket to refer to securities." 224 | ] 225 | }, 226 | { 227 | "cell_type": "markdown", 228 | "metadata": {}, 229 | "source": [ 230 | "If the `usstock-free-1d` database is the first historical database you've collected, your securities DataFrame will contain 9 rows of sample tickers: AAPL, HD, JNJ, MSFT, SPY, MON, KKD, XOM, and AA. However, if you previously collected the learning bundle, then querying the securities master database may have returned many thousands of rows. If that is the case, you can execute the following cell to load only the 9 sample symbols, which will keep you aligned with this tutorial. Instead of querying everything in the securities master database, the following code first uses `list_sids(...)` to obtain the list of sids that is present in the `usstock-free-1d` history database that we collected in the previous tutorial, then uses `get_securities(...)` to retrieve only those securities from the securities master database. " 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": 2, 236 | "metadata": {}, 237 | "outputs": [ 238 | { 239 | "name": "stdout", 240 | "output_type": "stream", 241 | "text": [ 242 | "loaded 9 securities\n" 243 | ] 244 | }, 245 | { 246 | "data": { 247 | "text/html": [ 248 | "
\n", 249 | "\n", 262 | "\n", 263 | " \n", 264 | " \n", 265 | " \n", 266 | " \n", 267 | " \n", 268 | " \n", 269 | " \n", 270 | " \n", 271 | " \n", 272 | " \n", 273 | " \n", 274 | " \n", 275 | " \n", 276 | " \n", 277 | " \n", 278 | " \n", 279 | " \n", 280 | " \n", 281 | " \n", 282 | " \n", 283 | " \n", 284 | " \n", 285 | " \n", 286 | " \n", 287 | " \n", 288 | " \n", 289 | " \n", 290 | " \n", 291 | " \n", 292 | " \n", 293 | " \n", 294 | " \n", 295 | " \n", 296 | " \n", 297 | " \n", 298 | " \n", 299 | " \n", 300 | " \n", 301 | " \n", 302 | " \n", 303 | " \n", 304 | " \n", 305 | " \n", 306 | " \n", 307 | " \n", 308 | " \n", 309 | " \n", 310 | " \n", 311 | " \n", 312 | " \n", 313 | " \n", 314 | " \n", 315 | " \n", 316 | " \n", 317 | " \n", 318 | " \n", 319 | " \n", 320 | " \n", 321 | " \n", 322 | " \n", 323 | " \n", 324 | " \n", 325 | " \n", 326 | " \n", 327 | " \n", 328 | " \n", 329 | " \n", 330 | " \n", 331 | " \n", 332 | " \n", 333 | " \n", 334 | " \n", 335 | " \n", 336 | " \n", 337 | " \n", 338 | " \n", 339 | " \n", 340 | " \n", 341 | " \n", 342 | " \n", 343 | " \n", 344 | " \n", 345 | " \n", 346 | " \n", 347 | " \n", 348 | " \n", 349 | " \n", 350 | " \n", 351 | " \n", 352 | " \n", 353 | " \n", 354 | " \n", 355 | " \n", 356 | " \n", 357 | " \n", 358 | " \n", 359 | " \n", 360 | " \n", 361 | " \n", 362 | " \n", 363 | " \n", 364 | " \n", 365 | " \n", 366 | " \n", 367 | " \n", 368 | " \n", 369 | " \n", 370 | " \n", 371 | " \n", 372 | " \n", 373 | " \n", 374 | " \n", 375 | " \n", 376 | " \n", 377 | " \n", 378 | " \n", 379 | " \n", 380 | " \n", 381 | " \n", 382 | " \n", 383 | " \n", 384 | " \n", 385 | " \n", 386 | "
SymbolExchangeCountryCurrencySecTypeEtfTimezoneNamePriceMagnifierMultiplierDelistedDateDelistedLastTradeDateRolloverDate
Sid
FIBBG000B9XRY4AAPLXNASUSUSDSTKFalseAmerica/New_YorkAPPLE INC11FalseNaTNaTNaT
FIBBG000BDTBL9SPYARCXUSUSDSTKTrueAmerica/New_YorkSPDR S&P 500 ETF TRUST11FalseNaTNaTNaT
FIBBG000BFWKC0MONXNYSUSUSDSTKFalseAmerica/New_YorkMONSANTO CO11True2018-06-06NaTNaT
FIBBG000BKZB36HDXNYSUSUSDSTKFalseAmerica/New_YorkHOME DEPOT INC11FalseNaTNaTNaT
FIBBG000BMHYD1JNJXNYSUSUSDSTKFalseAmerica/New_YorkJOHNSON & JOHNSON11FalseNaTNaTNaT
\n", 387 | "
" 388 | ], 389 | "text/plain": [ 390 | " Symbol Exchange Country ... DateDelisted LastTradeDate RolloverDate\n", 391 | "Sid ... \n", 392 | "FIBBG000B9XRY4 AAPL XNAS US ... NaT NaT NaT\n", 393 | "FIBBG000BDTBL9 SPY ARCX US ... NaT NaT NaT\n", 394 | "FIBBG000BFWKC0 MON XNYS US ... 2018-06-06 NaT NaT\n", 395 | "FIBBG000BKZB36 HD XNYS US ... NaT NaT NaT\n", 396 | "FIBBG000BMHYD1 JNJ XNYS US ... NaT NaT NaT\n", 397 | "\n", 398 | "[5 rows x 14 columns]" 399 | ] 400 | }, 401 | "execution_count": 2, 402 | "metadata": {}, 403 | "output_type": "execute_result" 404 | } 405 | ], 406 | "source": [ 407 | "from quantrocket.history import list_sids\n", 408 | "free_sids = list_sids(\"usstock-free-1d\")\n", 409 | "securities = get_securities(sids=free_sids)\n", 410 | "print(f\"loaded {len(securities)} securities\")\n", 411 | "securities.head()" 412 | ] 413 | }, 414 | { 415 | "cell_type": "markdown", 416 | "metadata": {}, 417 | "source": [ 418 | "## Filter securities\n", 419 | "\n", 420 | "Our strategy only targets stocks, so before creating our universe, we will filter the securities DataFrame to exclude ETFs (in our case, SPY is the only ETF in our sample data):" 421 | ] 422 | }, 423 | { 424 | "cell_type": "code", 425 | "execution_count": 3, 426 | "metadata": {}, 427 | "outputs": [ 428 | { 429 | "name": "stdout", 430 | "output_type": "stream", 431 | "text": [ 432 | "filtered to 8 securities\n" 433 | ] 434 | }, 435 | { 436 | "data": { 437 | "text/html": [ 438 | "
\n", 439 | "\n", 452 | "\n", 453 | " \n", 454 | " \n", 455 | " \n", 456 | " \n", 457 | " \n", 458 | " \n", 459 | " \n", 460 | " \n", 461 | " \n", 462 | " \n", 463 | " \n", 464 | " \n", 465 | " \n", 466 | " \n", 467 | " \n", 468 | " \n", 469 | " \n", 470 | " \n", 471 | " \n", 472 | " \n", 473 | " \n", 474 | " \n", 475 | " \n", 476 | " \n", 477 | " \n", 478 | " \n", 479 | " \n", 480 | " \n", 481 | " \n", 482 | " \n", 483 | " \n", 484 | " \n", 485 | " \n", 486 | " \n", 487 | " \n", 488 | " \n", 489 | " \n", 490 | " \n", 491 | " \n", 492 | " \n", 493 | " \n", 494 | " \n", 495 | " \n", 496 | " \n", 497 | " \n", 498 | " \n", 499 | " \n", 500 | " \n", 501 | " \n", 502 | " \n", 503 | " \n", 504 | " \n", 505 | " \n", 506 | " \n", 507 | " \n", 508 | " \n", 509 | " \n", 510 | " \n", 511 | " \n", 512 | " \n", 513 | " \n", 514 | " \n", 515 | " \n", 516 | " \n", 517 | " \n", 518 | " \n", 519 | " \n", 520 | " \n", 521 | " \n", 522 | " \n", 523 | " \n", 524 | " \n", 525 | " \n", 526 | " \n", 527 | " \n", 528 | " \n", 529 | " \n", 530 | " \n", 531 | " \n", 532 | " \n", 533 | " \n", 534 | " \n", 535 | " \n", 536 | " \n", 537 | " \n", 538 | " \n", 539 | " \n", 540 | " \n", 541 | " \n", 542 | " \n", 543 | " \n", 544 | " \n", 545 | " \n", 546 | " \n", 547 | " \n", 548 | " \n", 549 | " \n", 550 | " \n", 551 | " \n", 552 | " \n", 553 | " \n", 554 | " \n", 555 | " \n", 556 | " \n", 557 | " \n", 558 | " \n", 559 | " \n", 560 | " \n", 561 | " \n", 562 | " \n", 563 | " \n", 564 | " \n", 565 | " \n", 566 | " \n", 567 | " \n", 568 | " \n", 569 | " \n", 570 | " \n", 571 | " \n", 572 | " \n", 573 | " \n", 574 | " \n", 575 | " \n", 576 | "
SymbolExchangeCountryCurrencySecTypeEtfTimezoneNamePriceMagnifierMultiplierDelistedDateDelistedLastTradeDateRolloverDate
Sid
FIBBG000B9XRY4AAPLXNASUSUSDSTKFalseAmerica/New_YorkAPPLE INC11FalseNaTNaTNaT
FIBBG000BFWKC0MONXNYSUSUSDSTKFalseAmerica/New_YorkMONSANTO CO11True2018-06-06NaTNaT
FIBBG000BKZB36HDXNYSUSUSDSTKFalseAmerica/New_YorkHOME DEPOT INC11FalseNaTNaTNaT
FIBBG000BMHYD1JNJXNYSUSUSDSTKFalseAmerica/New_YorkJOHNSON & JOHNSON11FalseNaTNaTNaT
FIBBG000BPH459MSFTXNASUSUSDSTKFalseAmerica/New_YorkMICROSOFT CORP11FalseNaTNaTNaT
\n", 577 | "
" 578 | ], 579 | "text/plain": [ 580 | " Symbol Exchange Country ... DateDelisted LastTradeDate RolloverDate\n", 581 | "Sid ... \n", 582 | "FIBBG000B9XRY4 AAPL XNAS US ... NaT NaT NaT\n", 583 | "FIBBG000BFWKC0 MON XNYS US ... 2018-06-06 NaT NaT\n", 584 | "FIBBG000BKZB36 HD XNYS US ... NaT NaT NaT\n", 585 | "FIBBG000BMHYD1 JNJ XNYS US ... NaT NaT NaT\n", 586 | "FIBBG000BPH459 MSFT XNAS US ... NaT NaT NaT\n", 587 | "\n", 588 | "[5 rows x 14 columns]" 589 | ] 590 | }, 591 | "execution_count": 3, 592 | "metadata": {}, 593 | "output_type": "execute_result" 594 | } 595 | ], 596 | "source": [ 597 | "securities = securities[securities.Etf==False]\n", 598 | "print(f\"filtered to {len(securities)} securities\")\n", 599 | "securities.head()" 600 | ] 601 | }, 602 | { 603 | "cell_type": "markdown", 604 | "metadata": {}, 605 | "source": [ 606 | "## Create universe\n", 607 | "\n", 608 | "To create a universe consisting of these securities, we simply upload the list of sids to the `create_universe` function. We'll name the universe \"usstock-free\":" 609 | ] 610 | }, 611 | { 612 | "cell_type": "code", 613 | "execution_count": 4, 614 | "metadata": {}, 615 | "outputs": [ 616 | { 617 | "data": { 618 | "text/plain": [ 619 | "{'code': 'usstock-free', 'provided': 8, 'inserted': 8, 'total_after_insert': 8}" 620 | ] 621 | }, 622 | "execution_count": 4, 623 | "metadata": {}, 624 | "output_type": "execute_result" 625 | } 626 | ], 627 | "source": [ 628 | "from quantrocket.master import create_universe\n", 629 | "create_universe(\"usstock-free\", sids=securities.index.tolist())" 630 | ] 631 | }, 632 | { 633 | "cell_type": "markdown", 634 | "metadata": {}, 635 | "source": [ 636 | "The function output confirms the name and size of our new universe." 637 | ] 638 | }, 639 | { 640 | "cell_type": "markdown", 641 | "metadata": {}, 642 | "source": [ 643 | "***\n", 644 | "\n", 645 | "## *Next Up*\n", 646 | "\n", 647 | "Part 3: [Momentum Factor Research](Part3-Momentum-Factor-Research.ipynb)" 648 | ] 649 | } 650 | ], 651 | "metadata": { 652 | "kernelspec": { 653 | "display_name": "Python 3.11", 654 | "language": "python", 655 | "name": "python3" 656 | }, 657 | "language_info": { 658 | "codemirror_mode": { 659 | "name": "ipython", 660 | "version": 3 661 | }, 662 | "file_extension": ".py", 663 | "mimetype": "text/x-python", 664 | "name": "python", 665 | "nbconvert_exporter": "python", 666 | "pygments_lexer": "ipython3", 667 | "version": "3.11.0" 668 | } 669 | }, 670 | "nbformat": 4, 671 | "nbformat_minor": 4 672 | } 673 | -------------------------------------------------------------------------------- /intro_moonshot/Part3-Momentum-Factor-Research.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\"QuantRocket
\n", 8 | "Disclaimer" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "***\n", 16 | "[Moonshot Intro](Introduction.ipynb) › Part 3: Momentum Factor Research\n", 17 | "***" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# Researching the Momentum Factor" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "Momentum investing says that excess returns can be generated by buying recent winners and selling recent losers. In this notebook we will research the momentum factor on our universe of demo stocks. This will help us determine whether we have a profitable idea before turning to a full backtest. " 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "First, load your historical data into pandas." 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 1, 44 | "metadata": {}, 45 | "outputs": [ 46 | { 47 | "data": { 48 | "text/html": [ 49 | "
\n", 50 | "\n", 63 | "\n", 64 | " \n", 65 | " \n", 66 | " \n", 67 | " \n", 68 | " \n", 69 | " \n", 70 | " \n", 71 | " \n", 72 | " \n", 73 | " \n", 74 | " \n", 75 | " \n", 76 | " \n", 77 | " \n", 78 | " \n", 79 | " \n", 80 | " \n", 81 | " \n", 82 | " \n", 83 | " \n", 84 | " \n", 85 | " \n", 86 | " \n", 87 | " \n", 88 | " \n", 89 | " \n", 90 | " \n", 91 | " \n", 92 | " \n", 93 | " \n", 94 | " \n", 95 | " \n", 96 | " \n", 97 | " \n", 98 | " \n", 99 | " \n", 100 | " \n", 101 | " \n", 102 | " \n", 103 | " \n", 104 | " \n", 105 | " \n", 106 | " \n", 107 | " \n", 108 | " \n", 109 | " \n", 110 | " \n", 111 | " \n", 112 | " \n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | " \n", 139 | " \n", 140 | " \n", 141 | "
SidFIBBG000B9XRY4FIBBG000BFWKC0FIBBG000BKZB36FIBBG000BMHYD1FIBBG000BPH459FIBBG000GZQ728FIBBG00B3T3HD3
FieldDate
Close2018-01-0240.6197116.4411161.6012117.502780.080962.625653.8066
2018-01-0340.6126116.9167162.4434118.625180.453663.855553.1531
2018-01-0440.8013117.3845163.7326118.616781.161763.943953.3482
2018-01-0541.2658118.0514165.4429119.595782.168063.892352.7533
2018-01-0841.1125118.5491165.0475119.747682.251864.179653.6408
\n", 142 | "
" 143 | ], 144 | "text/plain": [ 145 | "Sid FIBBG000B9XRY4 ... FIBBG00B3T3HD3\n", 146 | "Field Date ... \n", 147 | "Close 2018-01-02 40.6197 ... 53.8066\n", 148 | " 2018-01-03 40.6126 ... 53.1531\n", 149 | " 2018-01-04 40.8013 ... 53.3482\n", 150 | " 2018-01-05 41.2658 ... 52.7533\n", 151 | " 2018-01-08 41.1125 ... 53.6408\n", 152 | "\n", 153 | "[5 rows x 7 columns]" 154 | ] 155 | }, 156 | "execution_count": 1, 157 | "metadata": {}, 158 | "output_type": "execute_result" 159 | } 160 | ], 161 | "source": [ 162 | "from quantrocket import get_prices\n", 163 | "prices = get_prices(\"usstock-free-1d\", universes=\"usstock-free\", start_date=\"2018-01-01\", end_date=\"2020-04-01\", fields=[\"Close\"])\n", 164 | "prices.head()" 165 | ] 166 | }, 167 | { 168 | "cell_type": "markdown", 169 | "metadata": {}, 170 | "source": [ 171 | "Next, we use closing prices to calculate our momentum factor. We calculate momentum using a twelve-month window but excluding the most recent month, as commonly recommended by academic papers. " 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 2, 177 | "metadata": {}, 178 | "outputs": [], 179 | "source": [ 180 | "closes = prices.loc[\"Close\"]\n", 181 | "\n", 182 | "MOMENTUM_WINDOW = 252 # 12 months = 252 trading days\n", 183 | "RANKING_PERIOD_GAP = 22 # 1 month = 22 trading days\n", 184 | "earlier_closes = closes.shift(MOMENTUM_WINDOW)\n", 185 | "later_closes = closes.shift(RANKING_PERIOD_GAP)\n", 186 | "momentum_returns = (later_closes - earlier_closes) / earlier_closes" 187 | ] 188 | }, 189 | { 190 | "cell_type": "markdown", 191 | "metadata": {}, 192 | "source": [ 193 | "Now that we have the twelve-month returns, we calculate the next day returns:" 194 | ] 195 | }, 196 | { 197 | "cell_type": "code", 198 | "execution_count": 3, 199 | "metadata": {}, 200 | "outputs": [], 201 | "source": [ 202 | "next_day_returns = closes.ffill().pct_change().shift(-1)" 203 | ] 204 | }, 205 | { 206 | "cell_type": "markdown", 207 | "metadata": {}, 208 | "source": [ 209 | "To see if the twelve-month returns predict next-day returns, we will split the twelve-month returns into bins and look at the mean next-day return of each bin. To do this, we first need to stack our wide-form DataFrames into Series. " 210 | ] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "execution_count": 4, 215 | "metadata": {}, 216 | "outputs": [], 217 | "source": [ 218 | "momentum_returns = momentum_returns.stack(dropna=False)\n", 219 | "next_day_returns = next_day_returns.stack(dropna=False)" 220 | ] 221 | }, 222 | { 223 | "cell_type": "markdown", 224 | "metadata": {}, 225 | "source": [ 226 | "Use pandas' `qcut` function to create the bins: " 227 | ] 228 | }, 229 | { 230 | "cell_type": "code", 231 | "execution_count": 5, 232 | "metadata": {}, 233 | "outputs": [], 234 | "source": [ 235 | "import pandas as pd\n", 236 | "\n", 237 | "# For a very small demo universe, you might only want 2 quantiles \n", 238 | "num_bins = 2\n", 239 | "bins = pd.qcut(momentum_returns, num_bins)" 240 | ] 241 | }, 242 | { 243 | "cell_type": "markdown", 244 | "metadata": {}, 245 | "source": [ 246 | "Now group the next day returns by momentum bin and plot the mean return:" 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": 6, 252 | "metadata": {}, 253 | "outputs": [ 254 | { 255 | "data": { 256 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAA8sAAAHOCAYAAACvo5s1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAAsTAAALEwEAmpwYAAA5lUlEQVR4nO3de5hWZb038C8DqCgoDg44o7jttZ2yUQzFAwKaNjio4ICFopamphepkaUl9SoqHbV0b/OYqZXv++ZGUg4aKKLtME1LM09ou9wgxAyHQEoUAWd4/3A7WxaIygw8wHw+18V1PWvd91rPb80Ms57v3Pdaq83q1atXBwAAAGhSVuoCAAAAYHMjLAMAAECBsAwAAAAFwjIAAAAUCMsAAABQICwDAABAgbAMQEndc889Ofnkk0tdRkl89rOfzfjx40tdxmblr3/9a/bee++89dZbpS5lq3LUUUflscceW2fbk08+mZqamk1cEcDmT1gGaGWOOuqoHHbYYXnjjTea1o0fPz6f/exnm73vLTn8bemh/dJLL01NTU322Wef3HPPPWu0TZgwISeccEIOOOCAHH744bnqqqs2mzC6vhC3NducjrtPnz554IEHSl0GwGZHWAZohRoaGnLHHXeUuoxNamOHw1KHz3322SeXX355/uVf/mWttuXLl+cb3/hGHn/88YwfPz6PP/54br/99hJUCQBbDmEZoBU666yzcvvtt+cf//jHOttffvnlnHHGGTn44INTU1OTKVOmJEnmzJmTgw8+OC+88EKSZMGCBTnkkEPyxBNP5F//9V/z5JNPZuzYsendu3fGjh27zn2/+uqrGTlyZA444IB8+tOfzpw5c9Zo/9a3vpUjjjgiBxxwQE444YQ8+eSTSZJFixZl//33z6uvvtrU9/nnn8+hhx6aVatWrfU+1113XUaNGpWLLrooBxxwQCZMmJDXXnst3/jGN9K/f/8MGDAg//qv/5qGhoa8/PLLueyyy/LHP/4xvXv3Tp8+fZKsPVJeHH3ee++98//+3//L0UcfnaOPPjpPPPFEDj/88Nx+++3p27dv+vfvn7vvvnu934s5c+bk05/+dA488MB84QtfyNKlS5Mk55xzTv7P//k/a/QdMmRIpk+fvs79nHrqqenbt2+23XbbtdpOOeWU9OnTJ9tss026deuWIUOG5A9/+MN71vTur13v3r0zZMiQzJo1Kz/60Y/St2/fHHHEEfnNb37T1H/BggUZOXJkDj744AwcODB33XXXGvv60pe+lK997Wvp3bt3jjvuuDz33HNJkq9+9aupq6vLyJEj07t37/z4xz9u2u7ee+/NJz7xiRxyyCG56aab3rPW0aNH5/LLL8/nP//59O7dOyNGjMiiRYvy7W9/OwcddFAGDRqUmTNnNvV/+eWX89nPfjZ9+vTJcccdl4ceemiD97VgwYJ88YtfzKGHHpqjjjpqjT9Afdjjfudn593ePfr8Yb8n6/Lcc8/l2GOPzUEHHZSvf/3rWbFiRZKs9d5HHXVUbrvttgwZMiQHHnhgLrjggqa+AK2JsAzQCu277745+OCDc9ttt63V9sYbb+TMM8/M4MGD89hjj+Waa67JFVdckT//+c/ZY489ctFFF+Wiiy5qGq084YQTcsghh+TLX/5y+vTpkzFjxuTpp5/OmDFj1vneY8eOzbbbbpvf/OY3+c53vrNWmNxvv/0yceLE/O53v8vgwYPzpS99KStWrEhFRUUOPvjgTJ06tanv5MmTc9xxx6V9+/brfK+HHnoogwYNypNPPpkhQ4bk4osvTrt27TJt2rRMnDgxjz76aMaPH5+99torV1xxRT7+8Y/n6aefbgroH8T06dNz1113Nf1B4W9/+1tee+21zJgxI9/+9rczduzY/P3vf3/P7SdOnJjvfOc7eeSRR9KuXbt861vfSpIMHTo0kydPbur30ksvZeHChWsFqg3x+9//Ph/96EfX2+dXv/pVamtr8/vf/z49evTIWWedlcbGxsyYMSPnnXfeGt/fCy+8MLvuumseeeSR/PCHP8w111yT3/72t03tDz/8cI477rg8+eSTOeqoo/LNb34zSfL9738/VVVVufnmm/P000/n7LPPbtrmqaeeyv3335+f/exnueGGG/Lyyy+/Z61Tp07NBRdckMcffzzbbLNNTjrppPTs2TOPP/54ampq8t3vfjdJsmrVqowcOTL9+vXLY489lksuuSQXXXRR/uu//utD76uxsTFf+MIXsvfee2fGjBn52c9+lp/97Gd55JFHmnXcLfU9WZd77703t912Wx588MHMmjUrN95443q/prfeemseeuih/OlPf1praj9AayAsA7RSo0aNyv/9v/83S5YsWWP9f/zHf2S33XbLpz71qbRr1y49e/ZMTU1N0zWNJ554Yv7pn/4pJ554YhYuXJgvf/nLH/g9GxoaMm3atIwaNSrbb799Pvaxj2XYsGFr9Kmtrc3OO++cdu3a5cwzz8zKlSsza9asJMmwYcOaAmRDQ0N++ctfpra29j3f7+Mf/3iqq6tTVlaWZcuWZcaMGfnGN76R7bffPl26dMnnPve5/PKXv/zA9a/LOeeck86dO2e77bZLkrRr1y7nnXde2rdvnyOOOCLbb799U/3rUltbm4997GPZfvvt86UvfSn3339/GhoaUl1dnVdeeSWzZ89OkkyaNCnHHHNMttlmm2bVe/fdd+f555/PmWeeud5+ffr0yYABA9KuXbsMGjQor776as4555y0b98+xx57bObNm5d//OMfqa+vz1NPPZWLLroo2267bXr06JHhw4dn0qRJTfs68MADc8QRR6Rt27apra3NSy+99L51nn/++dluu+2yzz77ZJ999lnvNgMHDsy+++6bbbfdNgMHDsy2226boUOHpm3btjn22GPz4osvJkmeeeaZvPHGGznnnHOyzTbbpG/fvjnyyCPX+Bn4oPt67rnnsmTJkpx//vnZZptt0r1795x44olNfzTZ0ONenw/6PXkvp556aiorK9O5c+d84QtfWO/P/mc/+9l069YtnTt3zpFHHtl03ACtSbtSFwBAaXzsYx/LJz7xidxyyy3Za6+9mtbPmzcvzz77bNNU5OTtYHr88cc3LZ944on5whe+kG9+85vrDW8333xzfvSjHyV5ewrxF7/4xbz11luprKxs6lNVVbXGNrfffnvGjx+fhQsXpk2bNlm2bFnT1OtPfvKTueyyyzJ37tzMmjUrHTt2TK9evd7z/Xfdddem13V1dXnrrbfSv3//pnWNjY1r1LIhitt37tw57dr9z+m1Q4cOa9xMbX3bV1VVZdWqVXn11Vezyy67ZNCgQZk8eXLOP//83HffffnhD3/YrFqnT5+eq6++Oj/5yU9SXl6e5O3R+csuuyzJ2+Hu1ltvTZJ06dKlabvtttsuO++8c9q2bdu0nLw9C2HhwoXZaaed0rFjxzWO4/nnn29a3mWXXdbY14oVK/LWW2+t8XUqevc27/c1LNZafL93tl24cGF23XXXlJX9z1hBVVVVFixY8KH3NW/evCxcuHCt/yfvXt6Q416fD/o92XHHHde5ffFnbeHChe/5XhUVFU2vO3TosN6+AFsrYRmgFRs1alSGDRu2xihjZWVlDjrooPzkJz9Z5zavv/56vvOd7+TTn/50rrvuuhx99NHp3LnzOvuOHDkyI0eObFpuaGhIu3btUl9f3xTQ6+vrm9qffPLJ/PjHP85Pf/rT/PM//3PKyspy0EEHZfXq1UmSbbfdNsccc0wmT56c//qv/1rvqHKStGnTpun1rrvumm222SaPP/74OsPKu/u+o0OHDlm+fHnT8t/+9rcPtN2H8e7jr6+vT/v27bPzzjsneXsk/Wtf+1oOPPDAdOjQIb17997g95kxY0YuueSS3HLLLdl7772b1h9//PFr/CHkw+ratWv+/ve/Z9myZU2Bub6+Pt26ddvgfW4sXbt2zfz589PY2NgUmOvr67Pnnnt+6H1VVlZm9913z7Rp01qktg4dOuTNN99sWm5oaFhr1kdzvftnra6uLl27dm3R/QNsbUzDBmjF/umf/inHHnvsGjeS+sQnPpHZs2dn4sSJWbVqVVatWpVnn3226ZrRb3/72+nZs2e+/e1v5xOf+ETTqGTy9kja3Llz3/P92rZtm4EDB+b666/P8uXL85e//CUTJkxoan/99dfTtm3blJeX56233sr111+fZcuWrbGP2traTJgwIQ8//PCHCnldu3ZNv3798r3vfS/Lli1LY2Nj5syZk9/97ndJ3h61W7BgQVauXNm0TY8ePfLggw9m+fLleeWVV/KLX/ziA7/fBzV58uT85S9/yfLly3PttdempqamabSwd+/eKSsry/e+9733PdaVK1dmxYoVWb16dd56662sWLEijY2NSZLf/va3+epXv5rrrrtuvSPxG6KysjK9e/fONddckxUrVuSll17KL37xiwwZMuQDbf9+PzMtqVevXunQoUNuvfXWrFq1Kk888UQefvjhHHvssRu0r44dO+aWW27Jm2++mYaGhvznf/5nnn322Q+0ffG4P/KRj2TFihX5j//4j6xatSo33XTTGj+LLeHnP/955s+fn6VLl+ZHP/rRBh03QGsiLAO0cuedd94aU1w7duyY2267LVOmTMmAAQPSv3///OAHP8jKlSszffr0PPLII7niiiuSvH334JkzZzZdR3zaaaflgQceyEEHHdR0o6qiMWPG5I033ki/fv0yevTonHDCCU1t/fv3z+GHH56ampocddRR2Xbbbdea5nzggQemrKwsPXv2zO677/6hjvWqq67KqlWrmu4IPGrUqCxatChJcuihh+ajH/1o+vfvn0MOOSRJcvrpp6d9+/Y57LDDcvHFF3/gAPhh1NbWZvTo0enXr19WrlyZ//2///da7f/5n//5vqPoZ511Vnr16pWnn346l156aXr16pXf//73SZIbb7wxr732Ws4555z07t07vXv3zuc///kWO4Zrrrkm8+bNy4ABA3L++efni1/8Yvr16/eBtj3nnHNy0003pU+fPuu84VxL2mabbXLTTTdlxowZOfTQQ3PFFVfkqquuWuMyhA+qbdu2uemmm/LSSy/lk5/8ZA499NBccskla/1x570Uj7tTp0657LLLcskll+Twww9Phw4d1riMoCUMHjw4Z555Zqqrq9O9e/d84QtfaNH9A2xt2qx+Z24bAGwhTjvttAwZMiTDhw8vdSkb3cSJEzNu3LjceeedpS4FAFoVI8sAbFGeffbZzJw5M8ccc0ypS9noli9fnp///Oc56aSTSl0KALQ6wjIAW4yLL744Z5xxRr7xjW+scfflrdEjjzySvn37pkuXLhk8eHCpywGAVsc0bAAAACgwsgwAAAAFwjIAAAAUCMsAAABQ0K7UBWzuXn319TQ2uqwbNrUuXTpm8eIP9rxSANjSOe/BpldW1iY777zDe7YLy++jsXG1sAwl4v8eAK2J8x5sXkzDBgAAgIIWG1meNWtWRo8enaVLl6Zz58658sors+eee67Rp6GhId/61rfyyCOPpE2bNjnnnHMyfPjwZrV97Wtfy5/+9Kem9/jTn/6UG264IZ/85Cdz3XXX5ec//3m6du2aJDnggANy2WWXtdQhAwAAsJVqsbB82WWX5ZRTTkltbW0mTZqUMWPG5I477lijz7333ps5c+Zk2rRpWbp0aYYOHZq+fftm99133+C2q666qmn/L730Uk4//fQMGDCgad3QoUNz8cUXt9RhAgAA0Aq0yDTsxYsXZ+bMmRk8eHCSZPDgwZk5c2aWLFmyRr8pU6Zk+PDhKSsrS3l5eaqrq3P//fc3q+3dfvGLX2TIkCHZZpttWuKwAAAAaKVaJCzX19enW7duadu2bZKkbdu26dq1a+rr69fqV1VV1bRcWVmZ+fPnN6vtHStXrsy9996bT33qU2us/+Uvf5khQ4bkzDPPzNNPP90CRwsAAMDWbqu5G/b06dNTVVWVHj16NK0bMWJERo4cmfbt2+fRRx/NueeemylTpmTnnXf+wPvt0qXjxigX+AAqKjqVugQA2GSc92Dz0iJhubKyMgsWLEhDQ0Patm2bhoaGLFy4MJWVlWv1q6urS69evZKsOWK8oW3vuPvuu9caVa6oqGh63a9fv1RWVubPf/5zDj744A98bIsXL3MbfyiBiopOWbTotVKXAQCbhPMebHplZW3WOzjaItOwu3Tpkh49euS+++5Lktx3333p0aNHysvL1+g3aNCgjB8/Po2NjVmyZEmmT5+empqaZrUlyfz58/PUU081XTP9jgULFjS9fvHFFzNv3rx85CMfaYlDBgAAYCvWYtOwL7/88owePTo33nhjdtxxx1x55ZVJkrPPPjujRo3Kfvvtl9ra2jzzzDM5+uijkyTnnXdeunfvniQb3JYkEyZMyJFHHpnOnTuvUdM111yTF154IWVlZWnfvn2uuuqqNUabAQAAYF3arF692hzj9TANG0rDdDQAWhPnPdj0Nsk0bAAAANiaCMsAAABQsNU8OgoAoDXrtGOHbLetj3ZbMo+O2nK9ueKtvPaP5aUugxbmNyoAwFZgu23bZciFk0pdBrRK915dG1ecb31MwwYAAIACYRkAAAAKhGUAAAAoEJYBAACgQFgGAACAAmEZAAAACoRlAAAAKBCWAQAAoEBYBgAAgAJhGQAAAAqEZQAAACgQlgEAAKBAWAYAAIACYRkAAAAKhGUAAAAoEJYBAACgQFgGAACAAmEZAAAACoRlAAAAKBCWAQAAoEBYBgAAgAJhGQAAAAqEZQAAACgQlgEAAKBAWAYAAIACYRkAAAAKhGUAAAAoEJYBAACgQFgGAACAAmEZAAAACoRlAAAAKBCWAQAAoEBYBgAAgIIWC8uzZs3KSSedlJqampx00kmZPXv2Wn0aGhpyxRVXpLq6OgMHDsz48eOb3Xbdddelb9++qa2tTW1tba644ooPtB0AAAC8l3YttaPLLrssp5xySmprazNp0qSMGTMmd9xxxxp97r333syZMyfTpk3L0qVLM3To0PTt2ze77777BrclydChQ3PxxRevVdP7bQcAAADr0iIjy4sXL87MmTMzePDgJMngwYMzc+bMLFmyZI1+U6ZMyfDhw1NWVpby8vJUV1fn/vvvb1bb+mzodgAAALRuLRKW6+vr061bt7Rt2zZJ0rZt23Tt2jX19fVr9auqqmparqyszPz585vVliS//OUvM2TIkJx55pl5+umnP9D7AQAAwHtpsWnYpTJixIiMHDky7du3z6OPPppzzz03U6ZMyc4779wi++/SpWOL7Af48CoqOpW6BACAD8Tnlq1Pi4TlysrKLFiwIA0NDWnbtm0aGhqycOHCVFZWrtWvrq4uvXr1SrLmyO+GtlVUVDTtv1+/fqmsrMyf//znHHzwwevd7oNavHhZGhtXf9gvCdBMFRWdsmjRa6UuA2CL4YM6lJbPLVuesrI26x0cbZFp2F26dEmPHj1y3333JUnuu+++9OjRI+Xl5Wv0GzRoUMaPH5/GxsYsWbIk06dPT01NTbPaFixY0LT/F198MfPmzctHPvKR990OAAAA3kuLTcO+/PLLM3r06Nx4443Zcccdc+WVVyZJzj777IwaNSr77bdfamtr88wzz+Too49Okpx33nnp3r17kmxw2zXXXJMXXnghZWVlad++fa666qqm0eb1bQcAAADvpc3q1avNMV4P07ChNEzDBvhwKio6ZciFk0pdBrRK915d63PLFmiTTMMGAACArYmwDAAAAAXCMgAAABQIywAAAFAgLAMAAECBsAwAAAAFwjIAAAAUCMsAAABQICwDAABAgbAMAAAABcIyAAAAFAjLAAAAUCAsAwAAQIGwDAAAAAXCMgAAABQIywAAAFAgLAMAAECBsAwAAAAFwjIAAAAUCMsAAABQICwDAABAgbAMAAAABcIyAAAAFAjLAAAAUCAsAwAAQIGwDAAAAAXCMgAAABQIywAAAFAgLAMAAECBsAwAAAAFwjIAAAAUCMsAAABQICwDAABAgbAMAAAABcIyAAAAFAjLAAAAUNBiYXnWrFk56aSTUlNTk5NOOimzZ89eq09DQ0OuuOKKVFdXZ+DAgRk/fnyz22644YYcd9xxOf7443PCCSfkkUceaWq77rrr0rdv39TW1qa2tjZXXHFFSx0uAAAAW7F2LbWjyy67LKecckpqa2szadKkjBkzJnfccccafe69997MmTMn06ZNy9KlSzN06ND07ds3u++++wa39erVK2eeeWY6dOiQl156KZ/5zGfym9/8Jtttt12SZOjQobn44otb6jABAABoBVpkZHnx4sWZOXNmBg8enCQZPHhwZs6cmSVLlqzRb8qUKRk+fHjKyspSXl6e6urq3H///c1qGzBgQDp06JAk2Xvv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8komT57crLYk2WuvvTJp0qRMmjQp48eP/0DvBwAAAOvT7LA8derUjBgxIkmy5557Zt99982MGTPW6vfAAw+kuro65eXlKSsry/DhwzNlypRmta3Phm4HAAAA7Zq7g7q6uuy2225Ny5WVlZk/f/5a/err61NVVdW0XFVVlfr6+ma1Jcns2bMzbNiwtGvXLqecckqGDRv2gbb7oLp06fihtwFo7SoqOpW6BIAtjt+dsHl537A8bNiw1NXVrbPtsccea/GCPoyePXvm17/+dTp16pS5c+fmjDPOSLdu3XLYYYe12HssXrwsjY2rW2x/bDpOOFA6ixa9VuoSALYoFRWd/O6ETaysrM16B0ffNyxPmDBhve1VVVWZN29eysvLk7w9onvIIYes1a+ysnKN0F1XV5fKyspmtXXs+D8H1r1791RXV+cPf/hDDjvssPVuBwAAAOvT7GuWBw0alHHjxiV5e0r0c889lwEDBqzVr6amJtOnT8+SJUvS2NiY8ePH55hjjmlW28KFC7N69dujvkuXLs2jjz6affbZ5323AwAAgPVp9jXLZ511VkaPHp2BAwemrKwsY8eObRrxvfbaa9O1a9ecfPLJ6d69e84999yceOKJSZJ+/frl+OOPT5INbps2bVruvPPOtGvXLg0NDamtrU11dfX7bgcAAADr02b1O0OzrJNrlrdcFRWdMuTCSaUuA1qde6+udd0dwIfkmmXY9N7vmuVmT8MGAACArY2wDAAAAAXCMgAAABQIywAAAFAgLAMAAECBsAwAAAAFwjIAAAAUCMsAAABQICwDAABAgbAMAAAABcIyAAAAFAjLAAAAUCAsAwAAQIGwDAAAAAXCMgAAABQIywAAAFAgLAMAAECBsAwAAAAFwjIAAAAUCMsAAABQICwDAABAgbAMAAAABcIyAAAAFAjLAAAAUCAsAwAAQIGwDAAAAAXCMgAAABQIywAAAFAgLAMAAECBsAwAAAAFwjIAAAAUCMsAAABQICwDAABAgbAMAAAABcIyAAAAFAjLAAAAUNCuuTtYvnx5vv71r+eFF15I27Ztc/HFF+fII49cZ9+77rorP/7xj7N69eocfvjhueSSS1JWVrbBbXfccUfuvvvupv3PnTs3w4cPz9e//vU88cQTOeecc7LnnnsmSbbZZpuMHz++uYcLAABAK9DssHzbbbdlhx12yIMPPpjZ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P/5xfv7zn+eWW27JPffck9tuu63E1QFAy7v88sszcODAXHPNNVm6dGlOP/30vPbaa0myRnAGSktYBkrqzTffTGNjY5KksbExe+yxR5KkS5cucUsFALZGixcvzqmnnpqePXvmu9/9bqqrq3P66afn1VdfTZs2bUpdHvDfhGWgpI455phceOGF+etf/5rq6urccsstmT9/fu66667stttupS4PAFpccfT485//fE444YScdtppTSPMQOm5GzZQUqtXr85Pf/rT/OQnP8mrr76aVatWZYcddshxxx2XL3/5y9l5551LXSIAtKhRo0Zl2LBhOfLII9dYP27cuFx++eV58cUXS1QZ8G7CMrDZWLZsWd5666107ty51KUAwEbzzsfvdU25fv3117PDDjts6pKAdRCWgc3K3Llz8+KLL+af//mf85GPfKTU5QDARvP8889n/vz5SZJdd901++67b4krAt6tXakLAFq3kSNH5jvf+U7Ky8vz0EMP5bLLLsu//Mu/5E9/+lO+8pWvpLa2ttQlAkCLevbZZ3PRRRdl2223TWVlZZK3nw6xYsWKfP/738/+++9f4gqBRFgGSmxdj47aY489snjx4pxxxhnCMgBbnTFjxuS73/1uDjzwwDXWP/nkkxkzZkwmTZpUosqAd3M3bKCkPDoKgNZm+fLlawXlJOnTp0/efPPNElQErIuwDJSUR0cB0Nrsvvvuufnmm7N06dKmdUuXLs1NN92Uqqqq0hUGrMENvoCS8ugoAFqbxYsX5+qrr87UqVPXWD9o0KBceOGF2WWXXUpUGfBuwjKw2fDoKABam3dGl537YPNjGjaw2ejYsWM6d+6ce+65p9SlAMBGN3PmzDz55JP54x//mLlz55a6HKDA3bCBkvr1r3+91rprrrkmXbp0SZIcccQRm7okANioZs6cmYsuuigLFizIG2+8kY9+9KNZsGBBDj744HznO9/JjjvuWOoSgZiGDZTYPvvsk49//ONp375907pnnnkm+++/f9q0aZM77rijhNUBQMsbPnx4Ro8enQMPPDAPP/xwfvOb3+TrX/96fvSjH2XWrFm5+uqrS10iEGEZKLEJEyZk3Lhx+cY3vpFevXolSY466qg8/PDDJa4MADaO448/PpMnT25aPuGEE5ouQaqpqckDDzxQqtKAd3HNMlBSw4YNy7XXXpvrr78+P/jBD7Jy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", 257 | "text/plain": [ 258 | "
" 259 | ] 260 | }, 261 | "metadata": {}, 262 | "output_type": "display_data" 263 | } 264 | ], 265 | "source": [ 266 | "next_day_returns.groupby(bins).mean().plot(kind=\"bar\", title=\"Next-day return by 12-month momentum bin\");" 267 | ] 268 | }, 269 | { 270 | "cell_type": "markdown", 271 | "metadata": {}, 272 | "source": [ 273 | "For a predictive factor, the higher quantiles should perform better than the lower quantiles." 274 | ] 275 | }, 276 | { 277 | "cell_type": "markdown", 278 | "metadata": {}, 279 | "source": [ 280 | "***\n", 281 | "\n", 282 | "## *Next Up*\n", 283 | "\n", 284 | "Part 4: [Moonshot Strategy Code](Part4-Moonshot-Strategy-Code.ipynb)" 285 | ] 286 | } 287 | ], 288 | "metadata": { 289 | "kernelspec": { 290 | "display_name": "Python 3.11", 291 | "language": "python", 292 | "name": "python3" 293 | }, 294 | "language_info": { 295 | "codemirror_mode": { 296 | "name": "ipython", 297 | "version": 3 298 | }, 299 | "file_extension": ".py", 300 | "mimetype": "text/x-python", 301 | "name": "python", 302 | "nbconvert_exporter": "python", 303 | "pygments_lexer": "ipython3", 304 | "version": "3.11.0" 305 | } 306 | }, 307 | "nbformat": 4, 308 | "nbformat_minor": 4 309 | } 310 | -------------------------------------------------------------------------------- /intro_moonshot/Part4-Moonshot-Strategy-Code.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\"QuantRocket
\n", 8 | "Disclaimer" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "***\n", 16 | "[Moonshot Intro](Introduction.ipynb) › Part 4: Moonshot Code\n", 17 | "***" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# Moonshot Strategy Code" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "Next we'll use Moonshot to backtest the momentum factor we explored in the previous notebook." 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "## What is Moonshot?\n", 39 | "\n", 40 | "Moonshot is an open-source, vectorized backtester created by QuantRocket. A vectorized backtester is one which uses vectorized operations to backtest an entire date range at once, in contrast to event-driven backtesters like Zipline which process one event at a time. Moonshot uses pandas to perform vectorized operations. You can learn more about Moonshot in the [usage guide](https://www.quantrocket.com/docs/#moonshot)." 41 | ] 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "metadata": {}, 46 | "source": [ 47 | "## Install UMD strategy file\n", 48 | "\n", 49 | "Cross-sectional momentum strategies which buy recent winners and sell recent losers commonly go by the name of UMD, or \"Up Minus Down.\" An implementation of UMD for Moonshot is available in [umd.py](umd.py).\n", 50 | "\n", 51 | "To \"install\" the strategy, execute the following cell to move the strategy file to the `/codeload/moonshot` directory, where Moonshot looks:\n", 52 | "\n", 53 | "> The ! sytax below lets us execute terminal commands from inside the notebook." 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 1, 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "# make directory if doesn't exist\n", 63 | "!mkdir -p /codeload/moonshot\n", 64 | "\n", 65 | "!mv umd.py /codeload/moonshot/" 66 | ] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "metadata": {}, 71 | "source": [ 72 | "## How a Moonshot backtest works\n", 73 | "\n", 74 | "The usage guide describes in detail how a Moonshot backtest works, but here we'll just cover a few highlights. Near the top of the file, you'll see the `UpMinusDown` class, which inherits from the `Moonshot` class.\n", 75 | "\n", 76 | "```python\n", 77 | "class UpMinusDown(Moonshot):\n", 78 | "\n", 79 | " CODE = \"umd\"\n", 80 | " MOMENTUM_WINDOW = 252\n", 81 | " RANKING_PERIOD_GAP = 22\n", 82 | " LOOKBACK_WINDOW = MOMENTUM_WINDOW\n", 83 | " TOP_N_PCT = 50\n", 84 | " REBALANCE_INTERVAL = \"M\"\n", 85 | "\n", 86 | " def prices_to_signals(self, prices: pd.DataFrame):\n", 87 | " closes = prices.loc[\"Close\"]\n", 88 | " returns = closes.shift(self.RANKING_PERIOD_GAP)/closes.shift(self.MOMENTUM_WINDOW) - 1\n", 89 | " ...\n", 90 | "```\n", 91 | "\n", 92 | "Strategy logic is implemented in class methods (for example, `prices_to_signals`), and parameters are stored as class attributes (for example, `REBALANCE_INTERVAL`).\n", 93 | "\n", 94 | "Now find the `UpMinusDownDemo` class further down in the file:\n", 95 | "\n", 96 | "```python\n", 97 | "class UpMinusDownDemo(UpMinusDown):\n", 98 | "\n", 99 | " CODE = \"umd-demo\"\n", 100 | " DB = \"usstock-free-1d\"\n", 101 | " UNIVERSES = \"usstock-free\"\n", 102 | " TOP_N_PCT = 50\n", 103 | " COMMISSION_CLASS = USStockCommission\n", 104 | "```\n", 105 | "\n", 106 | "This class is a subclass of `UpMinusDown` and thus inherits its functionality while overriding a few of its parameters. This is the actual strategy we will run in our backtest. Note the `DB` parameter: it tells the strategy to use the history database we created in an earlier tutorial. The optional `UNIVERSES` parameter indicates which subset of securities in the database to query. (Since the securities in our sample universe are the only securities in our history database, the `UNIVERSES` parameter could be omitted in this case.) The `CODE` parameter gives us an easy way to reference the strategy via the QuantRocket API.\n" 107 | ] 108 | }, 109 | { 110 | "cell_type": "markdown", 111 | "metadata": {}, 112 | "source": [ 113 | "***\n", 114 | "\n", 115 | "## *Next Up*\n", 116 | "\n", 117 | "Part 5: [Moonshot Backtest](Part5-Moonshot-Backtest.ipynb)" 118 | ] 119 | } 120 | ], 121 | "metadata": { 122 | "kernelspec": { 123 | "display_name": "Python 3.11", 124 | "language": "python", 125 | "name": "python3" 126 | }, 127 | "language_info": { 128 | "codemirror_mode": { 129 | "name": "ipython", 130 | "version": 3 131 | }, 132 | "file_extension": ".py", 133 | "mimetype": "text/x-python", 134 | "name": "python", 135 | "nbconvert_exporter": "python", 136 | "pygments_lexer": "ipython3", 137 | "version": "3.11.0" 138 | } 139 | }, 140 | "nbformat": 4, 141 | "nbformat_minor": 4 142 | } 143 | -------------------------------------------------------------------------------- /intro_moonshot/Part7-Debug-Moonshot-Strategies.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\"QuantRocket
\n", 8 | "Disclaimer" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "metadata": {}, 14 | "source": [ 15 | "***\n", 16 | "[Moonshot Intro](Introduction.ipynb) › Part 7: Debug Moonshot Strategies\n", 17 | "***" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# Debugging Moonshot Strategies" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "The recommended workflow for developing a Moonshot strategy is to start in a notebook, where you can develop your code interactively, then transfer the finished code to a `.py` file for backtesting. (This approach is described in more detail in the [usage guide](https://qrok.it/p/moonshot-interactive-dev).) Once you've transferred your code to a `.py` file, you can continue to debug your strategy interactively using the technique described in this notebook. The basic technique is to open the `.py` file in JupyterLab and attach a console for interactive exploration. " 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "The debugging steps are as follows:\n", 39 | "\n", 40 | "1. Open your Moonshot strategy file [umd.py](../moonshot/umd.py) in the JupyterLab editor.\n", 41 | "2. Right-click in the file and select \"Create Console for Editor\"\n", 42 | "3. Select the entire contents of the file (Ctrl+A on Windows or Cmd+A on Mac), then click Ctrl+Enter to load the file contents into the console.\n", 43 | "4. In the Console window, instantiate your Moonshot strategy and name the variable `self`. Here, we are exploring the `UpMinusDownDemo` strategy:\n", 44 | "\n", 45 | "```python\n", 46 | "self = UpMinusDownDemo()\n", 47 | "```\n", 48 | "\n", 49 | "5. Load prices for the desired date range by calling your strategy's `get_prices` method (this method is defined on the `Moonshot` base class):\n", 50 | "\n", 51 | "```python\n", 52 | "prices = self.get_prices(start_date=\"2017-01-01\", end_date=\"2017-02-01\")\n", 53 | "```\n", 54 | "\n", 55 | "6. To debug `prices_to_signals`, select the body of the method (everything excluding the method definition at the top and the return statement at the bottom), then click Shift+Enter. This copies the selected lines to the console and executes them. Normally the `prices` variable is passed as a parameter to `prices_to_signals`, but in this case we already loaded a `prices` variable in the previous step, so the executed code uses that.\n", 56 | "7. At this point, all the local variables from the `prices_to_signals` method are loaded in the console and can be inspected interactively.\n", 57 | "8. This process can be continued to explore additional methods like `signals_to_target_weights`. Since both a `signals` variable and `prices` variable are now loaded in the console, you can select and execute the body of `signals_to_target_weights` and it will use those variables. You can then inspect the variables created in `signals_to_target_weights`. \n", 58 | "\n", 59 | "See the video below for a step-by-step demonstration." 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": 1, 65 | "metadata": {}, 66 | "outputs": [ 67 | { 68 | "data": { 69 | "text/html": [ 70 | "\n", 71 | " \n", 79 | " " 80 | ], 81 | "text/plain": [ 82 | "" 83 | ] 84 | }, 85 | "execution_count": 1, 86 | "metadata": {}, 87 | "output_type": "execute_result" 88 | } 89 | ], 90 | "source": [ 91 | "from IPython.display import VimeoVideo\n", 92 | "VimeoVideo('925534381', width=700, height=394)" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "
⚠️ Due to an open issue in the current version of JupyterLab used in QuantRocket, please use Ctrl + Enter to copy code from the file editor to the console, not Shift + Enter as stated in the video.
" 100 | ] 101 | }, 102 | { 103 | "cell_type": "markdown", 104 | "metadata": {}, 105 | "source": [ 106 | "***\n", 107 | "\n", 108 | "[Back to Introduction](Introduction.ipynb)" 109 | ] 110 | } 111 | ], 112 | "metadata": { 113 | "kernelspec": { 114 | "display_name": "Python 3.11", 115 | "language": "python", 116 | "name": "python3" 117 | }, 118 | "language_info": { 119 | "codemirror_mode": { 120 | "name": "ipython", 121 | "version": 3 122 | }, 123 | "file_extension": ".py", 124 | "mimetype": "text/x-python", 125 | "name": "python", 126 | "nbconvert_exporter": "python", 127 | "pygments_lexer": "ipython3", 128 | "version": "3.11.0" 129 | } 130 | }, 131 | "nbformat": 4, 132 | "nbformat_minor": 4 133 | } 134 | -------------------------------------------------------------------------------- /intro_moonshot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantrocket-codeload/moonshot-intro/8717e87e279e3615bdd4c7787ce746b92d0713d7/intro_moonshot/__init__.py -------------------------------------------------------------------------------- /intro_moonshot/umd.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020-2024 QuantRocket LLC - All Rights Reserved 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import pandas as pd 16 | from moonshot import Moonshot 17 | from moonshot.commission import PerShareCommission 18 | 19 | class UpMinusDown(Moonshot): 20 | """ 21 | Strategy that buys recent winners and sells recent losers. 22 | 23 | Specifically: 24 | 25 | - rank stocks by their performance over the past MOMENTUM_WINDOW days 26 | - ignore very recent performance by excluding the last RANKING_PERIOD_GAP 27 | days from the ranking window (as commonly recommended for UMD) 28 | - buy the TOP_N_PCT percent of highest performing stocks and short the TOP_N_PCT 29 | percent of lowest performing stocks 30 | - rebalance the portfolio according to REBALANCE_INTERVAL 31 | """ 32 | 33 | CODE = "umd" 34 | MOMENTUM_WINDOW = 252 # rank by twelve-month returns 35 | RANKING_PERIOD_GAP = 22 # but exclude most recent 1 month performance 36 | TOP_N_PCT = 50 # Buy/sell the top/bottom 50% 37 | REBALANCE_INTERVAL = "M" # M = monthly; see https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases 38 | 39 | def prices_to_signals(self, prices: pd.DataFrame): 40 | """ 41 | This method receives a DataFrame of prices and should return a 42 | DataFrame of integer signals, where 1=long, -1=short, and 0=cash. 43 | """ 44 | closes = prices.loc["Close"] 45 | 46 | # Calculate the returns 47 | returns = closes.shift(self.RANKING_PERIOD_GAP)/closes.shift(self.MOMENTUM_WINDOW) - 1 48 | 49 | # Rank the best and worst 50 | top_ranks = returns.rank(axis=1, ascending=False, pct=True) 51 | bottom_ranks = returns.rank(axis=1, ascending=True, pct=True) 52 | 53 | top_n_pct = self.TOP_N_PCT / 100 54 | 55 | # Get long and short signals and convert to 1, 0, -1 56 | longs = (top_ranks <= top_n_pct) 57 | shorts = (bottom_ranks <= top_n_pct) 58 | 59 | longs = longs.astype(int) 60 | shorts = -shorts.astype(int) 61 | 62 | # Combine long and short signals 63 | signals = longs.where(longs == 1, shorts) 64 | 65 | # Resample using the rebalancing interval. 66 | # Keep only the last signal of the month, then fill it forward 67 | signals = signals.resample(self.REBALANCE_INTERVAL).last() 68 | signals = signals.reindex(closes.index, method="ffill") 69 | 70 | return signals 71 | 72 | def signals_to_target_weights(self, signals: pd.DataFrame, prices: pd.DataFrame): 73 | """ 74 | This method receives a DataFrame of integer signals (-1, 0, 1) and 75 | should return a DataFrame indicating how much capital to allocate to 76 | the signals, expressed as a percentage of the total capital allocated 77 | to the strategy (for example, -0.25, 0, 0.1 to indicate 25% short, 78 | cash, 10% long). 79 | """ 80 | weights = self.allocate_equal_weights(signals) 81 | return weights 82 | 83 | def target_weights_to_positions(self, weights: pd.DataFrame, prices: pd.DataFrame): 84 | """ 85 | This method receives a DataFrame of allocations and should return a 86 | DataFrame of positions. This allows for modeling the delay between 87 | when the signal occurs and when the position is entered, and can also 88 | be used to model non-fills. 89 | """ 90 | # Enter the position in the period/day after the signal 91 | return weights.shift() 92 | 93 | def positions_to_gross_returns(self, positions: pd.DataFrame, prices: pd.DataFrame): 94 | """ 95 | This method receives a DataFrame of positions and a DataFrame of 96 | prices, and should return a DataFrame of percentage returns before 97 | commissions and slippage. 98 | """ 99 | # We'll enter on the open, so our return is today's open to 100 | # tomorrow's open 101 | opens = prices.loc["Open"] 102 | # The return is the security's percent change over the period, 103 | # multiplied by the position. 104 | gross_returns = opens.pct_change() * positions.shift() 105 | return gross_returns 106 | 107 | class USStockCommission(PerShareCommission): 108 | BROKER_COMMISSION_PER_SHARE = 0.005 109 | 110 | class UpMinusDownDemo(UpMinusDown): 111 | 112 | CODE = "umd-demo" 113 | DB = "usstock-free-1d" 114 | UNIVERSES = "usstock-free" 115 | TOP_N_PCT = 50 116 | COMMISSION_CLASS = USStockCommission 117 | --------------------------------------------------------------------------------