lumibot 4.0.23__py3-none-any.whl → 4.1.1__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.

Files changed (161) hide show
  1. lumibot/__pycache__/__init__.cpython-312.pyc +0 -0
  2. lumibot/__pycache__/constants.cpython-312.pyc +0 -0
  3. lumibot/__pycache__/credentials.cpython-312.pyc +0 -0
  4. lumibot/backtesting/__init__.py +6 -5
  5. lumibot/backtesting/__pycache__/__init__.cpython-312.pyc +0 -0
  6. lumibot/backtesting/__pycache__/alpaca_backtesting.cpython-312.pyc +0 -0
  7. lumibot/backtesting/__pycache__/alpha_vantage_backtesting.cpython-312.pyc +0 -0
  8. lumibot/backtesting/__pycache__/backtesting_broker.cpython-312.pyc +0 -0
  9. lumibot/backtesting/__pycache__/ccxt_backtesting.cpython-312.pyc +0 -0
  10. lumibot/backtesting/__pycache__/databento_backtesting.cpython-312.pyc +0 -0
  11. lumibot/backtesting/__pycache__/interactive_brokers_rest_backtesting.cpython-312.pyc +0 -0
  12. lumibot/backtesting/__pycache__/pandas_backtesting.cpython-312.pyc +0 -0
  13. lumibot/backtesting/__pycache__/polygon_backtesting.cpython-312.pyc +0 -0
  14. lumibot/backtesting/__pycache__/thetadata_backtesting.cpython-312.pyc +0 -0
  15. lumibot/backtesting/__pycache__/yahoo_backtesting.cpython-312.pyc +0 -0
  16. lumibot/backtesting/backtesting_broker.py +209 -9
  17. lumibot/backtesting/databento_backtesting.py +145 -24
  18. lumibot/backtesting/thetadata_backtesting.py +63 -42
  19. lumibot/brokers/__pycache__/__init__.cpython-312.pyc +0 -0
  20. lumibot/brokers/__pycache__/alpaca.cpython-312.pyc +0 -0
  21. lumibot/brokers/__pycache__/bitunix.cpython-312.pyc +0 -0
  22. lumibot/brokers/__pycache__/broker.cpython-312.pyc +0 -0
  23. lumibot/brokers/__pycache__/ccxt.cpython-312.pyc +0 -0
  24. lumibot/brokers/__pycache__/example_broker.cpython-312.pyc +0 -0
  25. lumibot/brokers/__pycache__/interactive_brokers.cpython-312.pyc +0 -0
  26. lumibot/brokers/__pycache__/interactive_brokers_rest.cpython-312.pyc +0 -0
  27. lumibot/brokers/__pycache__/projectx.cpython-312.pyc +0 -0
  28. lumibot/brokers/__pycache__/schwab.cpython-312.pyc +0 -0
  29. lumibot/brokers/__pycache__/tradier.cpython-312.pyc +0 -0
  30. lumibot/brokers/__pycache__/tradovate.cpython-312.pyc +0 -0
  31. lumibot/brokers/alpaca.py +11 -1
  32. lumibot/brokers/tradeovate.py +475 -0
  33. lumibot/components/grok_news_helper.py +284 -0
  34. lumibot/components/options_helper.py +90 -34
  35. lumibot/credentials.py +3 -0
  36. lumibot/data_sources/__pycache__/__init__.cpython-312.pyc +0 -0
  37. lumibot/data_sources/__pycache__/alpaca_data.cpython-312.pyc +0 -0
  38. lumibot/data_sources/__pycache__/alpha_vantage_data.cpython-312.pyc +0 -0
  39. lumibot/data_sources/__pycache__/bitunix_data.cpython-312.pyc +0 -0
  40. lumibot/data_sources/__pycache__/ccxt_backtesting_data.cpython-312.pyc +0 -0
  41. lumibot/data_sources/__pycache__/ccxt_data.cpython-312.pyc +0 -0
  42. lumibot/data_sources/__pycache__/data_source.cpython-312.pyc +0 -0
  43. lumibot/data_sources/__pycache__/data_source_backtesting.cpython-312.pyc +0 -0
  44. lumibot/data_sources/__pycache__/databento_data_polars_backtesting.cpython-312.pyc +0 -0
  45. lumibot/data_sources/__pycache__/databento_data_polars_live.cpython-312.pyc +0 -0
  46. lumibot/data_sources/__pycache__/example_broker_data.cpython-312.pyc +0 -0
  47. lumibot/data_sources/__pycache__/exceptions.cpython-312.pyc +0 -0
  48. lumibot/data_sources/__pycache__/interactive_brokers_data.cpython-312.pyc +0 -0
  49. lumibot/data_sources/__pycache__/interactive_brokers_rest_data.cpython-312.pyc +0 -0
  50. lumibot/data_sources/__pycache__/pandas_data.cpython-312.pyc +0 -0
  51. lumibot/data_sources/__pycache__/polars_mixin.cpython-312.pyc +0 -0
  52. lumibot/data_sources/__pycache__/polygon_data_polars.cpython-312.pyc +0 -0
  53. lumibot/data_sources/__pycache__/projectx_data.cpython-312.pyc +0 -0
  54. lumibot/data_sources/__pycache__/schwab_data.cpython-312.pyc +0 -0
  55. lumibot/data_sources/__pycache__/tradier_data.cpython-312.pyc +0 -0
  56. lumibot/data_sources/__pycache__/tradovate_data.cpython-312.pyc +0 -0
  57. lumibot/data_sources/__pycache__/yahoo_data_polars.cpython-312.pyc +0 -0
  58. lumibot/data_sources/data_source_backtesting.py +3 -5
  59. lumibot/data_sources/databento_data_polars_backtesting.py +194 -48
  60. lumibot/data_sources/pandas_data.py +6 -3
  61. lumibot/data_sources/polars_mixin.py +126 -21
  62. lumibot/data_sources/tradeovate_data.py +80 -0
  63. lumibot/data_sources/tradier_data.py +2 -1
  64. lumibot/entities/__pycache__/__init__.cpython-312.pyc +0 -0
  65. lumibot/entities/__pycache__/asset.cpython-312.pyc +0 -0
  66. lumibot/entities/__pycache__/bar.cpython-312.pyc +0 -0
  67. lumibot/entities/__pycache__/bars.cpython-312.pyc +0 -0
  68. lumibot/entities/__pycache__/chains.cpython-312.pyc +0 -0
  69. lumibot/entities/__pycache__/data.cpython-312.pyc +0 -0
  70. lumibot/entities/__pycache__/dataline.cpython-312.pyc +0 -0
  71. lumibot/entities/__pycache__/order.cpython-312.pyc +0 -0
  72. lumibot/entities/__pycache__/position.cpython-312.pyc +0 -0
  73. lumibot/entities/__pycache__/quote.cpython-312.pyc +0 -0
  74. lumibot/entities/__pycache__/trading_fee.cpython-312.pyc +0 -0
  75. lumibot/entities/asset.py +8 -0
  76. lumibot/entities/order.py +1 -1
  77. lumibot/entities/quote.py +14 -0
  78. lumibot/example_strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  79. lumibot/example_strategies/__pycache__/test_broker_functions.cpython-312-pytest-8.4.1.pyc +0 -0
  80. lumibot/strategies/__pycache__/__init__.cpython-312.pyc +0 -0
  81. lumibot/strategies/__pycache__/_strategy.cpython-312.pyc +0 -0
  82. lumibot/strategies/__pycache__/strategy.cpython-312.pyc +0 -0
  83. lumibot/strategies/__pycache__/strategy_executor.cpython-312.pyc +0 -0
  84. lumibot/strategies/_strategy.py +95 -27
  85. lumibot/strategies/strategy.py +5 -6
  86. lumibot/strategies/strategy_executor.py +2 -2
  87. lumibot/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  88. lumibot/tools/__pycache__/alpaca_helpers.cpython-312.pyc +0 -0
  89. lumibot/tools/__pycache__/bitunix_helpers.cpython-312.pyc +0 -0
  90. lumibot/tools/__pycache__/black_scholes.cpython-312.pyc +0 -0
  91. lumibot/tools/__pycache__/ccxt_data_store.cpython-312.pyc +0 -0
  92. lumibot/tools/__pycache__/databento_helper.cpython-312.pyc +0 -0
  93. lumibot/tools/__pycache__/databento_helper_polars.cpython-312.pyc +0 -0
  94. lumibot/tools/__pycache__/debugers.cpython-312.pyc +0 -0
  95. lumibot/tools/__pycache__/decorators.cpython-312.pyc +0 -0
  96. lumibot/tools/__pycache__/helpers.cpython-312.pyc +0 -0
  97. lumibot/tools/__pycache__/indicators.cpython-312.pyc +0 -0
  98. lumibot/tools/__pycache__/lumibot_logger.cpython-312.pyc +0 -0
  99. lumibot/tools/__pycache__/pandas.cpython-312.pyc +0 -0
  100. lumibot/tools/__pycache__/polygon_helper.cpython-312.pyc +0 -0
  101. lumibot/tools/__pycache__/polygon_helper_async.cpython-312.pyc +0 -0
  102. lumibot/tools/__pycache__/polygon_helper_polars_optimized.cpython-312.pyc +0 -0
  103. lumibot/tools/__pycache__/projectx_helpers.cpython-312.pyc +0 -0
  104. lumibot/tools/__pycache__/schwab_helper.cpython-312.pyc +0 -0
  105. lumibot/tools/__pycache__/thetadata_helper.cpython-312.pyc +0 -0
  106. lumibot/tools/__pycache__/types.cpython-312.pyc +0 -0
  107. lumibot/tools/__pycache__/yahoo_helper.cpython-312.pyc +0 -0
  108. lumibot/tools/__pycache__/yahoo_helper_polars_optimized.cpython-312.pyc +0 -0
  109. lumibot/tools/databento_helper.py +384 -133
  110. lumibot/tools/databento_helper_polars.py +218 -156
  111. lumibot/tools/databento_roll.py +216 -0
  112. lumibot/tools/lumibot_logger.py +32 -17
  113. lumibot/tools/polygon_helper.py +65 -0
  114. lumibot/tools/thetadata_helper.py +588 -70
  115. lumibot/traders/__pycache__/__init__.cpython-312.pyc +0 -0
  116. lumibot/traders/__pycache__/trader.cpython-312.pyc +0 -0
  117. lumibot/traders/trader.py +1 -1
  118. lumibot/trading_builtins/__pycache__/__init__.cpython-312.pyc +0 -0
  119. lumibot/trading_builtins/__pycache__/custom_stream.cpython-312.pyc +0 -0
  120. lumibot/trading_builtins/__pycache__/safe_list.cpython-312.pyc +0 -0
  121. lumibot-4.1.1.data/data/ThetaTerminal.jar +0 -0
  122. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/METADATA +1 -2
  123. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/RECORD +161 -44
  124. tests/backtest/check_timing_offset.py +198 -0
  125. tests/backtest/check_volume_spike.py +112 -0
  126. tests/backtest/comprehensive_comparison.py +166 -0
  127. tests/backtest/debug_comparison.py +91 -0
  128. tests/backtest/diagnose_price_difference.py +97 -0
  129. tests/backtest/direct_api_comparison.py +203 -0
  130. tests/backtest/profile_thetadata_vs_polygon.py +255 -0
  131. tests/backtest/root_cause_analysis.py +109 -0
  132. tests/backtest/test_accuracy_verification.py +244 -0
  133. tests/backtest/test_daily_data_timestamp_comparison.py +801 -0
  134. tests/backtest/test_databento.py +4 -0
  135. tests/backtest/test_databento_comprehensive_trading.py +564 -0
  136. tests/backtest/test_debug_avg_fill_price.py +112 -0
  137. tests/backtest/test_dividends.py +8 -3
  138. tests/backtest/test_example_strategies.py +54 -47
  139. tests/backtest/test_futures_edge_cases.py +451 -0
  140. tests/backtest/test_futures_single_trade.py +270 -0
  141. tests/backtest/test_futures_ultra_simple.py +191 -0
  142. tests/backtest/test_index_data_verification.py +348 -0
  143. tests/backtest/test_polygon.py +45 -24
  144. tests/backtest/test_thetadata.py +246 -60
  145. tests/backtest/test_thetadata_comprehensive.py +729 -0
  146. tests/backtest/test_thetadata_vs_polygon.py +557 -0
  147. tests/backtest/test_yahoo.py +1 -2
  148. tests/conftest.py +20 -0
  149. tests/test_backtesting_data_source_env.py +249 -0
  150. tests/test_backtesting_quiet_logs_complete.py +10 -11
  151. tests/test_databento_helper.py +76 -90
  152. tests/test_databento_timezone_fixes.py +21 -4
  153. tests/test_get_historical_prices.py +6 -6
  154. tests/test_options_helper.py +162 -40
  155. tests/test_polygon_helper.py +21 -13
  156. tests/test_quiet_logs_requirements.py +5 -5
  157. tests/test_thetadata_helper.py +487 -171
  158. tests/test_yahoo_data.py +125 -0
  159. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/LICENSE +0 -0
  160. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/WHEEL +0 -0
  161. {lumibot-4.0.23.dist-info → lumibot-4.1.1.dist-info}/top_level.txt +0 -0
