lumibot 4.2.5__py3-none-any.whl → 4.2.9__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/backtesting/databento_backtesting_pandas.py +32 -7
- lumibot/backtesting/thetadata_backtesting_pandas.py +1 -1
- lumibot/components/options_helper.py +86 -23
- lumibot/strategies/_strategy.py +12 -6
- lumibot/tools/ccxt_data_store.py +1 -1
- lumibot/tools/databento_helper.py +17 -9
- lumibot/tools/thetadata_helper.py +348 -95
- {lumibot-4.2.5.dist-info → lumibot-4.2.9.dist-info}/METADATA +2 -2
- {lumibot-4.2.5.dist-info → lumibot-4.2.9.dist-info}/RECORD +16 -15
- tests/test_options_helper.py +45 -3
- tests/test_projectx_timestep_alias.py +1 -2
- tests/test_strategy_price_guard.py +50 -0
- tests/test_thetadata_helper.py +260 -63
- {lumibot-4.2.5.dist-info → lumibot-4.2.9.dist-info}/WHEEL +0 -0
- {lumibot-4.2.5.dist-info → lumibot-4.2.9.dist-info}/licenses/LICENSE +0 -0
- {lumibot-4.2.5.dist-info → lumibot-4.2.9.dist-info}/top_level.txt +0 -0
|
@@ -7,14 +7,14 @@ lumibot/backtesting/alpha_vantage_backtesting.py,sha256=LR3UNhJrdAKdvadiThVKdKrM
|
|
|
7
7
|
lumibot/backtesting/backtesting_broker.py,sha256=C0zn__i0aoyd6u147oizRYO2Akqa5JWs8znl6siLd8Y,76003
|
|
8
8
|
lumibot/backtesting/ccxt_backtesting.py,sha256=O-RjuNpx5y4f-hKKwwUbrU7hAVkGEegmnvH_9nQWhAo,246
|
|
9
9
|
lumibot/backtesting/databento_backtesting.py,sha256=zQS6IaamDTQREcjViguk_y4b_HF1xyV3gUrDuHnkUUY,430
|
|
10
|
-
lumibot/backtesting/databento_backtesting_pandas.py,sha256=
|
|
10
|
+
lumibot/backtesting/databento_backtesting_pandas.py,sha256=xIQ1SnPo47rxIH7hfrgWzhTuVxxG8LDklY3Z8Tu5idU,32440
|
|
11
11
|
lumibot/backtesting/databento_backtesting_polars.py,sha256=qanSLBsw4aBEFVfMuwSWzTDtaXQlOfcrvp6ERLWkRVo,45786
|
|
12
12
|
lumibot/backtesting/fix_debug.py,sha256=ETHl-O38xK9JOQC2x8IZVJRCQrR1cokk2Vb4vpGMvb8,1204
|
|
13
13
|
lumibot/backtesting/interactive_brokers_rest_backtesting.py,sha256=5HJ_sPX0uOUg-rbfOKDjwYVCLiXevlwtdK_3BcUwqXc,6602
|
|
14
14
|
lumibot/backtesting/pandas_backtesting.py,sha256=m-NvT4o-wFQjaZft6TXULzeZBrskO_7Z-jfy9AIkyAY,388
|
|
15
15
|
lumibot/backtesting/polygon_backtesting.py,sha256=u9kif_2_7k0P4-KDvbHhaMfSoBVejUUX7fh9H3PCVE0,12350
|
|
16
16
|
lumibot/backtesting/thetadata_backtesting.py,sha256=Xcz5f-4zTkKgWWcktNzItH2vrr8CysIMQWKKqLwugbA,345
|
|
17
|
-
lumibot/backtesting/thetadata_backtesting_pandas.py,sha256=
|
|
17
|
+
lumibot/backtesting/thetadata_backtesting_pandas.py,sha256=4jIxziBASbmCe5MCmjmvQBeo-5EQ90UTmblRpHrAaDE,51829
|
|
18
18
|
lumibot/backtesting/yahoo_backtesting.py,sha256=LT2524mGlrUSq1YSRnUqGW4-Xcq4USgRv2EhnV_zfs4,502
|
|
19
19
|
lumibot/brokers/__init__.py,sha256=MGWKHeH3mqseYRL7u-KX1Jp2x9EaFO4Ol8sfNSxzu1M,404
|
|
20
20
|
lumibot/brokers/alpaca.py,sha256=VQ17idfqiEFb2JCqqdMGmbvF789L7_PpsCbudiFRzmg,61595
|
|
@@ -34,7 +34,7 @@ lumibot/components/configs_helper.py,sha256=e4-2jBmjsIDeMQ9fbQ9j2U6WlHCe2z9bIQtJ
|
|
|
34
34
|
lumibot/components/drift_rebalancer_logic.py,sha256=KL1S9Su5gxibGzl2fw_DH8StzXvjFfFl23sOFGHgIZI,28653
|
|
35
35
|
lumibot/components/grok_helper.py,sha256=vgyPAs63VoNsfZojMoHYRLckb-CFiZkHIlY8HSYZLQs,15382
|
|
36
36
|
lumibot/components/grok_news_helper.py,sha256=7GFtSuRkBo4-3IcUmC6qhuf7iekoVyyP7q5jyUjAWTc,11007
|
|
37
|
-
lumibot/components/options_helper.py,sha256=
|
|
37
|
+
lumibot/components/options_helper.py,sha256=UTMJUrTX9AOq-SPkZR7IFOFc65JQRZDSSrzy6a7ptZY,67371
|
|
38
38
|
lumibot/components/perplexity_helper.py,sha256=0dhAIXunvUnTKIFFdYbFt8adnSIavKRqXkQ91fjsDPI,31432
|
|
39
39
|
lumibot/components/quiver_helper.py,sha256=s0Y-tL1y2S1mYlbtlRTcL5R4WV0HYOgiSJ9hT41Hzrw,14380
|
|
40
40
|
lumibot/components/vix_helper.py,sha256=tsWHFQzOJCjdmSGgmnYIty0K5ua-iAgBiRlyRIpGli0,61940
|
|
@@ -104,7 +104,7 @@ lumibot/example_strategies/test_broker_functions.py,sha256=wnVS-M_OtzMgaXVBgshVE
|
|
|
104
104
|
lumibot/resources/ThetaTerminal.jar,sha256=K6GeeFcN8-gvyL2x5iq5pzD79KfPJvMK8iiezi3TmNQ,11834389
|
|
105
105
|
lumibot/resources/conf.yaml,sha256=rjB9-10JP7saZ_edjX5bQDGfuc3amOQTUUUr-UiMpNA,597
|
|
106
106
|
lumibot/strategies/__init__.py,sha256=jEZ95K5hG0f595EXYKWwL2_UsnWWk5Pug361PK2My2E,79
|
|
107
|
-
lumibot/strategies/_strategy.py,sha256=
|
|
107
|
+
lumibot/strategies/_strategy.py,sha256=3Z2MPz3jYgJdAphaFyoEI5OUs_6ClAJgRCrLWy-b2sg,111718
|
|
108
108
|
lumibot/strategies/session_manager.py,sha256=Nze6UYNSPlCsf-tyHvtFqUeL44WSNHjwsKrIepvsyCY,12956
|
|
109
109
|
lumibot/strategies/strategy.py,sha256=toPeL5oIVWmCxBNcfXqIuTCF_EeCfIVj425PrSYImCo,170021
|
|
110
110
|
lumibot/strategies/strategy_executor.py,sha256=AnmXlKD2eMgKXs3TrD1u8T_Zsn_8GnG5KRcM_Pq-JBQ,70749
|
|
@@ -113,8 +113,8 @@ lumibot/tools/alpaca_helpers.py,sha256=nhBS-sv28lZfIQ85szC9El8VHLrCw5a5KbsGOOEjm
|
|
|
113
113
|
lumibot/tools/backtest_cache.py,sha256=A-Juzu0swZI_FP4U7cd7ruYTgJYgV8BPf_WJDI-NtrM,9556
|
|
114
114
|
lumibot/tools/bitunix_helpers.py,sha256=-UzrN3w_Y-Ckvhl7ZBoAcx7sgb6tH0KcpVph1Ovm3gw,25780
|
|
115
115
|
lumibot/tools/black_scholes.py,sha256=TBjJuDTudvqsbwqSb7-zb4gXsJBCStQFaym8xvePAjw,25428
|
|
116
|
-
lumibot/tools/ccxt_data_store.py,sha256=
|
|
117
|
-
lumibot/tools/databento_helper.py,sha256=
|
|
116
|
+
lumibot/tools/ccxt_data_store.py,sha256=PlP3MHPHZP7GisEZsk1OUxeWijoPXwiQbsOBTr7jkQI,21227
|
|
117
|
+
lumibot/tools/databento_helper.py,sha256=WBuQCm9Z7WFpde8qdsJEabtpU0ThGsjjPeK7BI2pUVA,43681
|
|
118
118
|
lumibot/tools/databento_helper_polars.py,sha256=FXvvES_Y-E-IzAmVBtquh1UtQ-eN6i6BEoflcP7y8s0,48674
|
|
119
119
|
lumibot/tools/databento_roll.py,sha256=48HAw3h6OngCK4UTl9ifpjo-ki8qmB6OoJUrHp0gRmE,6767
|
|
120
120
|
lumibot/tools/debugers.py,sha256=ga6npFsS9cpKtTXaygh9t2_txCElg3bfzfeqDBvSL8k,485
|
|
@@ -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=F5CdaWZkfJYFrParvN8KhowRWc5cRmPL4TnI9SH-VpI,90111
|
|
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.9.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
|
|
@@ -208,7 +208,7 @@ tests/test_lumibot_logger.py,sha256=76e5yl86NWk4OYtM_be-7yaHnPA-1bO2rBNH-piX8Dk,
|
|
|
208
208
|
tests/test_market_infinite_loop_bug.py,sha256=p8Wiq2sdNFYOH54Ij9MkDFWBLTC8V3vbJEphcPq6T8g,16181
|
|
209
209
|
tests/test_mes_symbols.py,sha256=Av5AM-pogp1STfWZ4A3bA2NmbaA3E9b0Dg449mfk-Ss,3106
|
|
210
210
|
tests/test_momentum.py,sha256=oakKjgnz5pNQkw0CagbbKKSux0HkOxP_iLW8-Yk0XOo,8492
|
|
211
|
-
tests/test_options_helper.py,sha256=
|
|
211
|
+
tests/test_options_helper.py,sha256=wVXafXP4KfL3T-u9dFjJZbGI9YOHApohW-NfQ36yTuk,29185
|
|
212
212
|
tests/test_options_helper_enhancements.py,sha256=3Ei-vy9IQfg1AnlBqofLNc-kuiQ64vouhJyIuVj3KIg,7324
|
|
213
213
|
tests/test_order.py,sha256=qN_sqsItl8L-WqSQTF0PUN0s2nxm4HKYOA6gYQ6EBJA,12110
|
|
214
214
|
tests/test_order_serialization.py,sha256=zcX5fgcYeo0jm6dlIQnUfuH56svTnKRFPtYCSI7xCJw,3710
|
|
@@ -226,7 +226,7 @@ tests/test_projectx_helpers.py,sha256=mKFcWZ7C2ve6UXk2LaJWo1E6QwhU09dkIYht1LElQW
|
|
|
226
226
|
tests/test_projectx_lifecycle.py,sha256=F7H08wg2b5lYh0vNzKNa5AkQnhCuTrbpPm0YvOZHN6A,3179
|
|
227
227
|
tests/test_projectx_lifecycle_unit.py,sha256=XqktdtFMS4s5RIF2ay9uZNW9Ij1AQL46OUbgStA9rhA,21926
|
|
228
228
|
tests/test_projectx_live_flow.py,sha256=vKnb_VDKCOAbmOdQ5TlyQroCNtNE8UFjHKcd-YpIjXg,412
|
|
229
|
-
tests/test_projectx_timestep_alias.py,sha256=
|
|
229
|
+
tests/test_projectx_timestep_alias.py,sha256=Kcq-q9-pB9h9mBSol_qb_VCztVdLItY8xClroGNf3zo,2043
|
|
230
230
|
tests/test_projectx_url_mappings.py,sha256=1E0xZe_cZtGiU-fPIfohbvsrrPjawkblR8F2Mvu_Ct8,10039
|
|
231
231
|
tests/test_quiet_logs_buy_and_hold.py,sha256=GjXGeHBcNrRJoHGWLuc_LkrzcpNVzshiiBkQACczIxI,2681
|
|
232
232
|
tests/test_quiet_logs_comprehensive.py,sha256=QVkZWLAUnPEb04Ec8qKXvLzdDUkp8alez2mU_Y1Umm0,6068
|
|
@@ -234,8 +234,9 @@ tests/test_quiet_logs_functionality.py,sha256=MlOBUICuTy1OXCDifOW05uD7hjnZRsQ2xx
|
|
|
234
234
|
tests/test_quiet_logs_requirements.py,sha256=YoUooSVLrFL8TlWPfxEiqxvSj4d8z6-qg58ja4dtOc0,7856
|
|
235
235
|
tests/test_session_manager.py,sha256=1qygN3aQ2Xe2uh4BMPm0E3V8KXLFNGq5qdL8KkZjef4,11632
|
|
236
236
|
tests/test_strategy_methods.py,sha256=j9Mhr6nnG1fkiVQXnx7gLjzGbeQmwt0UbJr_4plD36o,12539
|
|
237
|
+
tests/test_strategy_price_guard.py,sha256=3GJdlfROwx6-adsSi8ZBrWaLOy9e-0N6V1eqpikj8e4,1540
|
|
237
238
|
tests/test_thetadata_backwards_compat.py,sha256=RzNLhNZNJZ2hPkEDyG-T_4mRRXh5XqavK6r-OjfRASQ,3306
|
|
238
|
-
tests/test_thetadata_helper.py,sha256=
|
|
239
|
+
tests/test_thetadata_helper.py,sha256=4EdFHoCTTKELU216ZNYGjfo6Uvq-29kK-TVpFLPWT3M,67263
|
|
239
240
|
tests/test_thetadata_pandas_verification.py,sha256=MWUecqBY6FGFslWLRo_C5blGbom_unmXCZikAfZXLks,6553
|
|
240
241
|
tests/test_tradier.py,sha256=iCEM2FTxJSzJ2oLNaRqSx05XaX_DCiMzLx1aEYPANko,33280
|
|
241
242
|
tests/test_tradier_data.py,sha256=1jTxDzQtzaC42CQJVXMRMElBwExy1mVci3NFfKjjVH0,13363
|
|
@@ -281,7 +282,7 @@ tests/backtest/test_thetadata.py,sha256=xWYfC9C4EhbMDb29qyZWHO3sSWaLIPzzvcMbHCt5
|
|
|
281
282
|
tests/backtest/test_thetadata_comprehensive.py,sha256=-gN3xLJcJtlB-k4vlaK82DCZDGDmr0LNZZDzn-aN3l4,26120
|
|
282
283
|
tests/backtest/test_thetadata_vs_polygon.py,sha256=dZqsrOx3u3cz-1onIO6o5BDRjI1ey7U9vIkZupfXoig,22831
|
|
283
284
|
tests/backtest/test_yahoo.py,sha256=2FguUTUMC9_A20eqxnZ17rN3tT9n6hyvJHaL98QKpqY,3443
|
|
284
|
-
lumibot-4.2.
|
|
285
|
-
lumibot-4.2.
|
|
286
|
-
lumibot-4.2.
|
|
287
|
-
lumibot-4.2.
|
|
285
|
+
lumibot-4.2.9.dist-info/METADATA,sha256=Bis7WU6RY3Wp7YXHsNa7-xklQzQcXRi5zcJzZhC2j0w,12093
|
|
286
|
+
lumibot-4.2.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
287
|
+
lumibot-4.2.9.dist-info/top_level.txt,sha256=otUnUjDFVASauEDiTiAzNgMyqQ1B6jjS3QqqP-WSx38,14
|
|
288
|
+
lumibot-4.2.9.dist-info/RECORD,,
|
tests/test_options_helper.py
CHANGED
|
@@ -11,7 +11,7 @@ import pytest
|
|
|
11
11
|
# Add the lumibot path
|
|
12
12
|
sys.path.insert(0, '/Users/robertgrzesik/Documents/Development/lumivest_bot_server/strategies/lumibot')
|
|
13
13
|
|
|
14
|
-
from lumibot.components.options_helper import OptionsHelper
|
|
14
|
+
from lumibot.components.options_helper import OptionsHelper, OptionMarketEvaluation
|
|
15
15
|
from lumibot.entities import Asset
|
|
16
16
|
from lumibot.entities.chains import OptionsDataFormatError, normalize_option_chains
|
|
17
17
|
from lumibot.brokers.broker import Broker
|
|
@@ -311,7 +311,7 @@ class TestOptionsHelper(unittest.TestCase):
|
|
|
311
311
|
result = self.options_helper.get_expiration_on_or_after_date(target, expiries, "call")
|
|
312
312
|
self.assertEqual(result, _date(2024, 1, 9))
|
|
313
313
|
|
|
314
|
-
def
|
|
314
|
+
def test_get_expiration_on_or_after_date_returns_none_when_no_future_available(self):
|
|
315
315
|
from datetime import date as _date
|
|
316
316
|
|
|
317
317
|
expiries = {
|
|
@@ -325,7 +325,7 @@ class TestOptionsHelper(unittest.TestCase):
|
|
|
325
325
|
|
|
326
326
|
target = _date(2024, 2, 1)
|
|
327
327
|
result = self.options_helper.get_expiration_on_or_after_date(target, expiries, "call")
|
|
328
|
-
self.
|
|
328
|
+
self.assertIsNone(result)
|
|
329
329
|
|
|
330
330
|
def test_chains_backward_compatibility_string_access(self):
|
|
331
331
|
"""Test that existing code using string keys still works."""
|
|
@@ -676,6 +676,48 @@ class TestOptionsHelper(unittest.TestCase):
|
|
|
676
676
|
self.assertIsNone(evaluation.sell_price)
|
|
677
677
|
self.assertFalse(evaluation.used_last_price_fallback)
|
|
678
678
|
|
|
679
|
+
def test_evaluate_option_market_rejects_non_finite_quotes(self):
|
|
680
|
+
"""NaN or infinite quotes are treated as missing to prevent crashes."""
|
|
681
|
+
option_asset = Asset(
|
|
682
|
+
"TEST",
|
|
683
|
+
asset_type=Asset.AssetType.OPTION,
|
|
684
|
+
expiration=date.today() + timedelta(days=7),
|
|
685
|
+
strike=200,
|
|
686
|
+
right="call",
|
|
687
|
+
underlying_asset=Asset("TEST", asset_type=Asset.AssetType.STOCK),
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
self.mock_strategy.get_quote.return_value = Mock(bid=float("nan"), ask=float("inf"))
|
|
691
|
+
self.mock_strategy.get_last_price.return_value = float("nan")
|
|
692
|
+
self.mock_strategy.broker.data_source.option_quote_fallback_allowed = True
|
|
693
|
+
|
|
694
|
+
evaluation = self.options_helper.evaluate_option_market(option_asset, max_spread_pct=0.25)
|
|
695
|
+
|
|
696
|
+
self.assertIsNone(evaluation.buy_price)
|
|
697
|
+
self.assertIn("bid_non_finite", evaluation.data_quality_flags)
|
|
698
|
+
self.assertIn("ask_non_finite", evaluation.data_quality_flags)
|
|
699
|
+
self.assertTrue(evaluation.missing_bid_ask)
|
|
700
|
+
self.assertFalse(OptionsHelper.has_actionable_price(evaluation))
|
|
701
|
+
|
|
702
|
+
def test_has_actionable_price_requires_positive_finite_value(self):
|
|
703
|
+
"""has_actionable_price returns False for zero or negative values."""
|
|
704
|
+
evaluation = OptionMarketEvaluation(
|
|
705
|
+
bid=0.5,
|
|
706
|
+
ask=0.6,
|
|
707
|
+
last_price=0.55,
|
|
708
|
+
spread_pct=0.18,
|
|
709
|
+
has_bid_ask=True,
|
|
710
|
+
spread_too_wide=False,
|
|
711
|
+
missing_bid_ask=False,
|
|
712
|
+
missing_last_price=False,
|
|
713
|
+
buy_price=0.0,
|
|
714
|
+
sell_price=0.4,
|
|
715
|
+
used_last_price_fallback=False,
|
|
716
|
+
max_spread_pct=0.25,
|
|
717
|
+
data_quality_flags=["buy_price_non_positive"],
|
|
718
|
+
)
|
|
719
|
+
self.assertFalse(OptionsHelper.has_actionable_price(evaluation))
|
|
720
|
+
|
|
679
721
|
if __name__ == "__main__":
|
|
680
722
|
print("🧪 Running enhanced options helper tests...")
|
|
681
723
|
unittest.main(verbosity=2)
|
|
@@ -9,7 +9,7 @@ from lumibot.data_sources.projectx_data import ProjectXData
|
|
|
9
9
|
class DummyClient:
|
|
10
10
|
def history_retrieve_bars(self, contract_id, start_datetime, end_datetime, unit, unit_number, limit, include_partial_bar, live, is_est):
|
|
11
11
|
# Return a minimal DataFrame resembling expected structure
|
|
12
|
-
idx = pd.date_range(end=end_datetime, periods=unit_number, freq='
|
|
12
|
+
idx = pd.date_range(end=end_datetime, periods=unit_number, freq='min')
|
|
13
13
|
data = {
|
|
14
14
|
'open': [1.0]*len(idx),
|
|
15
15
|
'high': [1.1]*len(idx),
|
|
@@ -51,4 +51,3 @@ def test_projectx_get_bars_accepts_timestep_alias(projectx):
|
|
|
51
51
|
bars_map2 = projectx.get_bars(asset, 1, timestep='minute')
|
|
52
52
|
bars2 = list(bars_map2.values())[0]
|
|
53
53
|
assert bars2 is not None and not bars2.df.empty
|
|
54
|
-
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from lumibot.components.options_helper import OptionsHelper, OptionMarketEvaluation
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class _GuardedStrategy:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.logged = []
|
|
8
|
+
self.options_helper = OptionsHelper(self)
|
|
9
|
+
|
|
10
|
+
def log_message(self, message, color="white"):
|
|
11
|
+
self.logged.append((color, message))
|
|
12
|
+
|
|
13
|
+
def get_cash(self):
|
|
14
|
+
return 10_000.0
|
|
15
|
+
|
|
16
|
+
def size_position(self, evaluation: OptionMarketEvaluation):
|
|
17
|
+
if evaluation.spread_too_wide or not self.options_helper.has_actionable_price(evaluation):
|
|
18
|
+
self.log_message(
|
|
19
|
+
f"Skipping trade due to invalid quotes (flags={evaluation.data_quality_flags}).",
|
|
20
|
+
color="yellow",
|
|
21
|
+
)
|
|
22
|
+
return 0
|
|
23
|
+
|
|
24
|
+
buy_price = float(evaluation.buy_price)
|
|
25
|
+
budget = self.get_cash() * 0.1
|
|
26
|
+
return math.floor(budget / (buy_price * 100.0))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_strategy_guard_skips_non_finite_prices():
|
|
30
|
+
strategy = _GuardedStrategy()
|
|
31
|
+
evaluation = OptionMarketEvaluation(
|
|
32
|
+
bid=None,
|
|
33
|
+
ask=None,
|
|
34
|
+
last_price=None,
|
|
35
|
+
spread_pct=None,
|
|
36
|
+
has_bid_ask=False,
|
|
37
|
+
spread_too_wide=False,
|
|
38
|
+
missing_bid_ask=True,
|
|
39
|
+
missing_last_price=True,
|
|
40
|
+
buy_price=float("nan"),
|
|
41
|
+
sell_price=None,
|
|
42
|
+
used_last_price_fallback=False,
|
|
43
|
+
max_spread_pct=None,
|
|
44
|
+
data_quality_flags=["buy_price_non_finite"],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
contracts = strategy.size_position(evaluation)
|
|
48
|
+
|
|
49
|
+
assert contracts == 0
|
|
50
|
+
assert any("invalid quotes" in msg for _, msg in strategy.logged)
|
tests/test_thetadata_helper.py
CHANGED
|
@@ -138,7 +138,11 @@ def test_get_price_data_partial_cache_hit(mock_build_cache_filename, mock_load_c
|
|
|
138
138
|
assert df is not None
|
|
139
139
|
assert len(df) == 10 # Combined cached and fetched data
|
|
140
140
|
mock_get_historical_data.assert_called_once()
|
|
141
|
-
pd.testing.assert_frame_equal(
|
|
141
|
+
pd.testing.assert_frame_equal(
|
|
142
|
+
df,
|
|
143
|
+
updated_data.drop(columns="missing"),
|
|
144
|
+
check_dtype=False,
|
|
145
|
+
)
|
|
142
146
|
mock_update_cache.assert_called_once()
|
|
143
147
|
|
|
144
148
|
|
|
@@ -452,7 +456,7 @@ def test_get_price_data_invokes_remote_cache_manager(tmp_path, monkeypatch):
|
|
|
452
456
|
|
|
453
457
|
df = pd.DataFrame(
|
|
454
458
|
{
|
|
455
|
-
"datetime": pd.date_range("2024-01-01 09:30:00", periods=2, freq="
|
|
459
|
+
"datetime": pd.date_range("2024-01-01 09:30:00", periods=2, freq="min", tz=pytz.UTC),
|
|
456
460
|
"open": [100.0, 101.0],
|
|
457
461
|
"high": [101.0, 102.0],
|
|
458
462
|
"low": [99.5, 100.5],
|
|
@@ -1330,83 +1334,276 @@ class TestThetaDataProcessHealthCheck:
|
|
|
1330
1334
|
assert thetadata_helper.is_process_alive() is True, "New process should be alive"
|
|
1331
1335
|
|
|
1332
1336
|
|
|
1333
|
-
@pytest.mark.apitest
|
|
1334
1337
|
class TestThetaDataChainsCaching:
|
|
1335
|
-
"""
|
|
1338
|
+
"""Unit coverage for historical chain caching and normalization."""
|
|
1339
|
+
|
|
1340
|
+
def test_chains_cached_basic_structure(self, tmp_path, monkeypatch):
|
|
1341
|
+
asset = Asset("TEST", asset_type="stock")
|
|
1342
|
+
test_date = date(2024, 11, 7)
|
|
1343
|
+
|
|
1344
|
+
sample_chain = {
|
|
1345
|
+
"Multiplier": 100,
|
|
1346
|
+
"Exchange": "SMART",
|
|
1347
|
+
"Chains": {
|
|
1348
|
+
"CALL": {"2024-11-15": [100.0, 105.0]},
|
|
1349
|
+
"PUT": {"2024-11-15": [90.0, 95.0]},
|
|
1350
|
+
},
|
|
1351
|
+
}
|
|
1336
1352
|
|
|
1337
|
-
|
|
1338
|
-
"""Test chain caching returns correct structure."""
|
|
1339
|
-
username = os.environ.get("THETADATA_USERNAME")
|
|
1340
|
-
password = os.environ.get("THETADATA_PASSWORD")
|
|
1353
|
+
calls = []
|
|
1341
1354
|
|
|
1342
|
-
|
|
1343
|
-
|
|
1355
|
+
def fake_builder(**kwargs):
|
|
1356
|
+
calls.append(kwargs)
|
|
1357
|
+
return sample_chain
|
|
1344
1358
|
|
|
1345
|
-
|
|
1359
|
+
monkeypatch.setattr(thetadata_helper, "build_historical_chain", fake_builder)
|
|
1360
|
+
monkeypatch.setattr(thetadata_helper, "LUMIBOT_CACHE_FOLDER", str(tmp_path))
|
|
1346
1361
|
|
|
1347
|
-
|
|
1348
|
-
assert "Multiplier" in chains, "Missing Multiplier"
|
|
1349
|
-
assert chains["Multiplier"] == 100, f"Multiplier should be 100, got {chains['Multiplier']}"
|
|
1350
|
-
assert "Exchange" in chains, "Missing Exchange"
|
|
1351
|
-
assert "Chains" in chains, "Missing Chains"
|
|
1352
|
-
assert "CALL" in chains["Chains"], "Missing CALL chains"
|
|
1353
|
-
assert "PUT" in chains["Chains"], "Missing PUT chains"
|
|
1362
|
+
result = thetadata_helper.get_chains_cached("user", "pass", asset, test_date)
|
|
1354
1363
|
|
|
1355
|
-
|
|
1356
|
-
assert len(
|
|
1357
|
-
|
|
1364
|
+
assert result == sample_chain
|
|
1365
|
+
assert len(calls) == 1
|
|
1366
|
+
builder_call = calls[0]
|
|
1367
|
+
assert builder_call["asset"] == asset
|
|
1368
|
+
assert builder_call["as_of_date"] == test_date
|
|
1358
1369
|
|
|
1359
|
-
|
|
1370
|
+
def test_chains_cache_reuse(self, tmp_path, monkeypatch):
|
|
1371
|
+
asset = Asset("REUSE", asset_type="stock")
|
|
1372
|
+
test_date = date(2024, 11, 8)
|
|
1360
1373
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1374
|
+
sample_chain = {
|
|
1375
|
+
"Multiplier": 100,
|
|
1376
|
+
"Exchange": "SMART",
|
|
1377
|
+
"Chains": {"CALL": {"2024-11-22": [110.0]}, "PUT": {"2024-11-22": [95.0]}},
|
|
1378
|
+
}
|
|
1366
1379
|
|
|
1367
|
-
|
|
1368
|
-
password = os.environ.get("THETADATA_PASSWORD")
|
|
1380
|
+
call_count = {"total": 0}
|
|
1369
1381
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1382
|
+
def fake_builder(**kwargs):
|
|
1383
|
+
call_count["total"] += 1
|
|
1384
|
+
return sample_chain
|
|
1372
1385
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1386
|
+
monkeypatch.setattr(thetadata_helper, "build_historical_chain", fake_builder)
|
|
1387
|
+
monkeypatch.setattr(thetadata_helper, "LUMIBOT_CACHE_FOLDER", str(tmp_path))
|
|
1388
|
+
|
|
1389
|
+
first = thetadata_helper.get_chains_cached("user", "pass", asset, test_date)
|
|
1390
|
+
second = thetadata_helper.get_chains_cached("user", "pass", asset, test_date)
|
|
1391
|
+
|
|
1392
|
+
assert first == sample_chain
|
|
1393
|
+
assert second == sample_chain
|
|
1394
|
+
assert call_count["total"] == 1, "Builder should only run once due to cache reuse"
|
|
1395
|
+
|
|
1396
|
+
def test_chain_cache_respects_recent_file(self, tmp_path, monkeypatch):
|
|
1397
|
+
asset = Asset("RECENT", asset_type="stock")
|
|
1398
|
+
test_date = date(2024, 11, 30)
|
|
1399
|
+
|
|
1400
|
+
sample_chain = {
|
|
1401
|
+
"Multiplier": 100,
|
|
1402
|
+
"Exchange": "SMART",
|
|
1403
|
+
"Chains": {"CALL": {"2024-12-06": [120.0]}, "PUT": {"2024-12-06": [80.0]}},
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
monkeypatch.setattr(thetadata_helper, "LUMIBOT_CACHE_FOLDER", str(tmp_path))
|
|
1407
|
+
|
|
1408
|
+
cache_folder = Path(tmp_path) / "thetadata" / "stock" / "option_chains"
|
|
1409
|
+
cache_folder.mkdir(parents=True, exist_ok=True)
|
|
1410
|
+
|
|
1411
|
+
cache_file = cache_folder / f"{asset.symbol}_{test_date.isoformat()}.parquet"
|
|
1412
|
+
pd.DataFrame({"data": [sample_chain]}).to_parquet(cache_file, compression="snappy", engine="pyarrow")
|
|
1413
|
+
|
|
1414
|
+
# Builder should not be invoked because cache hit satisfies tolerance window
|
|
1415
|
+
def fail_builder(**kwargs):
|
|
1416
|
+
raise AssertionError("build_historical_chain should not be called when cache is fresh")
|
|
1417
|
+
|
|
1418
|
+
monkeypatch.setattr(thetadata_helper, "build_historical_chain", fail_builder)
|
|
1419
|
+
|
|
1420
|
+
result = thetadata_helper.get_chains_cached("user", "pass", asset, test_date)
|
|
1421
|
+
assert result == sample_chain
|
|
1422
|
+
|
|
1423
|
+
def test_chains_cached_handles_none_builder(self, tmp_path, monkeypatch, caplog):
|
|
1424
|
+
asset = Asset("NONE", asset_type="stock")
|
|
1425
|
+
test_date = date(2024, 11, 28)
|
|
1426
|
+
|
|
1427
|
+
monkeypatch.setattr(thetadata_helper, "build_historical_chain", lambda **kwargs: None)
|
|
1428
|
+
monkeypatch.setattr(thetadata_helper, "LUMIBOT_CACHE_FOLDER", str(tmp_path))
|
|
1429
|
+
monkeypatch.delenv("BACKTESTING_QUIET_LOGS", raising=False)
|
|
1430
|
+
caplog.set_level(logging.WARNING, logger="lumibot.tools.thetadata_helper")
|
|
1431
|
+
|
|
1432
|
+
with caplog.at_level(logging.WARNING, logger="lumibot.tools.thetadata_helper"):
|
|
1433
|
+
result = thetadata_helper.get_chains_cached("user", "pass", asset, test_date)
|
|
1434
|
+
|
|
1435
|
+
cache_folder = Path(tmp_path) / "thetadata" / "stock" / "option_chains"
|
|
1436
|
+
assert not cache_folder.exists() or not list(cache_folder.glob("*.parquet"))
|
|
1437
|
+
|
|
1438
|
+
assert result == {
|
|
1439
|
+
"Multiplier": 100,
|
|
1440
|
+
"Exchange": "SMART",
|
|
1441
|
+
"Chains": {"CALL": {}, "PUT": {}},
|
|
1442
|
+
}
|
|
1443
|
+
assert "ThetaData returned no option data" in caplog.text
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
def test_build_historical_chain_parses_quote_payload(monkeypatch):
|
|
1447
|
+
asset = Asset("CVNA", asset_type="stock")
|
|
1448
|
+
as_of_date = date(2024, 11, 7)
|
|
1449
|
+
as_of_int = int(as_of_date.strftime("%Y%m%d"))
|
|
1450
|
+
|
|
1451
|
+
def fake_get_request(url, headers, querystring, username, password):
|
|
1452
|
+
if url.endswith("/v2/list/expirations"):
|
|
1453
|
+
return {
|
|
1454
|
+
"header": {"format": ["date"]},
|
|
1455
|
+
"response": [[20241115], [20241205], [20250124]],
|
|
1456
|
+
}
|
|
1457
|
+
if url.endswith("/v2/list/strikes"):
|
|
1458
|
+
exp = querystring["exp"]
|
|
1459
|
+
if exp == "20241115":
|
|
1460
|
+
return {
|
|
1461
|
+
"header": {"format": ["strike"]},
|
|
1462
|
+
"response": [[100000], [105000]],
|
|
1463
|
+
}
|
|
1464
|
+
if exp == "20241205":
|
|
1465
|
+
return {
|
|
1466
|
+
"header": {"format": ["strike"]},
|
|
1467
|
+
"response": [[110000]],
|
|
1468
|
+
}
|
|
1469
|
+
return {
|
|
1470
|
+
"header": {"format": ["strike"]},
|
|
1471
|
+
"response": [[120000]],
|
|
1472
|
+
}
|
|
1473
|
+
if url.endswith("/list/dates/option/quote"):
|
|
1474
|
+
exp = querystring["exp"]
|
|
1475
|
+
if exp == "20241115":
|
|
1476
|
+
return {
|
|
1477
|
+
"header": {"format": None, "error_type": "null"},
|
|
1478
|
+
"response": [as_of_int, as_of_int + 1],
|
|
1479
|
+
}
|
|
1480
|
+
return {
|
|
1481
|
+
"header": {"format": None, "error_type": "NO_DATA"},
|
|
1482
|
+
"response": [],
|
|
1483
|
+
}
|
|
1484
|
+
raise AssertionError(f"Unexpected URL {url}")
|
|
1485
|
+
|
|
1486
|
+
monkeypatch.setattr(thetadata_helper, "get_request", fake_get_request)
|
|
1487
|
+
|
|
1488
|
+
result = thetadata_helper.build_historical_chain("user", "pass", asset, as_of_date)
|
|
1489
|
+
|
|
1490
|
+
assert result["Multiplier"] == 100
|
|
1491
|
+
assert set(result["Chains"].keys()) == {"CALL", "PUT"}
|
|
1492
|
+
assert list(result["Chains"]["CALL"].keys()) == ["2024-11-15"]
|
|
1493
|
+
assert result["Chains"]["CALL"]["2024-11-15"] == [100.0, 105.0]
|
|
1494
|
+
assert result["Chains"]["PUT"]["2024-11-15"] == [100.0, 105.0]
|
|
1495
|
+
|
|
1496
|
+
|
|
1497
|
+
def test_build_historical_chain_returns_none_when_no_dates(monkeypatch, caplog):
|
|
1498
|
+
asset = Asset("NONE", asset_type="stock")
|
|
1499
|
+
as_of_date = date(2024, 11, 28)
|
|
1500
|
+
|
|
1501
|
+
as_of_int = int(as_of_date.strftime("%Y%m%d"))
|
|
1502
|
+
|
|
1503
|
+
def fake_get_request(url, headers, querystring, username, password):
|
|
1504
|
+
if url.endswith("/v2/list/expirations"):
|
|
1505
|
+
return {"header": {"format": ["date"]}, "response": [[20241129], [20241206]]}
|
|
1506
|
+
if url.endswith("/v2/list/strikes"):
|
|
1507
|
+
return {"header": {"format": ["strike"]}, "response": [[150000], [155000]]}
|
|
1508
|
+
if url.endswith("/list/dates/option/quote"):
|
|
1509
|
+
return {"header": {"format": None, "error_type": "NO_DATA"}, "response": []}
|
|
1510
|
+
raise AssertionError(f"Unexpected URL {url}")
|
|
1511
|
+
|
|
1512
|
+
monkeypatch.setattr(thetadata_helper, "get_request", fake_get_request)
|
|
1513
|
+
monkeypatch.delenv("BACKTESTING_QUIET_LOGS", raising=False)
|
|
1514
|
+
caplog.set_level(logging.WARNING, logger="lumibot.tools.thetadata_helper")
|
|
1515
|
+
|
|
1516
|
+
with caplog.at_level(logging.WARNING, logger="lumibot.tools.thetadata_helper"):
|
|
1517
|
+
result = thetadata_helper.build_historical_chain("user", "pass", asset, as_of_date)
|
|
1518
|
+
|
|
1519
|
+
assert result is None
|
|
1520
|
+
assert f"No expirations with data found for {asset.symbol}" in caplog.text
|
|
1521
|
+
|
|
1522
|
+
def test_build_historical_chain_empty_response(monkeypatch, caplog):
|
|
1523
|
+
asset = Asset("EMPTY", asset_type="stock")
|
|
1524
|
+
as_of_date = date(2024, 11, 9)
|
|
1525
|
+
|
|
1526
|
+
def fake_get_request(url, headers, querystring, username, password):
|
|
1527
|
+
if url.endswith("/v2/list/expirations"):
|
|
1528
|
+
return {"header": {"format": ["date"]}, "response": []}
|
|
1529
|
+
raise AssertionError("Unexpected call after empty expirations")
|
|
1530
|
+
|
|
1531
|
+
monkeypatch.setattr(thetadata_helper, "get_request", fake_get_request)
|
|
1532
|
+
monkeypatch.delenv("BACKTESTING_QUIET_LOGS", raising=False)
|
|
1533
|
+
caplog.set_level(logging.WARNING, logger="lumibot.tools.thetadata_helper")
|
|
1534
|
+
|
|
1535
|
+
with caplog.at_level(logging.WARNING, logger="lumibot.tools.thetadata_helper"):
|
|
1536
|
+
result = thetadata_helper.build_historical_chain("user", "pass", asset, as_of_date)
|
|
1537
|
+
|
|
1538
|
+
assert result is None
|
|
1539
|
+
assert "returned no expirations" in caplog.text
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
class TestThetaDataConnectionSupervision:
|
|
1543
|
+
|
|
1544
|
+
def setup_method(self):
|
|
1545
|
+
thetadata_helper.reset_connection_diagnostics()
|
|
1546
|
+
|
|
1547
|
+
def test_check_connection_recovers_after_restart(self, monkeypatch):
|
|
1548
|
+
statuses = iter(["DISCONNECTED", "DISCONNECTED", "CONNECTED"])
|
|
1549
|
+
|
|
1550
|
+
class FakeResponse:
|
|
1551
|
+
def __init__(self, text):
|
|
1552
|
+
self.text = text
|
|
1553
|
+
|
|
1554
|
+
def fake_get(url, timeout):
|
|
1555
|
+
try:
|
|
1556
|
+
text = next(statuses)
|
|
1557
|
+
except StopIteration:
|
|
1558
|
+
text = "CONNECTED"
|
|
1559
|
+
return FakeResponse(text)
|
|
1560
|
+
|
|
1561
|
+
start_calls = []
|
|
1562
|
+
|
|
1563
|
+
def fake_start(username, password):
|
|
1564
|
+
start_calls.append((username, password))
|
|
1565
|
+
return object()
|
|
1566
|
+
|
|
1567
|
+
monkeypatch.setattr(thetadata_helper.requests, "get", fake_get)
|
|
1568
|
+
monkeypatch.setattr(thetadata_helper, "start_theta_data_client", fake_start)
|
|
1569
|
+
monkeypatch.setattr(thetadata_helper, "is_process_alive", lambda: True)
|
|
1570
|
+
monkeypatch.setattr(thetadata_helper, "CONNECTION_MAX_RETRIES", 2, raising=False)
|
|
1571
|
+
monkeypatch.setattr(thetadata_helper, "MAX_TERMINAL_RESTART_CYCLES", 2, raising=False)
|
|
1572
|
+
monkeypatch.setattr(thetadata_helper, "BOOT_GRACE_PERIOD", 0, raising=False)
|
|
1573
|
+
monkeypatch.setattr(thetadata_helper, "CONNECTION_RETRY_SLEEP", 0, raising=False)
|
|
1574
|
+
monkeypatch.setattr(thetadata_helper.time, "sleep", lambda *args, **kwargs: None)
|
|
1575
|
+
|
|
1576
|
+
client, connected = thetadata_helper.check_connection("user", "pass", wait_for_connection=True)
|
|
1389
1577
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
assert
|
|
1578
|
+
assert connected is True
|
|
1579
|
+
assert len(start_calls) == 1
|
|
1580
|
+
assert thetadata_helper.CONNECTION_DIAGNOSTICS["terminal_restarts"] >= 1
|
|
1393
1581
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
chains1 = thetadata_helper.get_chains_cached(username, password, asset, test_date)
|
|
1397
|
-
time1 = time.time() - start1
|
|
1582
|
+
def test_check_connection_raises_after_restart_cycles(self, monkeypatch):
|
|
1583
|
+
statuses = iter(["DISCONNECTED"] * 10)
|
|
1398
1584
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
time2 = time.time() - start2
|
|
1585
|
+
class FakeResponse:
|
|
1586
|
+
def __init__(self, text):
|
|
1587
|
+
self.text = text
|
|
1403
1588
|
|
|
1404
|
-
|
|
1405
|
-
|
|
1589
|
+
def fake_get(url, timeout):
|
|
1590
|
+
try:
|
|
1591
|
+
text = next(statuses)
|
|
1592
|
+
except StopIteration:
|
|
1593
|
+
text = "DISCONNECTED"
|
|
1594
|
+
return FakeResponse(text)
|
|
1406
1595
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1596
|
+
monkeypatch.setattr(thetadata_helper.requests, "get", fake_get)
|
|
1597
|
+
monkeypatch.setattr(thetadata_helper, "start_theta_data_client", lambda *args, **kwargs: object())
|
|
1598
|
+
monkeypatch.setattr(thetadata_helper, "is_process_alive", lambda: True)
|
|
1599
|
+
monkeypatch.setattr(thetadata_helper, "CONNECTION_MAX_RETRIES", 1, raising=False)
|
|
1600
|
+
monkeypatch.setattr(thetadata_helper, "MAX_TERMINAL_RESTART_CYCLES", 1, raising=False)
|
|
1601
|
+
monkeypatch.setattr(thetadata_helper, "BOOT_GRACE_PERIOD", 0, raising=False)
|
|
1602
|
+
monkeypatch.setattr(thetadata_helper, "CONNECTION_RETRY_SLEEP", 0, raising=False)
|
|
1603
|
+
monkeypatch.setattr(thetadata_helper.time, "sleep", lambda *args, **kwargs: None)
|
|
1604
|
+
|
|
1605
|
+
with pytest.raises(thetadata_helper.ThetaDataConnectionError):
|
|
1606
|
+
thetadata_helper.check_connection("user", "pass", wait_for_connection=True)
|
|
1410
1607
|
|
|
1411
1608
|
|
|
1412
1609
|
def test_finalize_day_frame_handles_dst_fallback():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|