Skip to content

Commit 4e5a8ab

Browse files
committed
[Orders] extract active trigger in trigger class
1 parent 4cf5d0e commit 4e5a8ab

28 files changed

+613
-202
lines changed

octobot_trading/enums.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,6 @@ class ExchangeConstantsOrderColumns(enum.Enum):
335335
VOLUME = "volume"
336336
BROKER_APPLIED = "broker_applied"
337337
IS_ACTIVE = "is_active"
338-
ACTIVE_TRIGGER_PRICE = "active_trigger_price"
339-
ACTIVE_TRIGGER_ABOVE = "active_trigger_above"
340338

341339

342340
class TradeExtraConstants(enum.Enum):
@@ -601,6 +599,9 @@ class StoredOrdersAttr(enum.Enum):
601599
STRATEGY_TIMEOUT = "sti"
602600
STRATEGY_TRIGGER_CONFIG = "stc"
603601
CHAINED_ORDERS = "co"
602+
ACTIVE_TRIGGER = "at"
603+
ACTIVE_TRIGGER_PRICE = "atp"
604+
ACTIVE_TRIGGER_ABOVE = "ata"
604605
TRAILING_PROFILE = "tp"
605606
TRAILING_PROFILE_TYPE = "tpt"
606607
TRAILING_PROFILE_DETAILS = "tpd"
@@ -637,3 +638,4 @@ class TradingModeActivityType(enum.Enum):
637638

638639
class ActiveOrderSwapTriggerPriceConfiguration(enum.Enum):
639640
FILLING_PRICE = "filling_price"
641+
ORDER_PARAMS_ONLY = "order_params_only"

octobot_trading/exchanges/traders/trader.py

