├── intro_zipline ├── __init__.py ├── Introduction.ipynb ├── winners.py ├── Part1-Historical-Data-Collection.ipynb ├── Part8-Debug-Zipline-Strategies.ipynb ├── Part5-Zipline-Strategy-Code.ipynb ├── Part4-Intraday-Trading-Rules.ipynb └── Part2-End-of-Day-Trading-Rules.ipynb ├── .qrignore ├── README.md └── LICENSE.txt /intro_zipline/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.qrignore: -------------------------------------------------------------------------------- 1 | README.md 2 | LICENSE.txt 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zipline-intro 2 | 3 | Introductory tutorial for Zipline demonstrating data collection, interactive research, and backtesting of a momentum strategy for equities. Uses free sample data. 4 | 5 | ## Clone in QuantRocket 6 | 7 | CLI: 8 | 9 | ```shell 10 | quantrocket codeload clone 'zipline-intro' 11 | ``` 12 | 13 | Python: 14 | 15 | ```python 16 | from quantrocket.codeload import clone 17 | clone("zipline-intro") 18 | ``` 19 | 20 | ## Browse in GitHub 21 | 22 | Start here: [intro_zipline/Introduction.ipynb](intro_zipline/Introduction.ipynb) 23 | 24 | *** 25 | 26 | Find more code in QuantRocket's [Code Library](https://www.quantrocket.com/code/) 27 | -------------------------------------------------------------------------------- /intro_zipline/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 Zipline\n", 16 | "\n", 17 | "This tutorial shows you how to retrieve 1-minute historical sample data for US equities, analyze your data in a research notebook, and backtest a Zipline strategy that combines an end-of-day momentum factor with intraday entry timing. \n", 18 | " \n", 19 | "**Data collection**\n", 20 | "\n", 21 | "* Part 1: [Historical Data Collection](Part1-Historical-Data-Collection.ipynb)\n", 22 | "\n", 23 | "**Research and Backtesting**\n", 24 | "\n", 25 | "*Research*\n", 26 | "\n", 27 | "* Part 2: [End-of-Day Trading Rules](Part2-End-of-Day-Trading-Rules.ipynb)\n", 28 | "* Part 3: [End-of-Day Analysis With Alphalens](Part3-End-of-Day-Analysis-With-Alphalens.ipynb)\n", 29 | "* Part 4: [Intraday Trading Rules](Part4-Intraday-Trading-Rules.ipynb)\n", 30 | "\n", 31 | "*Zipline*\n", 32 | "\n", 33 | "* Part 5: [Zipline Strategy Code](Part5-Zipline-Strategy-Code.ipynb)\n", 34 | "* Part 6: [Zipline Backtest](Part6-Zipline-Backtest.ipynb)\n", 35 | "* Part 7: [Zipline Parameter Scans](Part7-Zipline-Parameter-Scans.ipynb)\n", 36 | "* Part 8: [Debug Zipline Strategies](Part8-Debug-Zipline-Strategies.ipynb)" 37 | ] 38 | } 39 | ], 40 | "metadata": { 41 | "kernelspec": { 42 | "display_name": "Python 3.11", 43 | "language": "python", 44 | "name": "python3" 45 | }, 46 | "language_info": { 47 | "codemirror_mode": { 48 | "name": "ipython", 49 | "version": 3 50 | }, 51 | "file_extension": ".py", 52 | "mimetype": "text/x-python", 53 | "name": "python", 54 | "nbconvert_exporter": "python", 55 | "pygments_lexer": "ipython3", 56 | "version": "3.11.0" 57 | } 58 | }, 59 | "nbformat": 4, 60 | "nbformat_minor": 4 61 | } 62 | -------------------------------------------------------------------------------- /intro_zipline/winners.py: -------------------------------------------------------------------------------- 1 | # Copyright 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 zipline.api as algo 16 | from zipline.pipeline import Pipeline 17 | from zipline.pipeline.factors import AverageDollarVolume, Returns 18 | from zipline.finance.execution import MarketOrder 19 | 20 | MOMENTUM_WINDOW = 252 21 | 22 | def initialize(context: algo.Context): 23 | """ 24 | Called once at the start of a backtest, and once per day in 25 | live trading. 26 | """ 27 | # Attach the pipeline to the algo 28 | algo.attach_pipeline(make_pipeline(), 'pipeline') 29 | 30 | algo.set_benchmark(algo.symbol('SPY')) 31 | 32 | # Rebalance every day, 30 minutes before market close. 33 | algo.schedule_function( 34 | rebalance, 35 | algo.date_rules.every_day(), 36 | algo.time_rules.market_close(minutes=30), 37 | ) 38 | 39 | def make_pipeline(): 40 | """ 41 | Create a pipeline that filters by dollar volume and 42 | calculates return. 43 | """ 44 | pipeline = Pipeline( 45 | columns={ 46 | "returns": Returns(window_length=MOMENTUM_WINDOW), 47 | }, 48 | screen=AverageDollarVolume(window_length=30) > 10e6 49 | ) 50 | return pipeline 51 | 52 | def before_trading_start(context: algo.Context, data: algo.BarData): 53 | """ 54 | Called every day before market open. 55 | """ 56 | factors = algo.pipeline_output('pipeline') 57 | 58 | # Get the top 3 stocks by return 59 | returns = factors["returns"].sort_values(ascending=False) 60 | context.winners = returns.index[:3] 61 | 62 | def rebalance(context: algo.Context, data: algo.BarData): 63 | """ 64 | Execute orders according to our schedule_function() timing. 65 | """ 66 | 67 | # calculate intraday returns for our winners 68 | current_prices = data.current(context.winners, "price") 69 | prior_closes = data.history(context.winners, "close", 2, "1d").iloc[0] 70 | intraday_returns = (current_prices - prior_closes) / prior_closes 71 | 72 | positions = context.portfolio.positions 73 | 74 | # Exit positions we no longer want to hold 75 | for asset, position in positions.items(): 76 | if asset not in context.winners: 77 | algo.order_target_value(asset, 0, style=MarketOrder()) 78 | 79 | # Enter long positions 80 | for asset in context.winners: 81 | 82 | # if already long, nothing to do 83 | if asset in positions: 84 | continue 85 | 86 | # if the stock is up for the day, don't enter 87 | if intraday_returns[asset] > 0: 88 | continue 89 | 90 | # otherwise, allocate 1/6th of capital per asset 91 | algo.order_target_percent(asset, 1/6, style=MarketOrder()) 92 | -------------------------------------------------------------------------------- /intro_zipline/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 | "[Zipline Intro](Introduction.ipynb) › Part 1: Data Collection\n", 17 | "***" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# Collect 1-minute US Stock Data\n", 25 | "\n", 26 | "In Zipline terminology, databases are referred to as \"bundles.\" To collect 1-minute sample data for US stocks, create a bundle and specify `free=True`. We will name the bundle \"usstock-free-1min\":" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 1, 32 | "metadata": {}, 33 | "outputs": [ 34 | { 35 | "data": { 36 | "text/plain": [ 37 | "{'status': 'success', 'msg': 'successfully created usstock-free-1min bundle'}" 38 | ] 39 | }, 40 | "execution_count": 1, 41 | "metadata": {}, 42 | "output_type": "execute_result" 43 | } 44 | ], 45 | "source": [ 46 | "from quantrocket.zipline import create_usstock_bundle\n", 47 | "create_usstock_bundle(\"usstock-free-1min\", free=True)" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "This creates an empty bundle. Next, we need to fill up the bundle with data. This is called \"ingestion\" in Zipline terminology. Ingestion 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 ingest 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 data will be ingested asynchronously'}" 82 | ] 83 | }, 84 | "execution_count": 3, 85 | "metadata": {}, 86 | "output_type": "execute_result" 87 | } 88 | ], 89 | "source": [ 90 | "from quantrocket.zipline import ingest_bundle\n", 91 | "ingest_bundle(\"usstock-free-1min\")" 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.zipline: INFO [usstock-free-1min] Ingesting minute bars for 8 securities in usstock-free-1min bundle\n", 102 | "quantrocket.zipline: INFO [usstock-free-1min] Ingesting daily bars for usstock-free-1min bundle\n", 103 | "quantrocket.zipline: INFO [usstock-free-1min] Ingesting adjustments for usstock-free-1min bundle\n", 104 | "quantrocket.zipline: INFO [usstock-free-1min] Ingesting assets for usstock-free-1min bundle\n", 105 | "quantrocket.zipline: INFO [usstock-free-1min] Completed ingesting data for 8 securities in usstock-free-1min bundle\n", 106 | "```" 107 | ] 108 | }, 109 | { 110 | "cell_type": "markdown", 111 | "metadata": {}, 112 | "source": [ 113 | "***\n", 114 | "\n", 115 | "## *Next Up*\n", 116 | "\n", 117 | "Part 2: [End-of-Day Trading Rules](Part2-End-of-Day-Trading-Rules.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_zipline/Part8-Debug-Zipline-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 | "[Zipline Intro](Introduction.ipynb) › Part 8: Debug Zipline Strategies\n", 17 | "***" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# Debugging Zipline Strategies" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "The recommended workflow for developing a Zipline 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. Once you've transitioned to the backtesting phase, you can use JupyterLab's built-in debugger to debug your strategy. The debugger allows you to set breakpoints and inspect variables as they change over the duration of your backtest simulation. This notebooks explains how to use the debugger." 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "## Introducing the `%zipline_backtest` IPython magic command for debugging\n", 39 | "\n", 40 | "To debug Zipline with the JupyterLab debugger, we can't use the `quantrocket.zipline.backtest` function that we used in an earlier lesson. That's because this function executes the backtest in a separate Docker container from JupyterLab, the `zipline` container. Instead, we can use a special IPython magic command, `%zipline_backtest`, that runs the backtest inside JupyterLab. The magic command works similarly to `quantrocket.zipline.backtest` but lacks some of its bells and whistles and is only intended for debugging purposes. " 41 | ] 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "metadata": {}, 46 | "source": [ 47 | "The debugging steps are as follows:\n", 48 | "\n", 49 | "1. Open the algorithm file [winners.py](../zipline/winners.py) in the JupyterLab editor.\n", 50 | "2. Right-click in the file and select \"Create Console for Editor\"\n", 51 | "3. Select the entire contents of the file (Ctrl+A on Windows or Cmd+A on Mac), then click Ctrl+Enter to load the algorithm into the console.\n", 52 | "4. Enable the debugger by clicking the debugger icon in the editor or console.\n", 53 | "5. Set breakpoints by clicking (in the editor or console) next to the line(s) you want the debugger to stop on. A red dot will appear beside the line.\n", 54 | "6. Run the backtest by typing the `%zipline_backtest` magic command in the console, specifying a start date and end date and, optionally, the bundle name. (Run `%zipline_backtest?` to see the function docstring and all available options.)\n", 55 | "\n", 56 | "```python\n", 57 | "%zipline_backtest -s 2017-01-01 -e 2017-02-01\n", 58 | "```\n", 59 | "7. The backtest will begin to run and the debugger will pause execution at the first breakpoint.\n", 60 | "8. In the Variables section of the Debugger window in JupyterLab, click the magnifying glass next to the variable name to inspect the value of the variable. This will open a new tab displaying the variable's value.\n", 61 | "9. In the Callstack section of the Debugger window, click the Play icon (▷) to resume execution. Execution will resume until the next breakpoint is reached.\n", 62 | "\n", 63 | "See the video below for a step-by-step demonstration." 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": 1, 69 | "metadata": {}, 70 | "outputs": [ 71 | { 72 | "data": { 73 | "text/html": [ 74 | "\n", 75 | " \n", 83 | " " 84 | ], 85 | "text/plain": [ 86 | "" 87 | ] 88 | }, 89 | "execution_count": 1, 90 | "metadata": {}, 91 | "output_type": "execute_result" 92 | } 93 | ], 94 | "source": [ 95 | "from IPython.display import VimeoVideo\n", 96 | "VimeoVideo('925185155', width=700, height=394)" 97 | ] 98 | }, 99 | { 100 | "cell_type": "markdown", 101 | "metadata": {}, 102 | "source": [ 103 | "
⚠️ 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.
" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "***\n", 111 | "\n", 112 | "[Back to Introduction](Introduction.ipynb)" 113 | ] 114 | } 115 | ], 116 | "metadata": { 117 | "kernelspec": { 118 | "display_name": "Python 3.11", 119 | "language": "python", 120 | "name": "python3" 121 | }, 122 | "language_info": { 123 | "codemirror_mode": { 124 | "name": "ipython", 125 | "version": 3 126 | }, 127 | "file_extension": ".py", 128 | "mimetype": "text/x-python", 129 | "name": "python", 130 | "nbconvert_exporter": "python", 131 | "pygments_lexer": "ipython3", 132 | "version": "3.11.0" 133 | } 134 | }, 135 | "nbformat": 4, 136 | "nbformat_minor": 4 137 | } 138 | -------------------------------------------------------------------------------- /intro_zipline/Part5-Zipline-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 | "[Zipline Intro](Introduction.ipynb) › Part 5: Zipline Code\n", 17 | "***" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# Zipline Strategy Code" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "The strategy code is provided in [winners.py](winners.py)." 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "## Install strategy file\n", 39 | "\n", 40 | "To \"install\" the strategy, execute the following cell to move the strategy file to the `/codeload/zipline` directory, where Zipline looks:\n", 41 | "\n", 42 | "> The ! sytax below lets us execute terminal commands from inside the notebook." 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 1, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "# make directory if doesn't exist\n", 52 | "!mkdir -p /codeload/zipline\n", 53 | "\n", 54 | "!mv winners.py /codeload/zipline/" 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "metadata": {}, 60 | "source": [ 61 | "## Strategy overview\n", 62 | "\n", 63 | "We define our pipeline in a helper function called `make_pipeline`, using code copied from an earlier notebook (note that we factor out our 252-day momentum window into a module-level attribute, `MOMENTUM_WINDOW`, which will facilitate running a parameter scan later):\n", 64 | "\n", 65 | "```python\n", 66 | "\n", 67 | "MOMENTUM_WINDOW = 252\n", 68 | "\n", 69 | "def make_pipeline():\n", 70 | " \"\"\"\n", 71 | " Create a pipeline that filters by dollar volume and \n", 72 | " calculates return.\n", 73 | " \"\"\"\n", 74 | " pipeline = Pipeline(\n", 75 | " columns={\n", 76 | " \"returns\": Returns(window_length=MOMENTUM_WINDOW),\n", 77 | " },\n", 78 | " screen=AverageDollarVolume(window_length=30) > 10e6\n", 79 | " )\n", 80 | " return pipeline\n", 81 | "```\n", 82 | "\n", 83 | "In the `initialize` function (required in all Zipline strategies), we attach the pipeline to the algorithm, and we schedule a custom function called `rebalance` that will run every market day 30 minutes before the close:\n", 84 | "\n", 85 | "```python\n", 86 | "def initialize(context: algo.Context):\n", 87 | " \"\"\"\n", 88 | " Called once at the start of a backtest, and once per day in \n", 89 | " live trading.\n", 90 | " \"\"\"\n", 91 | " # Attach the pipeline to the algo\n", 92 | " algo.attach_pipeline(make_pipeline(), 'pipeline')\n", 93 | "\n", 94 | " # Rebalance every day, 30 minutes before market close.\n", 95 | " algo.schedule_function(\n", 96 | " rebalance,\n", 97 | " algo.date_rules.every_day(),\n", 98 | " algo.time_rules.market_close(minutes=30),\n", 99 | " )\n", 100 | " \n", 101 | "```\n", 102 | "\n", 103 | "In `before_trading_start`, another built-in function which Zipline calls once per day before the market opens, we gather the pipeline output for that day and select our winners (copying code from an earlier notebook):\n", 104 | "\n", 105 | "```python\n", 106 | "def before_trading_start(context: algo.Context, data: algo.BarData):\n", 107 | " \"\"\"\n", 108 | " Called every day before market open.\n", 109 | " \"\"\"\n", 110 | " factors = algo.pipeline_output('pipeline')\n", 111 | "\n", 112 | " # Get the top 3 stocks by return\n", 113 | " returns = factors[\"returns\"].sort_values(ascending=False)\n", 114 | " context.winners = returns.index[:3]\n", 115 | "```\n", 116 | "\n", 117 | "Finally, in the custom `rebalance` function which we scheduled to run before the close, we calculate the intraday returns (again copying code from an earlier notebook) and add logic for the entering and exiting of positions:\n", 118 | "\n", 119 | "```python\n", 120 | "def rebalance(context: algo.Context, data: algo.BarData): \n", 121 | " # calculate intraday returns for our winners\n", 122 | " current_prices = data.current(context.winners, \"price\")\n", 123 | " prior_closes = data.history(context.winners, \"close\", 2, \"1d\").iloc[0]\n", 124 | " intraday_returns = (current_prices - prior_closes) / prior_closes\n", 125 | " \n", 126 | " positions = context.portfolio.positions\n", 127 | " \n", 128 | " # Exit positions we no longer want to hold\n", 129 | " for asset, position in positions.items():\n", 130 | " ...\n", 131 | "```\n" 132 | ] 133 | }, 134 | { 135 | "cell_type": "markdown", 136 | "metadata": {}, 137 | "source": [ 138 | "***\n", 139 | "\n", 140 | "## *Next Up*\n", 141 | "\n", 142 | "Part 6: [Zipline Backtest](Part6-Zipline-Backtest.ipynb)" 143 | ] 144 | } 145 | ], 146 | "metadata": { 147 | "kernelspec": { 148 | "display_name": "Python 3.11", 149 | "language": "python", 150 | "name": "python3" 151 | }, 152 | "language_info": { 153 | "codemirror_mode": { 154 | "name": "ipython", 155 | "version": 3 156 | }, 157 | "file_extension": ".py", 158 | "mimetype": "text/x-python", 159 | "name": "python", 160 | "nbconvert_exporter": "python", 161 | "pygments_lexer": "ipython3", 162 | "version": "3.11.0" 163 | } 164 | }, 165 | "nbformat": 4, 166 | "nbformat_minor": 4 167 | } 168 | -------------------------------------------------------------------------------- /intro_zipline/Part4-Intraday-Trading-Rules.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 | "[Zipline Intro](Introduction.ipynb) › Part 4: Intraday Trading Rules\n", 17 | "***" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# Intraday Trading Rules\n", 25 | "\n", 26 | "Our trading strategy will buy recent winners as computed by our end-of-day pipeline. But we will try to improve the strategy performance by adding an intraday twist: we will only buy if the security is down for the day. In other words, we will enter positions on a counter-move. " 27 | ] 28 | }, 29 | { 30 | "cell_type": "markdown", 31 | "metadata": {}, 32 | "source": [ 33 | "Testing the performance of intraday trading rules generally requires running a full backtest. However, to facilitate writing the code, you can access intraday data in a notebook using the same API that is available in a Zipline strategy. This allows you to get your code semantics right before transitioning to a backtest. " 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "metadata": {}, 39 | "source": [ 40 | "## Introducing the Context and Data Objects\n", 41 | "In a Zipline strategy, two parameters are passed to user-defined functions: the `context` parameter, which provides access to account and portfolio details and also allows users to store custom variables about the algorithm's state, and the `data` parameter, which is used to access intraday (and optionally end-of-day) price data:" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": 1, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "import zipline.api as algo\n", 51 | "\n", 52 | "# click on Context or BarData and press control for more details about the object\n", 53 | "def handle_data(context: algo.Context, data: algo.BarData):\n", 54 | " ..." 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "metadata": {}, 60 | "source": [ 61 | "The `data` parameter passed to Zipline functions is always tied to the current simulation minute. That is, if it is currently 2020-07-01 at 3:30 PM within the backtest simulation, the `data` object allows you to query prices as of that minute and looking backward from that minute. \n", 62 | "\n", 63 | "You can access the `data` object in notebooks by specifying a particular \"as-of\" minute you want to use:" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": 2, 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "from zipline.research import get_data\n", 73 | "data = get_data(\"2020-07-01 15:30:00\") # no bundle specified, so the default bundle will be used" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "## Load assets\n", 81 | "\n", 82 | "The `data` object's methods take one or more Zipline assets (`zipline.assets.Asset`) as their first argument. There are two ways to get assets in a notebook.\n", 83 | "\n", 84 | "The first option is to run a pipeline and get the assets from the factor data. For example, using our previous pipeline, we could have accessed assets like this:\n", 85 | "\n", 86 | "```python\n", 87 | "factors = run_pipeline(pipeline, start_date=\"2018-01-01\", end_date=\"2012-01-01\")\n", 88 | "assets = factors.xs(\"2018-01-02\").index\n", 89 | "```\n", 90 | "\n", 91 | "The second option, which we will demonstrate here, is to use the `symbol` function to load asset objects by symbol." 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": 3, 97 | "metadata": {}, 98 | "outputs": [], 99 | "source": [ 100 | "from zipline.research import symbol\n", 101 | "\n", 102 | "aapl = symbol(\"AAPL\") # no bundle specified, so the default bundle will be used\n", 103 | "jnj = symbol(\"JNJ\")" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "Now we can use the `data` object to calculate the intraday return as of the simulation minute we chose:" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": 4, 116 | "metadata": {}, 117 | "outputs": [ 118 | { 119 | "data": { 120 | "text/plain": [ 121 | "Equity(FIBBG000B9XRY4 [AAPL]) -0.000713\n", 122 | "Equity(FIBBG000BMHYD1 [JNJ]) 0.001387\n", 123 | "dtype: float64" 124 | ] 125 | }, 126 | "execution_count": 4, 127 | "metadata": {}, 128 | "output_type": "execute_result" 129 | } 130 | ], 131 | "source": [ 132 | "current_prices = data.current([aapl, jnj], \"price\")\n", 133 | "prior_closes = data.history([aapl, jnj], \"close\", 2, \"1d\").iloc[0]\n", 134 | "intraday_returns = (current_prices - prior_closes) / prior_closes\n", 135 | "intraday_returns" 136 | ] 137 | }, 138 | { 139 | "cell_type": "markdown", 140 | "metadata": {}, 141 | "source": [ 142 | "We will later transfer this snippet of code to our Zipline `.py` file, where it will be used to determine whether to enter." 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "metadata": {}, 148 | "source": [ 149 | "***\n", 150 | "\n", 151 | "## *Next Up*\n", 152 | "\n", 153 | "Part 5: [Zipline Strategy Code](Part5-Zipline-Strategy-Code.ipynb)" 154 | ] 155 | } 156 | ], 157 | "metadata": { 158 | "kernelspec": { 159 | "display_name": "Python 3.11", 160 | "language": "python", 161 | "name": "python3" 162 | }, 163 | "language_info": { 164 | "codemirror_mode": { 165 | "name": "ipython", 166 | "version": 3 167 | }, 168 | "file_extension": ".py", 169 | "mimetype": "text/x-python", 170 | "name": "python", 171 | "nbconvert_exporter": "python", 172 | "pygments_lexer": "ipython3", 173 | "version": "3.11.0" 174 | } 175 | }, 176 | "nbformat": 4, 177 | "nbformat_minor": 4 178 | } 179 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /intro_zipline/Part2-End-of-Day-Trading-Rules.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 | "[Zipline Intro](Introduction.ipynb) › Part 2: End-of-Day Trading Rules\n", 17 | "***" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# End-of-Day Trading Rules" 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. In this notebook we will use Zipline's Pipeline API to research the momentum factor on our sample data." 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "## Zipline Strategy Structure\n", 39 | "\n", 40 | "When developing a Zipline strategy, a good first step is to consider which of your trading rules utilize end-of-day data and which ones utilize intraday data. \n", 41 | "\n", 42 | "A typical Zipline strategy uses the Pipeline API for end-of-day trading logic. Zipline \"pipelines\" run once per trading day before the market opens and are used to compute alpha factors based on prior day data, and also to filter large universes down to a manageable number of securities based on these alpha factors. \n", 43 | "\n", 44 | "A typical Zipline strategy then uses intraday data (which in live trading comes from a real-time data feed) to apply additional trading logic to this filtered universe of securities and make trading decisions throughout the trading day." 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "## Run Pipeline\n", 52 | "You can create and run pipelines interactively in a notebook. Although you could proceed directly to writing your Zipline strategy in a `.py` file, starting in a notebook is a great way to validate your code interactively before transitioning to a backtest. " 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "metadata": {}, 58 | "source": [ 59 | "The first step is to define the pipeline. A pipeline has two main attributes: `columns`, which is used to calculate one or more factors, and `screen`, which is used to filter the pipeline to a subset of securities. Here, we filter the starting universe to include only stocks with 30-day average dollar volume of at least 10 million dollars, and for these securities we calculate a 12-month return:" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": 1, 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "from zipline.pipeline import Pipeline\n", 69 | "from zipline.pipeline.factors import AverageDollarVolume, Returns\n", 70 | "\n", 71 | "pipeline = Pipeline(\n", 72 | " columns={\n", 73 | " \"returns\": Returns(window_length=252),\n", 74 | " },\n", 75 | " screen=AverageDollarVolume(window_length=30) > 10e6\n", 76 | ")" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "metadata": {}, 82 | "source": [ 83 | "The above code merely defines the pipeline but does not return any data. To compute the pipeline, we must run it on our sample data bundle. \n", 84 | "\n", 85 | "Since we will be using the same data bundle repeatedly in our analysis, we can set it as the default bundle to avoid always having to type the name of the bundle:" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 2, 91 | "metadata": {}, 92 | "outputs": [ 93 | { 94 | "data": { 95 | "text/plain": [ 96 | "{'status': 'successfully set default bundle'}" 97 | ] 98 | }, 99 | "execution_count": 2, 100 | "metadata": {}, 101 | "output_type": "execute_result" 102 | } 103 | ], 104 | "source": [ 105 | "from quantrocket.zipline import set_default_bundle\n", 106 | "set_default_bundle(\"usstock-free-1min\")" 107 | ] 108 | }, 109 | { 110 | "cell_type": "markdown", 111 | "metadata": {}, 112 | "source": [ 113 | "And now we run the pipeline:" 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": 3, 119 | "metadata": {}, 120 | "outputs": [ 121 | { 122 | "data": { 123 | "text/html": [ 124 | "
\n", 125 | "\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 | "
returns
dateasset
2018-01-02Equity(FIBBG00B3T3HD3 [AA])0.918447
Equity(FIBBG000B9XRY4 [AAPL])0.484643
Equity(FIBBG000BKZB36 [HD])0.446128
Equity(FIBBG000BMHYD1 [JNJ])0.244252
Equity(FIBBG000BFWKC0 [MON])0.131204
\n", 175 | "
" 176 | ], 177 | "text/plain": [ 178 | " returns\n", 179 | "date asset \n", 180 | "2018-01-02 Equity(FIBBG00B3T3HD3 [AA]) 0.918447\n", 181 | " Equity(FIBBG000B9XRY4 [AAPL]) 0.484643\n", 182 | " Equity(FIBBG000BKZB36 [HD]) 0.446128\n", 183 | " Equity(FIBBG000BMHYD1 [JNJ]) 0.244252\n", 184 | " Equity(FIBBG000BFWKC0 [MON]) 0.131204" 185 | ] 186 | }, 187 | "execution_count": 3, 188 | "metadata": {}, 189 | "output_type": "execute_result" 190 | } 191 | ], 192 | "source": [ 193 | "from zipline.research import run_pipeline\n", 194 | "factors = run_pipeline(pipeline, start_date=\"2018-01-01\", end_date=\"2020-01-01\")\n", 195 | "factors.head()" 196 | ] 197 | }, 198 | { 199 | "cell_type": "markdown", 200 | "metadata": {}, 201 | "source": [ 202 | "For each date in the requested date range, the resulting DataFrame contains a row for each security that passed our `screen` on that date, plus a column for each of our requested factors in `columns`. \n", 203 | "\n", 204 | "\n", 205 | "The `run_pipeline` function is only used in notebooks. In a Zipline strategy, you access pipeline results one date at a time (through the `pipeline_output` function). To get the exact data structure you'll use in Zipline, simply select a single date like this: " 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": 4, 211 | "metadata": {}, 212 | "outputs": [ 213 | { 214 | "data": { 215 | "text/html": [ 216 | "
\n", 217 | "\n", 230 | "\n", 231 | " \n", 232 | " \n", 233 | " \n", 234 | " \n", 235 | " \n", 236 | " \n", 237 | " \n", 238 | " \n", 239 | " \n", 240 | " \n", 241 | " \n", 242 | " \n", 243 | " \n", 244 | " \n", 245 | " \n", 246 | " \n", 247 | " \n", 248 | " \n", 249 | " \n", 250 | " \n", 251 | " \n", 252 | " \n", 253 | " \n", 254 | " \n", 255 | " \n", 256 | " \n", 257 | " \n", 258 | " \n", 259 | " \n", 260 | " \n", 261 | " \n", 262 | " \n", 263 | "
returns
asset
Equity(FIBBG00B3T3HD3 [AA])0.918447
Equity(FIBBG000B9XRY4 [AAPL])0.484643
Equity(FIBBG000BKZB36 [HD])0.446128
Equity(FIBBG000BMHYD1 [JNJ])0.244252
Equity(FIBBG000BFWKC0 [MON])0.131204
\n", 264 | "
" 265 | ], 266 | "text/plain": [ 267 | " returns\n", 268 | "asset \n", 269 | "Equity(FIBBG00B3T3HD3 [AA]) 0.918447\n", 270 | "Equity(FIBBG000B9XRY4 [AAPL]) 0.484643\n", 271 | "Equity(FIBBG000BKZB36 [HD]) 0.446128\n", 272 | "Equity(FIBBG000BMHYD1 [JNJ]) 0.244252\n", 273 | "Equity(FIBBG000BFWKC0 [MON]) 0.131204" 274 | ] 275 | }, 276 | "execution_count": 4, 277 | "metadata": {}, 278 | "output_type": "execute_result" 279 | } 280 | ], 281 | "source": [ 282 | "factors = factors.xs(\"2018-01-02\")\n", 283 | "factors.head()" 284 | ] 285 | }, 286 | { 287 | "cell_type": "markdown", 288 | "metadata": {}, 289 | "source": [ 290 | "By selecting a single day of pipeline output in a notebook, you can go ahead and write the end-of-day trading logic you will use in your strategy, and later transfer it to a `.py` file. Here, we sort the pipeline output by one-year returns and select the top 3 securities. These are the stocks our example strategy will buy." 291 | ] 292 | }, 293 | { 294 | "cell_type": "code", 295 | "execution_count": 5, 296 | "metadata": {}, 297 | "outputs": [ 298 | { 299 | "data": { 300 | "text/plain": [ 301 | "Index([ Equity(FIBBG00B3T3HD3 [AA]), Equity(FIBBG000B9XRY4 [AAPL]),\n", 302 | " Equity(FIBBG000BKZB36 [HD])],\n", 303 | " dtype='object', name='asset')" 304 | ] 305 | }, 306 | "execution_count": 5, 307 | "metadata": {}, 308 | "output_type": "execute_result" 309 | } 310 | ], 311 | "source": [ 312 | "returns = factors[\"returns\"].sort_values(ascending=False)\n", 313 | "winners = returns.index[:3]\n", 314 | "winners" 315 | ] 316 | }, 317 | { 318 | "cell_type": "markdown", 319 | "metadata": {}, 320 | "source": [ 321 | "***\n", 322 | "\n", 323 | "## *Next Up*\n", 324 | "\n", 325 | "Part 3: [End-of-Day Analysis With Alphalens](Part3-End-of-Day-Analysis-With-Alphalens.ipynb)" 326 | ] 327 | } 328 | ], 329 | "metadata": { 330 | "kernelspec": { 331 | "display_name": "Python 3.11", 332 | "language": "python", 333 | "name": "python3" 334 | }, 335 | "language_info": { 336 | "codemirror_mode": { 337 | "name": "ipython", 338 | "version": 3 339 | }, 340 | "file_extension": ".py", 341 | "mimetype": "text/x-python", 342 | "name": "python", 343 | "nbconvert_exporter": "python", 344 | "pygments_lexer": "ipython3", 345 | "version": "3.11.0" 346 | } 347 | }, 348 | "nbformat": 4, 349 | "nbformat_minor": 4 350 | } 351 | --------------------------------------------------------------------------------