lumibot 4.2.7__py3-none-any.whl → 4.2.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of lumibot might be problematic. Click here for more details.
- lumibot/brokers/broker.py +33 -0
- lumibot/brokers/tradovate.py +556 -10
- lumibot/data_sources/databento_data_polars.py +35 -34
- lumibot/entities/asset.py +11 -0
- lumibot/strategies/strategy.py +46 -1
- lumibot/tools/databento_helper.py +16 -0
- lumibot/tools/databento_helper_polars.py +41 -1
- lumibot/tools/futures_roll.py +71 -20
- lumibot/tools/thetadata_helper.py +103 -37
- {lumibot-4.2.7.dist-info → lumibot-4.2.10.dist-info}/METADATA +1 -1
- {lumibot-4.2.7.dist-info → lumibot-4.2.10.dist-info}/RECORD +18 -17
- tests/test_futures_roll.py +20 -0
- tests/test_strategy_close_position.py +83 -0
- tests/test_thetadata_helper.py +121 -4
- tests/test_tradovate.py +293 -0
- {lumibot-4.2.7.dist-info → lumibot-4.2.10.dist-info}/WHEEL +0 -0
- {lumibot-4.2.7.dist-info → lumibot-4.2.10.dist-info}/licenses/LICENSE +0 -0
- {lumibot-4.2.7.dist-info → lumibot-4.2.10.dist-info}/top_level.txt +0 -0
|
@@ -19,7 +19,7 @@ lumibot/backtesting/yahoo_backtesting.py,sha256=LT2524mGlrUSq1YSRnUqGW4-Xcq4USgR
|
|
|
19
19
|
lumibot/brokers/__init__.py,sha256=MGWKHeH3mqseYRL7u-KX1Jp2x9EaFO4Ol8sfNSxzu1M,404
|
|
20
20
|
lumibot/brokers/alpaca.py,sha256=VQ17idfqiEFb2JCqqdMGmbvF789L7_PpsCbudiFRzmg,61595
|
|
21
21
|
lumibot/brokers/bitunix.py,sha256=hwcYC4goXsukSHSevb9W5irJz2lJt5tSgK2X5S0VyUs,34555
|
|
22
|
-
lumibot/brokers/broker.py,sha256=
|
|
22
|
+
lumibot/brokers/broker.py,sha256=yCKQluRh52SGLWT3-41YQ8uO4GFS3M-AE1PzgWPfYpk,73249
|
|
23
23
|
lumibot/brokers/ccxt.py,sha256=9F8YeEF9HBRGgcwJ9WTSb2pKRXlh_zUj-CeA1j4K77w,31434
|
|
24
24
|
lumibot/brokers/example_broker.py,sha256=mjfBaPU8kJvLwigMKczSeFcmPYQIB5L1CkqvNGnvat4,8661
|
|
25
25
|
lumibot/brokers/interactive_brokers.py,sha256=qOTvOLOk01_LnF7B-t_5gtmuDtXitqV_WUkAZYFdRLw,60526
|
|
@@ -28,7 +28,7 @@ lumibot/brokers/projectx.py,sha256=JT7ysIQ4ek-yZdNrmuZYA6aKZMinSh1GnraetDrsjh0,7
|
|
|
28
28
|
lumibot/brokers/schwab.py,sha256=eiBEm-WXtzMZQhY1eyErNazkso8ONqJFtGNIcxdDOHE,91486
|
|
29
29
|
lumibot/brokers/tradeovate.py,sha256=NBGw79aWWL0JlNF34EAJQ5dfB3HkiGuWhuSVQ5yg1ZI,22091
|
|
30
30
|
lumibot/brokers/tradier.py,sha256=E45lj4LV-lrF3mKgtZtoYDXoetgbnFlsmYb5HI7bgbM,50863
|
|
31
|
-
lumibot/brokers/tradovate.py,sha256=
|
|
31
|
+
lumibot/brokers/tradovate.py,sha256=88T0578ALiWfE7kB2axJj9Uy2vHX95OS4k89n4m-lbo,61998
|
|
32
32
|
lumibot/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
33
|
lumibot/components/configs_helper.py,sha256=e4-2jBmjsIDeMQ9fbQ9j2U6WlHCe2z9bIQtJNlP_iEs,3066
|
|
34
34
|
lumibot/components/drift_rebalancer_logic.py,sha256=KL1S9Su5gxibGzl2fw_DH8StzXvjFfFl23sOFGHgIZI,28653
|
|
@@ -48,7 +48,7 @@ lumibot/data_sources/data_source.py,sha256=TrOlzYGrwc8PGru0c0DFK5DkoAc9Q0GrIzWce
|
|
|
48
48
|
lumibot/data_sources/data_source_backtesting.py,sha256=PPlfhuYzaqjs9IkS8CXMSBwgB4oEHoi6u3hWqjSiqCc,9619
|
|
49
49
|
lumibot/data_sources/databento_data.py,sha256=PIv0SWB6WJbu9sfxfuifXXKjDFuuiSVJMzgORal8K0Y,332
|
|
50
50
|
lumibot/data_sources/databento_data_pandas.py,sha256=H4T53OGZi1ymzMKW0F-BES9DIGFG7BLDW5iV_zeUaYA,16293
|
|
51
|
-
lumibot/data_sources/databento_data_polars.py,sha256=
|
|
51
|
+
lumibot/data_sources/databento_data_polars.py,sha256=9PqJ3OG9z_dKxRnCUzjWpvit5XPSJGhPSxExvVx-9L0,36790
|
|
52
52
|
lumibot/data_sources/databento_data_polars_backtesting.py,sha256=0YA8O7qe8aTxdFcc9ALhUZSHUJH70PnNG0eK1ZpLZt0,25864
|
|
53
53
|
lumibot/data_sources/databento_data_polars_live.py,sha256=NJc1nHU6P94uUZQWtQ0z5-bi1SRKCkW4-yCFmwZ5FDI,36479
|
|
54
54
|
lumibot/data_sources/example_broker_data.py,sha256=WkCc3rLyOOI4GzLptUwhqeFRkEWQsM4y3tdzUbHPfik,2066
|
|
@@ -67,7 +67,7 @@ lumibot/data_sources/tradovate_data.py,sha256=TY1MCXVTqiJTAYKN_5oinFHuvn4q6P5Re1
|
|
|
67
67
|
lumibot/data_sources/yahoo_data.py,sha256=dFgsUNEhLDY_Erimeczuz2LLrIB3abPPFJay6E9Czig,19132
|
|
68
68
|
lumibot/data_sources/yahoo_data_polars.py,sha256=0JB7RFoaoVS4JKLegT_kXxbVOLP3MlXRgeqrqCoiw2o,14244
|
|
69
69
|
lumibot/entities/__init__.py,sha256=-zmHEFGc5TfOl0CqHKOGUrSfnL_UZArW94r7HpJrc2Q,637
|
|
70
|
-
lumibot/entities/asset.py,sha256=
|
|
70
|
+
lumibot/entities/asset.py,sha256=VlW4iNW0ab6zCEIjx-SOWeNMh2ud1vVkOWDgfRiyuzY,29046
|
|
71
71
|
lumibot/entities/bar.py,sha256=_2gFtBzdbNPassLf1SU9J1mYKCZDner2h52qHl8e0oA,6003
|
|
72
72
|
lumibot/entities/bars.py,sha256=N5N6ouQdX8-w2h1sstXMczNUNdM6d12YukmqUgmxTQ4,24506
|
|
73
73
|
lumibot/entities/chains.py,sha256=4P42AUtoPrRQHnPVj-g6mJ5W2nVf17ju3gXdavZcVyk,6281
|
|
@@ -106,7 +106,7 @@ lumibot/resources/conf.yaml,sha256=rjB9-10JP7saZ_edjX5bQDGfuc3amOQTUUUr-UiMpNA,5
|
|
|
106
106
|
lumibot/strategies/__init__.py,sha256=jEZ95K5hG0f595EXYKWwL2_UsnWWk5Pug361PK2My2E,79
|
|
107
107
|
lumibot/strategies/_strategy.py,sha256=3Z2MPz3jYgJdAphaFyoEI5OUs_6ClAJgRCrLWy-b2sg,111718
|
|
108
108
|
lumibot/strategies/session_manager.py,sha256=Nze6UYNSPlCsf-tyHvtFqUeL44WSNHjwsKrIepvsyCY,12956
|
|
109
|
-
lumibot/strategies/strategy.py,sha256=
|
|
109
|
+
lumibot/strategies/strategy.py,sha256=UKZdwiHKEKwRL7SOefskGUt6UZ9wcLAuSixKSnJUmCc,172351
|
|
110
110
|
lumibot/strategies/strategy_executor.py,sha256=AnmXlKD2eMgKXs3TrD1u8T_Zsn_8GnG5KRcM_Pq-JBQ,70749
|
|
111
111
|
lumibot/tools/__init__.py,sha256=oRRoK2NBkfnc0kueAfY0HrWVKgzRBO1hlglVMR4jr5M,1501
|
|
112
112
|
lumibot/tools/alpaca_helpers.py,sha256=nhBS-sv28lZfIQ85szC9El8VHLrCw5a5KbsGOOEjm6w,3147
|
|
@@ -114,12 +114,12 @@ lumibot/tools/backtest_cache.py,sha256=A-Juzu0swZI_FP4U7cd7ruYTgJYgV8BPf_WJDI-Nt
|
|
|
114
114
|
lumibot/tools/bitunix_helpers.py,sha256=-UzrN3w_Y-Ckvhl7ZBoAcx7sgb6tH0KcpVph1Ovm3gw,25780
|
|
115
115
|
lumibot/tools/black_scholes.py,sha256=TBjJuDTudvqsbwqSb7-zb4gXsJBCStQFaym8xvePAjw,25428
|
|
116
116
|
lumibot/tools/ccxt_data_store.py,sha256=PlP3MHPHZP7GisEZsk1OUxeWijoPXwiQbsOBTr7jkQI,21227
|
|
117
|
-
lumibot/tools/databento_helper.py,sha256=
|
|
118
|
-
lumibot/tools/databento_helper_polars.py,sha256=
|
|
117
|
+
lumibot/tools/databento_helper.py,sha256=Tvljv-IZ9xZFFTcr91qkegLiYv8SFxCXrQV2MJMABy8,44493
|
|
118
|
+
lumibot/tools/databento_helper_polars.py,sha256=mr8gaUwtGu9uPh7v9syXkSbodEEsqBxzIMPoPAHtfR0,50272
|
|
119
119
|
lumibot/tools/databento_roll.py,sha256=48HAw3h6OngCK4UTl9ifpjo-ki8qmB6OoJUrHp0gRmE,6767
|
|
120
120
|
lumibot/tools/debugers.py,sha256=ga6npFsS9cpKtTXaygh9t2_txCElg3bfzfeqDBvSL8k,485
|
|
121
121
|
lumibot/tools/decorators.py,sha256=gokLv6s37C1cnbnFSVOUc4RaVJ5aMTU2C344Vvi3ycs,2275
|
|
122
|
-
lumibot/tools/futures_roll.py,sha256=
|
|
122
|
+
lumibot/tools/futures_roll.py,sha256=97xSI32Yko8jqOcTWl-XG-jdSMAgJCd02c7chjfas_w,8870
|
|
123
123
|
lumibot/tools/futures_symbols.py,sha256=hFV02dk9cKucdaFOQAiQrlS15AJzdZ0qCuzVn7PfoPg,7851
|
|
124
124
|
lumibot/tools/helpers.py,sha256=Q459K0aQGUME2CfwBCXmKbUQwiGR9FKSjUN2yLbBMIE,17873
|
|
125
125
|
lumibot/tools/indicators.py,sha256=sihuiQTJ92igCBBMBQcyhpJFc-AWyj94vLQlTp6fu6Q,38465
|
|
@@ -132,7 +132,7 @@ lumibot/tools/polygon_helper_async.py,sha256=YHDXa9kmkkn8jh7hToY6GP5etyXS9Tj-uky
|
|
|
132
132
|
lumibot/tools/polygon_helper_polars_optimized.py,sha256=NaIZ-5Av-G2McPEKHyJ-x65W72W_Agnz4lRgvXfQp8c,30415
|
|
133
133
|
lumibot/tools/projectx_helpers.py,sha256=EIemLfbG923T_RBV_i6s6A9xgs7dt0et0oCnhFwdWfA,58299
|
|
134
134
|
lumibot/tools/schwab_helper.py,sha256=CXnYhgsXOIb5MgmIYOp86aLxsBF9oeVrMGrjwl_GEv0,11768
|
|
135
|
-
lumibot/tools/thetadata_helper.py,sha256=
|
|
135
|
+
lumibot/tools/thetadata_helper.py,sha256=DAKQCQkItgzQ2MvPsAd2HcJYEOuuPc3NkWb1hsXmFhE,90220
|
|
136
136
|
lumibot/tools/types.py,sha256=x-aQBeC6ZTN2-pUyxyo69Q0j5e0c_swdfe06kfrWSVc,1978
|
|
137
137
|
lumibot/tools/yahoo_helper.py,sha256=htcKKkuktatIckVKfLc_ms0X75mXColysQhrZW244z8,19497
|
|
138
138
|
lumibot/tools/yahoo_helper_polars_optimized.py,sha256=g9xBN-ReHSW4Aj9EMU_OncBXVS1HpfL8LTHit9ZxFY4,7417
|
|
@@ -142,7 +142,7 @@ lumibot/traders/trader.py,sha256=KMif3WoZtnSxA0BzoK3kvkTITNELrDFIortx1BYBv8s,967
|
|
|
142
142
|
lumibot/trading_builtins/__init__.py,sha256=vH2QL5zLjL3slfEV1YW-BvQHtEYLCFkIWTZDfh3y8LE,87
|
|
143
143
|
lumibot/trading_builtins/custom_stream.py,sha256=8_XiPT0JzyXrgnXCXoovGGUrWEfnG4ohIYMPfB_Nook,5264
|
|
144
144
|
lumibot/trading_builtins/safe_list.py,sha256=IIjZOHSiZYK25A4WBts0oJaZNOJDsjZL65MOSHhE3Ig,1975
|
|
145
|
-
lumibot-4.2.
|
|
145
|
+
lumibot-4.2.10.dist-info/licenses/LICENSE,sha256=fYhGIyxjyNXACgpNQS3xxpxDOaVOWRVxZMCRbsDv8k0,35130
|
|
146
146
|
tests/__init__.py,sha256=3-VoT-nAuqMfwufd4ceN6fXaHl_zCfDCSXJOTp1ywYQ,393
|
|
147
147
|
tests/conftest.py,sha256=UBw_2fx7r6TZPKus2b1Qxrzmd4bg8EEBnX1vCHUuSVA,3311
|
|
148
148
|
tests/fixtures.py,sha256=wOHQsh1SGHnXe_PGi6kDWI30CS_Righi7Ig7vwSEKT4,9082
|
|
@@ -195,7 +195,7 @@ tests/test_databento_live.py,sha256=cydmUDbBruhVd-cZxN5uKjWRgXWt-gy4ZlajUfy0QSs,
|
|
|
195
195
|
tests/test_databento_timezone_fixes.py,sha256=NsfND7yTdKH2ddiYYhO6kU3m41V7se7C4_zTvqKOGv0,11562
|
|
196
196
|
tests/test_drift_rebalancer.py,sha256=AUuEd3WIunfx3gwVdLVtq8jOHlz65UeqpO4adY1xfcs,105289
|
|
197
197
|
tests/test_futures_integration.py,sha256=3Ut0M8d5xPwHd4WcTSmP4HLC7VG_xSUXeJPX0-c0Fe8,9179
|
|
198
|
-
tests/test_futures_roll.py,sha256=
|
|
198
|
+
tests/test_futures_roll.py,sha256=ykx67tWAb-fDoO-UQ6SsnvttjIPAnFOD3293vDngs1U,2036
|
|
199
199
|
tests/test_get_historical_prices.py,sha256=0JGlFuDyQFltKRpaFp2CLKEyleD2xMmgINVzLjrTdl8,15540
|
|
200
200
|
tests/test_helpers.py,sha256=8Ay1B6I8yn3trZKrYjOs6Kbda7jmM20-TFh8LfIWpmY,11659
|
|
201
201
|
tests/test_indicator_subplots.py,sha256=-nqUq1jNhTtSb0xH0WU16xfC-DLD-Ak9qzWo6Yx2bnE,13245
|
|
@@ -233,15 +233,16 @@ tests/test_quiet_logs_comprehensive.py,sha256=QVkZWLAUnPEb04Ec8qKXvLzdDUkp8alez2
|
|
|
233
233
|
tests/test_quiet_logs_functionality.py,sha256=MlOBUICuTy1OXCDifOW05uD7hjnZRsQ2xxQlcLkGebQ,3811
|
|
234
234
|
tests/test_quiet_logs_requirements.py,sha256=YoUooSVLrFL8TlWPfxEiqxvSj4d8z6-qg58ja4dtOc0,7856
|
|
235
235
|
tests/test_session_manager.py,sha256=1qygN3aQ2Xe2uh4BMPm0E3V8KXLFNGq5qdL8KkZjef4,11632
|
|
236
|
+
tests/test_strategy_close_position.py,sha256=88_A137S1dtfjmGcA5zcFpa0koef3B9Az1y9Lx226yA,2747
|
|
236
237
|
tests/test_strategy_methods.py,sha256=j9Mhr6nnG1fkiVQXnx7gLjzGbeQmwt0UbJr_4plD36o,12539
|
|
237
238
|
tests/test_strategy_price_guard.py,sha256=3GJdlfROwx6-adsSi8ZBrWaLOy9e-0N6V1eqpikj8e4,1540
|
|
238
239
|
tests/test_thetadata_backwards_compat.py,sha256=RzNLhNZNJZ2hPkEDyG-T_4mRRXh5XqavK6r-OjfRASQ,3306
|
|
239
|
-
tests/test_thetadata_helper.py,sha256=
|
|
240
|
+
tests/test_thetadata_helper.py,sha256=TlbmZXGUclJKcBbrRbbLpbhILbYdtz7N_2yml-gyHXQ,69090
|
|
240
241
|
tests/test_thetadata_pandas_verification.py,sha256=MWUecqBY6FGFslWLRo_C5blGbom_unmXCZikAfZXLks,6553
|
|
241
242
|
tests/test_tradier.py,sha256=iCEM2FTxJSzJ2oLNaRqSx05XaX_DCiMzLx1aEYPANko,33280
|
|
242
243
|
tests/test_tradier_data.py,sha256=1jTxDzQtzaC42CQJVXMRMElBwExy1mVci3NFfKjjVH0,13363
|
|
243
244
|
tests/test_tradingfee.py,sha256=2CBJgdU-73Ae4xuys-QkbCtpDTL9hwOUkRnCgLm4OmE,163
|
|
244
|
-
tests/test_tradovate.py,sha256=
|
|
245
|
+
tests/test_tradovate.py,sha256=U5mR61yh_6OkO_NJQLQ1XTGcAcsjIS5UiCNlXCJROZ0,48730
|
|
245
246
|
tests/test_unified_logger.py,sha256=Y2rhLk6GoUs9Vj-qRvGThRUTdNohxmH2yFbb3j8Yq3g,10849
|
|
246
247
|
tests/test_vix_helper.py,sha256=jE6TZ4ufVU_0W4Jx3zJ295srsy4Xjw9qU3KwfujjZ_s,8476
|
|
247
248
|
tests/test_yahoo_data.py,sha256=84R2jCl9z2U5qKZhR68tFJou2Rfwno0Qomc8yxPfvAs,4578
|
|
@@ -282,7 +283,7 @@ tests/backtest/test_thetadata.py,sha256=xWYfC9C4EhbMDb29qyZWHO3sSWaLIPzzvcMbHCt5
|
|
|
282
283
|
tests/backtest/test_thetadata_comprehensive.py,sha256=-gN3xLJcJtlB-k4vlaK82DCZDGDmr0LNZZDzn-aN3l4,26120
|
|
283
284
|
tests/backtest/test_thetadata_vs_polygon.py,sha256=dZqsrOx3u3cz-1onIO6o5BDRjI1ey7U9vIkZupfXoig,22831
|
|
284
285
|
tests/backtest/test_yahoo.py,sha256=2FguUTUMC9_A20eqxnZ17rN3tT9n6hyvJHaL98QKpqY,3443
|
|
285
|
-
lumibot-4.2.
|
|
286
|
-
lumibot-4.2.
|
|
287
|
-
lumibot-4.2.
|
|
288
|
-
lumibot-4.2.
|
|
286
|
+
lumibot-4.2.10.dist-info/METADATA,sha256=IVrYPlPQOaa67eTLhVKhGSWaJpLaGOISsHL_UN1wZc0,12094
|
|
287
|
+
lumibot-4.2.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
288
|
+
lumibot-4.2.10.dist-info/top_level.txt,sha256=otUnUjDFVASauEDiTiAzNgMyqQ1B6jjS3QqqP-WSx38,14
|
|
289
|
+
lumibot-4.2.10.dist-info/RECORD,,
|
tests/test_futures_roll.py
CHANGED
|
@@ -36,3 +36,23 @@ def test_resolve_symbols_for_range_produces_sequential_contracts():
|
|
|
36
36
|
|
|
37
37
|
symbols = futures_roll.resolve_symbols_for_range(asset, start, end, year_digits=1)
|
|
38
38
|
assert symbols == ["MESU5", "MESZ5", "MESH6"], symbols
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_comex_gold_rolls_on_third_last_business_day_offset():
|
|
42
|
+
asset_symbol = "GC"
|
|
43
|
+
|
|
44
|
+
year, month = futures_roll.determine_contract_year_month(asset_symbol, _dt(2025, 2, 14))
|
|
45
|
+
assert (year, month) == (2025, 2)
|
|
46
|
+
|
|
47
|
+
# Seven business days before the third last business day of February 2025 is Feb 17
|
|
48
|
+
year, month = futures_roll.determine_contract_year_month(asset_symbol, _dt(2025, 2, 17))
|
|
49
|
+
assert (year, month) == (2025, 4)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_comex_gold_symbol_sequence_uses_even_month_cycle():
|
|
53
|
+
asset = Asset("GC", asset_type=Asset.AssetType.CONT_FUTURE)
|
|
54
|
+
start = _dt(2025, 1, 1)
|
|
55
|
+
end = _dt(2025, 8, 1)
|
|
56
|
+
|
|
57
|
+
symbols = futures_roll.resolve_symbols_for_range(asset, start, end, year_digits=1)
|
|
58
|
+
assert symbols == ["GCG5", "GCJ5", "GCM5", "GCQ5"], symbols
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from types import SimpleNamespace
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
from lumibot.entities import Asset, Position
|
|
6
|
+
from lumibot.strategies import Strategy
|
|
7
|
+
from lumibot.strategies._strategy import Vars
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DummyBroker:
|
|
11
|
+
IS_BACKTESTING_BROKER = False
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.name = "dummy"
|
|
15
|
+
self.data_source = SimpleNamespace(SOURCE="TEST")
|
|
16
|
+
self.quote_assets = set()
|
|
17
|
+
self._filled_positions = []
|
|
18
|
+
self.close_calls = []
|
|
19
|
+
|
|
20
|
+
def get_tracked_position(self, strategy_name, asset):
|
|
21
|
+
for position in self._filled_positions:
|
|
22
|
+
if position.strategy == strategy_name and position.asset == asset:
|
|
23
|
+
return position
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
def get_tracked_positions(self, strategy_name=None):
|
|
27
|
+
return [
|
|
28
|
+
position
|
|
29
|
+
for position in self._filled_positions
|
|
30
|
+
if strategy_name is None or position.strategy == strategy_name
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
def close_position(self, strategy_name, asset, fraction=1.0):
|
|
34
|
+
position = self.get_tracked_position(strategy_name, asset)
|
|
35
|
+
if position is None or position.quantity == 0:
|
|
36
|
+
self.close_calls.append({"asset": asset, "order": None})
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
qty = position.quantity * fraction
|
|
40
|
+
order = SimpleNamespace(
|
|
41
|
+
identifier="CLOSE-ORDER",
|
|
42
|
+
asset=asset,
|
|
43
|
+
quantity=qty,
|
|
44
|
+
side="sell",
|
|
45
|
+
order_type="market",
|
|
46
|
+
)
|
|
47
|
+
self.close_calls.append({"asset": asset, "order": order})
|
|
48
|
+
return order
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DummyStrategy(Strategy):
|
|
52
|
+
parameters = {}
|
|
53
|
+
|
|
54
|
+
def __init__(self, broker):
|
|
55
|
+
self.broker = broker
|
|
56
|
+
self.logger = logging.getLogger("DummyStrategy")
|
|
57
|
+
self._name = "DummyStrategy"
|
|
58
|
+
self.vars = Vars()
|
|
59
|
+
self._quote_asset = Asset("USD", Asset.AssetType.FOREX)
|
|
60
|
+
self.broker.quote_assets.add(self._quote_asset)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestStrategyClosePosition(unittest.TestCase):
|
|
64
|
+
def test_close_position_resolves_continuous_future(self):
|
|
65
|
+
broker = DummyBroker()
|
|
66
|
+
strategy = DummyStrategy(broker)
|
|
67
|
+
|
|
68
|
+
contract_asset = Asset("ESZ4", asset_type=Asset.AssetType.FUTURE)
|
|
69
|
+
position = Position(strategy=strategy.name, asset=contract_asset, quantity=2)
|
|
70
|
+
broker._filled_positions.append(position)
|
|
71
|
+
|
|
72
|
+
cont_asset = Asset("ES", asset_type=Asset.AssetType.CONT_FUTURE)
|
|
73
|
+
result = strategy.close_position(cont_asset)
|
|
74
|
+
|
|
75
|
+
self.assertIsNotNone(result)
|
|
76
|
+
self.assertEqual(result.asset, contract_asset)
|
|
77
|
+
self.assertEqual(len(broker.close_calls), 1)
|
|
78
|
+
self.assertEqual(broker.close_calls[0]["asset"], contract_asset)
|
|
79
|
+
self.assertIsNotNone(broker.close_calls[0]["order"])
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
unittest.main()
|
tests/test_thetadata_helper.py
CHANGED
|
@@ -881,7 +881,13 @@ def test_get_request_error_in_json(mock_get, mock_check_connection):
|
|
|
881
881
|
password="test_password",
|
|
882
882
|
wait_for_connection=True,
|
|
883
883
|
)
|
|
884
|
-
assert mock_check_connection.call_count ==
|
|
884
|
+
assert mock_check_connection.call_count == 2
|
|
885
|
+
first_call_kwargs = mock_check_connection.call_args_list[0].kwargs
|
|
886
|
+
assert first_call_kwargs == {
|
|
887
|
+
"username": "test_user",
|
|
888
|
+
"password": "test_password",
|
|
889
|
+
"wait_for_connection": False,
|
|
890
|
+
}
|
|
885
891
|
|
|
886
892
|
|
|
887
893
|
@patch('lumibot.tools.thetadata_helper.check_connection')
|
|
@@ -907,6 +913,44 @@ def test_get_request_exception_handling(mock_get, mock_check_connection):
|
|
|
907
913
|
assert mock_check_connection.call_count == 3
|
|
908
914
|
|
|
909
915
|
|
|
916
|
+
|
|
917
|
+
@patch('lumibot.tools.thetadata_helper.start_theta_data_client')
|
|
918
|
+
@patch('lumibot.tools.thetadata_helper.check_connection')
|
|
919
|
+
def test_get_request_consecutive_474_triggers_restarts(mock_check_connection, mock_start_client, monkeypatch):
|
|
920
|
+
mock_check_connection.return_value = (object(), True)
|
|
921
|
+
|
|
922
|
+
responses = [MagicMock(status_code=474, text='Connection lost to Theta Data MDDS.') for _ in range(9)]
|
|
923
|
+
|
|
924
|
+
def fake_get(*args, **kwargs):
|
|
925
|
+
if not responses:
|
|
926
|
+
raise AssertionError('Test exhausted mock responses unexpectedly')
|
|
927
|
+
return responses.pop(0)
|
|
928
|
+
|
|
929
|
+
monkeypatch.setattr(thetadata_helper.requests, 'get', fake_get)
|
|
930
|
+
monkeypatch.setattr(thetadata_helper.time, 'sleep', lambda *args, **kwargs: None)
|
|
931
|
+
monkeypatch.setattr(thetadata_helper, 'BOOT_GRACE_PERIOD', 0, raising=False)
|
|
932
|
+
monkeypatch.setattr(thetadata_helper, 'CONNECTION_RETRY_SLEEP', 0, raising=False)
|
|
933
|
+
|
|
934
|
+
with pytest.raises(ValueError, match='Cannot connect to Theta Data!'):
|
|
935
|
+
thetadata_helper.get_request(
|
|
936
|
+
url='http://test.com',
|
|
937
|
+
headers={'Authorization': 'Bearer test_token'},
|
|
938
|
+
querystring={'param1': 'value1'},
|
|
939
|
+
username='test_user',
|
|
940
|
+
password='test_password',
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
assert mock_start_client.call_count == 3
|
|
944
|
+
# Initial liveness probe plus retry coordination checks
|
|
945
|
+
assert mock_check_connection.call_count > 3
|
|
946
|
+
first_call_kwargs = mock_check_connection.call_args_list[0].kwargs
|
|
947
|
+
assert first_call_kwargs == {
|
|
948
|
+
'username': 'test_user',
|
|
949
|
+
'password': 'test_password',
|
|
950
|
+
'wait_for_connection': False,
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
|
|
910
954
|
@patch('lumibot.tools.thetadata_helper.get_request')
|
|
911
955
|
def test_get_historical_data_stock(mock_get_request):
|
|
912
956
|
# Arrange
|
|
@@ -1426,8 +1470,10 @@ class TestThetaDataChainsCaching:
|
|
|
1426
1470
|
|
|
1427
1471
|
monkeypatch.setattr(thetadata_helper, "build_historical_chain", lambda **kwargs: None)
|
|
1428
1472
|
monkeypatch.setattr(thetadata_helper, "LUMIBOT_CACHE_FOLDER", str(tmp_path))
|
|
1473
|
+
monkeypatch.delenv("BACKTESTING_QUIET_LOGS", raising=False)
|
|
1474
|
+
caplog.set_level(logging.WARNING, logger="lumibot.tools.thetadata_helper")
|
|
1429
1475
|
|
|
1430
|
-
with caplog.at_level(logging.WARNING):
|
|
1476
|
+
with caplog.at_level(logging.WARNING, logger="lumibot.tools.thetadata_helper"):
|
|
1431
1477
|
result = thetadata_helper.get_chains_cached("user", "pass", asset, test_date)
|
|
1432
1478
|
|
|
1433
1479
|
cache_folder = Path(tmp_path) / "thetadata" / "stock" / "option_chains"
|
|
@@ -1508,8 +1554,10 @@ def test_build_historical_chain_returns_none_when_no_dates(monkeypatch, caplog):
|
|
|
1508
1554
|
raise AssertionError(f"Unexpected URL {url}")
|
|
1509
1555
|
|
|
1510
1556
|
monkeypatch.setattr(thetadata_helper, "get_request", fake_get_request)
|
|
1557
|
+
monkeypatch.delenv("BACKTESTING_QUIET_LOGS", raising=False)
|
|
1558
|
+
caplog.set_level(logging.WARNING, logger="lumibot.tools.thetadata_helper")
|
|
1511
1559
|
|
|
1512
|
-
with caplog.at_level(logging.WARNING):
|
|
1560
|
+
with caplog.at_level(logging.WARNING, logger="lumibot.tools.thetadata_helper"):
|
|
1513
1561
|
result = thetadata_helper.build_historical_chain("user", "pass", asset, as_of_date)
|
|
1514
1562
|
|
|
1515
1563
|
assert result is None
|
|
@@ -1525,14 +1573,83 @@ def test_build_historical_chain_empty_response(monkeypatch, caplog):
|
|
|
1525
1573
|
raise AssertionError("Unexpected call after empty expirations")
|
|
1526
1574
|
|
|
1527
1575
|
monkeypatch.setattr(thetadata_helper, "get_request", fake_get_request)
|
|
1576
|
+
monkeypatch.delenv("BACKTESTING_QUIET_LOGS", raising=False)
|
|
1577
|
+
caplog.set_level(logging.WARNING, logger="lumibot.tools.thetadata_helper")
|
|
1528
1578
|
|
|
1529
|
-
with caplog.at_level(logging.WARNING):
|
|
1579
|
+
with caplog.at_level(logging.WARNING, logger="lumibot.tools.thetadata_helper"):
|
|
1530
1580
|
result = thetadata_helper.build_historical_chain("user", "pass", asset, as_of_date)
|
|
1531
1581
|
|
|
1532
1582
|
assert result is None
|
|
1533
1583
|
assert "returned no expirations" in caplog.text
|
|
1534
1584
|
|
|
1535
1585
|
|
|
1586
|
+
class TestThetaDataConnectionSupervision:
|
|
1587
|
+
|
|
1588
|
+
def setup_method(self):
|
|
1589
|
+
thetadata_helper.reset_connection_diagnostics()
|
|
1590
|
+
|
|
1591
|
+
def test_check_connection_recovers_after_restart(self, monkeypatch):
|
|
1592
|
+
statuses = iter(["DISCONNECTED", "DISCONNECTED", "CONNECTED"])
|
|
1593
|
+
|
|
1594
|
+
class FakeResponse:
|
|
1595
|
+
def __init__(self, text):
|
|
1596
|
+
self.text = text
|
|
1597
|
+
|
|
1598
|
+
def fake_get(url, timeout):
|
|
1599
|
+
try:
|
|
1600
|
+
text = next(statuses)
|
|
1601
|
+
except StopIteration:
|
|
1602
|
+
text = "CONNECTED"
|
|
1603
|
+
return FakeResponse(text)
|
|
1604
|
+
|
|
1605
|
+
start_calls = []
|
|
1606
|
+
|
|
1607
|
+
def fake_start(username, password):
|
|
1608
|
+
start_calls.append((username, password))
|
|
1609
|
+
return object()
|
|
1610
|
+
|
|
1611
|
+
monkeypatch.setattr(thetadata_helper.requests, "get", fake_get)
|
|
1612
|
+
monkeypatch.setattr(thetadata_helper, "start_theta_data_client", fake_start)
|
|
1613
|
+
monkeypatch.setattr(thetadata_helper, "is_process_alive", lambda: True)
|
|
1614
|
+
monkeypatch.setattr(thetadata_helper, "CONNECTION_MAX_RETRIES", 2, raising=False)
|
|
1615
|
+
monkeypatch.setattr(thetadata_helper, "MAX_TERMINAL_RESTART_CYCLES", 2, raising=False)
|
|
1616
|
+
monkeypatch.setattr(thetadata_helper, "BOOT_GRACE_PERIOD", 0, raising=False)
|
|
1617
|
+
monkeypatch.setattr(thetadata_helper, "CONNECTION_RETRY_SLEEP", 0, raising=False)
|
|
1618
|
+
monkeypatch.setattr(thetadata_helper.time, "sleep", lambda *args, **kwargs: None)
|
|
1619
|
+
|
|
1620
|
+
client, connected = thetadata_helper.check_connection("user", "pass", wait_for_connection=True)
|
|
1621
|
+
|
|
1622
|
+
assert connected is True
|
|
1623
|
+
assert len(start_calls) == 1
|
|
1624
|
+
assert thetadata_helper.CONNECTION_DIAGNOSTICS["terminal_restarts"] >= 1
|
|
1625
|
+
|
|
1626
|
+
def test_check_connection_raises_after_restart_cycles(self, monkeypatch):
|
|
1627
|
+
statuses = iter(["DISCONNECTED"] * 10)
|
|
1628
|
+
|
|
1629
|
+
class FakeResponse:
|
|
1630
|
+
def __init__(self, text):
|
|
1631
|
+
self.text = text
|
|
1632
|
+
|
|
1633
|
+
def fake_get(url, timeout):
|
|
1634
|
+
try:
|
|
1635
|
+
text = next(statuses)
|
|
1636
|
+
except StopIteration:
|
|
1637
|
+
text = "DISCONNECTED"
|
|
1638
|
+
return FakeResponse(text)
|
|
1639
|
+
|
|
1640
|
+
monkeypatch.setattr(thetadata_helper.requests, "get", fake_get)
|
|
1641
|
+
monkeypatch.setattr(thetadata_helper, "start_theta_data_client", lambda *args, **kwargs: object())
|
|
1642
|
+
monkeypatch.setattr(thetadata_helper, "is_process_alive", lambda: True)
|
|
1643
|
+
monkeypatch.setattr(thetadata_helper, "CONNECTION_MAX_RETRIES", 1, raising=False)
|
|
1644
|
+
monkeypatch.setattr(thetadata_helper, "MAX_TERMINAL_RESTART_CYCLES", 1, raising=False)
|
|
1645
|
+
monkeypatch.setattr(thetadata_helper, "BOOT_GRACE_PERIOD", 0, raising=False)
|
|
1646
|
+
monkeypatch.setattr(thetadata_helper, "CONNECTION_RETRY_SLEEP", 0, raising=False)
|
|
1647
|
+
monkeypatch.setattr(thetadata_helper.time, "sleep", lambda *args, **kwargs: None)
|
|
1648
|
+
|
|
1649
|
+
with pytest.raises(thetadata_helper.ThetaDataConnectionError):
|
|
1650
|
+
thetadata_helper.check_connection("user", "pass", wait_for_connection=True)
|
|
1651
|
+
|
|
1652
|
+
|
|
1536
1653
|
def test_finalize_day_frame_handles_dst_fallback():
|
|
1537
1654
|
tz = pytz.timezone("America/New_York")
|
|
1538
1655
|
utc = pytz.UTC
|