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.

@@ -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=01cBrBjeiQGT9LMdZ2EgrSYT-xyoKz9JhVZkQQr1QxU,71951
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=8YaadpvoJyA-IkK6BCENLvCRfYSMnG-uTxkvDB2Sd94,38668
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=uPzhHHKdF-tavo6mzgdab75n4ixPpNkPfA6e8AVgbvE,36670
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=BwpgKjXpx5NGOb_aF34ZSvxZVN11SUQ0MW-GYAGozC4,28643
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=toPeL5oIVWmCxBNcfXqIuTCF_EeCfIVj425PrSYImCo,170021
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=WBuQCm9Z7WFpde8qdsJEabtpU0ThGsjjPeK7BI2pUVA,43681
118
- lumibot/tools/databento_helper_polars.py,sha256=FXvvES_Y-E-IzAmVBtquh1UtQ-eN6i6BEoflcP7y8s0,48674
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=k_c7X5bYuBX7_0vBdix8-RXjkvvC6RX9CJOFf6vweCE,7307
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=QXuFzfHi22L9sqzwL_mXZGhU2m9ortbE9U1-Kt7jSew,86743
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.7.dist-info/licenses/LICENSE,sha256=fYhGIyxjyNXACgpNQS3xxpxDOaVOWRVxZMCRbsDv8k0,35130
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=hg0_QIjBeN2-DHyGZmL3NAnSUJYprAgXX727sXFZjcM,1262
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=RQ-q7OfKc_2aO7-dInaVRTUrrI5iTh9oteYsdmfn4Lg,63735
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=XW0ZyiMbRYr16hqGJIa8C1Wg5O0V0tpiUMHvejIAnEg,37436
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.7.dist-info/METADATA,sha256=cCEnxjhYBqtO5Uvd2pEJ90ULy2OlCiX1FHSaJPEpXAI,12093
286
- lumibot-4.2.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
287
- lumibot-4.2.7.dist-info/top_level.txt,sha256=otUnUjDFVASauEDiTiAzNgMyqQ1B6jjS3QqqP-WSx38,14
288
- lumibot-4.2.7.dist-info/RECORD,,
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,,
@@ -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()
@@ -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 == 5
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