lumibot 4.2.9__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.

@@ -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
tests/test_tradovate.py CHANGED
@@ -23,6 +23,15 @@ import logging
23
23
  import time
24
24
  import requests
25
25
  from datetime import datetime
26
+ from types import SimpleNamespace
27
+
28
+ from lumibot.brokers.broker import Broker
29
+
30
+
31
+ @pytest.fixture(autouse=True)
32
+ def disable_tradovate_stream(monkeypatch):
33
+ """Prevent background polling threads during unit tests."""
34
+ monkeypatch.setattr(Broker, "_launch_stream", lambda self: None)
26
35
 
27
36
 
28
37
  class TestTradovateImports:
@@ -576,6 +585,290 @@ class TestTradovateAPIPayload:
576
585
  print("✅ Stop order payload format test passed")
577
586
 
578
587
 
588
+ class TestTradovateLifecycle:
589
+ """Tests for Tradovate order lifecycle wiring (polling, submit, cancel)."""
590
+
591
+ def _make_broker(self):
592
+ from lumibot.brokers import Tradovate
593
+ base_config = {
594
+ "USERNAME": "test_user",
595
+ "DEDICATED_PASSWORD": "test_pass",
596
+ "CID": "test_cid",
597
+ "SECRET": "test_secret",
598
+ "IS_PAPER": True,
599
+ }
600
+ tokens = {
601
+ "accessToken": "token",
602
+ "marketToken": "market",
603
+ "hasMarketData": True,
604
+ }
605
+ account_info = {"accountSpec": "TEST", "accountId": 123}
606
+ user_info = "user"
607
+
608
+ with patch.object(Tradovate, "_get_tokens", return_value=tokens), \
609
+ patch.object(Tradovate, "_get_account_info", return_value=account_info), \
610
+ patch.object(Tradovate, "_get_user_info", return_value=user_info):
611
+ broker = Tradovate(config=base_config)
612
+ return broker
613
+
614
+ def test_submit_order_emits_new_event(self):
615
+ from lumibot.entities import Asset, Order
616
+
617
+ broker = self._make_broker()
618
+ asset = Asset("MES", asset_type=Asset.AssetType.CONT_FUTURE)
619
+ strategy_name = "Strategy"
620
+ order = Order(
621
+ strategy=strategy_name,
622
+ asset=asset,
623
+ quantity=1,
624
+ side="buy",
625
+ order_type=Order.OrderType.MARKET,
626
+ )
627
+
628
+ mock_response = MagicMock()
629
+ mock_response.status_code = 200
630
+ mock_response.json.return_value = {"orderId": 999}
631
+
632
+ with patch.object(broker, "_request", return_value=mock_response), \
633
+ patch.object(broker, "_process_trade_event") as mock_process:
634
+ broker._submit_order(order)
635
+
636
+ mock_process.assert_called_once_with(order, broker.NEW_ORDER)
637
+
638
+ def test_do_polling_dispatches_fill_event(self):
639
+ from lumibot.entities import Asset, Order
640
+
641
+ broker = self._make_broker()
642
+ broker.stream = SimpleNamespace(dispatch=lambda event, **payload: broker._dispatched.append((event, payload)))
643
+ broker._dispatched = []
644
+
645
+ filled_order = Order(
646
+ strategy="Strategy",
647
+ asset=Asset("ESZ5", asset_type=Asset.AssetType.FUTURE),
648
+ quantity=2,
649
+ side="sell",
650
+ order_type=Order.OrderType.MARKET,
651
+ )
652
+ filled_order.set_identifier("321")
653
+ filled_order.status = Order.OrderStatus.FILLED
654
+
655
+ with patch.object(broker, "sync_positions", return_value=None), \
656
+ patch.object(broker, "_pull_broker_all_orders", return_value=[{"id": "321"}]), \
657
+ patch.object(broker, "_parse_broker_order", return_value=filled_order), \
658
+ patch.object(broker, "_extract_fill_details", return_value=(100.0, 2)):
659
+ broker.do_polling()
660
+
661
+ events = broker._dispatched
662
+ assert any(event == broker.FILLED_ORDER for event, _ in events)
663
+
664
+ def test_cancel_order_dispatches_cancel_event(self):
665
+ from lumibot.entities import Asset, Order
666
+
667
+ broker = self._make_broker()
668
+ dispatched = []
669
+ broker.stream = SimpleNamespace(dispatch=lambda event, **payload: dispatched.append((event, payload)))
670
+
671
+ order = Order(
672
+ strategy="Strategy",
673
+ asset=Asset("ESZ5", asset_type=Asset.AssetType.FUTURE),
674
+ quantity=1,
675
+ side="sell",
676
+ order_type=Order.OrderType.MARKET,
677
+ )
678
+ order.set_identifier("654")
679
+
680
+ mock_response = MagicMock()
681
+ mock_response.status_code = 200
682
+ mock_response.json.return_value = {}
683
+
684
+ with patch.object(broker, "_request", return_value=mock_response):
685
+ broker.cancel_order(order)
686
+
687
+ assert any(event == broker.CANCELED_ORDER for event, _ in dispatched)
688
+
689
+ def test_pull_all_orders_skips_first_iteration(self):
690
+ broker = self._make_broker()
691
+ broker._first_iteration = True
692
+ result = broker._pull_all_orders("Strategy", None)
693
+ assert result == []
694
+
695
+ broker._first_iteration = False
696
+ with patch("lumibot.brokers.broker.Broker._pull_all_orders", return_value=["order"]) as mock_super:
697
+ result = broker._pull_all_orders("Strategy", None)
698
+ mock_super.assert_called_once()
699
+ assert result == ["order"]
700
+
701
+ def test_do_polling_dispatches_new_for_active_order(self):
702
+ from lumibot.entities import Asset, Order
703
+
704
+ broker = self._make_broker()
705
+ broker.stream = SimpleNamespace(dispatch=lambda event, **payload: broker._dispatched.append((event, payload)))
706
+ broker._dispatched = []
707
+
708
+ active_order = Order(
709
+ strategy="Strategy",
710
+ asset=Asset("ESZ5", asset_type=Asset.AssetType.FUTURE),
711
+ quantity=1,
712
+ side="buy",
713
+ order_type=Order.OrderType.MARKET,
714
+ )
715
+ active_order.set_identifier("999")
716
+ active_order.status = Order.OrderStatus.NEW
717
+
718
+ with patch.object(broker, "sync_positions", return_value=None), \
719
+ patch.object(broker, "_pull_broker_all_orders", return_value=[{"id": "999"}]), \
720
+ patch.object(broker, "_parse_broker_order", return_value=active_order), \
721
+ patch.object(broker, "_extract_fill_details", return_value=(None, None)):
722
+ broker.do_polling()
723
+
724
+ events = broker._dispatched
725
+ assert any(event == broker.NEW_ORDER for event, _ in events)
726
+
727
+ def test_do_polling_skips_new_for_closed_order_even_after_startup(self):
728
+ from lumibot.entities import Asset, Order
729
+
730
+ broker = self._make_broker()
731
+ broker.stream = SimpleNamespace(dispatch=lambda event, **payload: broker._dispatched.append((event, payload)))
732
+ broker._dispatched = []
733
+
734
+ closed_order = Order(
735
+ strategy="Strategy",
736
+ asset=Asset("ESZ5", asset_type=Asset.AssetType.FUTURE),
737
+ quantity=1,
738
+ side="sell",
739
+ order_type=Order.OrderType.MARKET,
740
+ )
741
+ closed_order.set_identifier("777")
742
+ closed_order.status = Order.OrderStatus.FILLED
743
+
744
+ broker._first_iteration = False
745
+
746
+ with patch.object(broker, "sync_positions", return_value=None), \
747
+ patch.object(broker, "_pull_broker_all_orders", return_value=[{"id": "777"}]), \
748
+ patch.object(broker, "_parse_broker_order", return_value=closed_order), \
749
+ patch.object(broker, "_extract_fill_details", return_value=(100.0, 1)):
750
+ broker.do_polling()
751
+
752
+ events = broker._dispatched
753
+ assert any(event == broker.FILLED_ORDER for event, _ in events)
754
+ assert not any(event == broker.NEW_ORDER for event, _ in events)
755
+
756
+ def test_extract_fill_details_uses_fill_list_fallback(self):
757
+ from lumibot.entities import Asset, Order
758
+
759
+ broker = self._make_broker()
760
+
761
+ raw_order = {"id": "900", "ordStatus": "Filled"}
762
+ parsed_order = Order(
763
+ strategy="Strategy",
764
+ asset=Asset("ESZ5", asset_type=Asset.AssetType.FUTURE),
765
+ quantity=0,
766
+ side="buy",
767
+ order_type=Order.OrderType.MARKET,
768
+ )
769
+ parsed_order.set_identifier("900")
770
+
771
+ with patch.object(broker, "_fetch_recent_fill_details", return_value=(6788.5, 1)):
772
+ price, qty = broker._extract_fill_details(raw_order, parsed_order)
773
+
774
+ assert qty == 1
775
+ assert price == 6788.5
776
+
777
+ def test_missing_order_reconciles_to_fill_instead_of_cancel(self):
778
+ from lumibot.entities import Asset, Order
779
+
780
+ broker = self._make_broker()
781
+ broker.stream = SimpleNamespace(dispatch=lambda event, **payload: broker._dispatched.append((event, payload)))
782
+ broker._dispatched = []
783
+
784
+ missing_order = Order(
785
+ strategy="Strategy",
786
+ asset=Asset("ESZ5", asset_type=Asset.AssetType.FUTURE),
787
+ quantity=1,
788
+ side="buy",
789
+ order_type=Order.OrderType.MARKET,
790
+ )
791
+ missing_order.set_identifier("555")
792
+ missing_order.status = Order.OrderStatus.NEW
793
+
794
+ quote = SimpleNamespace(last=6788.25)
795
+
796
+ with patch.object(broker, "sync_positions", return_value=None), \
797
+ patch.object(broker, "_pull_broker_all_orders", return_value=[]), \
798
+ patch.object(broker, "get_all_orders", return_value=[missing_order]), \
799
+ patch.object(broker, "_fetch_recent_fill_details", return_value=(6788.5, 1)), \
800
+ patch.object(broker, "get_quote", return_value=quote):
801
+ broker.do_polling()
802
+
803
+ events = broker._dispatched
804
+ assert any(event == broker.FILLED_ORDER for event, _ in events)
805
+ assert not any(event == broker.CANCELED_ORDER for event, _ in events)
806
+
807
+ def test_cancel_open_orders_prunes_stale_locals(self):
808
+ from lumibot.entities import Asset, Order
809
+
810
+ broker = self._make_broker()
811
+
812
+ stale_order = Order(
813
+ strategy="Strategy",
814
+ asset=Asset("ESZ5", asset_type=Asset.AssetType.FUTURE),
815
+ quantity=1,
816
+ side="buy",
817
+ order_type=Order.OrderType.MARKET,
818
+ )
819
+ stale_order.set_identifier("111")
820
+ stale_order.status = Order.OrderStatus.NEW
821
+
822
+ live_order = Order(
823
+ strategy="Strategy",
824
+ asset=Asset("ESZ5", asset_type=Asset.AssetType.FUTURE),
825
+ quantity=1,
826
+ side="sell",
827
+ order_type=Order.OrderType.MARKET,
828
+ )
829
+ live_order.set_identifier("222")
830
+ live_order.status = Order.OrderStatus.NEW
831
+
832
+ broker._new_orders.append(stale_order)
833
+ broker._new_orders.append(live_order)
834
+ broker._active_broker_identifiers = {"222"}
835
+
836
+ with patch.object(broker, "_refresh_active_identifiers_snapshot", return_value={"222"}) as mock_refresh, \
837
+ patch.object(broker, "cancel_orders") as mock_cancel:
838
+ broker.cancel_open_orders("Strategy")
839
+
840
+ mock_refresh.assert_not_called()
841
+ mock_cancel.assert_called_once()
842
+ args, _ = mock_cancel.call_args
843
+ assert args[0] == [live_order]
844
+ assert stale_order.status == broker.CANCELED_ORDER
845
+ assert not stale_order.is_active()
846
+
847
+ def test_cancel_open_orders_refreshes_cache_when_missing(self):
848
+ from lumibot.entities import Asset, Order
849
+
850
+ broker = self._make_broker()
851
+
852
+ live_order = Order(
853
+ strategy="Strategy",
854
+ asset=Asset("ESZ5", asset_type=Asset.AssetType.FUTURE),
855
+ quantity=1,
856
+ side="sell",
857
+ order_type=Order.OrderType.MARKET,
858
+ )
859
+ live_order.set_identifier("333")
860
+ live_order.status = Order.OrderStatus.NEW
861
+ broker._new_orders.append(live_order)
862
+ broker._active_broker_identifiers = None
863
+
864
+ with patch.object(broker, "_refresh_active_identifiers_snapshot", return_value={"333"}) as mock_refresh, \
865
+ patch.object(broker, "cancel_orders") as mock_cancel:
866
+ broker.cancel_open_orders("Strategy")
867
+
868
+ mock_refresh.assert_called_once()
869
+ mock_cancel.assert_called_once()
870
+
871
+
579
872
  class TestTradovateTokenRenewal:
580
873
  """Test the token renewal functionality."""
581
874