Skip to content

Commit 31e0910

Browse files
committed
[PriceTrigger] synch on updated order price
1 parent 8527ac5 commit 31e0910

File tree

8 files changed

+171
-6
lines changed

8 files changed

+171
-6
lines changed

octobot_trading/personal_data/orders/active_order_swap_strategies/active_order_swap_strategy.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ async def apply_inactive_orders(self, orders: list):
4848
order_util.create_order_price_trigger(order, trigger_price, order.trigger_above)
4949
)
5050

51+
def on_order_update(self, order, update_time):
52+
if order.active_trigger:
53+
order.active_trigger.update(
54+
trigger_price=self._get_trigger_price(order), min_trigger_time=update_time,
55+
update_event=order.is_synchronization_enabled()
56+
)
57+
5158
def _get_trigger_price(self, order) -> decimal.Decimal:
5259
if self.trigger_price_configuration == enums.ActiveOrderSwapTriggerPriceConfiguration.FILLING_PRICE.value:
5360
return order.get_filling_price()

octobot_trading/personal_data/orders/order.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,9 @@ def update(
202202
if price and self.origin_price != price:
203203
previous_price = self.origin_price
204204
self.origin_price = price
205-
self._on_origin_price_change(previous_price,
206-
self.exchange_manager.exchange.get_exchange_current_time())
205+
self._on_origin_price_change(
206+
previous_price, self.exchange_manager.exchange.get_exchange_current_time()
207+
)
207208
changed = True
208209
should_update_total_cost = True
209210

@@ -342,6 +343,8 @@ def _on_origin_price_change(self, previous_price, price_time):
342343
:param previous_price: the previous origin_price
343344
:param price_time: time starting from when the price should be considered
344345
"""
346+
if self.order_group and self.order_group.active_order_swap_strategy:
347+
self.order_group.active_order_swap_strategy.on_order_update(self, price_time)
345348

346349
def add_chained_order(self, chained_order):
347350
"""

octobot_trading/personal_data/orders/triggers/base_trigger.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ def __init__(self, on_trigger_callback: typing.Callable, on_trigger_callback_arg
2525
self._trigger_event: asyncio.Event = None # will be set when the trigger is hit
2626
self._trigger_task: asyncio.Task = None
2727

28+
def triggers(self, *args) -> bool:
29+
raise NotImplementedError("triggers is not implemented")
30+
2831
def triggered(self) -> bool:
2932
return self._trigger_event is not None and self._trigger_event.is_set()
3033

@@ -34,6 +37,9 @@ def is_pending(self) -> bool:
3437
def update_from_other_trigger(self, other_trigger):
3538
raise NotImplementedError("update_from_other_trigger is not implemented")
3639

40+
def update(self, **kwargs):
41+
raise NotImplementedError("update is not implemented")
42+
3743
def __str__(self):
3844
return f"{self.__class__.__name__}({self.on_trigger_callback.__name__ if self.on_trigger_callback else None})"
3945

octobot_trading/personal_data/orders/triggers/price_trigger.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,33 @@ def update_from_other_trigger(self, other_trigger):
4343
self.trigger_price = other_trigger.trigger_price
4444
self.trigger_above = other_trigger.trigger_above
4545

46+
def update(self, trigger_price=None, min_trigger_time=None, update_event=True, **kwargs):
47+
if self.trigger_price != trigger_price:
48+
self.trigger_price = trigger_price
49+
if update_event and self._exchange_manager is not None:
50+
# replace event
51+
self._clear_event()
52+
self._create_event(min_trigger_time)
53+
4654
def clear(self):
4755
super().clear()
56+
self._clear_event()
57+
self._exchange_manager = None
58+
59+
def _create_event(self, min_trigger_time: float):
60+
self._trigger_event = self._exchange_manager.exchange_symbols_data.\
61+
get_exchange_symbol_data(self._symbol).price_events_manager.\
62+
new_event(self.trigger_price, min_trigger_time, self.trigger_above, False)
63+
64+
def _clear_event(self):
4865
if self._trigger_event is not None and self._exchange_manager is not None:
4966
self._exchange_manager.exchange_symbols_data. \
5067
get_exchange_symbol_data(self._symbol).price_events_manager.remove_event(self._trigger_event)
51-
self._exchange_manager = None
5268

5369
def __str__(self):
5470
return f"{super().__str__()}: trigger_price={self.trigger_price}, trigger_above={self.trigger_above}"
5571

5672
def _create_trigger_event(self, exchange_manager, symbol: str, min_trigger_time: float):
5773
self._exchange_manager = exchange_manager
5874
self._symbol = symbol
59-
self._trigger_event = self._exchange_manager.exchange_symbols_data.\
60-
get_exchange_symbol_data(self._symbol).price_events_manager.\
61-
new_event(self.trigger_price, min_trigger_time, self.trigger_above, False)
75+
self._create_event(min_trigger_time)

octobot_trading/personal_data/orders/types/limit/limit_order.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ async def update_order_status(self, force_refresh=False):
7575
self._create_hit_task()
7676

7777
def _on_origin_price_change(self, previous_price, price_time):
78+
super()._on_origin_price_change(previous_price, price_time)
7879
if previous_price is not constants.ZERO:
7980
# no need to reset events if previous price was 0 (unset)
8081
self._reset_events(price_time)

tests/personal_data/orders/active_order_swap_strategies/test_active_order_swap_strategy.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,43 @@ async def test_execute_with_reverse(swap_strategy, simulated_trader):
244244
assert stop_loss.is_closed()
245245
assert len(exchange_manager.exchange_personal_data.orders_manager.get_all_orders()) == 2
246246
assert len(exchange_manager.exchange_personal_data.orders_manager.get_open_orders()) == 2
247+
248+
249+
async def test_on_order_update(swap_strategy):
250+
# Setup
251+
order = mock.Mock()
252+
order.active_trigger = mock.Mock()
253+
order.is_synchronization_enabled = mock.Mock(return_value=True)
254+
order.get_filling_price = mock.Mock(return_value=decimal.Decimal("100"))
255+
update_time = 1234.56
256+
257+
# Test with default trigger price configuration (FILLING_PRICE)
258+
swap_strategy.on_order_update(order, update_time)
259+
260+
# Verify
261+
order.active_trigger.update.assert_called_once_with(
262+
trigger_price=decimal.Decimal("100"),
263+
min_trigger_time=update_time,
264+
update_event=True
265+
)
266+
267+
# Test with no active trigger
268+
order.active_trigger = None
269+
swap_strategy.on_order_update(order, update_time)
270+
# Should not raise any error when there's no active trigger
271+
272+
# Test with ORDER_PARAMS_ONLY configuration
273+
strategy = personal_data.ActiveOrderSwapStrategy(
274+
trigger_price_configuration=enums.ActiveOrderSwapTriggerPriceConfiguration.ORDER_PARAMS_ONLY.value
275+
)
276+
order.active_trigger = mock.Mock()
277+
order.active_trigger.trigger_price = decimal.Decimal("150")
278+
279+
strategy.on_order_update(order, update_time)
280+
281+
# Verify
282+
order.active_trigger.update.assert_called_once_with(
283+
trigger_price=decimal.Decimal("150"),
284+
min_trigger_time=update_time,
285+
update_event=True
286+
)

tests/personal_data/orders/triggers/test_price_trigger.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,93 @@ def test_update_from_other_trigger(price_trigger):
7777
# Original callback and args should remain unchanged
7878
assert price_trigger.on_trigger_callback_args == ("arg1", "arg2")
7979

80+
def test_update(price_trigger):
81+
# Setup
82+
callback = mock.Mock()
83+
84+
# Test update without exchange manager (no event creation)
85+
new_price = decimal.Decimal("150")
86+
price_trigger.update(trigger_price=new_price)
87+
assert price_trigger.trigger_price == new_price
88+
89+
# Setup exchange manager mock
90+
price_trigger._exchange_manager = mock.Mock()
91+
price_trigger._symbol = "BTC/USD"
92+
price_trigger._trigger_event = mock.Mock()
93+
94+
# Test update with exchange manager and event creation
95+
newer_price = decimal.Decimal("200")
96+
min_trigger_time = 1234.56
97+
98+
with mock.patch.object(price_trigger, '_create_event') as mock_create_event, \
99+
mock.patch.object(price_trigger, '_clear_event') as mock_clear_event:
100+
price_trigger.update(
101+
trigger_price=newer_price,
102+
min_trigger_time=min_trigger_time,
103+
update_event=True
104+
)
105+
106+
assert price_trigger.trigger_price == newer_price
107+
mock_clear_event.assert_called_once()
108+
mock_create_event.assert_called_once_with(min_trigger_time)
109+
110+
# Test update without event update
111+
newest_price = decimal.Decimal("250")
112+
with mock.patch.object(price_trigger, '_create_event') as mock_create_event, \
113+
mock.patch.object(price_trigger, '_clear_event') as mock_clear_event:
114+
price_trigger.update(
115+
trigger_price=newest_price,
116+
min_trigger_time=min_trigger_time,
117+
update_event=False
118+
)
119+
120+
assert price_trigger.trigger_price == newest_price
121+
mock_clear_event.assert_not_called()
122+
mock_create_event.assert_not_called()
123+
124+
# Test update with same price (should not trigger event updates)
125+
with mock.patch.object(price_trigger, '_create_event') as mock_create_event, \
126+
mock.patch.object(price_trigger, '_clear_event') as mock_clear_event:
127+
price_trigger.update(
128+
trigger_price=newest_price,
129+
min_trigger_time=min_trigger_time,
130+
update_event=True
131+
)
132+
133+
assert price_trigger.trigger_price == newest_price
134+
mock_clear_event.assert_not_called()
135+
mock_create_event.assert_not_called()
136+
137+
138+
def test_create_event(price_trigger):
139+
# Setup
140+
callback = mock.Mock()
141+
# Mock exchange manager and its components
142+
exchange_manager = mock.Mock()
143+
symbol_data = mock.Mock()
144+
price_events_manager = mock.Mock()
145+
146+
exchange_manager.exchange_symbols_data.get_exchange_symbol_data.return_value = symbol_data
147+
symbol_data.price_events_manager = price_events_manager
148+
149+
# Test event creation
150+
min_trigger_time = 1234.56
151+
price_trigger._exchange_manager = exchange_manager
152+
price_trigger._symbol = "BTC/USD"
153+
154+
price_trigger._create_event(min_trigger_time)
155+
156+
# Verify the event was created with correct parameters
157+
price_events_manager.new_event.assert_called_once_with(
158+
price_trigger.trigger_price,
159+
min_trigger_time,
160+
price_trigger.trigger_above,
161+
False
162+
)
163+
164+
# Verify the event was stored
165+
assert price_trigger._trigger_event == price_events_manager.new_event.return_value
166+
80167

81168
def test_str_representation(price_trigger):
82169
expected = (f"PriceTrigger({price_trigger.on_trigger_callback.__name__}): "

tests/personal_data/orders/types/limit/test_sell_limit_order.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from octobot_commons.asyncio_tools import wait_asyncio_next_cycle
2121
from octobot_trading.enums import TraderOrderType
2222
import octobot_trading.constants as trading_constants
23+
import octobot_trading.personal_data as personal_data
2324

2425
from tests import event_loop
2526
from tests.exchanges import simulated_trader, simulated_exchange_manager
@@ -65,11 +66,14 @@ async def test_sell_limit_order_trigger(sell_limit_order):
6566

6667
async def test_sell_limit_order_on_origin_price_change(sell_limit_order):
6768
order_price = decimal.Decimal(100)
69+
group = personal_data.OneCancelsTheOtherOrderGroup("name", sell_limit_order.exchange_manager.exchange_personal_data.orders_manager)
6870
sell_limit_order.update(
6971
price=order_price,
7072
quantity=decimal_random_quantity(max_value=DEFAULT_SYMBOL_QUANTITY),
7173
symbol=DEFAULT_ORDER_SYMBOL,
7274
order_type=TraderOrderType.SELL_LIMIT,
75+
group=group,
76+
active_trigger=personal_data.create_order_price_trigger(sell_limit_order, order_price, True)
7377
)
7478
sell_limit_order.exchange_manager.is_backtesting = True # force update_order_status
7579
await sell_limit_order.initialize()
@@ -83,13 +87,15 @@ async def test_sell_limit_order_on_origin_price_change(sell_limit_order):
8387
timestamp=sell_limit_order.timestamp)])
8488
await wait_asyncio_next_cycle()
8589
assert not sell_limit_order.is_filled()
90+
assert sell_limit_order.active_trigger.trigger_price == decimal.Decimal(100)
8691

8792
# do not update order price
8893
order_price = decimal.Decimal(10)
8994
sell_limit_order.update(
9095
symbol=sell_limit_order.symbol,
9196
quantity=decimal_random_quantity(max_value=DEFAULT_SYMBOL_QUANTITY)
9297
)
98+
assert sell_limit_order.active_trigger.trigger_price == decimal.Decimal(100) # did not change
9399
price_events_manager.handle_recent_trades(
94100
[decimal_random_recent_trade(price=decimal_random_price(max_value=order_price - trading_constants.ONE),
95101
timestamp=sell_limit_order.timestamp)])
@@ -103,6 +109,7 @@ async def test_sell_limit_order_on_origin_price_change(sell_limit_order):
103109
symbol=sell_limit_order.symbol,
104110
price=order_price
105111
)
112+
assert sell_limit_order.active_trigger.trigger_price == decimal.Decimal(10) # changed
106113
price_events_manager.handle_recent_trades([
107114
decimal_random_recent_trade(
108115
price=decimal.Decimal(20),

0 commit comments

Comments
 (0)