├── .qrignore
├── LICENSE.txt
├── README.md
└── fundamental_factors
├── Introduction.ipynb
├── Lesson01-Data-Collection.ipynb
├── Lesson02-Base-Universe.ipynb
├── Lesson03-Basic-Usage.ipynb
├── Lesson04-Periodic-Computations.ipynb
├── Lesson05-Exploratory-Data-Analysis.ipynb
├── Lesson06-Profitability.ipynb
├── Lesson07-Profitability-Growth.ipynb
├── Lesson08-Factor-Values-vs-Factor-Ranks.ipynb
├── Lesson09-Sector-Neutralization.ipynb
├── Lesson10-Macro-Analysis.ipynb
├── Lesson11-Altman-Z-Score.ipynb
├── Lesson12-Multi-Factor-Scores.ipynb
├── __init__.py
└── universe.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 | # fundamental-factors
2 |
3 | Learn how to research fundamental factors using the Pipeline API, Alphalens, and Sharadar US price and fundamental data.
4 |
5 | ## Clone in QuantRocket
6 |
7 | CLI:
8 |
9 | ```shell
10 | quantrocket codeload clone 'fundamental-factors'
11 | ```
12 |
13 | Python:
14 |
15 | ```python
16 | from quantrocket.codeload import clone
17 | clone("fundamental-factors")
18 | ```
19 |
20 | ## Browse in GitHub
21 |
22 | Start here: [fundamental_factors/Introduction.ipynb](fundamental_factors/Introduction.ipynb)
23 |
24 | ***
25 |
26 | Find more code in QuantRocket's [Code Library](https://www.quantrocket.com/code/)
27 |
--------------------------------------------------------------------------------
/fundamental_factors/Introduction.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | " \n",
8 | "Disclaimer"
9 | ]
10 | },
11 | {
12 | "cell_type": "markdown",
13 | "metadata": {
14 | "tags": []
15 | },
16 | "source": [
17 | "# Fundamental Factors\n",
18 | "\n",
19 | "This tutorial demonstrates how to research fundamental factors using the Pipeline API, Alphalens, and Sharadar US price and fundamental data.\n",
20 | "\n",
21 | "Lessons are organized around specific fundamental factors, but the techniques demonstrated can be applied to any factors. "
22 | ]
23 | },
24 | {
25 | "cell_type": "markdown",
26 | "metadata": {},
27 | "source": [
28 | "## Prerequisites\n",
29 | "\n",
30 | "The Pipeline Tutorial is recommended as a prerequisite to this tutorial:"
31 | ]
32 | },
33 | {
34 | "cell_type": "code",
35 | "execution_count": null,
36 | "metadata": {},
37 | "outputs": [],
38 | "source": [
39 | "from quantrocket.codeload import clone\n",
40 | "clone('pipeline-tutorial')"
41 | ]
42 | },
43 | {
44 | "cell_type": "markdown",
45 | "metadata": {},
46 | "source": [
47 | "## Data Permissions\n",
48 | "\n",
49 | "To complete the tutorial, you must have a Sharadar subscription that includes permission for the following datasets:\n",
50 | "\n",
51 | "* US Company Fundamentals\n",
52 | "* End-of-Day US Stock Prices (Fund Prices not required)"
53 | ]
54 | },
55 | {
56 | "cell_type": "markdown",
57 | "metadata": {},
58 | "source": [
59 | "## Contents\n",
60 | "\n",
61 | "### Introduction\n",
62 | "\n",
63 | "* Lesson 1: [Data Collection](Lesson01-Data-Collection.ipynb) - Sharadar price and fundamental data collection steps\n",
64 | "* Lesson 2: [Define a Base Universe](Lesson02-Base-Universe.ipynb) - learn how to define a base universe to use with multiple pipelines\n",
65 | "* Lesson 3: [Basic Usage](Lesson03-Basic-Usage.ipynb) - learn how to choose a dimension and define and derive factors from dataset columns\n",
66 | "* Lesson 4: [Periodic Computations](Lesson04-Periodic-Computations.ipynb) - learn how to create factors that measure the change in fundamental metrics over time\n",
67 | "\n",
68 | "\n",
69 | "### Factor 1: Profitability\n",
70 | "\n",
71 | "* Lesson 5: [Exploratory Data Analysis](Lesson05-Exploratory-Data-Analysis.ipynb) - learn how to get a basic overview of a factor's distribution and characteristics\n",
72 | "* Lesson 6: [Alphalens: Profitability](Lesson06-Profitability.ipynb) - learn how to create and interpret an Alphalens tear sheet, looking at the profitability factor \n",
73 | "* Lesson 7: [Alphalens: Profitability Growth](Lesson07-Profitability-Growth.ipynb) - an additional Alphalens walkthrough that looks at growth in profitability\n",
74 | "\n",
75 | "### Factor 2: Debt-to-Equity Ratio\n",
76 | "\n",
77 | "* Lesson 8: [Factor Values vs Factor Ranks](Lesson08-Factor-Values-vs-Factor-Ranks.ipynb) - learn how ranking factors can reduce the effect of outliers and lead to smaller maximum weights\n",
78 | "* Lesson 9: [Sector Neutralization](Lesson09-Sector-Neutralization.ipynb) - learn techniques that can be used to avoid unintended concentrated bets on sectors\n",
79 | "\n",
80 | "### Factor 3: Financial Distress Indicators\n",
81 | "\n",
82 | "* Lesson 10: [Macro Analysis: The Interest Coverage Ratio and Altman Z-Score](Lesson10-Macro-Analysis.ipynb) - learn how to perform macro analysis of US stocks, using segmented processing to analyze more data than can fit into memory\n",
83 | "* Lesson 11: [Alphalens: Altman Z-Score](Lesson11-Altman-Z-Score.ipynb) - learn how and when to define specific bin edges for your factor instead of using equal-sized quantiles\n",
84 | "* Lesson 12: [Multi-Factor Scores](Lesson12-Multi-Factor-Scores.ipynb) - learn how to combine multiple factors into a single score"
85 | ]
86 | }
87 | ],
88 | "metadata": {
89 | "kernelspec": {
90 | "display_name": "Python 3.11",
91 | "language": "python",
92 | "name": "python3"
93 | },
94 | "language_info": {
95 | "codemirror_mode": {
96 | "name": "ipython",
97 | "version": 3
98 | },
99 | "file_extension": ".py",
100 | "mimetype": "text/x-python",
101 | "name": "python",
102 | "nbconvert_exporter": "python",
103 | "pygments_lexer": "ipython3",
104 | "version": "3.11.0"
105 | }
106 | },
107 | "nbformat": 4,
108 | "nbformat_minor": 4
109 | }
110 |
--------------------------------------------------------------------------------
/fundamental_factors/Lesson01-Data-Collection.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | " \n",
8 | "Disclaimer"
9 | ]
10 | },
11 | {
12 | "attachments": {},
13 | "cell_type": "markdown",
14 | "metadata": {},
15 | "source": [
16 | "***\n",
17 | "[Fundamental Factors](Introduction.ipynb) › Lesson 1: Data Collection\n",
18 | "***"
19 | ]
20 | },
21 | {
22 | "attachments": {},
23 | "cell_type": "markdown",
24 | "metadata": {},
25 | "source": [
26 | "# Sharadar Data Collection \n",
27 | "\n",
28 | "This tutorial utilizes Sharadar price and fundamental data. "
29 | ]
30 | },
31 | {
32 | "cell_type": "markdown",
33 | "metadata": {
34 | "tags": []
35 | },
36 | "source": [
37 | "## Collect Sharadar Fundamentals\n",
38 | "\n",
39 | "First, collect the fundamental data:"
40 | ]
41 | },
42 | {
43 | "cell_type": "code",
44 | "execution_count": 1,
45 | "metadata": {
46 | "tags": []
47 | },
48 | "outputs": [
49 | {
50 | "data": {
51 | "text/plain": [
52 | "{'status': 'the fundamental data will be collected asynchronously'}"
53 | ]
54 | },
55 | "execution_count": 1,
56 | "metadata": {},
57 | "output_type": "execute_result"
58 | }
59 | ],
60 | "source": [
61 | "from quantrocket.fundamental import collect_sharadar_fundamentals\n",
62 | "collect_sharadar_fundamentals()"
63 | ]
64 | },
65 | {
66 | "cell_type": "markdown",
67 | "metadata": {},
68 | "source": [
69 | "Monitor flightlog for completion:\n",
70 | "\n",
71 | "```\n",
72 | "quantrocket.fundamental: INFO Collecting Sharadar US fundamentals\n",
73 | "quantrocket.fundamental: INFO Finished collecting Sharadar US fundamentals\n",
74 | "```"
75 | ]
76 | },
77 | {
78 | "cell_type": "markdown",
79 | "metadata": {
80 | "tags": []
81 | },
82 | "source": [
83 | "## Ingest the Sharadar Zipline Bundle\n",
84 | "\n",
85 | "Next, ingest the Sharadar Zipline bundle. To do so, first create the bundle:"
86 | ]
87 | },
88 | {
89 | "cell_type": "code",
90 | "execution_count": 2,
91 | "metadata": {
92 | "tags": []
93 | },
94 | "outputs": [
95 | {
96 | "data": {
97 | "text/plain": [
98 | "{'status': 'success', 'msg': 'successfully created sharadar-1d bundle'}"
99 | ]
100 | },
101 | "execution_count": 2,
102 | "metadata": {},
103 | "output_type": "execute_result"
104 | }
105 | ],
106 | "source": [
107 | "from quantrocket.zipline import create_sharadar_bundle\n",
108 | "create_sharadar_bundle(\"sharadar-1d\")"
109 | ]
110 | },
111 | {
112 | "cell_type": "markdown",
113 | "metadata": {},
114 | "source": [
115 | "Then, ingest data into the bundle:"
116 | ]
117 | },
118 | {
119 | "cell_type": "code",
120 | "execution_count": 3,
121 | "metadata": {
122 | "tags": []
123 | },
124 | "outputs": [
125 | {
126 | "data": {
127 | "text/plain": [
128 | "{'status': 'the data will be ingested asynchronously'}"
129 | ]
130 | },
131 | "execution_count": 3,
132 | "metadata": {},
133 | "output_type": "execute_result"
134 | }
135 | ],
136 | "source": [
137 | "from quantrocket.zipline import ingest_bundle\n",
138 | "ingest_bundle(\"sharadar-1d\")"
139 | ]
140 | },
141 | {
142 | "cell_type": "markdown",
143 | "metadata": {},
144 | "source": [
145 | "Monitor flightlog for completion:\n",
146 | "\n",
147 | "```\n",
148 | "quantrocket.zipline: INFO [sharadar-1d] Ingesting daily bars for sharadar-1d bundle\n",
149 | "quantrocket.zipline: INFO [sharadar-1d] Ingesting adjustments for sharadar-1d bundle\n",
150 | "quantrocket.zipline: INFO [sharadar-1d] Ingesting assets for sharadar-1d bundle\n",
151 | "quantrocket.zipline: INFO [sharadar-1d] Completed ingesting data for sharadar-1d bundle\n",
152 | "```"
153 | ]
154 | },
155 | {
156 | "cell_type": "markdown",
157 | "metadata": {},
158 | "source": [
159 | "## Set default bundle\n",
160 | "\n",
161 | "Next, set the Sharadar bundle as the default bundle. This is optional but saves us from having to specify the bundle explicitly every time we run the notebook. "
162 | ]
163 | },
164 | {
165 | "cell_type": "code",
166 | "execution_count": 4,
167 | "metadata": {
168 | "tags": []
169 | },
170 | "outputs": [
171 | {
172 | "data": {
173 | "text/plain": [
174 | "{'status': 'successfully set default bundle'}"
175 | ]
176 | },
177 | "execution_count": 4,
178 | "metadata": {},
179 | "output_type": "execute_result"
180 | }
181 | ],
182 | "source": [
183 | "from quantrocket.zipline import set_default_bundle\n",
184 | "set_default_bundle(\"sharadar-1d\")"
185 | ]
186 | },
187 | {
188 | "cell_type": "markdown",
189 | "metadata": {},
190 | "source": [
191 | "***\n",
192 | "\n",
193 | "## *Next Up*\n",
194 | "\n",
195 | "Lesson 2: [Define a Base Universe](Lesson02-Base-Universe.ipynb)"
196 | ]
197 | }
198 | ],
199 | "metadata": {
200 | "kernelspec": {
201 | "display_name": "Python 3.11",
202 | "language": "python",
203 | "name": "python3"
204 | },
205 | "language_info": {
206 | "codemirror_mode": {
207 | "name": "ipython",
208 | "version": 3
209 | },
210 | "file_extension": ".py",
211 | "mimetype": "text/x-python",
212 | "name": "python",
213 | "nbconvert_exporter": "python",
214 | "pygments_lexer": "ipython3",
215 | "version": "3.11.0"
216 | }
217 | },
218 | "nbformat": 4,
219 | "nbformat_minor": 4
220 | }
221 |
--------------------------------------------------------------------------------
/fundamental_factors/Lesson02-Base-Universe.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | " \n",
8 | "Disclaimer"
9 | ]
10 | },
11 | {
12 | "attachments": {},
13 | "cell_type": "markdown",
14 | "metadata": {},
15 | "source": [
16 | "***\n",
17 | "[Fundamental Factors](Introduction.ipynb) › Lesson 2: Define a Base Universe\n",
18 | "***"
19 | ]
20 | },
21 | {
22 | "attachments": {},
23 | "cell_type": "markdown",
24 | "metadata": {},
25 | "source": [
26 | "# Define a Base Universe\n",
27 | "\n",
28 | "Before researching specific factors, we will define a base universe. We don't want to include certain securities such as ETFs and ADRS in any of our subsequent analysis, and by defining a base universe in a separate file, we can import and use the definition in our notebooks without having to re-define the universe rules in each notebook. \n",
29 | "\n",
30 | "The base universe will still be quite broad, for two reasons. First, we can always add more rules to the base rules in any given notebook to narrow the universe. Second, using a broad universe will help us see how factors behave across the US equities market, even if we subsequently wish to narrow the universe for trading or further analysis. "
31 | ]
32 | },
33 | {
34 | "cell_type": "markdown",
35 | "metadata": {
36 | "tags": []
37 | },
38 | "source": [
39 | "## Explore Sharadar Categories\n",
40 | "\n",
41 | "Different types of securities are categorized in the `sharadar_Category` field of the securities master database. Let's query all Sharadar records in the securities master database and group by `sharadar_Category` to see a breakdown of security types. (You can also obtain this information by browsing the sharadar-1d bundle in the Data Browser and looking at the Universe tab.)"
42 | ]
43 | },
44 | {
45 | "cell_type": "code",
46 | "execution_count": 1,
47 | "metadata": {
48 | "tags": []
49 | },
50 | "outputs": [
51 | {
52 | "data": {
53 | "text/plain": [
54 | "sharadar_Category\n",
55 | "ADR 2\n",
56 | "ADR Common Stock 2118\n",
57 | "ADR Common Stock Primary Class 141\n",
58 | "ADR Common Stock Secondary Class 118\n",
59 | "ADR Common Stock Warrant 179\n",
60 | "ADR Preferred 6\n",
61 | "ADR Preferred Stock 96\n",
62 | "ADR Stock Warrant 1\n",
63 | "CEF 1067\n",
64 | "CEF Preferred 63\n",
65 | "CEF Warrant 41\n",
66 | "Canadian 1\n",
67 | "Canadian Common Stock 373\n",
68 | "Canadian Common Stock Primary Class 12\n",
69 | "Canadian Common Stock Secondary Class 3\n",
70 | "Canadian Common Stock Warrant 8\n",
71 | "Canadian Preferred Stock 3\n",
72 | "Canadian Stock Warrant 3\n",
73 | "Domestic 76\n",
74 | "Domestic Common Stock 13861\n",
75 | "Domestic Common Stock Primary Class 1121\n",
76 | "Domestic Common Stock Secondary Class 1098\n",
77 | "Domestic Common Stock Warrant 1455\n",
78 | "Domestic Preferred 45\n",
79 | "Domestic Preferred Stock 1169\n",
80 | "Domestic Primary 1\n",
81 | "Domestic Stock Warrant 208\n",
82 | "Domestic Warrant 16\n",
83 | "ETD 498\n",
84 | "ETF 4992\n",
85 | "ETMF 18\n",
86 | "ETN 403\n",
87 | "IDX 5\n",
88 | "UNIT 25\n",
89 | "Name: Symbol, dtype: int64"
90 | ]
91 | },
92 | "execution_count": 1,
93 | "metadata": {},
94 | "output_type": "execute_result"
95 | }
96 | ],
97 | "source": [
98 | "from quantrocket.master import get_securities\n",
99 | "securities = get_securities(vendors=\"sharadar\", fields=[\"Symbol\", \"sharadar_Category\"])\n",
100 | "\n",
101 | "securities.groupby(\"sharadar_Category\").Symbol.count()"
102 | ]
103 | },
104 | {
105 | "cell_type": "markdown",
106 | "metadata": {},
107 | "source": [
108 | "We will focus on domestic common stocks. Since some companies have multiple share classes, we will exclude \"Domestic Common Stock Secondary Class\". The following Pipeline expression will satisfy these requirements:"
109 | ]
110 | },
111 | {
112 | "cell_type": "code",
113 | "execution_count": 2,
114 | "metadata": {
115 | "tags": []
116 | },
117 | "outputs": [],
118 | "source": [
119 | "from zipline.pipeline import master\n",
120 | "\n",
121 | "category = master.SecuritiesMaster.sharadar_Category.latest\n",
122 | "common_stocks = (\n",
123 | " # domestic common stocks\n",
124 | " category.has_substring(\"Domestic Common\")\n",
125 | " # no secondary shares\n",
126 | " & ~category.has_substring(\"Secondary\")\n",
127 | ")"
128 | ]
129 | },
130 | {
131 | "cell_type": "markdown",
132 | "metadata": {},
133 | "source": [
134 | "Since `sharadar_Category` is a field from the `SecuritiesMaster` Dataset, this filter can be applied as the `initial_universe` argument of our Pipelines in the following notebooks. Applying the filter as `initial_universe` will completely exclude from the pipeline workspace any assets that aren't primary-share common stocks and will provides a speed boost compared to include these rules in the `screen` along with our other rules. For more information on the `initial_universe` parameter and how it relates to `screen`, see the Usage Guide or the Pipeline Tutorial.\n",
135 | "\n",
136 | "The additional filters below cannot be used with `initial_universe` and must be applied separately as the `screen` parameter of the Pipeline (or as a mask to other terms)."
137 | ]
138 | },
139 | {
140 | "cell_type": "markdown",
141 | "metadata": {},
142 | "source": [
143 | "## Liquidity Filter\n",
144 | "\n",
145 | "Even though we want our base universe to be broad and include companies of all sizes, it is still important to add a basic liquidity filter. We will limit the universe to stocks that have had at least some trading volume on each trading day of the past month (approximately 21 trading days). Stocks that have zero trading volume are not only untradable but are also more likely to have suspect prices that can cause unexpected results in Alphalens tear sheets and other analyses. "
146 | ]
147 | },
148 | {
149 | "cell_type": "code",
150 | "execution_count": 3,
151 | "metadata": {
152 | "tags": []
153 | },
154 | "outputs": [],
155 | "source": [
156 | "from zipline.pipeline import EquityPricing\n",
157 | "\n",
158 | "base_universe = (EquityPricing.volume.latest > 0).all(21)"
159 | ]
160 | },
161 | {
162 | "cell_type": "markdown",
163 | "metadata": {},
164 | "source": [
165 | "## Penny Stock Filter\n",
166 | "\n",
167 | "In addition to the liquidity filter, we will also add a rule to filter out penny stocks by requiring that the closing price must be above $1.00 for 21 consecutive days. Penny stocks often undergo dramatic price jumps and price drops that, if included in the analysis, can bias the results and make it harder to interpret overall factor performance. "
168 | ]
169 | },
170 | {
171 | "cell_type": "code",
172 | "execution_count": 4,
173 | "metadata": {
174 | "tags": []
175 | },
176 | "outputs": [],
177 | "source": [
178 | "base_universe = (EquityPricing.close.latest > 1.00).all(21, mask=base_universe)"
179 | ]
180 | },
181 | {
182 | "cell_type": "markdown",
183 | "metadata": {},
184 | "source": [
185 | "## Helper file\n",
186 | "\n",
187 | "To be able to reuse our universe filters, we put them in a separate file, [universe.py](universe.py). The universes can be imported as follows."
188 | ]
189 | },
190 | {
191 | "cell_type": "code",
192 | "execution_count": 5,
193 | "metadata": {
194 | "tags": []
195 | },
196 | "outputs": [],
197 | "source": [
198 | "from codeload.fundamental_factors.universe import CommonStocks, BaseUniverse\n",
199 | "\n",
200 | "initial_universe = CommonStocks()\n",
201 | "base_universe = BaseUniverse()"
202 | ]
203 | },
204 | {
205 | "cell_type": "markdown",
206 | "metadata": {},
207 | "source": [
208 | "The `CommonStocks()` filter will be used as the `initial_universe` of the Pipeline in the following notebooks, while the `BaseUniverse()` filter will be used as the `screen` parameter of the Pipeline and as a mask to various factors, and will sometimes be combined with additional filtering rules. (As a reminder from other tutorials, `screen` supports more kinds of filters than `initial_universe`, but using `initial_universe` reduces the size of the computational universe and thus provides a speed boost compared to using `screen`.)"
209 | ]
210 | },
211 | {
212 | "cell_type": "markdown",
213 | "metadata": {},
214 | "source": [
215 | "***\n",
216 | "\n",
217 | "## *Next Up*\n",
218 | "\n",
219 | "Lesson 3: [Basic Usage](Lesson03-Basic-Usage.ipynb)"
220 | ]
221 | }
222 | ],
223 | "metadata": {
224 | "kernelspec": {
225 | "display_name": "Python 3.11",
226 | "language": "python",
227 | "name": "python3"
228 | },
229 | "language_info": {
230 | "codemirror_mode": {
231 | "name": "ipython",
232 | "version": 3
233 | },
234 | "file_extension": ".py",
235 | "mimetype": "text/x-python",
236 | "name": "python",
237 | "nbconvert_exporter": "python",
238 | "pygments_lexer": "ipython3",
239 | "version": "3.11.0"
240 | }
241 | },
242 | "nbformat": 4,
243 | "nbformat_minor": 4
244 | }
245 |
--------------------------------------------------------------------------------
/fundamental_factors/Lesson03-Basic-Usage.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | " \n",
8 | "Disclaimer"
9 | ]
10 | },
11 | {
12 | "attachments": {},
13 | "cell_type": "markdown",
14 | "metadata": {},
15 | "source": [
16 | "***\n",
17 | "[Fundamental Factors](Introduction.ipynb) › Lesson 3: Basic Usage\n",
18 | "***"
19 | ]
20 | },
21 | {
22 | "attachments": {},
23 | "cell_type": "markdown",
24 | "metadata": {},
25 | "source": [
26 | "# Basic Usage\n"
27 | ]
28 | },
29 | {
30 | "cell_type": "markdown",
31 | "metadata": {
32 | "tags": []
33 | },
34 | "source": [
35 | "## Choosing a dimension\n",
36 | "\n",
37 | "The first step when using Sharadar fundamentals is to decide which \"dimension\" you want to use. There are 6 possible dimensions, resulting from the combination of 3 possible reporting windows and 2 ways of handling restatements. The 3 reporting windows are:\n",
38 | "\n",
39 | "* Q = Quarterly: metrics reflect financial results for the fiscal quarter\n",
40 | "* Y = Annual: metrics reflect financial results for the fiscal year\n",
41 | "* T = Trailing-Twelve Month: metrics reflect financial results for the trailing twelve months (previous 4 quarters)\n",
42 | "\n",
43 | "The 2 ways of handling restatements are:\n",
44 | "\n",
45 | "* AR = As-Reported: metrics reflect the values as originally reported by the company\n",
46 | "* MR = Most Recently Reported: metrics reflect the most recently reported values\n",
47 | "\n",
48 | "For historical research, most quants prefer to use As-Reported data, because it most accurately represents what would have originally been known at the time of trade.\n",
49 | "\n",
50 | "Thus the 6 possible dimensions are:\n",
51 | "\n",
52 | "* ARQ = As-Reported Quarterly\n",
53 | "* ARY = As-Reported Annual\n",
54 | "* ART = As-Reported Trailing-Twelve Month\n",
55 | "* MRQ = Most Recently Reported Quarterly\n",
56 | "* MRY = Most Recently Reported Annual\n",
57 | "* MRT = Most Recently Reported Trailing-Twelve Month\n",
58 | "\n",
59 | "In this notebook, we will use as-reported, trailing-twelve-month fundamentals. To do so, we use the `slice()` method of `zipline.pipeline.sharadar.Fundamentals` to select the desired dimension. `Fundamentals` is a `DataSetFamily`, and calling its `slice()` method returns a `DataSet`:"
60 | ]
61 | },
62 | {
63 | "cell_type": "code",
64 | "execution_count": 1,
65 | "metadata": {
66 | "tags": []
67 | },
68 | "outputs": [],
69 | "source": [
70 | "from zipline.pipeline import sharadar\n",
71 | "\n",
72 | "fundamentals = sharadar.Fundamentals.slice('ART')"
73 | ]
74 | },
75 | {
76 | "cell_type": "markdown",
77 | "metadata": {},
78 | "source": [
79 | "## Using Columns as Factors\n",
80 | "\n",
81 | "Many fundamental factors are directly available as columns in the Sharadar dataset. A list of available factors can be found in the docstring for `Fundamentals`, which can be viewed by clicking on `Fundamentals` in the above cell in JupyterLab and pressing `Ctrl`. Once you have identified a factor of interest, you can use it in Pipeline by accessing its `latest` property. \n",
82 | "\n",
83 | "If we want to look at profitability, one metric we could use is net margin, defined as the ratio of net income to revenue:"
84 | ]
85 | },
86 | {
87 | "cell_type": "code",
88 | "execution_count": 2,
89 | "metadata": {
90 | "tags": []
91 | },
92 | "outputs": [],
93 | "source": [
94 | "net_margin = fundamentals.NETMARGIN.latest"
95 | ]
96 | },
97 | {
98 | "cell_type": "markdown",
99 | "metadata": {},
100 | "source": [
101 | "## Deriving Factors from Multiple Columns\n",
102 | "\n",
103 | "Sometimes, a fundamental metric may not be directly available in the dataset, but you can derive the metric by combining other metrics. For example, operating margin, defined as the ratio of operating income to revenue, is not included in the dataset. But you can derive the metric like this:"
104 | ]
105 | },
106 | {
107 | "cell_type": "code",
108 | "execution_count": 3,
109 | "metadata": {
110 | "tags": []
111 | },
112 | "outputs": [],
113 | "source": [
114 | "operating_margin = fundamentals.OPINC.latest / fundamentals.REVENUE.latest "
115 | ]
116 | },
117 | {
118 | "cell_type": "markdown",
119 | "metadata": {},
120 | "source": [
121 | "***\n",
122 | "\n",
123 | "## *Next Up*\n",
124 | "\n",
125 | "Lesson 4: [Periodic Computations](Lesson04-Periodic-Computations.ipynb)"
126 | ]
127 | }
128 | ],
129 | "metadata": {
130 | "kernelspec": {
131 | "display_name": "Python 3.11",
132 | "language": "python",
133 | "name": "python3"
134 | },
135 | "language_info": {
136 | "codemirror_mode": {
137 | "name": "ipython",
138 | "version": 3
139 | },
140 | "file_extension": ".py",
141 | "mimetype": "text/x-python",
142 | "name": "python",
143 | "nbconvert_exporter": "python",
144 | "pygments_lexer": "ipython3",
145 | "version": "3.11.0"
146 | }
147 | },
148 | "nbformat": 4,
149 | "nbformat_minor": 4
150 | }
151 |
--------------------------------------------------------------------------------
/fundamental_factors/Lesson04-Periodic-Computations.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | " \n",
8 | "Disclaimer"
9 | ]
10 | },
11 | {
12 | "attachments": {},
13 | "cell_type": "markdown",
14 | "metadata": {},
15 | "source": [
16 | "***\n",
17 | "[Fundamental Factors](Introduction.ipynb) › Lesson 4: Periodic Computations\n",
18 | "***"
19 | ]
20 | },
21 | {
22 | "attachments": {},
23 | "cell_type": "markdown",
24 | "metadata": {},
25 | "source": [
26 | "# Periodic Computations\n",
27 | "\n",
28 | "When analyzing price data, it is common to compute changes in prices over time, such as calculating a 52-week high or a 50-day moving average. We call these \"windowed computations\" because they involve looking at a window of data rather than a single price observation. In Pipeline, factors that accept a `window_length` parameter are used to perform windowed computations. For example, `SimpleMovingAverage(inputs=EquityPricing.close, window_length=10)` computes the average of the last 10 days of closing prices.\n",
29 | "\n",
30 | "Just as with price data, it is often useful to compute changes in fundamental values over time. For example, you might want to compute 5-year dividend growth or screen for companies who have consistently grown their earnings over a certain number of quarters. Typical window-based Pipeline factors like `SimpleMovingAverage` aren't suitable for fundamental data because fundamental data changes quarterly, not daily. We don't want to compute the average dividend of the last N days but of the last N quarters.\n",
31 | "\n",
32 | "Pipeline makes it easy to perform computations on multiple quarters or years of fundamental data. These are referred to as \"periodic computations\" because they use fiscal periods rather than the daily values that are used in typical windowed computations like `SimpleMovingAverage`. There are ready-made factors to compute the average, high, low, percent change, or CAGR of a fundamental metric over time, or to screen for companies with metrics that are consistently above or below a certain value (such as consistently positive earnings or dividends), or to screen for consistently increasing or decreasing metrics (such as consistently increasing revenue). "
33 | ]
34 | },
35 | {
36 | "cell_type": "markdown",
37 | "metadata": {
38 | "tags": []
39 | },
40 | "source": [
41 | "## Choosing a period_offset\n",
42 | "\n",
43 | "Before we look at some of the ready-made periodic factors and filters, let's look at the `period_offset` parameter, which forms the basis of all periodic computations. \n",
44 | "\n",
45 | "As we saw in the previous lesson, you must specify a `dimension` when taking a slice of a fundamental dataset:"
46 | ]
47 | },
48 | {
49 | "cell_type": "code",
50 | "execution_count": 1,
51 | "metadata": {
52 | "tags": []
53 | },
54 | "outputs": [],
55 | "source": [
56 | "from zipline.pipeline import sharadar\n",
57 | "\n",
58 | "# ARQ = As-Reported Quarterly fundamentals\n",
59 | "fundamentals = sharadar.Fundamentals.slice('ARQ')"
60 | ]
61 | },
62 | {
63 | "cell_type": "markdown",
64 | "metadata": {},
65 | "source": [
66 | "The `slice()` method also accepts an optional second parameter, `period_offset`. If omitted, as in the above example, `period_offset` defaults to 0, which means that Pipeline will return data for the most recent fiscal period (as of the pipeline simulation date). In contrast, a negative `period_offset` means to return data for a previous fiscal period: -1 means the immediately preceding fiscal period, -2 means two fiscal periods ago, etc. For quarterly and trailing-twelve-month dimensions, previous period means previous quarter, while for annual dimensions, previous period means previous year.\n",
67 | "\n",
68 | "To illustrate the use of `period_offset`, let's look at Microsoft's current and previous EPS. First, we take two slices of `Fundamentals`, one representing the latest period and one representing the previous period, and from these slices create factors for the current and previous EPS:"
69 | ]
70 | },
71 | {
72 | "cell_type": "code",
73 | "execution_count": 2,
74 | "metadata": {
75 | "tags": []
76 | },
77 | "outputs": [],
78 | "source": [
79 | "from zipline.pipeline import sharadar\n",
80 | "\n",
81 | "current_fundamentals = sharadar.Fundamentals.slice('ART', period_offset=0)\n",
82 | "previous_fundamentals = sharadar.Fundamentals.slice('ART', period_offset=-1)\n",
83 | "\n",
84 | "eps = current_fundamentals.EPS.latest\n",
85 | "previous_eps = previous_fundamentals.EPS.latest"
86 | ]
87 | },
88 | {
89 | "cell_type": "markdown",
90 | "metadata": {},
91 | "source": [
92 | "Then, we include the factors as pipeline columns and limit the initial universe to MSFT only. We also include a column with the fiscal period end date for reference:"
93 | ]
94 | },
95 | {
96 | "cell_type": "code",
97 | "execution_count": 3,
98 | "metadata": {
99 | "tags": []
100 | },
101 | "outputs": [],
102 | "source": [
103 | "from zipline.pipeline import Pipeline\n",
104 | "from zipline.pipeline.filters import StaticAssets\n",
105 | "from zipline.research import symbol\n",
106 | "\n",
107 | "MSFT = symbol(\"MSFT\")\n",
108 | "\n",
109 | "pipeline = Pipeline(\n",
110 | " columns={\n",
111 | " 'fiscal_period_end_date': current_fundamentals.CALENDARDATE.latest,\n",
112 | " 'eps': eps,\n",
113 | " 'previous_eps': previous_eps,\n",
114 | " },\n",
115 | " initial_universe=StaticAssets([MSFT])\n",
116 | ")\n"
117 | ]
118 | },
119 | {
120 | "cell_type": "markdown",
121 | "metadata": {},
122 | "source": [
123 | "Finally, we run the pipeline. To see what's going on, we can use `drop_duplicates()` to limit the output to rows where the values changed from the previous row: "
124 | ]
125 | },
126 | {
127 | "cell_type": "code",
128 | "execution_count": 4,
129 | "metadata": {
130 | "tags": []
131 | },
132 | "outputs": [
133 | {
134 | "data": {
135 | "text/html": [
136 | "
\n",
137 | "\n",
150 | "
\n",
151 | " \n",
152 | "
\n",
153 | "
\n",
154 | "
\n",
155 | "
fiscal_period_end_date
\n",
156 | "
eps
\n",
157 | "
previous_eps
\n",
158 | "
\n",
159 | "
\n",
160 | "
date
\n",
161 | "
asset
\n",
162 | "
\n",
163 | "
\n",
164 | "
\n",
165 | "
\n",
166 | " \n",
167 | " \n",
168 | "
\n",
169 | "
2022-01-03
\n",
170 | "
Equity(FIBBG000BPH459 [MSFT])
\n",
171 | "
2021-09-30
\n",
172 | "
9.02
\n",
173 | "
8.12
\n",
174 | "
\n",
175 | "
\n",
176 | "
2022-01-26
\n",
177 | "
Equity(FIBBG000BPH459 [MSFT])
\n",
178 | "
2021-12-31
\n",
179 | "
9.47
\n",
180 | "
9.02
\n",
181 | "
\n",
182 | "
\n",
183 | "
2022-04-27
\n",
184 | "
Equity(FIBBG000BPH459 [MSFT])
\n",
185 | "
2022-03-31
\n",
186 | "
9.65
\n",
187 | "
9.47
\n",
188 | "
\n",
189 | "
\n",
190 | "
2022-07-29
\n",
191 | "
Equity(FIBBG000BPH459 [MSFT])
\n",
192 | "
2022-06-30
\n",
193 | "
9.70
\n",
194 | "
9.65
\n",
195 | "
\n",
196 | "
\n",
197 | "
2022-10-26
\n",
198 | "
Equity(FIBBG000BPH459 [MSFT])
\n",
199 | "
2022-09-30
\n",
200 | "
9.32
\n",
201 | "
9.70
\n",
202 | "
\n",
203 | " \n",
204 | "
\n",
205 | "
"
206 | ],
207 | "text/plain": [
208 | " fiscal_period_end_date ... previous_eps\n",
209 | "date asset ... \n",
210 | "2022-01-03 Equity(FIBBG000BPH459 [MSFT]) 2021-09-30 ... 8.12\n",
211 | "2022-01-26 Equity(FIBBG000BPH459 [MSFT]) 2021-12-31 ... 9.02\n",
212 | "2022-04-27 Equity(FIBBG000BPH459 [MSFT]) 2022-03-31 ... 9.47\n",
213 | "2022-07-29 Equity(FIBBG000BPH459 [MSFT]) 2022-06-30 ... 9.65\n",
214 | "2022-10-26 Equity(FIBBG000BPH459 [MSFT]) 2022-09-30 ... 9.70\n",
215 | "\n",
216 | "[5 rows x 3 columns]"
217 | ]
218 | },
219 | "execution_count": 4,
220 | "metadata": {},
221 | "output_type": "execute_result"
222 | }
223 | ],
224 | "source": [
225 | "from zipline.research import run_pipeline\n",
226 | "\n",
227 | "results = run_pipeline(pipeline, '2022-01-01', '2022-12-31')\n",
228 | "results.drop_duplicates()"
229 | ]
230 | },
231 | {
232 | "cell_type": "markdown",
233 | "metadata": {},
234 | "source": [
235 | "You can see that the `previous_eps` column contains the `eps` column value shifted down from the previous period. \n",
236 | "\n",
237 | "Using `period_offset`, we can do things like compare the current and previous EPS to create a new Filter that computes True if EPS increased from the previous period:"
238 | ]
239 | },
240 | {
241 | "cell_type": "code",
242 | "execution_count": 5,
243 | "metadata": {
244 | "tags": []
245 | },
246 | "outputs": [],
247 | "source": [
248 | "eps_increased = eps > previous_eps"
249 | ]
250 | },
251 | {
252 | "cell_type": "markdown",
253 | "metadata": {},
254 | "source": [
255 | "You can go back an arbitrary number of periods with `period_offset`, and you combine the different periods into arbitrarily complex expressions. Under the hood, this is what Pipeline's built-in periodic factors and filters do."
256 | ]
257 | },
258 | {
259 | "cell_type": "markdown",
260 | "metadata": {},
261 | "source": [
262 | "## Built-In Periodic Factors and Filters\n",
263 | "\n",
264 | "The Pipeline API includes a variety of built-in factors and filters for performing periodic computations. These live in the `zipline.pipeline.periodic` module. To see the full list of available factors, click on `periodic` in the following import statement in JupyterLab and press `Ctrl` to see the module docstring:"
265 | ]
266 | },
267 | {
268 | "cell_type": "code",
269 | "execution_count": 6,
270 | "metadata": {
271 | "tags": []
272 | },
273 | "outputs": [],
274 | "source": [
275 | "from zipline.pipeline import periodic"
276 | ]
277 | },
278 | {
279 | "cell_type": "markdown",
280 | "metadata": {},
281 | "source": [
282 | "Let's create some real-world examples."
283 | ]
284 | },
285 | {
286 | "cell_type": "markdown",
287 | "metadata": {},
288 | "source": [
289 | "### Average Earnings\n",
290 | "\n",
291 | "To smooth out variation in quarterly earnings, we can compute the average EBITDA over the last 4 quarters:"
292 | ]
293 | },
294 | {
295 | "cell_type": "code",
296 | "execution_count": 7,
297 | "metadata": {
298 | "tags": []
299 | },
300 | "outputs": [],
301 | "source": [
302 | "from zipline.pipeline.periodic import PeriodicAverage\n",
303 | "\n",
304 | "fundamentals = sharadar.Fundamentals.slice('ARQ')\n",
305 | "avg_earnings = PeriodicAverage(fundamentals.EBITDA, window_length=4)"
306 | ]
307 | },
308 | {
309 | "cell_type": "markdown",
310 | "metadata": {},
311 | "source": [
312 | "Note that the first argument we pass to `PeriodicAverage()` is the column itself (`fundamentals.EBITDA`), not the `latest` factor of the column (`fundamentals.EBITDA.latest`). This is true of all built-in periodic factors and filters."
313 | ]
314 | },
315 | {
316 | "cell_type": "markdown",
317 | "metadata": {},
318 | "source": [
319 | "### Revenue Growth\n",
320 | "\n",
321 | "We can use `PeriodicCAGR()` to compute the compound annual growth rate of revenue over the last 5 years:"
322 | ]
323 | },
324 | {
325 | "cell_type": "code",
326 | "execution_count": 8,
327 | "metadata": {
328 | "tags": []
329 | },
330 | "outputs": [],
331 | "source": [
332 | "from zipline.pipeline.periodic import PeriodicCAGR\n",
333 | "\n",
334 | "fundamentals = sharadar.Fundamentals.slice('ARY')\n",
335 | "revenue_growth = PeriodicCAGR(fundamentals.REVENUE, window_length=5)"
336 | ]
337 | },
338 | {
339 | "cell_type": "markdown",
340 | "metadata": {},
341 | "source": [
342 | "A similar factor is `PeriodicPercentChange()`, which differs only in that it calculates the total percent change over the window length rather than the annual growth rate."
343 | ]
344 | },
345 | {
346 | "cell_type": "markdown",
347 | "metadata": {},
348 | "source": [
349 | "### Consistent Dividend Payers\n",
350 | "\n",
351 | "In this example, we use `AllPeriodAbove()` to screen for companies that have paid dividends in each of the last 8 years:"
352 | ]
353 | },
354 | {
355 | "cell_type": "code",
356 | "execution_count": 9,
357 | "metadata": {
358 | "tags": []
359 | },
360 | "outputs": [],
361 | "source": [
362 | "from zipline.pipeline.periodic import AllPeriodsAbove\n",
363 | "\n",
364 | "fundamentals = sharadar.Fundamentals.slice('ARY')\n",
365 | "consistently_pay_dividends = AllPeriodsAbove(fundamentals.DPS, 0, window_length=8)"
366 | ]
367 | },
368 | {
369 | "cell_type": "markdown",
370 | "metadata": {},
371 | "source": [
372 | "### Never Cut Dividends\n",
373 | "\n",
374 | "This example builds on the previous one by using `AllPeriodsIncreasing()` to further limit the screen to companies that have never cut their dividends over the 8-year period. We use `allow_equal=True` to allow for equal or increasing dividends, and we provide the previous screen as a mask to limit the computation to dividend payers:"
375 | ]
376 | },
377 | {
378 | "cell_type": "code",
379 | "execution_count": 10,
380 | "metadata": {
381 | "tags": []
382 | },
383 | "outputs": [],
384 | "source": [
385 | "from zipline.pipeline.periodic import AllPeriodsIncreasing\n",
386 | "\n",
387 | "have_never_cut_dividends = AllPeriodsIncreasing(fundamentals.DPS, allow_equal=True, window_length=8, mask=consistently_pay_dividends)"
388 | ]
389 | },
390 | {
391 | "cell_type": "markdown",
392 | "metadata": {},
393 | "source": [
394 | "### EPS versus 4-year High\n",
395 | "\n",
396 | "Suppose we'd like to know how the current EPS compares to the 4-year high of EPS. We can use `PeriodicHigh()` to compute the 4-year high (16 quarters using trailing-twelve-month fundamentals), then compare it to EPS to get a ratio. We use `where()` to limit the output to companies with positive EPS:"
397 | ]
398 | },
399 | {
400 | "cell_type": "code",
401 | "execution_count": 11,
402 | "metadata": {
403 | "tags": []
404 | },
405 | "outputs": [],
406 | "source": [
407 | "from zipline.pipeline.periodic import PeriodicHigh\n",
408 | "\n",
409 | "fundamentals = sharadar.Fundamentals.slice('ART')\n",
410 | "eps = fundamentals.EPS.latest\n",
411 | "high_eps = PeriodicHigh(fundamentals.EPS, window_length=16)\n",
412 | "eps_vs_high = (eps / high_eps).where(eps > 0)"
413 | ]
414 | },
415 | {
416 | "cell_type": "markdown",
417 | "metadata": {},
418 | "source": [
419 | "### Periodic Computations as of Earlier Periods\n",
420 | "\n",
421 | "Let's look at a variation of the previous example. Suppose we want to find companies whose current EPS is higher than any of the previous 16 quarters. To do this, we need to compute the 16-quarter high of EPS *as of the previous quarter*, then see if the current EPS is higher than that. We can calculate the highest EPS as of the previous quarter by using `period_offset` to pass the previous quarter's EPS to `PeriodicHigh()`:"
422 | ]
423 | },
424 | {
425 | "cell_type": "code",
426 | "execution_count": 12,
427 | "metadata": {
428 | "tags": []
429 | },
430 | "outputs": [],
431 | "source": [
432 | "\n",
433 | "current_fundamentals = sharadar.Fundamentals.slice('ART', period_offset=0)\n",
434 | "previous_fundamentals = sharadar.Fundamentals.slice('ART', period_offset=-1)\n",
435 | "\n",
436 | "eps = current_fundamentals.EPS.latest\n",
437 | "previous_high_eps = PeriodicHigh(previous_fundamentals.EPS, window_length=16)\n",
438 | "is_new_high_eps = eps > previous_high_eps"
439 | ]
440 | },
441 | {
442 | "cell_type": "markdown",
443 | "metadata": {},
444 | "source": [
445 | "### Performing Periodic Computations with Derived Factors\n",
446 | "\n",
447 | "So far, we have passed fundamental columns (such as `REVENUE` or `EPS`) directly to the built-in periodic factors. What if we want to perform periodic computations using derived factors, such as operating margin, which as we saw in a previous notebook can be derived as follows:"
448 | ]
449 | },
450 | {
451 | "cell_type": "code",
452 | "execution_count": 13,
453 | "metadata": {
454 | "tags": []
455 | },
456 | "outputs": [],
457 | "source": [
458 | "operating_margin = fundamentals.OPINC.latest / fundamentals.REVENUE.latest "
459 | ]
460 | },
461 | {
462 | "cell_type": "markdown",
463 | "metadata": {},
464 | "source": [
465 | "To use a derived factor with any of the built-in periodic factors or filters, we must create a function that returns the derived factor, then pass the function to the periodic factor or filter. \n",
466 | "\n",
467 | "The function we create must accept two parameters: `period_offset` and `mask`. The function should use the `period_offset` parameter to derive the factor corresponding to that `period_offset`. The function should use the `mask` parameter (if provided) to mask the derived factor it returns. Here is a function that computes operating margin:"
468 | ]
469 | },
470 | {
471 | "cell_type": "code",
472 | "execution_count": 14,
473 | "metadata": {
474 | "tags": []
475 | },
476 | "outputs": [],
477 | "source": [
478 | "def OPMARGIN(period_offset=0, mask=None):\n",
479 | " fundamentals = sharadar.Fundamentals.slice(\"ART\", period_offset)\n",
480 | " operating_margin = fundamentals.OPINC.latest / fundamentals.REVENUE.latest\n",
481 | " if mask is not None:\n",
482 | " operating_margin = operating_margin.where(mask)\n",
483 | " return operating_margin"
484 | ]
485 | },
486 | {
487 | "cell_type": "markdown",
488 | "metadata": {},
489 | "source": [
490 | "We can now pass the `OPMARGIN` function to any of the built-in periodic factors and filters, just as we would pass a data column. Here, we compute the lowest and highest operating margin over the last 4 quarters: "
491 | ]
492 | },
493 | {
494 | "cell_type": "code",
495 | "execution_count": 15,
496 | "metadata": {
497 | "tags": []
498 | },
499 | "outputs": [],
500 | "source": [
501 | "from zipline.pipeline.periodic import PeriodicLow, PeriodicHigh\n",
502 | "\n",
503 | "high_opmargin = PeriodicHigh(OPMARGIN, window_length=4)\n",
504 | "low_opmargin = PeriodicLow(OPMARGIN, window_length=4)"
505 | ]
506 | },
507 | {
508 | "cell_type": "markdown",
509 | "metadata": {},
510 | "source": [
511 | "Make sure to pass the function itself to the periodic factor or filter, not the result of calling the function (`OPMARGIN`, not `OPMARGIN()`).\n",
512 | "\n",
513 | "If you were to pass a `mask` to `PeriodicHigh()` or `PeriodicLow()`, that mask would be passed in turn to your `OPMARGIN` function. If you don't pass a `mask` to `PeriodicHigh()` or `PeriodicLow()`, no mask will be passed to your `OPMARGIN` function. Regardless of whether you intend to pass a mask or not, your `OPMARGIN` function must accept a `mask` parameter. "
514 | ]
515 | },
516 | {
517 | "cell_type": "markdown",
518 | "metadata": {
519 | "tags": []
520 | },
521 | "source": [
522 | "***\n",
523 | "\n",
524 | "## *Next Up*\n",
525 | "\n",
526 | "Lesson 5: [Exploratory Data Analysis](Lesson05-Exploratory-Data-Analysis.ipynb)"
527 | ]
528 | }
529 | ],
530 | "metadata": {
531 | "kernelspec": {
532 | "display_name": "Python 3.11",
533 | "language": "python",
534 | "name": "python3"
535 | },
536 | "language_info": {
537 | "codemirror_mode": {
538 | "name": "ipython",
539 | "version": 3
540 | },
541 | "file_extension": ".py",
542 | "mimetype": "text/x-python",
543 | "name": "python",
544 | "nbconvert_exporter": "python",
545 | "pygments_lexer": "ipython3",
546 | "version": "3.11.0"
547 | }
548 | },
549 | "nbformat": 4,
550 | "nbformat_minor": 4
551 | }
552 |
--------------------------------------------------------------------------------
/fundamental_factors/Lesson05-Exploratory-Data-Analysis.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | " \n",
8 | "Disclaimer"
9 | ]
10 | },
11 | {
12 | "attachments": {},
13 | "cell_type": "markdown",
14 | "metadata": {},
15 | "source": [
16 | "***\n",
17 | "[Fundamental Factors](Introduction.ipynb) › Lesson 5: Exploratory Data Analysis\n",
18 | "***"
19 | ]
20 | },
21 | {
22 | "attachments": {},
23 | "cell_type": "markdown",
24 | "metadata": {},
25 | "source": [
26 | "# Exploratory Data Analysis\n",
27 | "\n",
28 | "Pipeline output can be fed into a Zipline strategy or analyzed with Alphalens. However, a useful first step in most cases is to explore the data to get a basic sense of the data's distribution and characteristics. This can often highlight ways that you must massage the data or tweak your computations to achieve the desired results. This process is referred to as Exploratory Data Analysis (EDA).\n"
29 | ]
30 | },
31 | {
32 | "cell_type": "markdown",
33 | "metadata": {},
34 | "source": [
35 | "## Exploring the Profitability Factor\n",
36 | "\n",
37 | "In an earlier lesson, we defined a factor for operating margin, defined as operating income divided by revenue. Operating margin is a measure of profitability. If a company has an operating margin of 10% (0.1), this means that for every dollar of revenue, the company earns 10 cents in operating income.\n",
38 | "\n",
39 | "> Although these lessons will refer to operating margin as the profitability factor, note that operating margin is a different profitability measure than the [Fama-French profitability factor](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2287202). The main differences are that the Fama-French version subtracts interest from revenue while operating margin does not, and the Fama-French version divides the numerator by book equity while operating margin divides by revenue. The choice of operating margin in these lessons is pedagogical and does not constitute an endorsement of one formula over the other. The Fama-French profitability factor can be computed with the following Sharadar fields: `(REVENUE - COR - INTEXP - SGNA) / EQUITY`\n",
40 | "\n",
41 | "\n",
42 | "Suppose we wanted to use Alphalens to assess the suitability of including operating margin in a long-short or long-only strategy. What might our exploratory data analysis look like? \n",
43 | "\n",
44 | "Let's begin by creating a pipeline with this factor, importing our universe filters from lesson 2: "
45 | ]
46 | },
47 | {
48 | "cell_type": "code",
49 | "execution_count": 1,
50 | "metadata": {
51 | "tags": []
52 | },
53 | "outputs": [],
54 | "source": [
55 | "from zipline.pipeline import Pipeline, sharadar\n",
56 | "from codeload.fundamental_factors.universe import CommonStocks, BaseUniverse\n",
57 | "\n",
58 | "fundamentals = sharadar.Fundamentals.slice('ART')\n",
59 | "\n",
60 | "operating_margin = fundamentals.OPINC.latest / fundamentals.REVENUE.latest \n",
61 | "\n",
62 | "pipeline = Pipeline(\n",
63 | " columns={\n",
64 | " 'operating_margin': operating_margin,\n",
65 | " },\n",
66 | " initial_universe=CommonStocks(),\n",
67 | " screen=BaseUniverse()\n",
68 | ")"
69 | ]
70 | },
71 | {
72 | "cell_type": "markdown",
73 | "metadata": {},
74 | "source": [
75 | "Then, we run the pipeline with a year of data. A year (or even less) is often sufficient for the purpose of exploratory data analysis, since the purpose is simply to get a basic understanding of the data distribution and characteristics. In contrast, longer date ranges are beneficial when analyzing factor performance with Alphalens or Zipline, in order to see how the factor performs over time."
76 | ]
77 | },
78 | {
79 | "cell_type": "code",
80 | "execution_count": 2,
81 | "metadata": {},
82 | "outputs": [],
83 | "source": [
84 | "from zipline.research import run_pipeline\n",
85 | "\n",
86 | "results = run_pipeline(pipeline, '2022-01-01', '2022-12-31')"
87 | ]
88 | },
89 | {
90 | "cell_type": "markdown",
91 | "metadata": {},
92 | "source": [
93 | "## Summarizing the data with pandas' `describe()` method\n",
94 | "\n",
95 | "An easy and fail-safe first way to explore the data is using pandas's `describe()` method, which computes summary statistics for each column in the DataFrame. We will visualize the data distribution with a histogram later, but `describe()` is nice because it works for any kind of data and doesn't require us to think in advance about what kind of plot is best suited to the data, a task that can be tricky when we don't yet have a basic understanding of the data distribution."
96 | ]
97 | },
98 | {
99 | "cell_type": "code",
100 | "execution_count": 3,
101 | "metadata": {},
102 | "outputs": [
103 | {
104 | "name": "stderr",
105 | "output_type": "stream",
106 | "text": [
107 | "/opt/conda/lib/python3.11/site-packages/numpy/core/_methods.py:49: RuntimeWarning: invalid value encountered in reduce\n",
108 | " return umr_sum(a, axis, dtype, out, keepdims, initial, where)\n"
109 | ]
110 | },
111 | {
112 | "data": {
113 | "text/html": [
114 | "
\n",
115 | "\n",
128 | "
\n",
129 | " \n",
130 | "
\n",
131 | "
\n",
132 | "
operating_margin
\n",
133 | "
\n",
134 | " \n",
135 | " \n",
136 | "
\n",
137 | "
count
\n",
138 | "
1.033654e+06
\n",
139 | "
\n",
140 | "
\n",
141 | "
mean
\n",
142 | "
NaN
\n",
143 | "
\n",
144 | "
\n",
145 | "
std
\n",
146 | "
NaN
\n",
147 | "
\n",
148 | "
\n",
149 | "
min
\n",
150 | "
-inf
\n",
151 | "
\n",
152 | "
\n",
153 | "
25%
\n",
154 | "
-2.414564e-01
\n",
155 | "
\n",
156 | "
\n",
157 | "
50%
\n",
158 | "
6.528241e-02
\n",
159 | "
\n",
160 | "
\n",
161 | "
75%
\n",
162 | "
2.049988e-01
\n",
163 | "
\n",
164 | "
\n",
165 | "
max
\n",
166 | "
inf
\n",
167 | "
\n",
168 | " \n",
169 | "
\n",
170 | "
"
171 | ],
172 | "text/plain": [
173 | " operating_margin\n",
174 | "count 1.033654e+06\n",
175 | "mean NaN\n",
176 | "std NaN\n",
177 | "min -inf\n",
178 | "25% -2.414564e-01\n",
179 | "50% 6.528241e-02\n",
180 | "75% 2.049988e-01\n",
181 | "max inf"
182 | ]
183 | },
184 | "execution_count": 3,
185 | "metadata": {},
186 | "output_type": "execute_result"
187 | }
188 | ],
189 | "source": [
190 | "results.describe()"
191 | ]
192 | },
193 | {
194 | "cell_type": "markdown",
195 | "metadata": {},
196 | "source": [
197 | "We immediately notice the `NaN` and `inf` values in the `describe()` output. What's going on here? This is happening because we are dividing operating income by revenue, and revenue can be 0. Dividing by zero causes numpy to compute +/-infinity for max and min, and the `inf` values cause the `NaN` values for mean and std. A quick check of `describe()` has highlighted a problem that we should correct before going any further. \n",
198 | "\n",
199 | "A simple solution is to use the Factor `where()` method to ignore observations where revenue is 0. The `where()` method takes a Filter (Pipeline's version of a boolean condition) as its first argument, and returns a new Factor with only those values where the Filter is True. A replacement value can be passed as an optional second argument, but if this is omitted, as we do below, the Factor will be masked with `NaN` values where the Filter is False. These `NaN`s will then be ignored in subsequent analyses.\n",
200 | "\n",
201 | "We re-write the operating margin factor to ignore observations with no revenue as follows:"
202 | ]
203 | },
204 | {
205 | "cell_type": "code",
206 | "execution_count": 4,
207 | "metadata": {
208 | "tags": [],
209 | "vscode": {
210 | "languageId": "python"
211 | }
212 | },
213 | "outputs": [],
214 | "source": [
215 | "revenue = fundamentals.REVENUE.latest\n",
216 | "operating_margin = fundamentals.OPINC.latest / revenue.where(revenue > 0) \n",
217 | "\n",
218 | "pipeline = Pipeline(\n",
219 | " columns={\n",
220 | " 'operating_margin': operating_margin,\n",
221 | " },\n",
222 | " initial_universe=CommonStocks(),\n",
223 | " screen=BaseUniverse()\n",
224 | ")"
225 | ]
226 | },
227 | {
228 | "cell_type": "markdown",
229 | "metadata": {},
230 | "source": [
231 | "Re-running the pipeline and `describe()` method, we see that the `NaN` and `inf` values are gone:"
232 | ]
233 | },
234 | {
235 | "cell_type": "code",
236 | "execution_count": 5,
237 | "metadata": {
238 | "tags": [],
239 | "vscode": {
240 | "languageId": "python"
241 | }
242 | },
243 | "outputs": [
244 | {
245 | "data": {
246 | "text/html": [
247 | "
\n",
248 | "\n",
261 | "
\n",
262 | " \n",
263 | "
\n",
264 | "
\n",
265 | "
operating_margin
\n",
266 | "
\n",
267 | " \n",
268 | " \n",
269 | "
\n",
270 | "
count
\n",
271 | "
945562.000000
\n",
272 | "
\n",
273 | "
\n",
274 | "
mean
\n",
275 | "
-40.700615
\n",
276 | "
\n",
277 | "
\n",
278 | "
std
\n",
279 | "
4985.586530
\n",
280 | "
\n",
281 | "
\n",
282 | "
min
\n",
283 | "
-841260.400000
\n",
284 | "
\n",
285 | "
\n",
286 | "
25%
\n",
287 | "
-0.067687
\n",
288 | "
\n",
289 | "
\n",
290 | "
50%
\n",
291 | "
0.081445
\n",
292 | "
\n",
293 | "
\n",
294 | "
75%
\n",
295 | "
0.222025
\n",
296 | "
\n",
297 | "
\n",
298 | "
max
\n",
299 | "
7.001749
\n",
300 | "
\n",
301 | " \n",
302 | "
\n",
303 | "
"
304 | ],
305 | "text/plain": [
306 | " operating_margin\n",
307 | "count 945562.000000\n",
308 | "mean -40.700615\n",
309 | "std 4985.586530\n",
310 | "min -841260.400000\n",
311 | "25% -0.067687\n",
312 | "50% 0.081445\n",
313 | "75% 0.222025\n",
314 | "max 7.001749"
315 | ]
316 | },
317 | "execution_count": 5,
318 | "metadata": {},
319 | "output_type": "execute_result"
320 | }
321 | ],
322 | "source": [
323 | "results = run_pipeline(pipeline, '2022-01-01', '2022-12-31')\n",
324 | "results.describe()"
325 | ]
326 | },
327 | {
328 | "cell_type": "markdown",
329 | "metadata": {},
330 | "source": [
331 | "What else can we learn from `describe()`? Simplistically, we can think of profit margin as the amount of revenue left over after paying expenses. A company with no revenue left over after expenses would have a profit margin of 0, while a company with no expenses would have a profit margin of 1. But `describe()` reminds us that operating margin is not bounded by 0 and 1. First of all, operating income can be negative, so operating margin can also be negative: a company can spend arbitrarily more than it brings in as revenue. This isn't surprising or unintuitive but it is a useful reminder that our universe currently doesn't just include profitable companies with wider or narrower profit margins but also unprofitable companies that are losing varying amounts of money. Depending on our goals, we may want to include those unprofitable companies in our analysis or exclude them. \n",
332 | "\n",
333 | "A second revelation of `describe()` is more puzzling: operating margin can be greater than 1. This violates the intuitive understanding of profit margin as the amount of revenue left over after paying expenses. How can there be more than 100% of revenue left over after paying expenses? \n",
334 | "\n",
335 | "To see what's going on, we should look at some specific examples. We'll re-run the pipeline, but this time we'll screen for stocks with operating margin greater than 1, and we will include in the output all of the relevant columns that form the basis of operating margin, to help us see where the unexpected result is coming from. To know what columns are relevant, you can start by adding `REVENUE` and `OPINC`, which are the basis of operating margin, then clicking on them in JupyterLab and pressing CTRL to view their definitions, which will show you the names of any underlying columns from which they're derived. `REVENUE` comes directly from the income statement, while `OPINC` is derived from `GP` (gross profit) and `OPEX` (operating expenses). `GP`, in turn, depends on `COR` (cost of revenue).\n",
336 | "\n",
337 | "We run a pipeline with these columns and look at a few of the results:"
338 | ]
339 | },
340 | {
341 | "cell_type": "code",
342 | "execution_count": 6,
343 | "metadata": {
344 | "tags": [],
345 | "vscode": {
346 | "languageId": "python"
347 | }
348 | },
349 | "outputs": [
350 | {
351 | "data": {
352 | "text/html": [
353 | "
\n",
354 | "\n",
367 | "
\n",
368 | " \n",
369 | "
\n",
370 | "
\n",
371 | "
\n",
372 | "
operating_margin
\n",
373 | "
revenue
\n",
374 | "
operating_income
\n",
375 | "
gross_profit
\n",
376 | "
cost_of_revenue
\n",
377 | "
operating_expenses
\n",
378 | "
\n",
379 | "
\n",
380 | "
date
\n",
381 | "
asset
\n",
382 | "
\n",
383 | "
\n",
384 | "
\n",
385 | "
\n",
386 | "
\n",
387 | "
\n",
388 | "
\n",
389 | " \n",
390 | " \n",
391 | "
\n",
392 | "
2023-01-03
\n",
393 | "
Equity(FIBBG000D6DM44 [STRS])
\n",
394 | "
7.001749
\n",
395 | "
10866000.0
\n",
396 | "
76081000.0
\n",
397 | "
8507000.0
\n",
398 | "
2359000.0
\n",
399 | "
-67574000.0
\n",
400 | "
\n",
401 | "
\n",
402 | "
2022-08-29
\n",
403 | "
Equity(FIBBG000D6DM44 [STRS])
\n",
404 | "
4.515059
\n",
405 | "
16369000.0
\n",
406 | "
73907000.0
\n",
407 | "
8058000.0
\n",
408 | "
8311000.0
\n",
409 | "
-65849000.0
\n",
410 | "
\n",
411 | "
\n",
412 | "
2022-06-03
\n",
413 | "
Equity(FIBBG000D6DM44 [STRS])
\n",
414 | "
3.936445
\n",
415 | "
16820000.0
\n",
416 | "
66211000.0
\n",
417 | "
3317000.0
\n",
418 | "
13503000.0
\n",
419 | "
-62894000.0
\n",
420 | "
\n",
421 | "
\n",
422 | "
2022-04-01
\n",
423 | "
Equity(FIBBG000D6DM44 [STRS])
\n",
424 | "
2.962884
\n",
425 | "
28236000.0
\n",
426 | "
83660000.0
\n",
427 | "
4024000.0
\n",
428 | "
24212000.0
\n",
429 | "
-79636000.0
\n",
430 | "
\n",
431 | "
\n",
432 | "
2022-12-19
\n",
433 | "
Equity(FIBBG004HQHKK0 [AMBC])
\n",
434 | "
1.517751
\n",
435 | "
338000000.0
\n",
436 | "
513000000.0
\n",
437 | "
694000000.0
\n",
438 | "
-356000000.0
\n",
439 | "
181000000.0
\n",
440 | "
\n",
441 | " \n",
442 | "
\n",
443 | "
"
444 | ],
445 | "text/plain": [
446 | " operating_margin ... operating_expenses\n",
447 | "date asset ... \n",
448 | "2023-01-03 Equity(FIBBG000D6DM44 [STRS]) 7.001749 ... -67574000.0\n",
449 | "2022-08-29 Equity(FIBBG000D6DM44 [STRS]) 4.515059 ... -65849000.0\n",
450 | "2022-06-03 Equity(FIBBG000D6DM44 [STRS]) 3.936445 ... -62894000.0\n",
451 | "2022-04-01 Equity(FIBBG000D6DM44 [STRS]) 2.962884 ... -79636000.0\n",
452 | "2022-12-19 Equity(FIBBG004HQHKK0 [AMBC]) 1.517751 ... 181000000.0\n",
453 | "\n",
454 | "[5 rows x 6 columns]"
455 | ]
456 | },
457 | "execution_count": 6,
458 | "metadata": {},
459 | "output_type": "execute_result"
460 | }
461 | ],
462 | "source": [
463 | "pipeline = Pipeline(\n",
464 | " columns={\n",
465 | " 'operating_margin': operating_margin, # operating_margin = OPINC / REVENUE\n",
466 | " 'revenue': fundamentals.REVENUE.latest,\n",
467 | " 'operating_income': fundamentals.OPINC.latest, # OPINC = GP - OPEX\n",
468 | " 'gross_profit': fundamentals.GP.latest, # GP = REVENUE - COR\n",
469 | " 'cost_of_revenue': fundamentals.COR.latest,\n",
470 | " 'operating_expenses': fundamentals.OPEX.latest\n",
471 | " },\n",
472 | " initial_universe=CommonStocks(),\n",
473 | " screen=BaseUniverse() & (operating_margin > 1)\n",
474 | ")\n",
475 | "\n",
476 | "results = run_pipeline(pipeline, '2022-01-01', '2022-12-31')\n",
477 | "results.sort_values('operating_margin', ascending=False).drop_duplicates().head()"
478 | ]
479 | },
480 | {
481 | "cell_type": "markdown",
482 | "metadata": {},
483 | "source": [
484 | "In these examples, operating expenses or cost of revenue is negative, which accounts for operating margin being greater than 1. A negative operating expense or cost of revenue is unexpected and may indicate an unusual one-time accounting adjustment made by the company; further investigation (such as viewing the full report on the SEC website) would be required to determine with certainty. Regardless, for the purpose of our profitability analysis, we probably don't want to treat a company with negative operating expenses or negative cost of revenue as though it were extraordinarily profitable. Therefore, we can refine our profitability factor further by excluding these companies."
485 | ]
486 | },
487 | {
488 | "cell_type": "code",
489 | "execution_count": 7,
490 | "metadata": {
491 | "tags": [],
492 | "vscode": {
493 | "languageId": "python"
494 | }
495 | },
496 | "outputs": [],
497 | "source": [
498 | "revenue = fundamentals.REVENUE.latest\n",
499 | "operating_margin = fundamentals.OPINC.latest / revenue.where(revenue > 0) \n",
500 | "\n",
501 | "# exclude companies with negative operating expenses or negative cost of revenue\n",
502 | "opex = fundamentals.OPEX.latest\n",
503 | "cor = fundamentals.COR.latest\n",
504 | "operating_margin = operating_margin.where((opex > 0) & (cor > 0))\n",
505 | "\n",
506 | "pipeline = Pipeline(\n",
507 | " columns={\n",
508 | " 'operating_margin': operating_margin,\n",
509 | " },\n",
510 | " initial_universe=CommonStocks(),\n",
511 | " screen=BaseUniverse()\n",
512 | ")"
513 | ]
514 | },
515 | {
516 | "cell_type": "markdown",
517 | "metadata": {},
518 | "source": [
519 | "Re-running the refined pipeline, the output from `describe()` conforms better to expectations, as the maximum operating margin is now slightly below 1.0:"
520 | ]
521 | },
522 | {
523 | "cell_type": "code",
524 | "execution_count": 8,
525 | "metadata": {
526 | "tags": [],
527 | "vscode": {
528 | "languageId": "python"
529 | }
530 | },
531 | "outputs": [
532 | {
533 | "data": {
534 | "text/html": [
535 | "
\n",
536 | "\n",
549 | "
\n",
550 | " \n",
551 | "
\n",
552 | "
\n",
553 | "
operating_margin
\n",
554 | "
\n",
555 | " \n",
556 | " \n",
557 | "
\n",
558 | "
count
\n",
559 | "
778943.000000
\n",
560 | "
\n",
561 | "
\n",
562 | "
mean
\n",
563 | "
-40.017840
\n",
564 | "
\n",
565 | "
\n",
566 | "
std
\n",
567 | "
5476.397958
\n",
568 | "
\n",
569 | "
\n",
570 | "
min
\n",
571 | "
-841260.400000
\n",
572 | "
\n",
573 | "
\n",
574 | "
25%
\n",
575 | "
-0.065510
\n",
576 | "
\n",
577 | "
\n",
578 | "
50%
\n",
579 | "
0.067233
\n",
580 | "
\n",
581 | "
\n",
582 | "
75%
\n",
583 | "
0.165055
\n",
584 | "
\n",
585 | "
\n",
586 | "
max
\n",
587 | "
0.965168
\n",
588 | "
\n",
589 | " \n",
590 | "
\n",
591 | "
"
592 | ],
593 | "text/plain": [
594 | " operating_margin\n",
595 | "count 778943.000000\n",
596 | "mean -40.017840\n",
597 | "std 5476.397958\n",
598 | "min -841260.400000\n",
599 | "25% -0.065510\n",
600 | "50% 0.067233\n",
601 | "75% 0.165055\n",
602 | "max 0.965168"
603 | ]
604 | },
605 | "execution_count": 8,
606 | "metadata": {},
607 | "output_type": "execute_result"
608 | }
609 | ],
610 | "source": [
611 | "results = run_pipeline(pipeline, '2022-01-01', '2022-12-31')\n",
612 | "results.describe()"
613 | ]
614 | },
615 | {
616 | "cell_type": "markdown",
617 | "metadata": {},
618 | "source": [
619 | "\n",
620 | "## Visualizing the data distribution\n",
621 | "\n",
622 | "Now that we've refined our factor to exclude unusual cases, let's look at a histogram of operating margin to get a better feel for its distribution. We can use pandas's `plot.hist()` method to do so. However, the plot we get for the data on the first try is not very informative, as all the observations are crammed into a single bin: "
623 | ]
624 | },
625 | {
626 | "cell_type": "code",
627 | "execution_count": 9,
628 | "metadata": {
629 | "tags": [],
630 | "vscode": {
631 | "languageId": "python"
632 | }
633 | },
634 | "outputs": [
635 | {
636 | "data": {
637 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAA88AAAFuCAYAAAC/VCqhAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAAsTAAALEwEAmpwYAAArr0lEQVR4nO3dfXScdZ03/ncypYVCa5uQlgCuWG+EKKusgA8s3ioVUjRQdl3vsFnxqDwoiKCi2wrSyoPrBuUgAio3CNxicd39rTxFpSiw4MrqIiqC4UFq6/IQmjYtpi1I6WR+f7Bml7XtlZZmJum8Xuf0nM71mZnr0+l8zuSd7zXX1VCpVCoBAAAANqmx1g0AAADAWCc8AwAAQAHhGQAAAAoIzwAAAFBAeAYAAIACwjMAAAAUEJ4BAACgwIRaNzAerV69LkNDLo+9NZqbd8nAwNpatwE1Ywaod2aAemcGqHdjfQYaGxsyffrOG60Jz1thaKgiPL8IXjvqnRmg3pkB6p0ZoN6N1xmo2mHbt99+e44++ujMnTs3Rx55ZG655ZYkydKlS9PZ2Zn29vZ0dnZm2bJlw4+pdg0AAAA2pirhuVKp5G//9m9z/vnn54YbbsjnP//5zJs3L0NDQ1m4cGG6urqyePHidHV1ZcGCBcOPq3YNAAAANqZqh203NjZmzZo1SZI1a9ZkxowZWb16dXp7e3PVVVclSTo6OnLuuedm1apVqVQqVa01NTVt9b+tXN6Q1atXZMOG9Vv9HPWiv78xQ0NDtW6j6iZMmJjp01tSKvmmBAAAjEdV+Um+oaEhX/ziF3PyySdn8uTJWbduXS677LL09fVl5syZKZVKSZJSqZQZM2akr68vlUqlqrUXE55Xr16RHXecnJ133i0NDQ0v5qXa7k2Y0JgNG+orPFcqlaxbN5jVq1dk111ba90OAACwFaoSnjds2JDLLrssX/7yl3PAAQfknnvuycc+9rGcf/751dj9NtfcvMsLbvf3P5qXvGSa4DxCEybU3xXSXvKSaXn66cG0tEypdSuMAd4H1DszQL0zA9S78ToDVQnPDzzwQPr7+3PAAQckSQ444IDstNNOmTRpUpYvX55yuZxSqZRyuZz+/v60tramUqlUtbYlBgbWvuAMcUNDQymXK0nG51njqqkeV57/YGhoKCtWrKl1G9RYS8sU7wPqmhmg3pkB6t1Yn4HGxoY/WiwdrlWjgd122y1PPvlkfvOb3yRJlixZkpUrV+ZlL3tZ2tra0tPTkyTp6elJW1tbmpqa0tzcXNUaAAAAbEpDpVKpynLpjTfemMsvv3z40OZTTz01b3/727NkyZLMnz8/g4ODmTp1arq7uzNr1qwkqXptpP7nyvOTT/42u+32shfcZ8rUnbLjpG2/sP/7ZzdkzeAz2/x5q6WeV5439j6h/oz137bCaDMD1DszQL0b6zOwuZXnqoXn7clIwnNLy5QcefoN23zfN10wd0y/2ZLka1+7LO997weyww47JEmuuOKrefnLZ2X27MPHfXheuXJFzj7707n44su2+LHCM8nY/8CA0WYGqHdmgHo31meg5odts30pl8ubrV911eV57rnnhm8ff/yHMnv24aPd1jYxNDSUzf0+adddW7YqOAMAAOObi85up37847ty2WWXZGhoKNOmTc8nP3lG+vuX56KLLsg+++ybRx55OKVSKWec8Zm8/OXPH7b+ve/15Nvf/qeUy+Xssssu+cQn5udP/mSvfPe7N+UHP7gl06dPy9KlS/OpT52Vn/707tx66y0plzdk4sRJ+cQn5mfvvffJBRd0J0lOOukDaWhozMUXX5YvfemC7LtvW971rs5cfvlXs2zZsqxbtzZPPPF49thjz5x7bnd23HHHrF27Np/73NlZuvQ3aWmZkV13bcn06U055ZSPbvLfecghB+aEE07KD394R373u99l3rwz89Of/nt+8pO7smHDhpx7bnf22uvlGRhYmc985sysW7cu69evz8EH/3lOPvm0JM+vlD/++GN55pmn8/jjj+WSSy7P97//vfzTP/1DdtllSt70pj/Pt7/9j/nOd25NX98TOf74Y/Od79w6vP8TTzw5d975L/nd736XD3/41Lz1rbNH9z8XAACoOivP26HVq1flvPMWZMGC8/L//t8/5LDD2nP22Z9OkixZ8usccURHrrxyUf7yL/9PzjtvYZLk3nt/nttu+34uvfTyXHnlN/LXf31sPve5c4af8777fpEPfOCDufLKb2TvvffJnDnvzBVXfD1XXXVtjj/+Q/n85z+XJDn99HlJkq985cpcffW1mTLlj09D/9BDD2Thws9m0aL/Lxs2bMgtt3wvyfMr1lOmTM211/5zzj337/PLX/5iRP/eXXaZkiuu+HpOOukj+dSnTs9rXrN/rrrq2syZ8858/etXDt+nu/vCXHnlN3L11dfmwQcfyI9/fNfwc/ziFz/LvHln5etf/1b6+5fnmmuuzle+cmWuuOLrWbt27Wb3v/POO+eKK76es846O1/84hdG1DMAADC+WHneDv3qV/fnFa945fCK8jvecVQuuKA7Tz/9dPbc86X5sz97/pJh7e3vyPnnfzbr1q3Nj350Zx555Nc58cT3JUkqlUrWrBkcfs4//dP9s8ceew7ffuihB3LNNVdlcPB3aWxszKOP/seI+3v96984HKpf9ar98vjjjyVJfv7zn+ajH/1kkmTq1JfkzW9+y4ie7w+HhO+zz75JGnLwwYf85+223HHH7UmePxz7y1++KPfd98sklQwMDOTXv344b3zjwUmSN73pzzNt2rT/7OOevOlNf57p06f/5+t3ZG655bub2X97kuTVr/7TrFy5Is8++2wmTZo0wlcDAIDxYrROClxP1j+3+a+AjmX+57dLlfznSc1H/ohK8s53HpXjj//QRuuTJ+80/PfnnnsuZ501L5dccnn22WffrFy5IkcffcSI9zVx4n8Fy8bGxuHvUFcqleGzsW+JiRMnDj/XxIk7bPS5v/WtRVmzZjD/9/9enUmTJqW7+7NZv/7Z4fvutNPk4b8//53nkffxh/2XSqUkxd8JBwBgfNpx0oRROSlwPbnpgrm1bmGrOWx7O/TqV78mjzzycH7722VJnv8u895775PJkyfnsccezb33/jxJ8v3v35xZs/5Xdt55l/z5n785N9/8nfT3L0/yfAB88MEHNvr869c/m3K5nBkzZiZJvv3tf3pBffLknbNu3eYPdd6Y173uwHzve89fg3twcDA//OGdW/wcm7JmzZo0N++aSZMmZcWK/vzrv96xyfv+2Z8dkB//+Ed56qmnkiQ339yzzfoAAADGJyvPo+T3z24Yld+q/P7ZDYX3mT59ej796XNy9tlnplwuZ9q06Vmw4Nz09y/P3nu/Mt///uJcdNEFKZUa8+lPn50k2X//1+XEE0/O/PkfT7k8lA0bnsvb3vb27Ltv2x89/84775LjjvtgTjjhvZk5c7fhQ5//4Jhj/iannvqhTJq04xadmfp97zshf/d3Z+c97/k/aW1tzWte85rsssvGTxO/pd797mNy1lnz8v73d2XGjJk54ICDNnnfvfd+Zbq63psPfej9aWpqzoEHvj4777xt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4bVuYdwbrzNQl4dtAwAAwJaou/Dc2tqa5cuXp1wuJ0nK5XL6+/vT2tpa484AAAAYq+ouPDc3N6etrS09PT1Jkp6enrS1tY34kG0AAADqT0OlUqnUuolqW7JkSebPn5/BwcFMnTo13d3dmTVrVq3bAgAAYIyqy/AMAAAAW6LuDtsGAACALSU8AwAAQAHhGQAAAAoIzwAAAFBAeGaLLF26NMcee2zmzp2bI444IhdffPFw7ZlnnslHP/rRHHbYYZkzZ05uv/32mtVgNF1zzTWZM2dOjjzyyBx99NHD280A9eQnP/lJ2tra8o1vfGN4mxlge3f22Wdnzpw5Oeqoo3LMMcfkvvvuG655/8PmLV26NJ2dnWlvb09nZ2eWLVtW65a2XAW2wEknnVS55pprKpVKpbJ27drKW9/61sq9995bqVQqlYsvvrhyxhlnVCqVSmXp0qWVgw8+uLJ27dqa1GC0LF68uNLV1VVZs2ZNpVKpVPr7+4drZoB6sWbNmspf/dVfVU488cThz4RKxQyw/bvtttsq69evH/777Nmzh2ve/7B5xx57bOX666+vVCqVyvXXX1859thja9zRlrPyzBZpaGjImjVrkiS///3v09DQkKampiTJ9773vRxzzDFJkr322iv77bdf7rzzzprUYLRceeWVOeWUU7LLLrskSVpaWoZrZoB68fd///c57rjjMn369BdsNwNs7972trdlhx12SJLsv//+efLJJzM0NJTE+x82Z2BgIL29veno6EiSdHR0pLe3N6tWrapxZ1tGeGaLnHHGGfnud7+bN7/5zTn00ENz3HHHZc8990ySPPHEE9ljjz2G79va2ponn3yyJjUYLUuWLMm9996bY445Jn/5l3+Zf/zHfxyumQHqwR133JHBwcHMmTPnj2pmgHqyaNGivPWtb01j4/M/Tnv/w6b19fVl5syZKZVKSZJSqZQZM2akr6+vxp1tmQm1boCx5S/+4i/yxBNPbLR211135Vvf+lbmzp2b448/Pv39/Tn22GOz33775bWvfW2VO4XRUTQD5XI5fX19ufbaa7N69er89V//dV7+8pfnoIMOqnKnMDo2NwM333xzLrjgglx11VVV7gqqo+gz4A8/+H/nO9/JTTfdlEWLFlWzPaDGhGde4Lrrrtts/ZprrskPfvCDJMmMGTPyxje+MXfffXde+9rXZvfdd8/jjz8+fBh3X19f3vCGNyRJ1WuwtYpmYPfdd09HR0caGxvT3Nycgw8+OL/85S9z0EEHmQG2C5ubgZ/+9KdZsWJF3v3udydJVq9endtvvz1PPfVUTjnlFDPAuFf0GZAk3//+93PhhRfm6quvzq677jq83fsfNq21tTXLly9PuVxOqVRKuVxOf39/Wltba93aFnHYNltkzz33zA9/+MMkydq1a3PPPfdk7733TpLMmTMn3/rWt5Iky5Yty3333Zc3v/nNNanBaOno6Biegaeffjr33HNP9t133yRmgO3fgQcemH/7t3/Lbbfdlttuuy3t7e35yEc+klNOOSWJGWD7d/vtt+dzn/tcvva1rw1/be0PvP9h05qbm9PW1paenp4kSU9PT9ra2oZ/+TNeNFQqlUqtm2D8uP/++3Peeefl6aefzoYNG/KOd7xj+Iemp59+OvPnz88DDzyQxsbGfPKTn8zb3/72mtRgtPz+97/PWWedld7e3iTJ3Llzc+KJJyYxA9Sf+fPnZ7/99st73vOeJGaA7d8b3/jG7LDDDi/4gf/qq6/O9OnTvf+hwJIlSzJ//vwMDg5m6tSp6e7uzqxZs2rd1hYRngEAAKCAw7YBAACggPAMAAAABYRnAAAAKCA8AwAAQAHhGQAAAAoIzwAAAFBAeAYAAIACwjMAAAAU+P8B47An+a6sSqIAAAAASUVORK5CYII=",
638 | "text/plain": [
639 | ""
640 | ]
641 | },
642 | "metadata": {},
643 | "output_type": "display_data"
644 | }
645 | ],
646 | "source": [
647 | "results.plot.hist();"
648 | ]
649 | },
650 | {
651 | "cell_type": "markdown",
652 | "metadata": {},
653 | "source": [
654 | "The problem is that negative outliers (companies with extremely negative operating margins) are causing most values to be crammed in a single bin. We can fix this by using the `range` argument to `hist()` to zoom in on the bulk of the distribution. In addition, we'll increase the number of bins to 20, from the default 10."
655 | ]
656 | },
657 | {
658 | "cell_type": "code",
659 | "execution_count": 10,
660 | "metadata": {
661 | "tags": [],
662 | "vscode": {
663 | "languageId": "python"
664 | }
665 | },
666 | "outputs": [
667 | {
668 | "data": {
669 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAA88AAAFuCAYAAAC/VCqhAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAzjElEQVR4nO3de3wU9b3/8fdmwwaQ5Jds3MQVqBQquMULLXiXwxGwQRuaWh4Uu1ofhYL1hhdAiWITDFBdC4eCgnpK0YNG2sNRiAnRpGgrVh9WWrwSROREVIhJ2EATLiUwO78/qHsMJJlNspudJK/nX9n57mw+O5+d7L4z35l1mKZpCgAAAAAAtCgh3gUAAAAAAGB3hGcAAAAAACwQngEAAAAAsEB4BgAAAADAAuEZAAAAAAALhGcAAAAAACwQngEAAAAAsJAY7wK6ov37DykUsufXY6en91MweDDeZeAk9MV+6Ik90Rf7oSf2RF/sh57YDz2xJ7v3JSHBobS005odIzy3Qyhk2jY8S7J1bT0ZfbEfemJP9MV+6Ik90Rf7oSf2Q0/sqav2hWnbAAAAAABYIDwDAAAAAGCBadsAAAAAuhXTNFVTU6Pa2qBCISPe5eBramoSFAqF4l2GEhNdSkvzyOmMPBITngEAAAB0K/v31yoxMUFud6aczkQ5HI54l4R/SUxM0PHj8Q3Ppmnq0KF67d9fq9NP90a8HtO2AQAAAHQrjY3/VFra6UpM7EVwxikcDodOOy1Fx483tmk9wjMAAACAbsaUw0HUQcva808VXlEAAAAAAFjgnGcAAAAA3V5ySh/1Top+/Pnn0eNqqD8S9ceF/RCeAQAAAHR7vZMSNXF2UdQft3hJjhqi/qjR9bvfPakbb5ymXr16SZJWrXpC3/zmYI0b9704V9Zx+/bV6sEHH9Cjjz4Z89/FtG0AAAAA6MIMo/Wv43rqqd/q2LFj4dvTp9/cZYJzKBSSaZotjp9+uqdTgrPEkWcAAAAAiLm33npTTz75mEKhkFJT03TPPferpqZay5Yt0bBh5+iTTz6W0+nU/ffP1ze/OViS9NJLJXrhhXUyDEP9+vXTnDm5+sY3Bqm0tFibNpUrLS1VlZWVuu++X+pvf9uiV14pl2Ecl8uVpDlzcnX22cO0ZElAknTLLdPkcCTo0Uef1PLlS3TOOT5NmjRFv/vdk/rss906dOig9u7do/79B2jBgoB69+6tgwcP6qGHHlRl5f/K48nQ6ad7lJbm1u2339Xi87ziilGaMeMWvf76a/rHP/6huXPn6W9/e1t//eubOn78uH71q0c0cOAgBYP7NH/+PB06dEiNjY267LLLdeutd0o6caR8z54vdOTIYe3Z84Uee+y3+uMfX9K6db9Xv37JuvTSy/XCC/+tjRtfUVXVXk2f/lNt3PhK+PffdNOt2rz5z/rHP/6h2267Q//+7+Oi0kOOPAMAAABADO3fX6eFC/OUl7dQ//Vfv9dVV2XpwQcfkCTt2rVTV1+drdWrC/WjH/1YCxfmS5Lee+8dvfrqH7VixW+1evWz+slPfqqHHioIP+YHH7yradN+odWrn9XZZw/ThAnf16pVa/TUU89p+vSb9etfPyRJmj17riTp8cdX6+mnn1NycvIp9e3YsV35+YtUWPg/On78uMrLX5J04oh1cnKKnnvueS1Y8LDef//diJ5vv37JWrVqjW65Zabuu2+2zj9/hJ566jlNmPB9Pf3078L3CQSWavXqZ/X008/po4+266233gw/xrvvbtXcub/UmjV/UE1NtZ555mk9/vhqrVq1RgcPHmz195922mlatWqNfvnLB/Wb3yyOqOZIcOQZAAAAAGJo27YPNWTI0PAR5Wuu+YGWLAno8OHDGjBgoL7znZGSpKysa/TII4t06NBBvfHGZn3yyU7ddNPPJEmmaaqhoT78mOedN0L9+w8I396xY7ueeeYp1df/QwkJCfr8888iru+iiy4Jh+pvf/tc7dnzhSTpnXf+prvuukeSlJLy/zR69JiIHu+rKeHDhp0jyaHLLrviX7d92rz5T5JOTMdeuXKZPvjgfUmmgsGgdu78WJdccpkk6dJLL1dqauq/6vi7Lr30cqWlpf1r+01UeXlpK78/S5I0fPh52revVkePHlVSUlKEW6NlhGcAANCixmOGPJ5Tj1LYCVe6BWB/ptr6tcKmKX3/+z/Q9Ok3Nzvet2+f8M/Hjh3TL385V4899lsNG3aO9u2r1Q9/eHXEv8vl+r9gmZCQED6H2jTNdn0fssvlCj+Wy9Wr2cf+wx8K1dBQr//8z6eVlJSkQGCRGhuPhu/bp0/f8M8nznmOvI6vfr/T6ZRkfU54pAjPAACgRa5ezphcnTaausKVbgH0bMOHn6+HH16g3bs/1VlnDdJLL5Xo7LOHqW/fvvrii8/13nvv6IILvqM//vFlDR78LZ12Wj9dfvloLVyYrx/84FplZGTKMAzt3PmxzjnHd8rjNzYelWEYysjIlCS98MK6JuN9+56mQ4cOqm/fvqes25rvfneUXnqpROedd4Hq6+v1+uubNWbMle3fEF/T0NCg9PTTlZSUpNraGv3lL6/phz+c1Ox9v/OdkVq79hkdOHBAqampevnlkqjU0FaEZwAAAADd3j+PHlfxkpyYPK6VtLQ0PfBAgR58cJ4Mw1Bqapry8haopqZaZ589VH/8Y5mWLVsipzNBDzzwoCRpxIjv6qabblVu7iwZRkjHjx/TlVeObzY8n3ZaP/3857/QjBk3KjPzjPDU569cd931uuOOm5WU1LtNV6b+2c9m6Fe/elA33PBjeb1enX/++erXr1/E67dm8uTr9MtfztXUqX5lZGRq5MgLW7zv2WcPld9/o26+earc7nSNGnWRTjstOnW0hcNs7brfaFYweFChkD03m8eTrNpa/v9uN/TFfuiJPdEX+/F4krvEkeee9rphX7EfemIvX365WwMGfFPHj4fiXUqrtm79m1asWKbf/e6ZeJfSrOPHj8swDCUlJenQoYO69dbpuv32u3XhhRe3+zETExPa1ZfDhw+pb9/TJP3f1bjz8ha0uw7pxOvkjDPOarIsIcGh9PTmgzlHngEAAAAAp2hoqNfs2XcoFAqpsfGorrpqQoeCc0c8/vhj+uCD93T8+DGdeWZ/3XvvvE6vgfAMAAAAAHHw3e+Osu1RZ0lKS3Nr9epnT1n+1FO/1Wuv/emU5UuXPqa0NHdMavnqK7fiifAMAAAAAIjY1KkzNHXqjHiX0ekS4l0AAAAAAESXQ6Zp7/OdEV/tufQX4RkAAABAt+Jy9VZd3T4dP36sXSEJ3Ztpmjp0qF6Jia42rce0bQAAAADdSlqaR9I/VVtbrVDIiHc5+JqEhASFQvGfFZCY6PrX66QN68SoFgAAAACIC4fDIY8nQw5Hn3iXgpN05a91Y9o2AAAAAAAWCM8AAAAAAFggPAMAAAAAYIHwDAAAAACABcIzAAAAAAAWOiU879+/XzNmzFBWVpYmTpyo22+/XXV1dZKkyspKTZkyRVlZWZoyZYo+/fTT8HqdPQYAAAAAQHM6JTw7HA5Nnz5dZWVlKi4u1sCBA7V48WJJUn5+vvx+v8rKyuT3+5WXlxder7PHAAAAAABoTqeE59TUVF188cXh2yNGjNDevXsVDAZVUVGh7OxsSVJ2drYqKipUV1fX6WMAAAAAALQksbN/YSgU0tq1azV27FhVVVUpMzNTTqdTkuR0OpWRkaGqqiqZptmpY263u7M3BQAAAACgi+j08LxgwQL17dtXN9xwgyoqKjr710dFenq/eJfQKo8nOd4loBn0xX7oiT3RF7RHT3zd9MTnbHf0xH7oiT111b50angOBALavXu3nnjiCSUkJMjr9aq6ulqGYcjpdMowDNXU1Mjr9co0zU4da4tg8KBCITNGW6ljPJ5k1dY2xLsMnIS+2A89sSf6Yj9d5QNOT3vdsK/YDz2xH3piT3bvS0KCo8WDpZ32VVVLly7Vhx9+qBUrVsjlckmS0tPT5fP5VFJSIkkqKSmRz+eT2+3u9DEAAAAAAFriME0z5odQd+7cqezsbA0aNEi9e/eWJA0YMEArVqzQrl27lJubq/r6eqWkpCgQCGjw4MGS1OljkeLIM9qKvtgPPbEn+mI/Hk+yJs4uincZrSpektPjXjfsK/ZDT+yHntiT3fvS2pHnTgnP3Q3hGW1FX+yHntgTfbEfwrM9sa/YDz2xH3piT3bviy2mbQMAAAAA0FURngEAAAAAsEB4BgAAAADAAuEZAAAAAAALhGcAAAAAACwQngEAAAAAsEB4BgAAAADAAuEZAAAAAAALhGcAAAAAACwQngEAAAAAsEB4BgAAAADAAuEZAAAAAAALhGcAAAAAACwQngEAAAAAsEB4BgAAAADAAuEZAAAAAAALhGcAAAAAACwQngEAAAAAsEB4BgAAAADAAuEZAAAAAAALhGcAAAAAACwQngEAAAAAsEB4BgAAAADAAuEZAAAAAAALhGcAAAAAACwQngEAAAAAsEB4BgAAAADAAuEZAAAAAAALiZ3xSwKBgMrKyrRnzx4VFxdr6NCh+uKLL3TbbbeF79PQ0KCDBw/q7bffliSNHTtWLpdLSUlJkqQ5c+Zo9OjRkqTKykrl5ubqwIEDSk1NVSAQ0KBBgzo0BgAAAABASzolPI8bN0433nijrr/++vCyAQMGqKioKHx70aJFMgyjyXrLly/X0KFDT3m8/Px8+f1+5eTkqKioSHl5eVqzZk2HxgAAAAAAaEmnTNseNWqUvF5vi+ONjY0qLi7WpEmTLB8rGAyqoqJC2dnZkqTs7GxVVFSorq6u3WMAAAAAALSmU448W3n11VeVmZmp4cOHN1k+Z84cmaapkSNHatasWUpJSVFVVZUyMzPldDolSU6nUxkZGaqqqpJpmu0ac7vdnfuEAQAAAABdii3C8/PPP3/KUefCwkJ5vV41NjZq0aJFKigo0OLFi+NUYVPp6f3iXUKrPJ7keJeAZtAX+6En9kRf0B498XXTE5+z3dET+6En9tRV+xL38FxdXa0tW7bokUceabL8q2neLpdLfr9ft9xyS3h5dXW1DMOQ0+mUYRiqqamR1+uVaZrtGmurYPCgQiGz408+BjyeZNXWNsS7DJyEvtgPPbEn+mI/XeUDTk973bCv2A89sR96Yk9270tCgqPFg6Vx/6qq9evXa8yYMUpLSwsvO3z4sBoaTmxQ0zRVWloqn88nSUpPT5fP51NJSYkkqaSkRD6fT263u91jAAAAAAC0plOOPC9cuFDl5eXat2+fpk6dqtTUVG3cuFHSifA8b968JvcPBoOaOXOmDMNQKBTSkCFDlJ+fHx6fP3++cnNztXLlSqWkpCgQCHR4DAAAAACAljhM07Tn/GMbY9o22oq+2A89sSf6Yj8eT7Imzi6yvmMcFS/J6XGvG/YV+6En9kNP7MnufbH1tG0AAAAAAOyO8AwAAAAAgAXCMwAAAAAAFgjPAAAAAABYIDwDAAAAAGCB8AwAAAAAgAXCMwAAAAAAFgjPAAAAAABYIDwDAAAAAGCB8AwAAAAAgAXCMwAAAAAAFgjPAAAAAABYIDwDAAAAAGCB8AwAAAAAgAXCMwAAAAAAFgjPAAAAAABYIDwDAAAAAGCB8AwAAAAAgAXCMwAAAAAAFhLjXQAAAEBHNB4z5PEkx7uMFv3z6HE11B+JdxkAgA4iPAMAgC7N1cupibOL4l1Gi4qX5Kgh3kUAADqMadsAAAAAAFggPAMAAAAAYIHwDAAAAACABcIzAAAAAAAWCM8AAAAAAFggPAMAAAAAYIHwDAAAAACAhU4Jz4FAQGPHjtWwYcP08ccfh5ePHTtWEyZMUE5OjnJycvT666+HxyorKzVlyhRlZWVpypQp+vTTT2M6BgAAAABASzolPI8bN06FhYXq37//KWPLly9XUVGRioqKNHr06PDy/Px8+f1+lZWVye/3Ky8vL6ZjAAAAAAC0pFPC86hRo+T1eiO+fzAYVEVFhbKzsyVJ2dnZqqioUF1dXUzGAAAAAABoTWK8C5gzZ45M09TIkSM1a9YspaSkqKqqSpmZmXI6nZIkp9OpjIwMVVVVyTTNqI+53e74PHkAAAAAQJcQ1/BcWFgor9erxsZGLVq0SAUFBVq8eHE8S4pIenq/eJfQKo8nOd4loBn0xX7oiT3RF3RHsXhds6/YDz2xH3piT121L3ENz19N5Xa5XPL7/brlllvCy6urq2UYhpxOpwzDUE1Njbxer0zTjPpYWwWDBxUKmVHdFtHi8SSrtrYh3mXgJPTFfuiJPdEX++mqH3DsJtqva/YV+6En9kNP7MnufUlIcLR4sDRuX1V1+PBhNTSc2Gimaaq0tFQ+n0+SlJ6eLp/Pp5KSEklSSUmJfD6f3G53TMYAAAAAAGhNpxx5XrhwocrLy7Vv3z5NnTpVqampeuKJJzRz5kwZhqFQKKQhQ4YoPz8/vM78+fOVm5urlStXKiUlRYFAIKZjAAAAAAC0xGGapj3nH9sY07bRVvTFfuiJPdEX+/F4kjVxdlG8y2hV8ZIcW9dYvCSHads9AD2xH3piT3bviy2nbQMAAAAA0FUQngEAAAAAsEB4BgAAAADAAuEZAAAAAAALhGcAAAAAACwQngEAAAAAsEB4BgAAAADAAuEZAAAAAAALhGcAAAAAACwQngEAAAAAsEB4BgAAAADAAuEZAAAAAAALhGcAAAAAACwQngEAAAAAsEB4BgAAAADAAuEZAAAAAAALhGcAAAAAACwQngEAAAAAsEB4BgAAAADAAuEZAAAAAAALhGcAAAAAACwQngEAAAAAsEB4BgAAAADAQsTh+ZVXXtHx48djWQsAAAAAALYUcXhetmyZrrjiChUUFOi9996LZU0AAAAAANhKxOH5xRdf1NNPP62kpCTNnDlTWVlZWrlypb744otY1gcAAAAAQNy16Zznc845R3PnztVrr72m/Px8vfzyy7rqqqt0/fXX68UXX1QoFIpVnQAAAAAAxE1iW1f47LPP9OKLL+rFF1+Uw+HQHXfcIa/Xq8LCQpWXl+uxxx6LRZ0AAAAAAMRNxOG5sLBQRUVF2r17t66++mo98sgjGjFiRHg8KytLl112WbPrBgIBlZWVac+ePSouLtbQoUO1f/9+3Xvvvfrss8/kcrl01llnqaCgQG63W5I0duxYuVwuJSUlSZLmzJmj0aNHS5IqKyuVm5urAwcOKDU1VYFAQIMGDerQGAAAAAAALYl42vbmzZs1depUvf7665o/f36T4CxJffr00aOPPtrsuuPGjVNhYaH69+8fXuZwODR9+nSVlZWpuLhYAwcO1OLFi5ust3z5chUVFamoqCgcnCUpPz9ffr9fZWVl8vv9ysvL6/AYAAAAAAAtiTg8L1++XOPHj5fL5QovO3bsmBobG8O3r7jiimbXHTVqlLxeb5Nlqampuvjii8O3R4wYob1791rWEQwGVVFRoezsbElSdna2KioqVFdX1+4xAAAAAABaE3F4njZtmrZt29Zk2bZt2/Tzn/+8w0WEQiGtXbtWY8eObbJ8zpw5mjhxoubPn6/6+npJUlVVlTIzM+V0OiVJTqdTGRkZqqqqavcYAAAAAACtific5x07duiCCy5osuz888/XRx991OEiFixYoL59++qGG24ILyssLJTX61VjY6MWLVqkgoKCU6Z1x0t6er94l9Aqjyc53iWgGfTFfuiJPdEXdEexeF2zr9gPPbEfemJPXbUvEYfnlJQU7du3Tx6PJ7xs37596tOnT4cKCAQC2r17t5544gklJPzfgfCvpnm7XC75/X7dcsst4eXV1dUyDENOp1OGYaimpkZer1emabZrrK2CwYMKhcwOPe9Y8XiSVVvbEO8ycBL6Yj/0xJ7oi/101Q84dhPt1zX7iv3QE/uhJ/Zk974kJDhaPFga8bTt733ve5o9e7Y+/vhjHTlyRDt27NDcuXN19dVXt7uwpUuX6sMPP9SKFSuanEt9+PBhNTSc2KCmaaq0tFQ+n0+SlJ6eLp/Pp5KSEklSSUmJfD6f3G53u8cAAAAAAGhNxEee7777bj388MOaPHmyGhsblZSUpB/96EeaNWuW5boLFy5UeXm59u3bp6lTpyo1NVW/+c1v9MQTT2jQoEG67rrrJEkDBgzQihUrFAwGNXPmTBmGoVAopCFDhig/Pz/8ePPnz1dubq5WrlyplJQUBQKBDo8BAAAAANASh2mabZp/bJqm9u/fr7S0NDkcjljVZWtM20Zb0Rf7oSf2RF/sx+NJ1sTZRfEuo1XFS3JsXWPxkhymbfcA9MR+6Ik92b0vrU3bjvjIsyQ1NDSosrJShw4darL80ksvbX91AAAAAADYXMTh+YUXXlBBQYH69u2r3r17h5c7HA698sorMSkOAAAAAAA7iDg8L126VMuWLdOYMWNiWQ8AAAAAALYT8dW2DcPQFVdcEctaAAAAAACwpYjD84wZM/T4448rFArFsh4AAAAAAGwn4mnbTz/9tPbt26dVq1YpNTW1ydif//znKJcFAAAAAIB9RByef/3rX8eyDgAAAAAAbCvi8HzRRRfFsg4AAAAAAGwr4nOeGxsbtXTpUo0bN04jR46UJP3lL3/Rs88+G7PiAAAAAACwg4jD869+9St9/PHHWrx4sRwOhyTp7LPP1tq1a2NWHAAAAAAAdhDxtO1NmzapvLxcffv2VULCicydmZmp6urqmBUHAAAAAIAdRHzkuVevXjIMo8myurq6U668DQAAAABAdxNxeJ4wYYLmzp2rzz//XJJUU1OjgoICff/7349ZcQAAAAAA2EHE4fnuu+9W//799YMf/ED19fXKyspSRkaGbrvttljWBwAAAABA3EV8zrPL5dK8efM0b9481dXVKS0tLXzhMAAAAAAAurOIw/NX07W/cujQofDPAwcOjF5FAAAAAADYTMTh+aqrrpLD4ZBpmuFlXx153r59e/QrAwAAAADAJiIOzx999FGT27W1tXrsscc0atSoqBcFAAAAAICdRHzBsJN5PB7NmzdP//Ef/xHNegAAAAAAsJ12h2dJ+t///V8dOXIkWrUAAAAAAGBLEU/b9vv9Ta6ufeTIEX3yySd8VRUAAAAAoNuLODxPnjy5ye0+ffronHPO0aBBg6JdEwAAAAAAthJxeL722mtjWQcAAAAAALYVcXhetmxZRPe78847210MAAAAAAB2FHF43r17t8rLy3Xuueeqf//+2rt3rz744AN973vfU1JSUixrBAAAAAAgriIOz6ZpasmSJcrKygovKy8v18svv6yHHnooJsUBAAAAAGAHEX9V1ebNmzV+/Pgmy8aNG6fXXnst6kUBAAAAAGAnEYfns846S4WFhU2WPffcc/rGN74R9aIAAAAAALCTiKdtL1y4ULfffrtWrVqlzMxMVVdXKzExUY8++mgs6wMAAAAAIO4iDs/f/va3VVZWpvfee081NTXyeDwaMWKEevXqZbluIBBQWVmZ9uzZo+LiYg0dOlSSVFlZqdzcXB04cECpqakKBALh743u7DEAAAAAAFoS8bTtk1144YU6duyYDh8+bHnfcePGqbCwUP3792+yPD8/X36/X2VlZfL7/crLy4vbGAAAAAAALYk4PO/YsUNZ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",
670 | "text/plain": [
671 | ""
672 | ]
673 | },
674 | "metadata": {},
675 | "output_type": "display_data"
676 | }
677 | ],
678 | "source": [
679 | "results.plot.hist(bins=20, range=(-1, 1));"
680 | ]
681 | },
682 | {
683 | "cell_type": "markdown",
684 | "metadata": {},
685 | "source": [
686 | "## Clipping Outliers\n",
687 | "\n",
688 | "Using `range` to zoom in on the distribution is useful for viewing the histogram, but it doesn't remove the outliers from the pipeline output itself. Before using the pipeline output in Alphalens or Zipline, perhaps it would be a good idea to adjust the pipeline to deal with the outliers. Beyond a certain point, increasingly negative operating margins don't provide useful additional information; it's enough to know that the company is very unprofitable. So, a reasonable solution is to clip the values to -1. This means that any values less than -1 will be replaced with -1. The Factor `clip()` method requires both a lower and upper bound. We'll set the upper bound to 1, knowing that since we already excluded companies with operating margins above 1, the upper bound is redundant. (The lower and upper bound can be set to `-np.inf` and `np.inf`, respectively, to indicate no bound.) "
689 | ]
690 | },
691 | {
692 | "cell_type": "code",
693 | "execution_count": 11,
694 | "metadata": {
695 | "tags": [],
696 | "vscode": {
697 | "languageId": "python"
698 | }
699 | },
700 | "outputs": [],
701 | "source": [
702 | "pipeline = Pipeline(\n",
703 | " columns={\n",
704 | " 'operating_margin': operating_margin.clip(min_bound=-1, max_bound=1),\n",
705 | " },\n",
706 | " initial_universe=CommonStocks(),\n",
707 | " screen=BaseUniverse()\n",
708 | ")"
709 | ]
710 | },
711 | {
712 | "cell_type": "markdown",
713 | "metadata": {},
714 | "source": [
715 | "If we re-run the pipeline, we can now plot the histogram without using `range`. Notice that, unlike the previous histogram which ignored data outside the (-1, 1) range, in this histogram the clipped values cluster at -1. In other words, the previous histogram included a subset of the data, while this histogram includes all the data. "
716 | ]
717 | },
718 | {
719 | "cell_type": "code",
720 | "execution_count": 12,
721 | "metadata": {
722 | "tags": [],
723 | "vscode": {
724 | "languageId": "python"
725 | }
726 | },
727 | "outputs": [
728 | {
729 | "data": {
730 | "image/png": "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",
731 | "text/plain": [
732 | ""
733 | ]
734 | },
735 | "metadata": {},
736 | "output_type": "display_data"
737 | }
738 | ],
739 | "source": [
740 | "results = run_pipeline(pipeline, '2022-01-01', '2022-12-31')\n",
741 | "results.plot.hist(bins=20);"
742 | ]
743 | },
744 | {
745 | "cell_type": "markdown",
746 | "metadata": {},
747 | "source": [
748 | "An alternative to using `clip()` would be to use `winsorize()`. The difference is that `winsorize()` trims values above and below a certain percentile in the distribution, while `clip()` trims values above and below specific fixed values. "
749 | ]
750 | },
751 | {
752 | "cell_type": "markdown",
753 | "metadata": {},
754 | "source": [
755 | "***\n",
756 | "\n",
757 | "## *Next Up*\n",
758 | "\n",
759 | "Lesson 6: [Alphalens: Profitability](Lesson06-Profitability.ipynb)"
760 | ]
761 | }
762 | ],
763 | "metadata": {
764 | "kernelspec": {
765 | "display_name": "Python 3.11",
766 | "language": "python",
767 | "name": "python3"
768 | },
769 | "language_info": {
770 | "codemirror_mode": {
771 | "name": "ipython",
772 | "version": 3
773 | },
774 | "file_extension": ".py",
775 | "mimetype": "text/x-python",
776 | "name": "python",
777 | "nbconvert_exporter": "python",
778 | "pygments_lexer": "ipython3",
779 | "version": "3.11.0"
780 | }
781 | },
782 | "nbformat": 4,
783 | "nbformat_minor": 4
784 | }
785 |
--------------------------------------------------------------------------------
/fundamental_factors/Lesson08-Factor-Values-vs-Factor-Ranks.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | " \n",
8 | "Disclaimer"
9 | ]
10 | },
11 | {
12 | "attachments": {},
13 | "cell_type": "markdown",
14 | "metadata": {},
15 | "source": [
16 | "***\n",
17 | "[Fundamental Factors](Introduction.ipynb) › Lesson 8: Factor Values vs Factor Ranks\n",
18 | "***"
19 | ]
20 | },
21 | {
22 | "attachments": {},
23 | "cell_type": "markdown",
24 | "metadata": {},
25 | "source": [
26 | "# Factor Values vs Factor Ranks\n",
27 | "\n",
28 | "When using a factor in Pipeline, one choice you must make is whether use the raw factor values or whether to rank the values and use the ranks as your factor. Before explaining why you might choose one or the other, let's see how to rank a factor in Pipeline. \n",
29 | "\n",
30 | "In the following example, we look at the debt-to-equity ratio, which is the ratio of a company's total liabilities to its shareholder equity. The D/E ratio is a measure of a company's finanical leverage, indicating the degree to which a company's operations are funded by debt. Ranking by D/E ratio in Pipeline is a simple matter of using the `rank()` method. However, note the use of `where()` to mask the D/E ratio with our base universe before calling `rank()`. We do this to avoid including securities that aren't part of our universe in the ranks. (Equivalently, we could have passed our universe as the `mask` argument to `rank()`.)"
31 | ]
32 | },
33 | {
34 | "cell_type": "code",
35 | "execution_count": 1,
36 | "metadata": {
37 | "tags": []
38 | },
39 | "outputs": [],
40 | "source": [
41 | "from zipline.pipeline import sharadar, Pipeline\n",
42 | "from codeload.fundamental_factors.universe import CommonStocks, BaseUniverse\n",
43 | "\n",
44 | "universe = BaseUniverse()\n",
45 | "\n",
46 | "fundamentals = sharadar.Fundamentals.slice('ART')\n",
47 | "\n",
48 | "de = fundamentals.DE.latest\n",
49 | "\n",
50 | "# Mask d/e with the base universe, before using rank() below \n",
51 | "de = de.where(universe)\n",
52 | "\n",
53 | "pipeline = Pipeline(\n",
54 | " columns={\n",
55 | " 'de': de,\n",
56 | " 'de_rank': de.rank(),\n",
57 | " },\n",
58 | " initial_universe=CommonStocks(),\n",
59 | " screen=universe\n",
60 | ")"
61 | ]
62 | },
63 | {
64 | "cell_type": "markdown",
65 | "metadata": {},
66 | "source": [
67 | "Is it better to use factor ranks or factor values? It depends on what you plan to do with your pipeline output. \n",
68 | "\n",
69 | "There are times when you care about the actual factor values, perhaps because you want to examine their distribution or because you want to bin the values using cutoffs that are meaningful for the specific factor. In those cases, ranking may not be helpful. For example, if you wanted to classify D/E ratios below 1 as \"safe\", D/E ratios between 1 and 2 as \"borderline\", and D/E ratios above 2 as \"risky\", you would need the actual factor values, not the ranks. \n",
70 | "\n",
71 | "An example where ranking can be helpful is when looking at the long-short, factor-weighted cumulative returns plot in Alphalens. \"Factor-weighted\" means that the asset weights are proportional to the factor value. This makes the plot sensitive to outliers, as assets with extreme positive or negative values will have extreme positive or negative weights. This situation can cause the factor-weighted cumulative returns plot to show extreme up and down moves. Using factor ranks instead of factor values can mitigate this problem. The outliers will still have larger weights, but much more moderately so, since the weights will be proportional to the ranks (which are linear), rather than to the values on which the ranks are based.\n",
72 | "\n",
73 | "To illustrate the different weighting that can occur from using factor values vs factor ranks, let's create a two-column pipeline that computes weights based on the D/E ratio and the D/E ratio rank: "
74 | ]
75 | },
76 | {
77 | "cell_type": "code",
78 | "execution_count": 2,
79 | "metadata": {
80 | "tags": []
81 | },
82 | "outputs": [],
83 | "source": [
84 | "pipeline = Pipeline(\n",
85 | " columns={\n",
86 | " 'factor_weighted': de / de.sum(),\n",
87 | " 'rank_weighted': de.rank() / de.rank().sum()\n",
88 | " },\n",
89 | " initial_universe=CommonStocks(),\n",
90 | " screen=universe\n",
91 | ")"
92 | ]
93 | },
94 | {
95 | "cell_type": "markdown",
96 | "metadata": {},
97 | "source": [
98 | "If we run this pipeline and plot the maximum weight in each column, we see that weighting on raw factors can result in much more concentrated portfolios than weighting on ranks:"
99 | ]
100 | },
101 | {
102 | "cell_type": "code",
103 | "execution_count": 3,
104 | "metadata": {
105 | "tags": []
106 | },
107 | "outputs": [
108 | {
109 | "data": {
110 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAA+4AAAF+CAYAAAAcHUDUAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAjoklEQVR4nO3dd5TW5Z338c/ASJFBDYigUVexYD3RACqxrVhQdBxBUWOEZFFzVk8OJyqWo65tkdiwm9h2F2vc2MCuq0asiKjRqNhAYxcUNjAo0u7nDx8nSxgFZAYu4uv1Fze/dt3D9/w877mLVZVKpRIAAACgSC2W9wIAAACAbybcAQAAoGDCHQAAAAom3AEAAKBgwh0AAAAKJtwBAACgYMIdAPhWW2+9dd57773lvYxmceedd2bw4MGLte/tt9+en/70p828IgBYmHAHgBVY7969s8UWW2Tq1KkL/H1dXV26deuW999/f6mv8cILL2SdddZZ6vOUaN99981//ud/Nsm5Bg4cmFtuuaVJzgUA/5dwB4AV3A9/+MPcc889DY9ff/31zJo1azmuCABoSsIdAFZwdXV1GTVqVMPjUaNGZb/99ltgn0cffTT77bdffvzjH2fnnXfOpZde2rDt3nvvza677pr6+vokyZgxY7L99ts3vIrfrVu3/OUvf0mSnHjiiTn99NNz+OGHZ+utt87BBx+cKVOm5KyzzkrPnj2z55575tVXX2049/899uvjL7zwwiTJM888k5122ilXX311evXqlR122CEPPfRQxowZkz59+mSbbbbJFVdc0ehzfu+999KjR4/Mnz8/SXLyySenV69eDduHDh2akSNHJklmzJiRk046KTvssEN23HHHXHjhhZk3b16Shd/+/sQTT6RPnz7p3r17Tj/99Bx66KELvYp+zjnnpGfPnundu3fGjBmTJLnwwgszfvz4nHnmmdl6661z5plnplKpZPjw4enVq1e6d++e2travPHGG40+HwD4NsIdAFZwW221Verr6zNx4sTMmzcv9957b/bdd98F9mnbtm3OOeecjB8/PldeeWV+//vf56GHHkqS9O3bN1tttVWGDRuWadOm5eSTT86wYcPSoUOHRq9333335de//nXGjh2bVq1a5aCDDsrmm2+esWPHpk+fPvnNb36z2Gv/9NNP8+WXX+axxx7LkCFDcsopp+TOO+/MbbfdlhtvvDGXX355o5+vX2eddVJTU9PwS4Lx48dn5ZVXzsSJExseb7PNNkmSE044IdXV1XnwwQczatSoPPnkk42+pX3q1KkZMmRIjj322DzzzDNZf/3188ILLyywz0svvZT1118/Y8eOzeGHH56TTz45lUolRx99dHr06JFTTz01L7zwQk499dQ88cQTGT9+fB544IGMHz8+F110UVZbbbXF/tkAwNeEOwD8A/j6Vfcnn3wyXbt2TefOnRfYvu2226Zbt25p0aJFNtlkk+y9994ZN25cw/bTTjstY8eOzaBBg9K7d+/ssssu33it3XffPVtssUVat26d3XffPa1bt85+++2Xli1bpm/fvpkwYcJir7u6ujpHHnlkVlpppfTt2zfTpk3LoEGDUlNTk4022igbbbRRXn/99UaP7dmzZ5599tlMmTIlSdKnT5+MGzcu7733Xurr67PJJpvk008/zWOPPZaTTjopK6+8cjp27Jhf/OIXC3y04GuPPfZYNtpoo+yxxx6prq7OoEGDsvrqqy+wz1prrZUDDzwwLVu2TL9+/TJlypR8+umn3/jcZs6cmUmTJqVSqWSDDTbIGmussdg/GwD4WvXyXgAAsPTq6upy6KGH5v33309dXd1C21988cWcf/75efPNNzNnzpzMnj07e+65Z8P2VVZZJXvuuWf+67/+K5dccsm3Xqtjx44Nf27Tps0CcdumTZt8/vnni73u1VZbLS1btmw49u/P37p168ycObPRY7fZZps8/PDD6dy5c3r27Jltt902o0ePTuvWrdOjR4+0aNEiH374YebOnZsddtih4bj58+dnzTXXXOh8kydPTpcuXRoeV1VVLfA4yQLPtW3btknyjc+3V69e+dnPfpYzzzwzH374YXbfffeccMIJqamp+dafCQD8Pa+4A8A/gB/+8IdZe+21M2bMmOyxxx4LbT/22GOz6667ZsyYMXnuuedy8MEHp1KpNGyfMGFCbrvttuyzzz4ZNmxYk62rbdu2+eKLLxoef/3qeFPo2bNnnnvuuYwbNy49e/ZM9+7d8/zzz+fZZ59Nz549kyRdunRJq1atMnbs2IwfPz7jx4/P888/3+gr7p06dconn3zS8LhSqeTjjz9eqjUOGjQot99+e+6555688847ueaaa5bqfAB8Pwl3APgHcdZZZ+Xaa6/NyiuvvNC2mTNnZtVVV03r1q3z0ksv5e67727Y9uWXX+a4447L0Ucfnd/85jeZPHlybrzxxiZZ0yabbJK777478+bNy2OPPZZnn322Sc6bJOutt15at26dO++8Mz179kxNTU06duyYBx54oCHc11hjjWy//fY5++yzU19fn/nz5+fdd99d4GMCX9t5553z+uuv56GHHsrcuXNz4403fuPb4Buz+uqrL/B5/Jdeeikvvvhi5syZk7Zt26ZVq1YN7y4AgCUh3AHgH8S6666bLbfcstFtp512Wi655JJsvfXWufzyy7PXXns1bBsxYkQ6d+6cQw45JK1atcp5552Xiy++OO+8885Sr+nkk0/OH//4x/To0SN33XVXdtttt6U+5/+1zTbbZLXVVstaa63V8LhSqWSzzTZr2Ofcc8/NnDlz0rdv3/Ts2TNDhgxp9JX/Dh065OKLL855552XbbfdNm+99Va22GKLrLTSSou1lkGDBjX80mDYsGGZOXNmTjnllGyzzTbZZZddstpqq2Xw4MFN88QB+F6pqvzf98kBAJDkq8/C77TTTjn//POz3XbbLe/lAPA95hV3AID/7/HHH8/06dMze/bshv+H/FZbbbV8FwXA955vlQcA+P/+9Kc/ZejQoZk9e3Y23HDDXH755Q3fdg8Ay4u3ygMAAEDBvFUeAAAACibcAQAAoGDCHQAAAArmy+lWINOmzcz8+b6SgL/p2LEmn31Wv7yXQWHMBY0xFzTGXNAYc0FjzEXzatGiKj/4Qbtv3C7cVyDz51eEOwsxEzTGXNAYc0FjzAWNMRc0xlwsP94qDwAAAAUT7gAAAFAw4Q4AAAAFE+4AAABQMOEOAAAABRPuAAAAUDDhDgAAAAUT7gAAAFAw4Q4AAAAFE+4AAABQMOEOAAAABRPuAAAAUDDhDgAAAAUT7gAAAFAw4Q4AAAAFE+4AAABQMOEOAAAABRPuAAAAUDDhDgAAAAUT7gAAAFAw4Q4AAAAFE+4AAABQMOEOAAAABRPuAAAAUDDhDgAAAAUT7gAAAFAw4Q4AAAAFE+4AAABQMOEOAAAABRPuAAAAUDDhDgAAAAUT7gAAAFAw4Q4AAAAFE+4AAABQMOEOAAAABRPuAAAAUDDhDgAAAAUT7gAAAFCw6uW9ABZfx441S3zMrC/nZsb0L5phNQAAACwLwn0FctiwBzN52pJF+F0j6jKjmdYDAABA8/NWeQAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAAChYMeF+4okn5oYbblhm1zviiCPy7rvvLnK/gQMH5o9//GOj226//fa8/fbb3+n6l156ac4555zvdCwAAADfH9VNebK5c+emurpJT9lsrr766qU+xx133JEf/OAHWX/99ZtgRQAAALCwpX7FvVu3brnmmmsycODAXHbZZXn99ddzyCGHpF+/funbt29GjhzZsO+JJ56YU089NYMGDcoee+yR448/PpVKZaFzjh07NrW1tXnjjTcaveakSZOy9957J/nqlwXdu3fPNddckyS59957c+yxxyZJJk+enCFDhuSAAw5IbW1trrjiioZz9O7du+H8b731VgYMGJB99tknQ4cOzYEHHrjAq+zjxo3LT3/60+y66645//zzkyS33XZbXn755QwbNix1dXV56qmnknz1C4EDDjgg/fr1y7/+679mypQpSZIZM2ZkyJAh6du3bw477LDFerUfAAAAmuTl8fnz5+f6669PktTX12fkyJFp1apVZs6cmQEDBmTHHXfMBhtskCR58803M3LkyFRVVaVfv3556qmnsv322zec684778y1116ba665Jp07d270el27dk19fX0mT56cDz74IBtttFGefvrpHH744Rk7dmy22267JMkJJ5yQo446Kj179szs2bPzi1/8IltuueUC10uS448/Pj//+c9TV1eXP//5zznwwAMX2P7RRx/lxhtvzMyZM7PbbrvlgAMOyP77759Ro0Zl8ODB2WWXXZIko0ePzrvvvps//OEPadGiRW666aacffbZGTFiRC6//PK0a9cu9957b6ZOnZr+/ftnr732aoof/yJ16tR+mVyH5cO/L40xFzTGXNAYc0FjzAWNMRfLT5OEe79+/Rr+PGvWrJx++ul5/fXXU1VVlcmTJ+e1115rCPfddtstrVu3TpJsttlmeffddxtC+vbbb0/r1q1z7bXXpqam5luvue222+bpp5/O+++/n4MOOijXXHNNZs+enaeeeipHHHFEPv/884wbNy5Tp05tOGbmzJmZOHHiAuFeX1+fN954I7W1tUmSLbfcMt26dVvgWnvuuWdatGiR9u3bZ4MNNsi7776b9dZbb6E1PfLII3n55Zcbfh7z5s1reB7PPPNMTjnllCRJhw4dsvvuuy/6B9tEpkyZscyuxbLVqVN7/74sxFzQGHNBY8wFjTEXNMZcNK8WLarSseM3N3CThPvKK6/c8OcLLrggnTp1ytlnn53q6uoMHjw4X375ZcP2r6M9SVq2bJl58+Y1PO7WrVvGjx+ft956K1tttdW3XrNXr14ZO3Zs3n///Zx33nl59tlnc8899yRJ1llnndTX16eqqiq33nprVlpppW88T6VSSVVVVaqqqr5xn29b89+f68gjj8wBBxzQ6DYAAABYUk3+rfIzZsxIly5dUl1dnTfeeCPjx49f7GM333zzXHbZZTnuuOMybty4b923V69eefzxx/PXv/41Xbp0yU9+8pNceumlDW+Tr6mpSffu3XPVVVc1HPPRRx81fOb8a+3bt8+GG26Yu+++O0nyyiuvfONn6/9eu3btMmPG337r1Lt379x0003561//miSZPXt2XnvttYb13n777UmSadOm5aGHHlqsawAAAPD91uThfuSRR+aWW27J/vvvn9/97nfp2bPnEh3frVu3XHHFFTnllFPy+OOPf+N+Xbp0Sbt27dK9e/ckyXbbbZcPP/ywIdyT5Pzzz8/EiRNTW1ub2traHH300Zk+ffpC5zrnnHNy7bXXpn///rn55puzySabpH37RX9+46CDDspvf/vb7Lfffnnqqaey3377Zd99982hhx6a2tra9O/fP88991yS5Kijjsr06dPTt2/fHH/88Qt9zh4AAAAaU1XxHu58/vnnadu2baqqqvLWW29l4MCBuf/++7Pqqqsu76Ut4LBhD2bytC+W6Ji7RtT5LMo/MJ81ojHmgsaYCxpjLmiMuaAx5qJ5LZPPuK/onn/++Zx77rkNn0P/93//9+KiHQAAgO+nosN9zJgxueCCCxb6+2OOOSY777xzk11nhx12yA477NBk5wMAAICmUnS477zzzk0a6AAAALCiafIvpwMAAACajnAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAglUv7wWw+P7jlD2W+JhZX85thpUAAACwrAj3Fchnn9Vn/vzK8l4GAAAAy5C3ygMAAEDBhDsAAAAUTLgDAABAwYQ7AAAAFEy4AwAAQMGEOwAAABRMuAMAAEDBhDsAAAAUTLgDAABAwYQ7AAAAFEy4AwAAQMGEOwAAABRMuAMAAEDBhDsAAAAUTLgDAABAwYQ7AAAAFEy4AwAAQMGEOwAAABRMuAMAAEDBhDsAAAAUTLgDAABAwYQ7AAAAFEy4AwAAQMGEOwAAABRMuAMAAEDBhDsAAAAUTLgDAABAwYQ7AAAAFEy4AwAAQMGEOwAAABRMuAMAAEDBhDsAAAAUTLgDAABAwYQ7AAAAFEy4AwAAQMGEOwAAABRMuAMAAEDBhDsAAAAUTLgDAABAwYQ7AAAAFEy4AwAAQMGEOwAAABRMuAMAAEDBhDsAAAAUTLgDAABAwYQ7AAAAFEy4AwAAQMGEOwAAABRMuAMAAEDBhDsAAAAUTLgDAABAwYQ7AAAAFEy4AwAAQMGEOwAAABRMuAMAAEDBhDsAAAAUTLgDAABAwYQ7AAAAFEy4AwAAQMGEOwAAABRMuAMAAEDBhDsAAAAUTLgDAABAwYQ7AAAAFEy4AwAAQMGEOwAAABRMuAMAAEDBhDsAAAAUTLgDAABAwYQ7AAAAFEy4AwAAQMGEOwAAABRMuAMAAEDBhDsAAAAUTLgDAABAwYQ7AAAAFEy4AwAAQMGEOwAAABRMuAMAAEDBhDsAAAAUTLgDAABAwYQ7AAAAFKyqUqlUlvciAAAAoKnN+nJuZkz/YnkvY5FatKhKx44137i9ehmuhaV02LAHM3la+UMHAABQgrtG1GXG8l5EE/BWeQAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACjYYoX7Qw89lL322iv77bdfJk2atEQXmD59eq6++urvtLjmVFdXl1mzZi1yv969e+eNN95odNvIkSPz2Weffafrn3jiibnhhhu+07EAAAB8fyxWuN98880ZMmRIRo0ala5duy7RBaZPn55rrrnmOy1u7ty53+m4xTF69Oi0adNmqc5x3XXXfedwBwAAgMVRvagdhg8fnueeey5vv/12brrppqyxxhp5++23M2fOnKy77roZPnx4Vl111STJrbfemuuuuy5JstJKK+XKK6/MmWeemRkzZqSuri5t27bNzTffnL/85S859dRTM3Xq1FRXV+foo4/OTjvtlCTp1q1bjjvuuIwZMybdu3fPr3/964XW9Pjjj+f666/PVVddlc8++yw/+clPctFFF2WvvfbK1VdfnRkzZuSYY47JpEmTMnz48EybNi1z5szJz3/+8+y///4N13n++efTrl27jB8/PmeccUaSZNttt83DDz+cK6+8MhtvvHGS5L777su//du/ZcqUKRk8eHAOPfTQ/O53v8vkyZMzZMiQtG7dOiNGjMi6666bCy+8MM8++2zmzJmTjTfeOKeffnratWuXTz75JMcff3ymTZuWtddeO/PmzVv6fz0AAAC+VadO7Zf3EpbaIsP9pJNOyoQJEzJ48ODssssumTp1ajp06JAkufDCC3P11Vdn6NCheeaZZ3LllVfmpptuSqdOnTJz5sxUV1fn1FNPzf7775/Ro0c3nHPo0KE58MADM2DAgLz11lv52c9+lvvuu6/hvPPnz8/111//jWvq0aNHhg4dmjlz5uTpp5/O1ltvnaeffjp77bVXxo4dm8MPPzxz587N0KFDc95552WDDTZIfX199t9//2y11VbZYIMNGs41e/bsHHPMMbngggvSo0eP/M///M9C1541a1b++7//O++//35qa2vTr1+/HHnkkbnllltyySWXNAT+b3/727Rv3z633nprkuS8887LVVddlaOPPjrDhg1Lz54986tf/Srvvfde9t133+y4446L++8EAADAdzBlyozlvYRFatGiKh071nzj9kWG+98bPXp07rrrrsyZMyeff/551ltvvSTJo48+mrq6unTq1ClJ0q5du0aPr6+vz4QJExpe+d5www2z6aab5k9/+lN69+6dJOnXr9+3rqFt27bZcMMN8+KLL+app57KUUcdlfPOOy+zZ8/Oyy+/nB//+Md55513MnHixBxzzDENx82ZMyeTJk1aINwnTZqUNm3apEePHkmS3XffPausssoC1+vbt2+SZO21184qq6ySjz/+eIFzfO2RRx5JfX19HnjggSRf/VJgk002SZI888wzOeWUU5Ik66yzTnr16vWtzxEAAACSJQz38ePH5/e//31uvvnmdOjQIXfddVf+8Ic/NMlCqqqqGv688sorL3L/Xr16ZezYsXnxxRdz+umnp2PHjrn77rvTrVu3tG7dOpVKJT/4wQ8WeKV/ca7dmNatWzf8uWXLlt/4NvdKpZLTTjtNlAMAANBkluh/Bzd9+vTU1NRktdVWy+zZs3Pbbbc1bNtll10yevTofPrpp0mSmTNnZvbs2ampqcmsWbMavmiupqYmm266ae64444kycSJE/Paa6/lRz/60RItfLvttsvtt9+eLl26pFWrVunVq1cuu+yyhmhef/3106ZNm4waNarhmIkTJ6a+vn6B83Tt2jWff/55nnvuuSRffYP+9OnTF2sN7dq1y4wZf3vbRe/evTNy5MiGb6uvr6/PxIkTG9b79c/rvffey9NPP71EzxcAAIDvpyV6xX2nnXbKnXfemb322iudO3fOFltskT//+c9Jkm222Sa//OUv8y//8i+pqqpKq1atcsUVV2T11VdPbW1tamtrs+qqq+bmm2/O+eefn1NPPTUjR45MdXV1zj333IbPty+uH/3oR5k2bVoOOeSQJF+9An/BBRdku+22++qJVVfniiuuyPDhw/Mf//EfmT9/fjp27JiLLrpogfO0atUqI0aMyOmnn542bdpku+22y+qrr5727Rf9BQaDBg3KSSedlDZt2mTEiBH55S9/mcsuuywHHHBAqqqqUlVVlV/96lfZYIMNcvLJJ+f444/P/fffn/XXXz/bb7/9Ej1fAAAAvp+qKpVKZXkvYnmrr69PTc1XXwQwduzYnHjiiXnkkUfSosUSvSGh2R027MFMnvbF8l4GAADACuGuEXXfzy+n+0f04IMPZuTIkalUKg2vwJcW7QAAAHw/FR3ut9xyS2644YaF/v7ss8/Opptu2mTX6d+/f/r3799k5wMAAICmUnS4DxgwIAMGDFjeywAAAIDlxvvBAQAAoGDCHQAAAAom3AEAAKBgwh0AAAAKJtwBAACgYMIdAAAACibcAQAAoGDCHQAAAAom3AEAAKBgwh0AAAAKJtwBAACgYMIdAAAACibcAQAAoGDCHQAAAAom3AEAAKBgwh0AAAAKJtwBAACgYMIdAAAACibcAQAAoGDCHQAAAAom3AEAAKBgwh0AAAAKJtwBAACgYMIdAAAACibcAQAAoGDCHQAAAAom3AEAAKBgwh0AAAAKJtwBAACgYMIdAAAACibcAQAAoGDCHQAAAApWValUKst7EQAAANDUZn05NzOmf7G8l7FILVpUpWPHmm/cXr0M18JS+uyz+syf7/cs/E2nTu0zZcqM5b0MCmMuaIy5oDHmgsaYCxpjLpYvb5UHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKJhwBwAAgIIJdwAAACiYcAcAAICCCXcAAAAomHAHAACAggl3AAAAKFj18l4Ai69Fi6rlvQQKZC5ojLmgMeaCxpgLGmMuaIy5aD6L+tlWVSqVyjJaCwAAALCEvFUeAAAACibcAQAAoGDCHQAAAAom3AEAAKBgwh0AAAAKJtwBAACgYMIdAAAACibcAQAAoGDCHQAAAAom3Jext99+OwcddFD69OmTgw46KO+8885C+8ybNy9nnHFGdtttt+y+++655ZZblnobZWvOubj00kvTq1ev1NXVpa6uLmecccayeEo0gaWdiyeeeCL9+/fPFltskXPOOWexj6NszTkX7hcrrqWdi8svvzx777139t133/Tv3z+PP/74Yh1H2ZpzLtwvVlxLOxe33XZbamtrU1dXl9ra2lx33XWLdRxLqcIyNXDgwMqoUaMqlUqlMmrUqMrAgQMX2ueOO+6oDB48uDJv3rzKZ599Vtlxxx0r77333lJto2zNOReXXHJJ5eyzz152T4Yms7Rz8c4771ReeeWVygUXXLDQDLhfrLiacy7cL1ZcSzsXjz32WOXzzz+vVCqVyoQJEyrdu3evfPHFF4s8jrI151y4X6y4lnYuZsyYUZk/f37Dn//5n/+5MmHChEUex9Lxivsy9Nlnn+XVV1/NPvvskyTZZ5998uqrr2bq1KkL7HfvvfdmwIABadGiRTp06JDddtst999//1Jto1zNPResmJpiLv7pn/4pm222Waqrqxc6v5lZMTX3XLBiaoq52HHHHdO2bdskSbdu3VKpVPK///u/izyOcjX3XLBiaoq5qKmpSVVVVZJk1qxZmTNnTsNj94vmI9yXoY8++iidO3dOy5YtkyQtW7bMGmuskY8++mih/dZaa62Gx2uuuWY+/vjjpdpGuZp7LpLknnvuSW1tbQYPHpwXXnihOZ8OTaQp5mJR53e/WPE091wk7hcroqaei1GjRmXddddNly5dlug4ytLcc5G4X6yImmouHn744ey9997ZZZddcvjhh6dbt26LdRzfnXCHf3AHH3xwHn744dx111057LDDctRRR2XatGnLe1lAgdwvGDduXC6++OKMGDFieS+FgjQ2F+4X32+77rpr7rnnnjzwwAMZPXp0Jk2atLyX9A9PuC9Da665Zj755JPMmzcvyVdf3jB58uSsueaaC+334YcfNjz+6KOPGn67+V23Ua7mnotOnTplpZVWSpJsv/32WXPNNfPmm28263Ni6TXFXCzq/O4XK57mngv3ixVTU83FCy+8kOOOOy6XX355unbtutjHUabmngv3ixVTU/93ZK211sqWW26ZRx99dImOY8kJ92WoY8eO2XTTTXP33XcnSe6+++5suumm6dChwwL77bnnnrnlllsyf/78TJ06NQ899FD69OmzVNsoV3PPxSeffNJwjgkTJuSDDz7I+uuvv4yeHd9VU8zFt3G/WDE191y4X6yYmmIuXnrppRx99NG55JJLsvnmmy/2cZSruefC/WLF1BRzMXHixIb9pk6dmmeeeSYbb7zxIo9j6VRVKpXK8l7E98nEiRNz4oknZvr06VlllVVyzjnnpGvXrjniiCMyZMiQbLnllpk3b17OPPPMPPnkk0mSI444IgcddFCSfOdtlK055+KEE07IK6+8khYtWmSllVbKkCFDsvPOOy+fJ8oSWdq5GD9+fI455pjU19enUqmkffv2Oeuss7Ljjju6X6zAmnMu3C9WXEs7F/vvv38++OCDdO7cueGc5557brp16+Z+sQJrzrlwv1hxLe1cDB8+PE8++WSqq6tTqVQyYMCADBw4MIkeaU7CHQAAAArmrfIAAABQMOEOAAAABRPuAAAAUDDhDgAAAAUT7gAAAFAw4Q4AAAAFE+4AAABQMOEOAAAABft/JoedSHSAJdoAAAAASUVORK5CYII=",
111 | "text/plain": [
112 | ""
113 | ]
114 | },
115 | "metadata": {},
116 | "output_type": "display_data"
117 | }
118 | ],
119 | "source": [
120 | "from zipline.research import run_pipeline\n",
121 | "\n",
122 | "results = run_pipeline(pipeline, '2022-12-30', '2022-12-30')\n",
123 | "\n",
124 | "results.max().plot(kind=\"barh\", title=\"Maximum weights\");"
125 | ]
126 | },
127 | {
128 | "cell_type": "markdown",
129 | "metadata": {},
130 | "source": [
131 | "## Don't confuse yourself\n",
132 | "\n",
133 | "You can rank values in ascending order (the default) or in descending order (`rank(ascending=False)`). Whichever you choose, it's worth making a mental note of where \"good\" and \"bad\" values will end up in the ranked results, as it's easy to get confused. For some metrics, like D/E ratio, small numbers are \"good\" and large numbers are \"bad,\" while for other metrics, like return on equity, small numbers are \"bad\" and large numbers are \"good.\" Ranking in ascending order puts the small numbers first (ranks 1, 2, 3, etc.), while ranking in descending order puts the large numbers first. To avoid confusion, you may find it helpful to consistently choose the ranking order that will put the \"good\" numbers first."
134 | ]
135 | },
136 | {
137 | "cell_type": "markdown",
138 | "metadata": {},
139 | "source": [
140 | "***\n",
141 | "\n",
142 | "## *Next Up*\n",
143 | "\n",
144 | "Lesson 9: [Sector Neutralization](Lesson09-Sector-Neutralization.ipynb)"
145 | ]
146 | }
147 | ],
148 | "metadata": {
149 | "kernelspec": {
150 | "display_name": "Python 3.11",
151 | "language": "python",
152 | "name": "python3"
153 | },
154 | "language_info": {
155 | "codemirror_mode": {
156 | "name": "ipython",
157 | "version": 3
158 | },
159 | "file_extension": ".py",
160 | "mimetype": "text/x-python",
161 | "name": "python",
162 | "nbconvert_exporter": "python",
163 | "pygments_lexer": "ipython3",
164 | "version": "3.11.0"
165 | }
166 | },
167 | "nbformat": 4,
168 | "nbformat_minor": 4
169 | }
170 |
--------------------------------------------------------------------------------
/fundamental_factors/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/quantrocket-codeload/fundamental-factors/0c9c2c6d6c2979446c29d32584dbd06c7b38dfc3/fundamental_factors/__init__.py
--------------------------------------------------------------------------------
/fundamental_factors/universe.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 | from zipline.pipeline import master, EquityPricing
16 |
17 | def CommonStocks():
18 | """
19 | Return a Pipeline filter that is limited to domestic common stocks, excluding
20 | secondary shares. This filter is intended to be used as the initial_universe
21 | of the Pipeline.
22 | """
23 | category = master.SecuritiesMaster.sharadar_Category.latest
24 | common_stocks = (
25 | # domestic common stocks
26 | category.has_substring("Domestic Common")
27 | # no secondary shares
28 | & ~category.has_substring("Secondary")
29 | )
30 | return common_stocks
31 |
32 | def BaseUniverse():
33 | """
34 | Return a Pipeline filter that excludes illiquid stocks and penny stocks.
35 | This filter is intended to be used as the screen of the Pipeline or the
36 | mask of factors in the Pipeline.
37 | """
38 | base_universe = (EquityPricing.volume.latest > 0).all(21)
39 | base_universe = (EquityPricing.close.latest > 1.00).all(21, mask=base_universe)
40 |
41 | return base_universe
42 |
--------------------------------------------------------------------------------