+8-6
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.use_active_trigger(order_util.create_order_price_trigger(
323+
updated_order, 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/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
is_associated_pending_order,
5959
apply_pending_order_from_created_order,
6060
get_up_to_date_price,
61+
create_order_price_trigger,
6162
create_as_active_order_using_strategy_if_any,
6263
create_as_active_order_on_exchange,
6364
update_order_as_inactive_on_exchange,
@@ -79,6 +80,8 @@
7980
create_filled_take_profit_trailing_profile,
8081
ActiveOrderSwapStrategy,
8182
StopFirstActiveOrderSwapStrategy,
83+
BaseTrigger,
84+
PriceTrigger,
8285
OrdersUpdater,
8386
adapt_price,
8487
get_minimal_order_amount,
@@ -301,6 +304,7 @@
301304
"is_associated_pending_order",
302305
"apply_pending_order_from_created_order",
303306
"get_up_to_date_price",
307+
"create_order_price_trigger",
304308
"create_as_active_order_using_strategy_if_any",
305309
"create_as_active_order_on_exchange",
306310
"update_order_as_inactive_on_exchange",
@@ -322,6 +326,8 @@
322326
"create_filled_take_profit_trailing_profile",
323327
"ActiveOrderSwapStrategy",
324328
"StopFirstActiveOrderSwapStrategy",
329+
"BaseTrigger",
330+
"PriceTrigger",
325331
"OrdersUpdater",
326332
"adapt_price",
327333
"get_minimal_order_amount",

octobot_trading/personal_data/exchange_personal_data.py

+1-1
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/__init__.py

+9
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@
4444
ActiveOrderSwapStrategy,
4545
StopFirstActiveOrderSwapStrategy,
4646
)
47+
from octobot_trading.personal_data.orders import triggers
48+
from octobot_trading.personal_data.orders.triggers import (
49+
BaseTrigger,
50+
PriceTrigger,
51+
)
4752
from octobot_trading.personal_data.orders import order
4853
from octobot_trading.personal_data.orders.order import (
4954
Order,
@@ -115,6 +120,7 @@
115120
is_stop_trade_order_type,
116121
is_take_profit_order,
117122
get_trade_order_type,
123+
create_order_price_trigger,
118124
create_as_active_order_using_strategy_if_any,
119125
create_as_active_order_on_exchange,
120126
update_order_as_inactive_on_exchange,
@@ -188,6 +194,7 @@
188194
"parse_is_pending_cancel",
189195
"parse_is_open",
190196
"get_up_to_date_price",
197+
"create_order_price_trigger",
191198
"create_as_active_order_using_strategy_if_any",
192199
"create_as_active_order_on_exchange",
193200
"update_order_as_inactive_on_exchange",
@@ -222,6 +229,8 @@
222229
"create_filled_take_profit_trailing_profile",
223230
"ActiveOrderSwapStrategy",
224231
"StopFirstActiveOrderSwapStrategy",
232+
"BaseTrigger",
233+
"PriceTrigger",
225234
"OrdersUpdater",
226235
"adapt_price",
227236
"get_minimal_order_amount",

octobot_trading/personal_data/orders/active_order_swap_strategies/active_order_swap_strategy.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,25 @@ async def apply_inactive_orders(self, orders: list):
3939
for order in orders:
4040
trigger_price = self._get_trigger_price(order)
4141
if self.is_priority_order(order):
42-
# still register active trigger price and trigger above in case this order becomes inactive
43-
order.update(active_trigger_price=trigger_price, active_trigger_above=order.trigger_above)
42+
# still register active trigger in case this order becomes inactive
43+
order.update(
44+
active_trigger=order_util.create_order_price_trigger(order, trigger_price, order.trigger_above)
45+
)
4446
else:
45-
await order.set_as_inactive(trigger_price, order.trigger_above)
47+
await order.set_as_inactive(
48+
order_util.create_order_price_trigger(order, trigger_price, order.trigger_above)
49+
)
4650

4751
def _get_trigger_price(self, order) -> decimal.Decimal:
4852
if self.trigger_price_configuration == enums.ActiveOrderSwapTriggerPriceConfiguration.FILLING_PRICE.value:
4953
return order.get_filling_price()
54+
if self.trigger_price_configuration == enums.ActiveOrderSwapTriggerPriceConfiguration.ORDER_PARAMS_ONLY.value:
55+
if order.active_trigger is None or order.active_trigger.trigger_price is None:
56+
raise ValueError(
57+
f"order.active_trigger.trigger_price must be set when using "
58+
f"ActiveOrderSwapTriggerPriceConfiguration.ORDER_PARAMS_ONLY. Order: {order}"
59+
)
60+
return order.active_trigger.trigger_price
5061
raise ValueError(f"Unknown trigger price configuration: {self.trigger_price_configuration}")
5162

5263
async def execute(self, inactive_order, wait_for_fill_callback: typing.Optional[typing.Callable]):
@@ -57,6 +68,8 @@ async def execute(self, inactive_order, wait_for_fill_callback: typing.Optional[
5768
active_order, now_maybe_partially_inactive_orders, reverse_update_callback = (
5869
await self._update_group_and_activate_order(inactive_order)
5970
)
71+
if active_order is None:
72+
raise ValueError("No active order was created")
6073
if not any(self.is_priority_order(inactive_order) for inactive_order in now_maybe_partially_inactive_orders):
6174
# nothing else to do: no priority order has been deactivated
6275
return

octobot_trading/personal_data/orders/channel/orders.py

+2-2
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

+49-69
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.base_trigger as base_trigger_import
3132
import octobot_trading.util as util
3233

3334

@@ -97,14 +98,10 @@ 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+
# active_trigger is used for active/inactive switch trigger mechanism, it stores relevant data.
104+
self.active_trigger: typing.Optional[base_trigger_import.BaseTrigger] = None
108105

109106
# future trading attributes
110107
# when True: reduce position quantity only without opening a new position if order.quantity > position.quantity
@@ -154,7 +151,7 @@ def update(
154151
order_type=None, reduce_only=None, close_position=None, position_side=None, fees_currency_side=None,
155152
group=None, tag=None, quantity_currency=None, exchange_creation_params=None,
156153
associated_entry_id=None, trigger_above=None, trailing_profile: trailing_profiles.TrailingProfile=None,
157-
is_active=None, active_trigger_price=None, active_trigger_above=None
154+
is_active=None, active_trigger: base_trigger_import.BaseTrigger = None,
158155
) -> bool:
159156
changed: bool = False
160157
should_update_total_cost = False
@@ -304,13 +301,9 @@ def update(
304301
changed = True
305302
self.is_active = is_active
306303

307-
if active_trigger_price is not None and self.active_trigger_price != active_trigger_price:
304+
if active_trigger is not None and active_trigger != self.active_trigger:
308305
changed = True
309-
self.active_trigger_price = active_trigger_price
310-
311-
if active_trigger_above is not None and self.active_trigger_above != active_trigger_above:
312-
changed = True
313-
self.active_trigger_above = active_trigger_above
306+
self.use_active_trigger(active_trigger)
314307

315308
if should_update_total_cost and not total_cost:
316309
self._update_total_cost()
@@ -435,18 +428,28 @@ def active_or_inactive_transition(self):
435428
finally:
436429
self.is_in_active_inactive_transition = previous_value
437430

438-
async def set_as_inactive(self, active_trigger_price: decimal.Decimal, active_trigger_above: bool):
431+
def use_active_trigger(self, active_trigger: base_trigger_import.BaseTrigger):
432+
if active_trigger is None:
433+
raise ValueError("active_trigger must be provided")
434+
if self.active_trigger is None:
435+
self.active_trigger = active_trigger
436+
elif self.active_trigger.is_pending():
437+
logging.get_logger(self.get_logger_name()).error(
438+
f"The current active trigger ({str(self.active_trigger)}) is still pending, canceling it "
439+
f"and replacing it by this new one, this is works but is unexpected."
440+
)
441+
self.active_trigger.clear()
442+
self.active_trigger = active_trigger
443+
else:
444+
self.active_trigger.update_from_other_trigger(active_trigger)
445+
446+
async def set_as_inactive(self, active_trigger: base_trigger_import.BaseTrigger):
439447
"""
440448
Marks the instance as inactive and ensures the inactive order watcher is scheduled.
441449
"""
442-
if active_trigger_price is None or active_trigger_above is None:
443-
raise ValueError(
444-
f"Both active_trigger_price and active_trigger_above must be provided to set an order as inactive"
445-
)
446450
logging.get_logger(self.get_logger_name()).info("Order is switching to inactive")
451+
self.use_active_trigger(active_trigger)
447452
self.is_active = False
448-
self.active_trigger_price = active_trigger_price
449-
self.active_trigger_above = active_trigger_above
450453
# enforce attributes in case order has been canceled
451454
self.status = enums.OrderStatus.OPEN
452455
self.canceled_time = 0
@@ -456,12 +459,7 @@ def should_become_active(self, price_time: float, current_price: decimal.Decimal
456459
if self.is_active:
457460
return False
458461
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-
)
462+
return self.active_trigger.triggers(current_price)
465463
return False
466464

467465
async def _ensure_inactive_order_watcher(self):
@@ -474,40 +472,10 @@ async def _ensure_inactive_order_watcher(self):
474472
f"Unexpected inactive order (simulated={self.simulated} self_managed={self.is_self_managed()}): {self}"
475473
)
476474
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)
475+
if self.active_trigger is None:
476+
logging.get_logger(self.get_logger_name()).error("self.active_trigger is None")
477+
return
478+
await self.active_trigger.create_watcher(self.exchange_manager, self.symbol, self.creation_time)
511479

512480
@contextlib.contextmanager
513481
def order_state_creation(self):
@@ -520,7 +488,11 @@ async def on_inactive_from_active(self):
520488
"""
521489
Update the order to be considered as "confirmed" inactive. Called when the order was active before
522490
"""
523-
await self.set_as_inactive(self.active_trigger_price, self.active_trigger_above)
491+
if self.active_trigger is None:
492+
raise ValueError(
493+
f"self.active_trigger must be provided to set an order as inactive"
494+
)
495+
await self.set_as_inactive(self.active_trigger)
524496
self.clear_active_order_elements()
525497

526498
async def on_active_from_inactive(self):
@@ -941,13 +913,20 @@ def update_from_storage_order_details(self, order_details):
941913
order_dict[enums.ExchangeConstantsOrderColumns.TAKER_OR_MAKER.value]
942914
).value if order_dict.get(enums.ExchangeConstantsOrderColumns.TAKER_OR_MAKER.value) else self.taker_or_maker
943915
self.is_active = order_dict.get(enums.ExchangeConstantsOrderColumns.IS_ACTIVE.value, self.is_active)
944-
self.active_trigger_price = (
945-
decimal.Decimal(str(order_dict[enums.ExchangeConstantsOrderColumns.ACTIVE_TRIGGER_PRICE.value]))
946-
if order_dict.get(enums.ExchangeConstantsOrderColumns.ACTIVE_TRIGGER_PRICE.value) else None
947-
)
948-
self.active_trigger_above = order_dict.get(
949-
enums.ExchangeConstantsOrderColumns.ACTIVE_TRIGGER_ABOVE.value, self.active_trigger_above
950-
)
916+
if active_trigger := order_details.get(enums.StoredOrdersAttr.ACTIVE_TRIGGER.value):
917+
active_trigger_price = (
918+
decimal.Decimal(str(active_trigger[enums.StoredOrdersAttr.ACTIVE_TRIGGER_PRICE.value]))
919+
if active_trigger.get(enums.StoredOrdersAttr.ACTIVE_TRIGGER_PRICE.value) else None
920+
)
921+
active_trigger_above = active_trigger.get(enums.StoredOrdersAttr.ACTIVE_TRIGGER_ABOVE.value)
922+
if active_trigger_price is not None and active_trigger_above is not None:
923+
self.use_active_trigger(
924+
order_util.create_order_price_trigger(self, active_trigger_price, active_trigger_above)
925+
)
926+
else:
927+
logging.get_logger(self.__class__.__name__).error(
928+
f"Ignored unknown trigger configuration: {active_trigger}"
929+
)
951930
self.trader_creation_kwargs = order_details.get(enums.StoredOrdersAttr.TRADER_CREATION_KWARGS.value,
952931
self.trader_creation_kwargs)
953932
self.exchange_creation_params = order_details.get(enums.StoredOrdersAttr.EXCHANGE_CREATION_PARAMS.value,
@@ -1080,7 +1059,8 @@ def clear_active_order_elements(self):
10801059

10811060

10821061
def clear(self):
1083-
self._clear_active_trigger_event_and_tasks()
1062+
if self.active_trigger:
1063+
self.active_trigger.clear()
10841064
self.clear_active_order_elements()
10851065
self.trader = None
10861066
self.exchange_manager = None

octobot_trading/personal_data/orders/order_factory.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ def create_order_instance(
112112
trigger_above=trigger_above,
113113
trailing_profile=trailing_profile,
114114
is_active=is_active,
115-
active_trigger_price=active_trigger_price,
116-
active_trigger_above=active_trigger_above
115+
active_trigger=personal_data.create_order_price_trigger(order, active_trigger_price, active_trigger_above)
116+
if active_trigger_price else None,
117117
)
118118
return order
119119

0 commit comments

Comments
 (0)