Skip to content

Commit f773a3c

Browse files
committed
[Orders] extract active trigger in trigger class
1 parent 2a0f2cf commit f773a3c

19 files changed

+260
-138
lines changed

octobot_trading/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,3 +637,4 @@ class TradingModeActivityType(enum.Enum):
637637

638638
class ActiveOrderSwapTriggerPriceConfiguration(enum.Enum):
639639
FILLING_PRICE = "filling_price"
640+
ORDER_PARAMS_ONLY = "order_params_only"

octobot_trading/exchanges/traders/trader.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -318,8 +318,10 @@ async def _create_new_order(
318318
updated_order.associated_entry_ids = new_order.associated_entry_ids
319319
updated_order.update_with_triggering_order_fees = new_order.update_with_triggering_order_fees
320320
updated_order.trailing_profile = new_order.trailing_profile
321-
updated_order.active_trigger_price = new_order.active_trigger_price
322-
updated_order.active_trigger_above = new_order.active_trigger_above
321+
if new_order.active_trigger is not None:
322+
updated_order.create_or_update_active_trigger(
323+
new_order.active_trigger.trigger_price, new_order.active_trigger.trigger_above
324+
)
323325
updated_order.is_in_active_inactive_transition = new_order.is_in_active_inactive_transition
324326

325327
if is_pending_creation:
@@ -405,8 +407,8 @@ async def update_order_as_inactive(
405407
self, order, ignored_order=None, wait_for_cancelling=True,
406408
cancelling_timeout=octobot_trading.constants.INDIVIDUAL_ORDER_SYNC_TIMEOUT
407409
) -> bool:
408-
if self.simulate:
409-
self.logger.error(f"Can't update order as inactive on simulated trading.")
410+
if not self.enable_inactive_orders:
411+
self.logger.error(f"Can't update order as inactive when {self.enable_inactive_orders=}.")
410412
return False
411413
cancelled = False
412414
if order and order.is_open():
@@ -422,8 +424,8 @@ async def update_order_as_active(
422424
self, order, params: dict = None, wait_for_creation=True, raise_all_creation_error=False,
423425
creation_timeout=octobot_trading.constants.INDIVIDUAL_ORDER_SYNC_TIMEOUT
424426
):
425-
if self.simulate:
426-
self.logger.error(f"Can't update order as active on simulated trading.")
427+
if not self.enable_inactive_orders:
428+
self.logger.error(f"Can't update order as active when {self.enable_inactive_orders=}.")
427429
return order
428430
with order.active_or_inactive_transition():
429431
return await self.create_order(

octobot_trading/personal_data/exchange_personal_data.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ async def check_and_update_inactive_orders_when_necessary(
299299
self, symbol: str, current_price: decimal.Decimal, price_time: float,
300300
strategy_timeout: typing.Optional[float], wait_for_fill_callback: typing.Optional[typing.Callable]
301301
):
302-
for order in self.orders_manager.get_inactive_orders(symbol=symbol):
302+
for order in self.orders_manager.get_all_orders(symbol=symbol, active=False):
303303
if order.should_become_active(price_time, current_price):
304304
try:
305305
await order.on_active_trigger(strategy_timeout, wait_for_fill_callback)

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
@@ -47,6 +47,13 @@ async def apply_inactive_orders(self, orders: list):
4747
def _get_trigger_price(self, order) -> decimal.Decimal:
4848
if self.trigger_price_configuration == enums.ActiveOrderSwapTriggerPriceConfiguration.FILLING_PRICE.value:
4949
return order.get_filling_price()
50+
if self.trigger_price_configuration == enums.ActiveOrderSwapTriggerPriceConfiguration.ORDER_PARAMS_ONLY.value:
51+
if order.active_trigger is None or order.active_trigger.trigger_price is None:
52+
raise ValueError(
53+
f"order.active_trigger.trigger_price must be set when using "
54+
f"ActiveOrderSwapTriggerPriceConfiguration.ORDER_PARAMS_ONLY. Order: {order}"
55+
)
56+
return order.active_trigger.trigger_price
5057
raise ValueError(f"Unknown trigger price configuration: {self.trigger_price_configuration}")
5158

5259
async def execute(self, inactive_order, wait_for_fill_callback: typing.Optional[typing.Callable]):

octobot_trading/personal_data/orders/channel/orders.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,9 @@ async def _check_missing_open_orders(self, symbol, orders):
234234
set(
235235
order.exchange_order_id for order in
236236
self.channel.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(
237-
symbol
237+
symbol, active=True
238238
) + self.channel.exchange_manager.exchange_personal_data.orders_manager.get_pending_cancel_orders(
239-
symbol
239+
symbol, active=True
240240
)
241241
if not (order.is_cleared() or order.is_self_managed())) -
242242
set(

octobot_trading/personal_data/orders/order.py

Lines changed: 36 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import octobot_trading.personal_data.orders.order_util as order_util
2929
import octobot_trading.personal_data.orders.trailing_profiles as trailing_profiles
3030
import octobot_trading.personal_data.orders.decimal_order_adapter as decimal_order_adapter
31+
import octobot_trading.personal_data.orders.triggers.price_trigger as price_trigger
3132
import octobot_trading.util as util
3233

3334

@@ -97,14 +98,9 @@ def __init__(self, trader, side=None):
9798

9899
# order activity
99100
self.is_active = True # When is_active=False order is not pushed to exchanges
100-
self.active_trigger_price: decimal.Decimal = None # price threshold from which the order becomes active
101-
# when True, order becomes active when current price >= active_trigger_price
102-
self.active_trigger_above: bool = None
103-
self._active_trigger_event: asyncio.Event = None # will be set when the price is hit
104-
# waiter that will call on_active_trigger() when active_trigger_event is set
105-
self._active_trigger_task: asyncio.Task = None
106101
# True when a transition between active and inactive is being made
107102
self.is_in_active_inactive_transition = False
103+
self.active_trigger: typing.Optional[price_trigger.PriceTrigger] = None
108104

109105
# future trading attributes
110106
# when True: reduce position quantity only without opening a new position if order.quantity > position.quantity
@@ -304,13 +300,17 @@ def update(
304300
changed = True
305301
self.is_active = is_active
306302

307-
if active_trigger_price is not None and self.active_trigger_price != active_trigger_price:
303+
if (active_trigger_price is not None or active_trigger_above is not None) and self.active_trigger is None:
308304
changed = True
309-
self.active_trigger_price = active_trigger_price
305+
self.create_or_update_active_trigger(active_trigger_price, active_trigger_above)
310306

311-
if active_trigger_above is not None and self.active_trigger_above != active_trigger_above:
307+
if active_trigger_price is not None and self.active_trigger.trigger_price != active_trigger_price:
312308
changed = True
313-
self.active_trigger_above = active_trigger_above
309+
self.active_trigger.trigger_price = active_trigger_price
310+
311+
if active_trigger_above is not None and self.active_trigger.trigger_above != active_trigger_above:
312+
changed = True
313+
self.active_trigger.trigger_above = active_trigger_above
314314

315315
if should_update_total_cost and not total_cost:
316316
self._update_total_cost()
@@ -435,6 +435,15 @@ def active_or_inactive_transition(self):
435435
finally:
436436
self.is_in_active_inactive_transition = previous_value
437437

438+
def create_or_update_active_trigger(self, active_trigger_price: decimal.Decimal, active_trigger_above: bool):
439+
if self.active_trigger is None:
440+
self.active_trigger = price_trigger.PriceTrigger(
441+
self.on_active_trigger, (None, None), active_trigger_price, active_trigger_above
442+
)
443+
else:
444+
self.active_trigger.trigger_price = active_trigger_price
445+
self.active_trigger.trigger_above = active_trigger_above
446+
438447
async def set_as_inactive(self, active_trigger_price: decimal.Decimal, active_trigger_above: bool):
439448
"""
440449
Marks the instance as inactive and ensures the inactive order watcher is scheduled.
@@ -445,8 +454,7 @@ async def set_as_inactive(self, active_trigger_price: decimal.Decimal, active_tr
445454
)
446455
logging.get_logger(self.get_logger_name()).info("Order is switching to inactive")
447456
self.is_active = False
448-
self.active_trigger_price = active_trigger_price
449-
self.active_trigger_above = active_trigger_above
457+
self.create_or_update_active_trigger(active_trigger_price, active_trigger_above)
450458
# enforce attributes in case order has been canceled
451459
self.status = enums.OrderStatus.OPEN
452460
self.canceled_time = 0
@@ -456,12 +464,7 @@ def should_become_active(self, price_time: float, current_price: decimal.Decimal
456464
if self.is_active:
457465
return False
458466
if price_time >= self.creation_time:
459-
return (
460-
(self.active_trigger_above and current_price >= self.active_trigger_price)
461-
or (
462-
not self.active_trigger_above and current_price <= self.active_trigger_price
463-
)
464-
)
467+
return self.active_trigger.triggers(current_price)
465468
return False
466469

467470
async def _ensure_inactive_order_watcher(self):
@@ -474,40 +477,10 @@ async def _ensure_inactive_order_watcher(self):
474477
f"Unexpected inactive order (simulated={self.simulated} self_managed={self.is_self_managed()}): {self}"
475478
)
476479
return
477-
await self._create_active_trigger_watcher()
478-
479-
async def _create_active_trigger_watcher(self):
480-
# ensure active triggers are ready
481-
if self._active_trigger_event is None:
482-
self._create_active_trigger_event(self.creation_time)
483-
else:
484-
self._active_trigger_event.clear()
485-
if self._active_trigger_task is None or self._active_trigger_task.done():
486-
if self._active_trigger_event.is_set():
487-
await self.on_active_trigger(None, None)
488-
else:
489-
self._create_active_trigger_task()
490-
491-
def _create_active_trigger_event(self, price_time):
492-
self._active_trigger_event = self.exchange_manager.exchange_symbols_data.\
493-
get_exchange_symbol_data(self.symbol).price_events_manager.\
494-
new_event(self.active_trigger_price, price_time, self.active_trigger_above, False)
495-
496-
async def _wait_for_active_trigger_set(self):
497-
await asyncio.wait_for(self._active_trigger_event.wait(), timeout=None)
498-
await self.on_active_trigger(None, None)
499-
500-
def _create_active_trigger_task(self):
501-
self._active_trigger_task = asyncio.create_task(self._wait_for_active_trigger_set())
502-
503-
def _clear_active_trigger_event_and_tasks(self):
504-
if self._active_trigger_task is not None:
505-
if not self._active_trigger_event.is_set():
506-
self._active_trigger_task.cancel()
507-
self._active_trigger_task = None
508-
if self._active_trigger_event is not None:
509-
self.exchange_manager.exchange_symbols_data. \
510-
get_exchange_symbol_data(self.symbol).price_events_manager.remove_event(self._active_trigger_event)
480+
if self.active_trigger is None:
481+
logging.get_logger(self.get_logger_name()).error("self.active_trigger is None")
482+
return
483+
await self.active_trigger.create_watcher(self.exchange_manager, self.symbol, self.creation_time)
511484

512485
@contextlib.contextmanager
513486
def order_state_creation(self):
@@ -520,7 +493,11 @@ async def on_inactive_from_active(self):
520493
"""
521494
Update the order to be considered as "confirmed" inactive. Called when the order was active before
522495
"""
523-
await self.set_as_inactive(self.active_trigger_price, self.active_trigger_above)
496+
if self.active_trigger is None:
497+
raise ValueError(
498+
f"Both active_trigger_price and active_trigger_above must be provided to set an order as inactive"
499+
)
500+
await self.set_as_inactive(self.active_trigger.trigger_price, self.active_trigger.trigger_above)
524501
self.clear_active_order_elements()
525502

526503
async def on_active_from_inactive(self):
@@ -941,13 +918,13 @@ def update_from_storage_order_details(self, order_details):
941918
order_dict[enums.ExchangeConstantsOrderColumns.TAKER_OR_MAKER.value]
942919
).value if order_dict.get(enums.ExchangeConstantsOrderColumns.TAKER_OR_MAKER.value) else self.taker_or_maker
943920
self.is_active = order_dict.get(enums.ExchangeConstantsOrderColumns.IS_ACTIVE.value, self.is_active)
944-
self.active_trigger_price = (
921+
active_trigger_price = (
945922
decimal.Decimal(str(order_dict[enums.ExchangeConstantsOrderColumns.ACTIVE_TRIGGER_PRICE.value]))
946923
if order_dict.get(enums.ExchangeConstantsOrderColumns.ACTIVE_TRIGGER_PRICE.value) else None
947924
)
948-
self.active_trigger_above = order_dict.get(
949-
enums.ExchangeConstantsOrderColumns.ACTIVE_TRIGGER_ABOVE.value, self.active_trigger_above
950-
)
925+
active_trigger_above = order_dict.get(enums.ExchangeConstantsOrderColumns.ACTIVE_TRIGGER_ABOVE.value)
926+
if active_trigger_price is not None:
927+
self.create_or_update_active_trigger(active_trigger_price, active_trigger_above)
951928
self.trader_creation_kwargs = order_details.get(enums.StoredOrdersAttr.TRADER_CREATION_KWARGS.value,
952929
self.trader_creation_kwargs)
953930
self.exchange_creation_params = order_details.get(enums.StoredOrdersAttr.EXCHANGE_CREATION_PARAMS.value,
@@ -1080,7 +1057,8 @@ def clear_active_order_elements(self):
10801057

10811058

10821059
def clear(self):
1083-
self._clear_active_trigger_event_and_tasks()
1060+
if self.active_trigger:
1061+
self.active_trigger.clear()
10841062
self.clear_active_order_elements()
10851063
self.trader = None
10861064
self.exchange_manager = None

octobot_trading/personal_data/orders/order_group.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def _default_active_order_swap_strategy(self, timeout: float) -> active_order_sw
7171
"""
7272
Called when an order of this group is becoming active
7373
"""
74+
raise NotImplementedError("_default_active_order_swap_strategy is not implemented")
7475

7576

7677
async def enable(self, enabled):

octobot_trading/personal_data/orders/orders_manager.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,26 +53,28 @@ async def initialize_impl(self):
5353

5454
def get_all_orders(
5555
self, symbol=None, since=constants.NO_DATA_LIMIT,
56-
until=constants.NO_DATA_LIMIT, limit=constants.NO_DATA_LIMIT, tag=None):
56+
until=constants.NO_DATA_LIMIT, limit=constants.NO_DATA_LIMIT,
57+
tag=None, active=None):
5758
return self._select_orders(
5859
None, symbol=symbol, since=since,
59-
until=until, limit=limit, tag=tag
60+
until=until, limit=limit, tag=tag, active=active
6061
)
6162

6263
def get_open_orders(
6364
self, symbol=None, since=constants.NO_DATA_LIMIT, until=constants.NO_DATA_LIMIT,
64-
limit=constants.NO_DATA_LIMIT, tag=None):
65+
limit=constants.NO_DATA_LIMIT, tag=None, active=None
66+
):
6567
return self._select_orders(
6668
enums.OrderStatus.OPEN, symbol, since=since,
67-
until=until, limit=limit, tag=tag
69+
until=until, limit=limit, tag=tag, active=active
6870
)
6971

7072
def get_pending_cancel_orders(
7173
self, symbol=None, since=constants.NO_DATA_LIMIT, until=constants.NO_DATA_LIMIT,
72-
limit=constants.NO_DATA_LIMIT, tag=None):
74+
limit=constants.NO_DATA_LIMIT, tag=None, active=None):
7375
return self._select_orders(
7476
enums.OrderStatus.PENDING_CANCEL, symbol, since=since,
75-
until=until, limit=limit, tag=tag
77+
until=until, limit=limit, tag=tag, active=active
7678
)
7779

7880
def get_closed_orders(
@@ -184,13 +186,6 @@ def get_all_active_and_pending_orders_id(self) -> list:
184186
for order in self.pending_creation_orders
185187
]
186188

187-
def get_inactive_orders(self, symbol=None) -> list:
188-
return [
189-
order
190-
for order in self._select_orders(symbol=symbol)
191-
if not order.is_active
192-
]
193-
194189
async def upsert_order_close_from_raw(self, exchange_order_id, raw_order) -> typing.Optional[order_class.Order]:
195190
if self.has_order(None, exchange_order_id=exchange_order_id):
196191
order = self.get_order(None, exchange_order_id=exchange_order_id)
@@ -266,7 +261,9 @@ def _check_orders_size(self):
266261

267262
def _select_orders(
268263
self, state=None, symbol=None, since=constants.NO_DATA_LIMIT,
269-
until=constants.NO_DATA_LIMIT, limit=constants.NO_DATA_LIMIT, tag=None):
264+
until=constants.NO_DATA_LIMIT, limit=constants.NO_DATA_LIMIT,
265+
tag=None, active=None
266+
):
270267
orders = [
271268
order
272269
for order in self.orders.values()
@@ -275,7 +272,8 @@ def _select_orders(
275272
(symbol is None or (symbol and order.symbol == symbol)) and
276273
(since == constants.NO_DATA_LIMIT or (since and order.timestamp >= since)) and
277274
(until == constants.NO_DATA_LIMIT or (until and order.timestamp <= until)) and
278-
(tag is None or order.tag == tag)
275+
(tag is None or order.tag == tag) and
276+
(active is None or order.is_active == active)
279277
)
280278
]
281279
return orders if limit == constants.NO_DATA_LIMIT else orders[0:limit]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Drakkar-Software OctoBot-Trading
2+
# Copyright (c) Drakkar-Software, All rights reserved.
3+
#
4+
# This library is free software; you can redistribute it and/or
5+
# modify it under the terms of the GNU Lesser General Public
6+
# License as published by the Free Software Foundation; either
7+
# version 3.0 of the License, or (at your option) any later version.
8+
#
9+
# This library is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
# Lesser General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU Lesser General Public
15+
# License along with this library.
16+
17+
18+
from octobot_trading.personal_data.orders.triggers import base_trigger
19+
from octobot_trading.personal_data.orders.triggers.base_trigger import (
20+
BaseTrigger,
21+
)
22+
23+
24+
from octobot_trading.personal_data.orders.triggers import price_trigger
25+
from octobot_trading.personal_data.orders.triggers.price_trigger import (
26+
PriceTrigger,
27+
)
28+
29+
__all__ = [
30+
"BaseTrigger",
31+
"PriceTrigger",
32+
]

0 commit comments

Comments
 (0)