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.
- lumibot/brokers/broker.py +33 -0
- lumibot/brokers/tradovate.py +556 -10
- lumibot/data_sources/databento_data_polars.py +35 -34
- lumibot/entities/asset.py +11 -0
- lumibot/strategies/strategy.py +46 -1
- lumibot/tools/databento_helper.py +16 -0
- lumibot/tools/databento_helper_polars.py +41 -1
- lumibot/tools/futures_roll.py +71 -20
- lumibot/tools/thetadata_helper.py +4 -1
- {lumibot-4.2.9.dist-info → lumibot-4.2.10.dist-info}/METADATA +1 -1
- {lumibot-4.2.9.dist-info → lumibot-4.2.10.dist-info}/RECORD +18 -17
- tests/test_futures_roll.py +20 -0
- tests/test_strategy_close_position.py +83 -0
- tests/test_thetadata_helper.py +45 -1
- tests/test_tradovate.py +293 -0
- {lumibot-4.2.9.dist-info → lumibot-4.2.10.dist-info}/WHEEL +0 -0
- {lumibot-4.2.9.dist-info → lumibot-4.2.10.dist-info}/licenses/LICENSE +0 -0
- {lumibot-4.2.9.dist-info → lumibot-4.2.10.dist-info}/top_level.txt +0 -0
|
@@ -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()
|
tests/test_thetadata_helper.py
CHANGED
|
@@ -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 ==
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|