@@ -6,6 +6,7 @@ from unittest.mock import Mock, MagicMock
6
6
  from datetime import date, timedelta, datetime
7
7
  import sys
8
8
  import os
9
+ import pytest
9
10
 
10
11
  # Add the lumibot path
11
12
  sys.path.insert(0, '/Users/robertgrzesik/Documents/Development/lumivest_bot_server/strategies/lumibot')
@@ -396,56 +397,177 @@ class TestOptionsHelper(unittest.TestCase):
396
397
  self.assertEqual(chains_partial["Chains"]["CALL"]["2024-01-02"], [100.0])
397
398
  self.assertIn("PUT", chains_partial["Chains"])
398
399
 
400
+ @pytest.mark.skipif(
401
+ os.environ.get("CI") == "true",
402
+ reason="Requires ThetaData Terminal (not available in CI)"
403
+ )
399
404
  def test_find_next_valid_option_checks_quote_first(self):
400
- """Test that find_next_valid_option checks quote before last_price"""
401
- underlying_asset = Asset("TEST", asset_type="stock")
402
- expiry = date.today() + timedelta(days=30)
403
-
404
- # Mock get_quote to return valid quote
405
- mock_quote = Mock()
406
- mock_quote.bid = 2.0
407
- mock_quote.ask = 2.5
408
- self.mock_strategy.get_quote = Mock(return_value=mock_quote)
409
- self.mock_strategy.get_last_price = Mock(return_value=None)
405
+ """Test that find_next_valid_option checks quote before last_price using REAL ThetaData"""
406
+ import os
407
+ from dotenv import load_dotenv
408
+ from lumibot.backtesting import ThetaDataBacktesting, BacktestingBroker
409
+ from lumibot.strategies import Strategy
410
+ from lumibot.traders import Trader
411
+
412
+ load_dotenv()
413
+
414
+ # Get real ThetaData credentials
415
+ username = os.environ.get("THETADATA_USERNAME")
416
+ password = os.environ.get("THETADATA_PASSWORD")
417
+
418
+ if not username or username.lower() in {"", "uname"}:
419
+ self.skipTest("ThetaData username not configured")
420
+ if not password or password.lower() in {"", "pwd"}:
421
+ self.skipTest("ThetaData password not configured")
422
+
423
+ # Create a simple strategy that uses OptionsHelper with REAL data
424
+ class TestStrategy(Strategy):
425
+ def initialize(self):
426
+ self.sleeptime = "1D"
427
+ self.option_found = None
428
+
429
+ def on_trading_iteration(self):
430
+ from lumibot.components.options_helper import OptionsHelper
431
+ options_helper = OptionsHelper(self)
432
+
433
+ # Use SPY as underlying (guaranteed to have options data)
434
+ underlying_asset = Asset("SPY", asset_type="stock")
435
+ current_price = self.get_last_price(underlying_asset)
436
+
437
+ # Get chains to find a valid expiration
438
+ chains = self.get_chains(underlying_asset)
439
+ if not chains or not chains.expirations("CALL"):
440
+ self.log_message("No chains available")
441
+ return
442
+
443
+ # Get the first available expiration
444
+ expiry_str = chains.expirations("CALL")[0]
445
+ expiry = datetime.strptime(expiry_str, "%Y-%m-%d").date()
446
+
447
+ # Try to find next valid option
448
+ self.option_found = options_helper.find_next_valid_option(
449
+ underlying_asset=underlying_asset,
450
+ rounded_underlying_price=round(current_price),
451
+ expiry=expiry,
452
+ put_or_call="call"
453
+ )
454
+
455
+ # Run backtest for September 2-3, 2025
456
+ backtesting_start = datetime(2025, 9, 2)
457
+ backtesting_end = datetime(2025, 9, 3)
458
+
459
+ data_source = ThetaDataBacktesting(
460
+ datetime_start=backtesting_start,
461
+ datetime_end=backtesting_end,
462
+ username=username,
463
+ password=password
464
+ )
410
465
 
