Skip to content

Commit 6b28636

Browse files
committed
[Orders] make apply_inactive_orders more flexible
1 parent 7a8a46e commit 6b28636

File tree

4 files changed

+103
-8
lines changed

4 files changed

+103
-8
lines changed

octobot_trading/exchanges/exchange_builder.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,10 @@ async def _build_trading_modes_if_required(self, trading_mode_class, tentacles_s
132132
if self._is_using_trading_modes:
133133
# self.exchange_manager.trader can be None if neither simulator or real trader has be set
134134
if self.exchange_manager.is_trading and (
135-
trading_mode_class.get_is_using_trading_mode_on_exchange(
136-
self.exchange_name, tentacles_setup_config
135+
self.exchange_manager.trader is None or (
136+
trading_mode_class.get_is_using_trading_mode_on_exchange(
137+
self.exchange_name, tentacles_setup_config
138+
)
137139
)
138140
):
139141
if self.exchange_manager.trader is None:

octobot_trading/personal_data/orders/active_order_swap_strategies/active_order_swap_strategy.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,23 @@ class ActiveOrderSwapStrategy:
3535
def is_priority_order(self, order) -> bool:
3636
raise NotImplementedError("is_priority_order is not implemented")
3737

38-
async def apply_inactive_orders(self, orders: list):
38+
async def apply_inactive_orders(
39+
self, orders: list,
40+
trigger_above_by_order_id: typing.Optional[dict[str, bool]] = None
41+
):
3942
for order in orders:
43+
trigger_above = trigger_above_by_order_id.get(
44+
order.order_id, order.trigger_above
45+
) if trigger_above_by_order_id else order.trigger_above
4046
trigger_price = self._get_trigger_price(order)
4147
if self.is_priority_order(order):
4248
# still register active trigger in case this order becomes inactive
4349
order.update(
44-
active_trigger=order_util.create_order_price_trigger(order, trigger_price, order.trigger_above)
50+
active_trigger=order_util.create_order_price_trigger(order, trigger_price, trigger_above)
4551
)
4652
else:
4753
await order.set_as_inactive(
48-
order_util.create_order_price_trigger(order, trigger_price, order.trigger_above)
54+
order_util.create_order_price_trigger(order, trigger_price, trigger_above)
4955
)
5056

5157
def on_order_update(self, order, update_time):

tests/exchanges/test_exchange_builder.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,36 @@
3030
async def test_build_trading_modes_if_required(exchange_manager):
3131
builder = exchanges.ExchangeBuilder({}, "binanceus")
3232
builder.exchange_manager = exchange_manager
33+
tentacles_setup_config = exchange_manager.tentacles_setup_config
3334

3435
# no set trader: no trading mode creation attempt
3536
assert builder.exchange_manager.trader is None
36-
await builder._build_trading_modes_if_required(None)
37+
await builder._build_trading_modes_if_required(None, tentacles_setup_config)
3738
assert builder.exchange_manager.trader is None
3839

3940
# with trader simulator: will attempt to create a trading mode and fail (because of the None arg)
4041
builder.is_simulated()
42+
trading_mode_class = mock.Mock(
43+
get_is_using_trading_mode_on_exchange=mock.Mock(return_value=True),
44+
get_supported_exchange_types=mock.Mock(return_value=[]),
45+
return_value=mock.Mock(
46+
initialize=mock.AsyncMock(),
47+
),
48+
)
4149
with mock.patch.object(
4250
commons_authentication.Authenticator, "has_open_source_package", mock.Mock(return_value=False)
4351
) as has_open_source_package_mock:
4452
with pytest.raises(AttributeError):
45-
await builder._build_trading_modes_if_required(None)
53+
await builder._build_trading_modes_if_required(None, tentacles_setup_config)
54+
has_open_source_package_mock.assert_not_called()
55+
await builder._build_trading_modes_if_required(trading_mode_class, tentacles_setup_config)
56+
trading_mode_class.get_is_using_trading_mode_on_exchange.assert_called_once_with(builder.exchange_name, tentacles_setup_config)
57+
trading_mode_class.get_supported_exchange_types.assert_called_once()
58+
trading_mode_class.return_value.initialize.assert_awaited_once()
4659
has_open_source_package_mock.assert_called_once()
4760
# raised by default has_open_source_package (which should be overriden)
4861
with pytest.raises(NotImplementedError):
49-
await builder._build_trading_modes_if_required(None)
62+
await builder._build_trading_modes_if_required(trading_mode_class, tentacles_setup_config)
5063

5164

5265
async def test_build_collector_exchange(exchange_manager):

tests/personal_data/orders/active_order_swap_strategies/test_active_order_swap_strategy.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,80 @@ async def test_apply_inactive_orders(swap_strategy, simulated_trader):
6868
assert swap_strategy.is_priority_order.call_count == 3
6969
assert set_as_inactive_mock.call_count == 2
7070

71+
# Test with trigger_above_by_order_id parameter
72+
swap_strategy.is_priority_order = mock.Mock(side_effect=lambda o: o.order_type is enums.TraderOrderType.STOP_LOSS)
73+
with mock.patch.object(personal_data.Order, "set_as_inactive", mock.AsyncMock()) as set_as_inactive_mock, \
74+
mock.patch.object(personal_data.Order, "update", mock.Mock()) as update_mock:
75+
stop_loss = created_order(personal_data.StopLossLimitOrder, enums.TraderOrderType.STOP_LOSS,
76+
trader_instance, side=enums.TradeOrderSide.SELL)
77+
stop_loss.trigger_above = False
78+
stop_loss.order_id = "stop_loss_id"
79+
stop_loss.get_filling_price = mock.Mock(return_value=decimal.Decimal("100"))
80+
81+
sell_limit = created_order(personal_data.SellLimitOrder, enums.TraderOrderType.SELL_LIMIT,
82+
trader_instance, side=enums.TradeOrderSide.SELL)
83+
sell_limit.trigger_above = True
84+
sell_limit.order_id = "sell_limit_id"
85+
sell_limit.get_filling_price = mock.Mock(return_value=decimal.Decimal("200"))
86+
87+
buy_limit = created_order(personal_data.SellLimitOrder, enums.TraderOrderType.BUY_LIMIT,
88+
trader_instance, side=enums.TradeOrderSide.SELL)
89+
buy_limit.trigger_above = False
90+
buy_limit.order_id = "buy_limit_id"
91+
buy_limit.get_filling_price = mock.Mock(return_value=decimal.Decimal("300"))
92+
93+
# Test: trigger_above_by_order_id overrides order's trigger_above
94+
trigger_above_by_order_id = {
95+
"stop_loss_id": True, # Override False to True
96+
"sell_limit_id": False, # Override True to False
97+
# buy_limit_id not in dict, should use its own trigger_above (False)
98+
}
99+
await swap_strategy.apply_inactive_orders([stop_loss, sell_limit, buy_limit],
100+
trigger_above_by_order_id=trigger_above_by_order_id)
101+
102+
# Verify priority order (stop_loss) gets active_trigger with overridden trigger_above=True
103+
assert update_mock.call_count == 1
104+
update_call_kwargs = update_mock.call_args[1]
105+
assert "active_trigger" in update_call_kwargs
106+
active_trigger = update_call_kwargs["active_trigger"]
107+
assert active_trigger.trigger_above is True # Overridden from False to True
108+
109+
# Verify non-priority orders (sell_limit, buy_limit) get set_as_inactive with correct trigger_above
110+
assert set_as_inactive_mock.call_count == 2
111+
assert set_as_inactive_mock.call_args_list[0][0][0].trigger_above is False # sell_limit order's trigger_above is overridden from True to False
112+
assert set_as_inactive_mock.call_args_list[0][0][0].trigger_price == decimal.Decimal("200") # sell_limit order's trigger_price
113+
assert set_as_inactive_mock.call_args_list[1][0][0].trigger_above is False # buy limit order's trigger_above's origin value
114+
assert set_as_inactive_mock.call_args_list[1][0][0].trigger_price == decimal.Decimal("300") # buy_limit order's trigger_price
115+
116+
# Test: trigger_above_by_order_id is None, should use order's trigger_above
117+
swap_strategy.is_priority_order = mock.Mock(side_effect=lambda o: o.order_type is enums.TraderOrderType.STOP_LOSS)
118+
with mock.patch.object(personal_data.Order, "set_as_inactive", mock.AsyncMock()) as set_as_inactive_mock, \
119+
mock.patch.object(personal_data.Order, "update", mock.Mock()) as update_mock:
120+
stop_loss = created_order(personal_data.StopLossLimitOrder, enums.TraderOrderType.STOP_LOSS,
121+
trader_instance, side=enums.TradeOrderSide.SELL)
122+
stop_loss.trigger_above = True
123+
stop_loss.order_id = "stop_loss_id"
124+
stop_loss.get_filling_price = mock.Mock(return_value=decimal.Decimal("100"))
125+
126+
sell_limit = created_order(personal_data.SellLimitOrder, enums.TraderOrderType.SELL_LIMIT,
127+
trader_instance, side=enums.TradeOrderSide.SELL)
128+
sell_limit.trigger_above = False
129+
sell_limit.order_id = "sell_limit_id"
130+
sell_limit.get_filling_price = mock.Mock(return_value=decimal.Decimal("200"))
131+
132+
await swap_strategy.apply_inactive_orders([stop_loss, sell_limit],
133+
trigger_above_by_order_id=None)
134+
135+
# Verify priority order uses its own trigger_above=True
136+
assert update_mock.call_count == 1
137+
update_call_kwargs = update_mock.call_args[1]
138+
active_trigger = update_call_kwargs["active_trigger"]
139+
assert active_trigger.trigger_above is True # Uses order's own value
140+
141+
# Verify non-priority order uses its own trigger_above=False
142+
assert set_as_inactive_mock.call_count == 1
143+
assert sell_limit.trigger_above is False # Uses order's own value
144+
71145

72146

73147
async def test_execute_no_reverse(swap_strategy, simulated_trader):

0 commit comments

Comments
 (0)