├── .qrignore ├── README.md ├── backtest_speed_comparison ├── zipline_large_universe.py ├── moonshot_large_universe.py ├── Speed-Tests.ipynb └── lean_large_universe.py └── LICENSE.txt /.qrignore: -------------------------------------------------------------------------------- 1 | README.md 2 | LICENSE.txt 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backtest speed comparison 2 | 3 | Trading strategies used to test the speed of Zipline, Moonshot, and Lean. See the [accompanying article](https://www.quantrocket.com/blog/backtest-speed-comparison/) to see the results. 4 | -------------------------------------------------------------------------------- /backtest_speed_comparison/zipline_large_universe.py: -------------------------------------------------------------------------------- 1 | import zipline.api as algo 2 | from zipline.pipeline import Pipeline, sharadar 3 | from zipline.finance.execution import MarketOrder 4 | 5 | MARKETCAP_TOP_N = 1000 6 | BUNDLE = "sharadar-1d" 7 | 8 | def initialize(context): 9 | 10 | schedule = algo.date_rules.month_start() 11 | 12 | algo.schedule_function( 13 | rebalance, 14 | schedule 15 | ) 16 | 17 | # Create a pipeline to select stocks each day. 18 | algo.attach_pipeline(make_pipeline(), 'pipeline') 19 | 20 | def make_pipeline(): 21 | """ 22 | Create a pipeline to select stocks each day. 23 | """ 24 | 25 | in_universe = sharadar.Fundamentals.slice("ART").MARKETCAP.latest.top(MARKETCAP_TOP_N) 26 | f_score = sharadar.PiotroskiFScore(mask=in_universe) 27 | 28 | pipe = Pipeline( 29 | screen=f_score >= 7 30 | ) 31 | return pipe 32 | 33 | def rebalance(context, data): 34 | """ 35 | Execute orders according to our schedule_function() timing. 36 | """ 37 | context.output = algo.pipeline_output('pipeline') 38 | context.desired_portfolio = context.output.index 39 | 40 | positions = context.portfolio.positions 41 | 42 | target_pct = 1 / len(context.desired_portfolio) 43 | 44 | # open new positions 45 | for asset in context.desired_portfolio: 46 | if asset in positions: 47 | continue 48 | algo.order_target_percent( 49 | asset, target_pct, style=MarketOrder()) 50 | 51 | for asset in positions: 52 | 53 | # close positions we no longer want 54 | if asset not in context.desired_portfolio: 55 | algo.order_target_percent(asset, 0, style=MarketOrder()) 56 | -------------------------------------------------------------------------------- /backtest_speed_comparison/moonshot_large_universe.py: -------------------------------------------------------------------------------- 1 | from moonshot import Moonshot 2 | from quantrocket.fundamental import get_sharadar_fundamentals_reindexed_like 3 | 4 | class PiotroskiFScore(Moonshot): 5 | 6 | CODE = "moonshot-large-universe" 7 | DB = "sharadar-1d" 8 | DB_FIELDS = ["Close"] 9 | MIN_F_SCORE = 7 10 | MARKETCAP_TOP_N = 1000 11 | REBALANCE_INTERVAL = "M" 12 | 13 | def prices_to_signals(self, prices): 14 | 15 | closes = prices.loc["Close"] 16 | marketcaps = get_sharadar_fundamentals_reindexed_like( 17 | closes, fields="MARKETCAP", dimension="ART").loc["MARKETCAP"] 18 | marketcap_ranks = marketcaps.rank(axis=1, ascending=False) 19 | in_universe = marketcap_ranks <= self.MARKETCAP_TOP_N 20 | 21 | f_scores = get_f_scores(closes) 22 | long_signals = in_universe & (f_scores >= self.MIN_F_SCORE) 23 | 24 | return long_signals.astype(int) 25 | 26 | def signals_to_target_weights(self, signals, prices): 27 | # Step 4: equal weights 28 | daily_signal_counts = signals.abs().sum(axis=1) 29 | weights = signals.div(daily_signal_counts, axis=0).fillna(0) 30 | 31 | if self.REBALANCE_INTERVAL: 32 | # Step 5: Rebalance monthly 33 | # Resample daily to monthly, taking the last day's signal 34 | # For pandas offset aliases, see https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases 35 | weights = weights.resample(self.REBALANCE_INTERVAL).last() 36 | # Reindex back to daily and fill forward 37 | weights = weights.reindex(prices.loc["Close"].index, method="ffill") 38 | 39 | return weights 40 | 41 | def target_weights_to_positions(self, weights, prices): 42 | # Enter the position the day after the signal 43 | return weights.shift() 44 | 45 | def positions_to_gross_returns(self, positions, prices): 46 | 47 | closes = prices.loc["Close"] 48 | position_ends = positions.shift() 49 | 50 | # The return is the security's percent change over the period, 51 | # multiplied by the position. 52 | gross_returns = closes.pct_change() * position_ends 53 | 54 | return gross_returns 55 | 56 | def get_f_scores(closes): 57 | 58 | # Step 1: query relevant indicators 59 | fundamentals = get_sharadar_fundamentals_reindexed_like( 60 | closes, 61 | dimension="ART", # As-reported trailing twelve month reports 62 | fields=[ 63 | "ROA", # Return on assets 64 | "ASSETS", # Total Assets 65 | "NCFO", # Net Cash Flow from Operations 66 | "DE", # Debt to Equity Ratio 67 | "CURRENTRATIO", # Current ratio 68 | "SHARESWA", # Outstanding shares 69 | "GROSSMARGIN", # Gross margin 70 | "ASSETTURNOVER", # Asset turnover 71 | ]) 72 | return_on_assets = fundamentals.loc["ROA"] 73 | total_assets = fundamentals.loc["ASSETS"] 74 | operating_cash_flows = fundamentals.loc["NCFO"] 75 | leverages = fundamentals.loc["DE"] 76 | current_ratios = fundamentals.loc["CURRENTRATIO"] 77 | shares_out = fundamentals.loc["SHARESWA"] 78 | gross_margins = fundamentals.loc["GROSSMARGIN"] 79 | asset_turnovers = fundamentals.loc["ASSETTURNOVER"] 80 | 81 | # Step 2: many Piotroski F-score components compare current to previous 82 | # values, so get DataFrames of previous values 83 | 84 | # Step 2.a: get a boolean mask of the first day of each newly reported fiscal 85 | # period 86 | fundamentals = get_sharadar_fundamentals_reindexed_like( 87 | closes, 88 | dimension="ART", # As-reported trailing twelve month reports 89 | fields=["REPORTPERIOD"]) 90 | fiscal_periods = fundamentals.loc["REPORTPERIOD"] 91 | are_new_fiscal_periods = fiscal_periods != fiscal_periods.shift() 92 | 93 | # Step 2.b: shift the ROAs forward one fiscal period by (1) shifting the ratios one day, 94 | # (2) keeping only the ones that fall on the first day of the newly reported 95 | # fiscal period, and (3) forward-filling 96 | previous_return_on_assets = return_on_assets.shift().where(are_new_fiscal_periods).fillna(method="ffill") 97 | 98 | # Step 2.c: Repeat for other indicators 99 | previous_leverages = leverages.shift().where(are_new_fiscal_periods).fillna(method="ffill") 100 | previous_current_ratios = current_ratios.shift().where(are_new_fiscal_periods).fillna(method="ffill") 101 | previous_shares_out = shares_out.shift().where(are_new_fiscal_periods).fillna(method="ffill") 102 | previous_gross_margins = gross_margins.shift().where(are_new_fiscal_periods).fillna(method="ffill") 103 | previous_asset_turnovers = asset_turnovers.shift().where(are_new_fiscal_periods).fillna(method="ffill") 104 | 105 | # Step 3: calculate F-Score components; each resulting component is a DataFrame 106 | # of booleans 107 | have_positive_return_on_assets = return_on_assets > 0 108 | have_positive_operating_cash_flows = operating_cash_flows > 0 109 | have_increasing_return_on_assets = return_on_assets > previous_return_on_assets 110 | have_more_cash_flow_than_incomes = operating_cash_flows / total_assets > return_on_assets 111 | have_decreasing_leverages = leverages < previous_leverages 112 | have_increasing_current_ratios = current_ratios > previous_current_ratios 113 | have_no_new_shares = shares_out <= previous_shares_out 114 | have_increasing_gross_margins = gross_margins > previous_gross_margins 115 | have_increasing_asset_turnovers = asset_turnovers > previous_asset_turnovers 116 | 117 | # Step 4: convert the booleans to integers and sum to get F-Score (0-9) 118 | f_scores = ( 119 | have_positive_return_on_assets.astype(int) 120 | + have_positive_operating_cash_flows.astype(int) 121 | + have_increasing_return_on_assets.astype(int) 122 | + have_more_cash_flow_than_incomes.astype(int) 123 | + have_decreasing_leverages.astype(int) 124 | + have_increasing_current_ratios.astype(int) 125 | + have_no_new_shares.astype(int) 126 | + have_increasing_gross_margins.astype(int) 127 | + have_increasing_asset_turnovers.astype(int) 128 | ) 129 | return f_scores 130 | 131 | class PiotroskiFScoreSmallUniverse(PiotroskiFScore): 132 | 133 | CODE = "moonshot-small-universe" 134 | SIDS = [ 135 | "FIBBG000B9XRY4", # AAPL 136 | "FIBBG000BVPV84", # AMZN 137 | "FIBBG000BMHYD1", # JNJ 138 | "FIBBG000BKZB36", # HD 139 | "FIBBG000GZQ728", # XOM 140 | "FIBBG000BPH459", # MSFT 141 | "FIBBG000BNSZP1", # MCD 142 | "FIBBG000L9CV04", # UPS 143 | "FIBBG000CH5208", # UNH 144 | "FIBBG000HS77T5", # VZ 145 | ] 146 | -------------------------------------------------------------------------------- /backtest_speed_comparison/Speed-Tests.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 34, 6 | "id": "5e4da9c2-cc0e-4215-ac93-79a5c3987d3d", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from quantrocket.zipline import backtest as backtest_zipline\n", 11 | "from quantrocket.moonshot import backtest as backtest_moonshot" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "id": "f68cd847-e669-4df6-8f37-bfb4c8d73c70", 17 | "metadata": {}, 18 | "source": [ 19 | "## 2021 MacBook Pro M1" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "id": "6d06e060-4b0a-42f1-86d9-1c65188cef2c", 25 | "metadata": {}, 26 | "source": [ 27 | "### Small Universe (10 stocks)" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "id": "81e07abb-0af4-432a-bc55-e809330b6594", 33 | "metadata": {}, 34 | "source": [ 35 | "#### Zipline" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": null, 41 | "id": "a747c364", 42 | "metadata": {}, 43 | "outputs": [ 44 | { 45 | "name": "stdout", 46 | "output_type": "stream", 47 | "text": [ 48 | "CPU times: user 19.4 ms, sys: 30 ms, total: 49.4 ms\n", 49 | "Wall time: 11.4 s\n" 50 | ] 51 | } 52 | ], 53 | "source": [ 54 | "%%time \n", 55 | "\n", 56 | "backtest_zipline(\"zipline_small_universe\",\n", 57 | " start_date=\"2012-01-01\",\n", 58 | " end_date=\"2021-12-31\",\n", 59 | " capital_base=100e3,\n", 60 | " filepath_or_buffer=\"zipline_small_universe.csv\"\n", 61 | ")" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "id": "1a14f9e6-25c5-4451-b3c0-9a3fb67bef86", 67 | "metadata": {}, 68 | "source": [ 69 | "#### Moonshot" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "id": "625cad77", 76 | "metadata": {}, 77 | "outputs": [ 78 | { 79 | "name": "stdout", 80 | "output_type": "stream", 81 | "text": [ 82 | "CPU times: user 4.81 ms, sys: 2.74 ms, total: 7.55 ms\n", 83 | "Wall time: 836 ms\n" 84 | ] 85 | } 86 | ], 87 | "source": [ 88 | "%%time\n", 89 | "\n", 90 | "backtest_moonshot(\n", 91 | " \"moonshot-small-universe\",\n", 92 | " start_date=\"2012-01-01\",\n", 93 | " end_date=\"2022-01-01\",\n", 94 | " filepath_or_buffer=\"moonshot-small-universe.csv\",\n", 95 | ")" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "id": "b2d6218c-60f5-4dcb-a186-a6ea9521ce4c", 101 | "metadata": {}, 102 | "source": [ 103 | "### Large Universe" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "id": "2dd2fc0b-33fa-4de0-8768-a272eb13b898", 109 | "metadata": {}, 110 | "source": [ 111 | "#### Zipline" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "id": "2a9cb88f", 118 | "metadata": {}, 119 | "outputs": [ 120 | { 121 | "name": "stdout", 122 | "output_type": "stream", 123 | "text": [ 124 | "CPU times: user 481 ms, sys: 1.21 s, total: 1.69 s\n", 125 | "Wall time: 4min 36s\n" 126 | ] 127 | } 128 | ], 129 | "source": [ 130 | "%%time \n", 131 | "\n", 132 | "backtest_zipline(\"zipline_large_universe\",\n", 133 | " start_date=\"2012-01-01\",\n", 134 | " end_date=\"2021-12-31\",\n", 135 | " capital_base=100e3,\n", 136 | " progress=\"Y\",\n", 137 | " filepath_or_buffer=\"zipline_large_universe.csv\"\n", 138 | ")" 139 | ] 140 | }, 141 | { 142 | "cell_type": "markdown", 143 | "id": "acf88d72-2fff-4254-b5ea-ab70ebd9b30c", 144 | "metadata": {}, 145 | "source": [ 146 | "#### Moonshot" 147 | ] 148 | }, 149 | { 150 | "cell_type": "code", 151 | "execution_count": null, 152 | "id": "3dc48e02", 153 | "metadata": {}, 154 | "outputs": [ 155 | { 156 | "name": "stdout", 157 | "output_type": "stream", 158 | "text": [ 159 | "CPU times: user 29.7 ms, sys: 0 ns, total: 29.7 ms\n", 160 | "Wall time: 44.3 s\n" 161 | ] 162 | } 163 | ], 164 | "source": [ 165 | "%%time\n", 166 | "\n", 167 | "backtest_moonshot(\n", 168 | " \"moonshot-large-universe\",\n", 169 | " start_date=\"2012-01-01\",\n", 170 | " end_date=\"2021-12-31\",\n", 171 | " filepath_or_buffer=\"moonshot-large-universe.csv\",\n", 172 | " segment=\"Y\"\n", 173 | ")" 174 | ] 175 | }, 176 | { 177 | "cell_type": "markdown", 178 | "id": "75c8f6b1-0864-4caf-bd27-4f8d35a22f15", 179 | "metadata": {}, 180 | "source": [ 181 | "## 2014 Mac Pro Intel" 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "id": "b5d37555-d790-49a0-88b7-ac78f7078879", 187 | "metadata": {}, 188 | "source": [ 189 | "### Large Universe" 190 | ] 191 | }, 192 | { 193 | "cell_type": "markdown", 194 | "id": "107ab4ad-8d5b-4d16-aaf5-2e61af864314", 195 | "metadata": {}, 196 | "source": [ 197 | "#### Zipline" 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": null, 203 | "id": "9fb50b6c", 204 | "metadata": {}, 205 | "outputs": [ 206 | { 207 | "name": "stdout", 208 | "output_type": "stream", 209 | "text": [ 210 | "CPU times: user 1.44 s, sys: 1.73 s, total: 3.17 s\n", 211 | "Wall time: 12min 16s\n" 212 | ] 213 | } 214 | ], 215 | "source": [ 216 | "%%time \n", 217 | "backtest_zipline(\"zipline_large_universe\",\n", 218 | " start_date=\"2012-01-01\",\n", 219 | " end_date=\"2021-12-31\",\n", 220 | " capital_base=100e3,\n", 221 | " progress=\"Y\",\n", 222 | " filepath_or_buffer=\"zipline_large_universe.csv\",\n", 223 | ")" 224 | ] 225 | }, 226 | { 227 | "cell_type": "markdown", 228 | "id": "a870aeac-5a45-4370-a90f-73adaab3828a", 229 | "metadata": {}, 230 | "source": [ 231 | "#### Moonshot" 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": null, 237 | "id": "24491dca", 238 | "metadata": {}, 239 | "outputs": [ 240 | { 241 | "name": "stdout", 242 | "output_type": "stream", 243 | "text": [ 244 | "CPU times: user 20.3 ms, sys: 14.2 ms, total: 34.5 ms\n", 245 | "Wall time: 3min 40s\n" 246 | ] 247 | } 248 | ], 249 | "source": [ 250 | "%%time\n", 251 | "\n", 252 | "backtest_moonshot(\n", 253 | " \"moonshot-large-universe\",\n", 254 | " start_date=\"2012-01-01\",\n", 255 | " end_date=\"2021-12-31\",\n", 256 | " filepath_or_buffer=\"moonshot-large-universe.csv\",\n", 257 | " segment=\"Y\"\n", 258 | ")" 259 | ] 260 | }, 261 | { 262 | "cell_type": "markdown", 263 | "id": "1265aebf-5fd2-4874-8221-3018e83caee7", 264 | "metadata": {}, 265 | "source": [ 266 | "## Digital Ocean Basic Droplet, Shared CPU (4 VCPUs)" 267 | ] 268 | }, 269 | { 270 | "cell_type": "markdown", 271 | "id": "d88ef07a-d6ad-4c00-9d6a-e20a8cbb21d9", 272 | "metadata": {}, 273 | "source": [ 274 | "### Large Universe" 275 | ] 276 | }, 277 | { 278 | "cell_type": "markdown", 279 | "id": "e642e900-a2b6-409f-ab87-05275a3cfc76", 280 | "metadata": {}, 281 | "source": [ 282 | "#### Zipline" 283 | ] 284 | }, 285 | { 286 | "cell_type": "code", 287 | "execution_count": null, 288 | "id": "eb0ca9f0", 289 | "metadata": {}, 290 | "outputs": [ 291 | { 292 | "name": "stdout", 293 | "output_type": "stream", 294 | "text": [ 295 | "CPU times: user 1.07 s, sys: 1.33 s, total: 2.4 s\n", 296 | "Wall time: 16min 17s\n" 297 | ] 298 | } 299 | ], 300 | "source": [ 301 | "%%time \n", 302 | "backtest_zipline(\"zipline_large_universe\",\n", 303 | " start_date=\"2012-01-01\",\n", 304 | " end_date=\"2021-12-31\",\n", 305 | " capital_base=100e3,\n", 306 | " progress=\"Y\",\n", 307 | " filepath_or_buffer=\"zipline_large_universe.csv\",\n", 308 | ")" 309 | ] 310 | } 311 | ], 312 | "metadata": { 313 | "kernelspec": { 314 | "display_name": "Python 3.9", 315 | "language": "python", 316 | "name": "python3" 317 | }, 318 | "language_info": { 319 | "codemirror_mode": { 320 | "name": "ipython", 321 | "version": 3 322 | }, 323 | "file_extension": ".py", 324 | "mimetype": "text/x-python", 325 | "name": "python", 326 | "nbconvert_exporter": "python", 327 | "pygments_lexer": "ipython3", 328 | "version": "3.9.12" 329 | } 330 | }, 331 | "nbformat": 4, 332 | "nbformat_minor": 5 333 | } 334 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /backtest_speed_comparison/lean_large_universe.py: -------------------------------------------------------------------------------- 1 | # Adapted from Piotroski-F-Score-Investing strategy in QuantConnect Research Library: 2 | # https://www.quantconnect.com/explore/15314047/Piotroski-F-Score-Investing 3 | 4 | # region imports 5 | from AlgorithmImports import * 6 | # endregion 7 | 8 | def GetROAScore(fine): 9 | '''Get the Profitability - Return of Asset sub-score of Piotroski F-Score 10 | Arg: 11 | fine: Fine fundamental object of a stock 12 | Return: 13 | Profitability - Return of Asset sub-score''' 14 | # Nearest ROA as current year data 15 | roa = fine.OperationRatios.ROA.ThreeMonths 16 | # 1 score if ROA datum exists and positive, else 0 17 | score = 1 if roa and roa > 0 else 0 18 | return score 19 | 20 | def GetOperatingCashFlowScore(fine): 21 | '''Get the Profitability - Operating Cash Flow sub-score of Piotroski F-Score 22 | Arg: 23 | fine: Fine fundamental object of a stock 24 | Return: 25 | Profitability - Operating Cash Flow sub-score''' 26 | # Nearest Operating Cash Flow as current year data 27 | operating_cashflow = fine.FinancialStatements.CashFlowStatement.CashFlowFromContinuingOperatingActivities.ThreeMonths 28 | # 1 score if operating cash flow datum exists and positive, else 0 29 | score = 1 if operating_cashflow and operating_cashflow > 0 else 0 30 | return score 31 | 32 | def GetROAChangeScore(fine): 33 | '''Get the Profitability - Change in Return of Assets sub-score of Piotroski F-Score 34 | Arg: 35 | fine: Fine fundamental object of a stock 36 | Return: 37 | Profitability - Change in Return of Assets sub-score''' 38 | # if current or previous year's ROA data does not exist, return 0 score 39 | roa = fine.OperationRatios.ROA 40 | if not roa.ThreeMonths or not roa.OneYear: 41 | return 0 42 | 43 | # 1 score if change in ROA positive, else 0 score 44 | score = 1 if roa.ThreeMonths > roa.OneYear else 0 45 | return score 46 | 47 | def GetAccrualsScore(fine): 48 | '''Get the Profitability - Accruals sub-score of Piotroski F-Score 49 | Arg: 50 | fine: Fine fundamental object of a stock 51 | Return: 52 | Profitability - Accruals sub-score''' 53 | # Nearest Operating Cash Flow, Total Assets, ROA as current year data 54 | operating_cashflow = fine.FinancialStatements.CashFlowStatement.CashFlowFromContinuingOperatingActivities.ThreeMonths 55 | total_assets = fine.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths 56 | roa = fine.OperationRatios.ROA.ThreeMonths 57 | # 1 score if operating cash flow, total assets and ROA exists, and operating cash flow / total assets > ROA, else 0 58 | score = 1 if operating_cashflow and total_assets and roa and operating_cashflow / total_assets > roa else 0 59 | return score 60 | 61 | def GetLeverageScore(fine): 62 | '''Get the Leverage, Liquidity and Source of Funds - Change in Leverage sub-score of Piotroski F-Score 63 | Arg: 64 | fine: Fine fundamental object of a stock 65 | Return: 66 | Leverage, Liquidity and Source of Funds - Change in Leverage sub-score''' 67 | # if current or previous year's long term debt to equity ratio data does not exist, return 0 score 68 | long_term_debt_ratio = fine.OperationRatios.LongTermDebtEquityRatio 69 | if not long_term_debt_ratio.ThreeMonths or not long_term_debt_ratio.OneYear: 70 | return 0 71 | 72 | # 1 score if long term debt ratio is lower in the current year, else 0 score 73 | score = 1 if long_term_debt_ratio.ThreeMonths < long_term_debt_ratio.OneYear else 0 74 | return score 75 | 76 | def GetLiquidityScore(fine): 77 | '''Get the Leverage, Liquidity and Source of Funds - Change in Liquidity sub-score of Piotroski F-Score 78 | Arg: 79 | fine: Fine fundamental object of a stock 80 | Return: 81 | Leverage, Liquidity and Source of Funds - Change in Liquidity sub-score''' 82 | # if current or previous year's current ratio data does not exist, return 0 score 83 | current_ratio = fine.OperationRatios.CurrentRatio 84 | if not current_ratio.ThreeMonths or not current_ratio.OneYear: 85 | return 0 86 | 87 | # 1 score if current ratio is higher in the current year, else 0 score 88 | score = 1 if current_ratio.ThreeMonths > current_ratio.OneYear else 0 89 | return score 90 | 91 | def GetShareIssuedScore(fine): 92 | '''Get the Leverage, Liquidity and Source of Funds - Change in Number of Shares sub-score of Piotroski F-Score 93 | Arg: 94 | fine: Fine fundamental object of a stock 95 | Return: 96 | Leverage, Liquidity and Source of Funds - Change in Number of Shares sub-score''' 97 | # if current or previous year's issued shares data does not exist, return 0 score 98 | shares_issued = fine.FinancialStatements.BalanceSheet.ShareIssued 99 | if not shares_issued.ThreeMonths or not shares_issued.TwelveMonths: 100 | return 0 101 | 102 | # 1 score if shares issued did not increase in the current year, else 0 score 103 | score = 1 if shares_issued.ThreeMonths <= shares_issued.TwelveMonths else 0 104 | return score 105 | 106 | def GetGrossMarginScore(fine): 107 | '''Get the Leverage, Liquidity and Source of Funds - Change in Gross Margin sub-score of Piotroski F-Score 108 | Arg: 109 | fine: Fine fundamental object of a stock 110 | Return: 111 | Leverage, Liquidity and Source of Funds - Change in Gross Margin sub-score''' 112 | # if current or previous year's gross margin data does not exist, return 0 score 113 | gross_margin = fine.OperationRatios.GrossMargin 114 | if not gross_margin.ThreeMonths or not gross_margin.OneYear: 115 | return 0 116 | 117 | # 1 score if gross margin is higher in the current year, else 0 score 118 | score = 1 if gross_margin.ThreeMonths > gross_margin.OneYear else 0 119 | return score 120 | 121 | def GetAssetTurnoverScore(fine): 122 | '''Get the Leverage, Liquidity and Source of Funds - Change in Asset Turnover Ratio sub-score of Piotroski F-Score 123 | Arg: 124 | fine: Fine fundamental object of a stock 125 | Return: 126 | Leverage, Liquidity and Source of Funds - Change in Asset Turnover Ratio sub-score''' 127 | # if current or previous year's asset turnover data does not exist, return 0 score 128 | asset_turnover = fine.OperationRatios.AssetsTurnover 129 | if not asset_turnover.ThreeMonths or not asset_turnover.OneYear: 130 | return 0 131 | 132 | # 1 score if asset turnover is higher in the current year, else 0 score 133 | score = 1 if asset_turnover.ThreeMonths > asset_turnover.OneYear else 0 134 | return score 135 | 136 | MARKETCAP_TOP_N = 1000 137 | 138 | class FScoreUniverseSelectionModel(FineFundamentalUniverseSelectionModel): 139 | 140 | def __init__(self, algorithm, fscore_threshold): 141 | super().__init__(self.SelectCoarse, self.SelectFine) 142 | self.algorithm = algorithm 143 | self.fscore_threshold = fscore_threshold 144 | 145 | def SelectCoarse(self, coarse): 146 | '''Defines the coarse fundamental selection function. 147 | Args: 148 | algorithm: The algorithm instance 149 | coarse: The coarse fundamental data used to perform filtering 150 | Returns: 151 | An enumerable of symbols passing the filter''' 152 | filtered = [x.Symbol for x in coarse if x.HasFundamentalData] 153 | 154 | return filtered 155 | 156 | def SelectFine(self, fine): 157 | '''Defines the fine fundamental selection function. 158 | Args: 159 | algorithm: The algorithm instance 160 | fine: The fine fundamental data used to perform filtering 161 | Returns: 162 | An enumerable of symbols passing the filter''' 163 | # We use a dictionary to hold the F-Score of each stock 164 | f_scores = {} 165 | 166 | fine = sorted(fine, key=lambda x: x.MarketCap, reverse=True)[:MARKETCAP_TOP_N] 167 | 168 | for f in fine: 169 | # Calculate the Piotroski F-Score of the given stock 170 | f_scores[f.Symbol] = self.GetPiotroskiFScore(f) 171 | 172 | # Select the stocks with F-Score higher than the threshold 173 | selected = [symbol for symbol, fscore in f_scores.items() if fscore >= self.fscore_threshold] 174 | 175 | return selected 176 | 177 | def GetPiotroskiFScore(self, fine): 178 | '''A helper function to calculate the Piotroski F-Score of a stock 179 | Arg: 180 | fine: MorningStar fine fundamental data of the stock 181 | return: 182 | the Piotroski F-Score of the stock 183 | ''' 184 | # initial F-Score as 0 185 | fscore = 0 186 | # Add up the sub-scores in different aspects 187 | fscore += GetROAScore(fine) 188 | fscore += GetOperatingCashFlowScore(fine) 189 | fscore += GetROAChangeScore(fine) 190 | fscore += GetAccrualsScore(fine) 191 | fscore += GetLeverageScore(fine) 192 | fscore += GetLiquidityScore(fine) 193 | fscore += GetShareIssuedScore(fine) 194 | fscore += GetGrossMarginScore(fine) 195 | fscore += GetAssetTurnoverScore(fine) 196 | return fscore 197 | 198 | # wrap ConstantAlphaModel to only rebalance monthly; see 199 | # https://www.quantconnect.com/forum/discussion/7977/modifying-insight-duration-within-bootcamp-algorithm-framework/p1/comment-22242 200 | class AlphaModel(ConstantAlphaModel): 201 | 202 | def __init__(self, *args, **kwargs): 203 | self.month = -1 204 | super().__init__(*args, **kwargs) 205 | 206 | def Update(self, algorithm, data): 207 | if self.month == algorithm.Time.month: 208 | return [] 209 | self.month = algorithm.Time.month 210 | return super().Update(algorithm, data) 211 | 212 | class PiotroskiFScoreInvesting(QCAlgorithm): 213 | 214 | def Initialize(self): 215 | self.SetStartDate(2012, 1, 1) 216 | self.SetEndDate(2021, 12, 31) 217 | self.SetCash(10000000) 218 | 219 | fscore_threshold = self.GetParameter("fscore_threshold", 7) 220 | 221 | self.UniverseSettings.Resolution = Resolution.Daily 222 | 223 | # Our universe is selected by Piotroski's F-Score 224 | self.AddUniverseSelection(FScoreUniverseSelectionModel(self, fscore_threshold)) 225 | # Buy and hold the selected stocks 226 | self.AddAlpha(AlphaModel(InsightType.Price, InsightDirection.Up, timedelta(1))) 227 | # Equal-weighted portfolio 228 | self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel()) 229 | 230 | def OnSecuritiesChanged(self, changes): 231 | pass 232 | --------------------------------------------------------------------------------