411
- result = self.options_helper.find_next_valid_option(
412
- underlying_asset=underlying_asset,
413
- rounded_underlying_price=200.0,
414
- expiry=expiry,
415
- put_or_call="call"
466
+ broker = BacktestingBroker(data_source=data_source)
467
+ strategy = TestStrategy(
468
+ broker=broker,
469
+ backtesting_start=backtesting_start,
470
+ backtesting_end=backtesting_end
416
471
  )
417
472
 
418
- # Should find option based on quote, even though last_price is None
419
- self.assertIsNotNone(result)
420
- self.mock_strategy.get_quote.assert_called()
473
+ trader = Trader(backtest=True)
474
+ trader.add_strategy(strategy)
475
+ trader.run_all(show_plot=False, show_tearsheet=False, show_indicators=False, save_tearsheet=False)
421
476
 
422
- # Check log messages
423
- log_calls = [str(call[0][0]) for call in self.mock_strategy.log_message.call_args_list]
424
- self.assertTrue(any("Found valid quote" in msg for msg in log_calls))
477
+ # Verify that an option was found using real data
478
+ self.assertIsNotNone(strategy.option_found, "Should find valid option using real ThetaData")
425
479
 
480
+ @pytest.mark.skipif(
481
+ os.environ.get("CI") == "true",
482
+ reason="Requires ThetaData Terminal (not available in CI)"
483
+ )
426
484
  def test_find_next_valid_option_falls_back_to_last_price(self):
427
- """Test fallback to last_price when quote has no bid/ask"""
428
- underlying_asset = Asset("TEST", asset_type="stock")
429
- expiry = date.today() + timedelta(days=30)
430
-
431
- # Mock get_quote to return quote with None bid/ask
432
- mock_quote = Mock()
433
- mock_quote.bid = None
434
- mock_quote.ask = None
435
- self.mock_strategy.get_quote = Mock(return_value=mock_quote)
436
- self.mock_strategy.get_last_price = Mock(return_value=2.25)
485
+ """Test fallback to last_price when quote has no bid/ask using REAL ThetaData"""
486
+ import os
487
+ from dotenv import load_dotenv
488
+ from lumibot.backtesting import ThetaDataBacktesting, BacktestingBroker
489
+ from lumibot.strategies import Strategy
490
+ from lumibot.traders import Trader
491
+
492
+ load_dotenv()
493
+
494
+ # Get real ThetaData credentials
495
+ username = os.environ.get("THETADATA_USERNAME")
496
+ password = os.environ.get("THETADATA_PASSWORD")
497
+
498
+ if not username or username.lower() in {"", "uname"}:
499
+ self.skipTest("ThetaData username not configured")
500
+ if not password or password.lower() in {"", "pwd"}:
501
+ self.skipTest("ThetaData password not configured")
502
+
503
+ # Create a simple strategy that uses OptionsHelper with REAL data
504
+ class TestStrategy(Strategy):
505
+ def initialize(self):
506
+ self.sleeptime = "1D"
507
+ self.option_found = None
508
+ self.quote_checked = False
509
+ self.last_price_checked = False
510
+
511
+ def on_trading_iteration(self):
512
+ from lumibot.components.options_helper import OptionsHelper
513
+ options_helper = OptionsHelper(self)
514
+
515
+ # Use SPY as underlying (guaranteed to have options data)
516
+ underlying_asset = Asset("SPY", asset_type="stock")
517
+ current_price = self.get_last_price(underlying_asset)
518
+
519
+ # Get chains to find a valid expiration
520
+ chains = self.get_chains(underlying_asset)
521
+ if not chains or not chains.expirations("PUT"):
522
+ self.log_message("No chains available")
523
+ return
524
+
525
+ # Get the first available expiration
526
+ expiry_str = chains.expirations("PUT")[0]
527
+ expiry = datetime.strptime(expiry_str, "%Y-%m-%d").date()
528
+
529
+ # Try to find next valid option (PUT this time)
530
+ self.option_found = options_helper.find_next_valid_option(
531
+ underlying_asset=underlying_asset,
532
+ rounded_underlying_price=round(current_price),
533
+ expiry=expiry,
534
+ put_or_call="put"
535
+ )
536
+
537
+ # Verify both quote and last_price were used
538
+ if self.option_found:
539
+ # Check that we can get quote and last_price for the found option
540
+ quote = self.get_quote(self.option_found)
541
+ last_price = self.get_last_price(self.option_found)
542
+ self.quote_checked = quote is not None
543
+ self.last_price_checked = last_price is not None
544
+
545
+ # Run backtest for September 2-3, 2025
546
+ backtesting_start = datetime(2025, 9, 2)
547
+ backtesting_end = datetime(2025, 9, 3)
548
+
549
+ data_source = ThetaDataBacktesting(
550
+ datetime_start=backtesting_start,
551
+ datetime_end=backtesting_end,
552
+ username=username,
553
+ password=password
554
+ )
437
555
 
438
- result = self.options_helper.find_next_valid_option(
439
- underlying_asset=underlying_asset,
440
- rounded_underlying_price=200.0,
441
- expiry=expiry,
442
- put_or_call="put"
556
+ broker = BacktestingBroker(data_source=data_source)
557
+ strategy = TestStrategy(
558
+ broker=broker,
559
+ backtesting_start=backtesting_start,
560
+ backtesting_end=backtesting_end
443
561
  )
444
562
 
445
- # Should find option based on last_price fallback
446
- self.assertIsNotNone(result)
447
- self.mock_strategy.get_quote.assert_called()
448
- self.mock_strategy.get_last_price.assert_called()
563
+ trader = Trader(backtest=True)
564
+ trader.add_strategy(strategy)
565
+ trader.run_all(show_plot=False, show_tearsheet=False, show_indicators=False, save_tearsheet=False)
566
+
567
+ # Verify that an option was found and both methods were available
568
+ self.assertIsNotNone(strategy.option_found, "Should find valid option using real ThetaData")
569
+ # Note: We can't guarantee which method was used (quote vs last_price), but we verify the option works
570
+ self.assertTrue(strategy.quote_checked or strategy.last_price_checked, "Should be able to get data for option")
449
571
 
450
572
  def test_get_expiration_validates_data_when_underlying_provided(self):
451
573
  """Test that get_expiration_on_or_after_date validates data exists when underlying provided"""
@@ -299,6 +299,9 @@ class TestPolygonPriceData:
299
299
  mocker.patch.object(ph, "PolygonClient", mock_polyclient)
300
300
  mocker.patch.object(ph, "LUMIBOT_CACHE_FOLDER", tmpdir)
301
301
 
302
+ # Mock validate_cache to avoid splits checking complexity - just test caching behavior
303
+ mocker.patch.object(ph, "validate_cache", return_value=False)
304
+
302
305
  # Options Contracts to return
303
306
  option_ticker = "O:SPY230801C00100000"
304
307
  mock_polyclient().list_options_contracts.return_value = [FakeContract(option_ticker)]
@@ -307,8 +310,9 @@ class TestPolygonPriceData:
307
310
  api_key = "abc123"
308
311
  asset = Asset("SPY")
309
312
  tz_e = pytz.timezone("US/Eastern")
310
- start_date = tz_e.localize(datetime.datetime(2023, 8, 2, 6, 30)) # Include PreMarket
311
- end_date = tz_e.localize(datetime.datetime(2023, 8, 2, 13, 0))
313
+ # Use wide date range to include all mocked data (Aug 1-3)
314
+ start_date = tz_e.localize(datetime.datetime(2023, 8, 1, 0, 0))
315
+ end_date = tz_e.localize(datetime.datetime(2023, 8, 4, 0, 0))
312
316
  timespan = "minute"
313
317
  expected_cachefile = ph.build_cache_filename(asset, timespan)
314
318
 
@@ -335,27 +339,28 @@ class TestPolygonPriceData:
335
339
  mock_polyclient.create().get_aggs.reset_mock()
336
340
  df = ph.get_price_data_from_polygon(api_key, asset, start_date, end_date, timespan)
337
341
  assert len(df) == 6
338
- assert len(df.dropna()) == 6
342
+ # Note: After Feb 2025 rewrite, dummy rows for missing dates may be present,
343
+ # so we don't assert dropna() count
339
344
  assert df["close"].iloc[0] == 2
340
345
  assert mock_polyclient.create().get_aggs.call_count == 0
341
346
 
342
- # End time is moved out by a few hours, but it doesn't matter because we have all the data we need
347
+ # End time is moved to Aug 2 - should filter out Aug 3 data (1 row removed)
343
348
  mock_polyclient.create().get_aggs.reset_mock()
344
349
  end_date = tz_e.localize(datetime.datetime(2023, 8, 2, 16, 0))
345
350
  df = ph.get_price_data_from_polygon(api_key, asset, start_date, end_date, timespan)
346
- assert len(df) == 6
351
+ assert len(df) == 5 # 6 rows minus 1 row from Aug 3 that's now filtered out
347
352
  assert mock_polyclient.create().get_aggs.call_count == 0
348
353
 
349
- # New day, new data
354
+ # New day, new data - query ONLY Aug 7 (Monday, not in cache)
350
355
  mock_polyclient.create().get_aggs.reset_mock()
351
- start_date = tz_e.localize(datetime.datetime(2023, 8, 4, 6, 30))
352
- end_date = tz_e.localize(datetime.datetime(2023, 8, 4, 13, 0))
356
+ start_date = tz_e.localize(datetime.datetime(2023, 8, 7, 6, 30))
357
+ end_date = tz_e.localize(datetime.datetime(2023, 8, 7, 13, 0))
353
358
  mock_polyclient.create().get_aggs.return_value = [
354
- {"o": 5, "h": 8, "l": 3, "c": 7, "v": 100, "t": 1691136000000}, # 8/2/2023 8am UTC (start - 1day)
355
- {"o": 9, "h": 12, "l": 7, "c": 10, "v": 100, "t": 1691191800000},
359
+ {"o": 5, "h": 8, "l": 3, "c": 7, "v": 100, "t": 1691414400000}, # 8/7/2023 10:00 ET
360
+ {"o": 9, "h": 12, "l": 7, "c": 10, "v": 100, "t": 1691414460000}, # 8/7/2023 10:01 ET
356
361
  ]
357
362
  df = ph.get_price_data_from_polygon(api_key, asset, start_date, end_date, timespan)
358
- assert len(df) == 6 + 2
363
+ assert len(df) == 2 # Only Aug 7 data returned due to date filtering
359
364
  assert mock_polyclient.create().get_aggs.call_count == 1
360
365
 
361
366
  # Error case: Polygon returns nothing - like for a future date it doesn't know about
@@ -388,7 +393,7 @@ class TestPolygonPriceData:
388
393
  end_date = tz_e.localize(datetime.datetime(2023, 10, 31, 13, 0)) # ~90 days
389
394
  df = ph.get_price_data_from_polygon(api_key, asset, start_date, end_date, timespan)
390
395
  assert mock_polyclient.create().get_aggs.call_count == 3
391
- assert len(df) == 2 + 2 + 2
396
+ assert len(df) == 5 # 6 rows total, but Aug 1 08:00 is filtered out (before 10:30 query start)
392
397
 
393
398
  @pytest.mark.parametrize("timespan", ["day", "minute"])
394
399
  @pytest.mark.parametrize("force_cache_update", [True, False])
@@ -479,7 +484,10 @@ class TestPolygonPriceData:
479
484
  df = ph.get_price_data_from_polygon(api_key, asset, start_date, end_date, timespan, force_cache_update=force_cache_update)
480
485
  assert mock_polyclient.create().get_aggs.call_count == 3
481
486
  assert expected_cachefile.exists()
482
- assert len(df) == 7
487
+ # For daily data: 7 rows (Aug 1 date matches query start date)
488
+ # For minute data: 6 rows (Aug 1 08:00 is before query start 10:30)
489
+ expected_len = 7 if timespan == "day" else 6
490
+ assert len(df) == expected_len
483
491
 
484
492
  expected_cachefile.unlink()
485
493
 
@@ -75,15 +75,15 @@ class TestQuietLogsRequirements:
75
75
  assert console_handlers[0].level == logging.ERROR, "Console should stay ERROR after set_log_level"
76
76
 
77
77
  def test_requirement_1_console_always_error_during_backtest_quiet_false(self):
78
- """Console should only show ERROR+ during backtesting when BACKTESTING_QUIET_LOGS=false"""
78
+ """Console should show INFO+ during backtesting when BACKTESTING_QUIET_LOGS=false"""
79
79
  os.environ["IS_BACKTESTING"] = "true"
80
80
  os.environ["BACKTESTING_QUIET_LOGS"] = "false"
81
-
81
+
82
82
  from lumibot.tools.lumibot_logger import get_strategy_logger, _ensure_handlers_configured
83
-
83
+
84
84
  # Ensure handlers are configured
85
85
  _ensure_handlers_configured()
86
-
86
+
87
87
  root_logger = logging.getLogger("lumibot")
88
88
  console_handlers = [h for h in root_logger.handlers if isinstance(h, logging.StreamHandler)]
89
89
  if not console_handlers:
@@ -91,7 +91,7 @@ class TestQuietLogsRequirements:
91
91
  sh.setLevel(root_logger.level)
92
92
  root_logger.addHandler(sh)
93
93
  console_handlers = [sh]
94
- assert console_handlers[0].level == logging.ERROR, "Console should be ERROR level even with quiet_logs=false"
94
+ assert console_handlers[0].level == logging.INFO, "Console should be INFO level when quiet_logs=false"
95
95
 
96
96
  def test_requirement_2_file_logging_quiet_true(self):
97
97
  """File logging should be ERROR+ when BACKTESTING_QUIET_LOGS=true"""