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.

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