Skip to content

Commit 460d583

Browse files
committed
[Orders] add cancel policies
1 parent c8ba9f8 commit 460d583

26 files changed

+961
-32
lines changed

octobot_trading/enums.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,8 @@ class TradingSignalOrdersAttrs(enum.Enum):
570570
ACTIVE_TRIGGER_PRICE = "active_trigger_price"
571571
ACTIVE_TRIGGER_ABOVE = "active_trigger_above"
572572
TRAILING_PROFILE = "trailing_profile"
573+
CANCEL_POLICY_TYPE = "cancel_policy_type"
574+
CANCEL_POLICY_KWARGS = "cancel_policy_kwargs"
573575
BUNDLED_WITH = "bundled_with"
574576
CHAINED_TO = "chained_to"
575577
ADDITIONAL_ORDERS = "additional_orders"
@@ -614,6 +616,8 @@ class StoredOrdersAttr(enum.Enum):
614616
ACTIVE_TRIGGER = "at"
615617
ACTIVE_TRIGGER_PRICE = "atp"
616618
ACTIVE_TRIGGER_ABOVE = "ata"
619+
CANCEL_POLICY = "cp"
620+
CANCEL_KWARGS = "cpk"
617621
TRAILING_PROFILE = "tp"
618622
TRAILING_PROFILE_TYPE = "tpt"
619623
TRAILING_PROFILE_DETAILS = "tpd"

octobot_trading/errors.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,12 @@ class InvalidOrderState(Exception):
254254
"""
255255

256256

257+
class InvalidCancelPolicyError(Exception):
258+
"""
259+
Raised when a cancel policy is invalid
260+
"""
261+
262+
257263
class InvalidLeverageValue(Exception):
258264
"""
259265
Raised when a leverage is being updated with an invalid value

octobot_trading/exchanges/traders/trader.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ async def _create_new_order(
334334
updated_order.associated_entry_ids = new_order.associated_entry_ids
335335
updated_order.update_with_triggering_order_fees = new_order.update_with_triggering_order_fees
336336
updated_order.trailing_profile = new_order.trailing_profile
337+
updated_order.cancel_policy = new_order.cancel_policy
337338
if new_order.active_trigger is not None:
338339
updated_order.use_active_trigger(order_util.create_order_price_trigger(
339340
updated_order, new_order.active_trigger.trigger_price, new_order.active_trigger.trigger_above

octobot_trading/modes/channel/abstract_mode_consumer.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ async def internal_callback(
8989
self.logger.info(f"Failed {symbol} order creation on: {self.exchange_manager.exchange_name} "
9090
f"an unexpected error happened when creating order. This is likely due to "
9191
f"the order being refused by the exchange.")
92+
except errors.InvalidCancelPolicyError as err:
93+
self.previous_call_error_per_symbol[symbol] = err
94+
self.logger.error(f"Invalid cancel policy error on {self.exchange_manager.exchange_name}: {err}. "
95+
f"Please make sure that the provided cancel policy is valid.")
9296

9397
def get_minimal_funds_error(self, symbol, final_note):
9498
if symbol is None:

octobot_trading/modes/channel/abstract_mode_producer.py

Lines changed: 64 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -425,41 +425,77 @@ def get_should_cancel_loaded_orders(cls) -> bool:
425425
"""
426426
raise NotImplementedError("get_should_cancel_loaded_orders not implemented")
427427

428+
def _get_to_cancel_orders(
429+
self, symbol=None, side=None, tag=None, active=None, exchange_order_ids=None
430+
) -> list:
431+
cancel_loaded_orders = self.get_should_cancel_loaded_orders()
432+
return [
433+
order
434+
for order in self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(
435+
symbol=symbol, tag=tag, active=active
436+
)
437+
if (
438+
not (order.is_cancelled() or order.is_closed())
439+
and (cancel_loaded_orders or order.is_from_this_octobot)
440+
and (side is None or (side is order.side))
441+
and (exchange_order_ids is None or (order.exchange_order_id in exchange_order_ids))
442+
)
443+
]
444+
445+
async def _cancel_orders(
446+
self, orders: list,
447+
dependencies: typing.Optional[commons_signals.SignalDependencies] = None
448+
) -> tuple[bool, typing.Optional[commons_signals.SignalDependencies]]:
449+
cancelled = False
450+
failed_to_cancel = False
451+
cancelled_dependencies = commons_signals.SignalDependencies()
452+
for order in orders:
453+
try:
454+
is_cancelled, new_dependencies = await self.trading_mode.cancel_order(
455+
order, dependencies=dependencies
456+
)
457+
if is_cancelled:
458+
cancelled = True
459+
cancelled_dependencies.extend(new_dependencies)
460+
else:
461+
failed_to_cancel = True
462+
except errors.OctoBotExchangeError as err:
463+
# do not propagate exchange error when canceling order
464+
self.logger.exception(err, True, f"Error when cancelling order [{order}]: {err}")
465+
failed_to_cancel = True
466+
return (cancelled and not failed_to_cancel), cancelled_dependencies or None
467+
428468
async def cancel_symbol_open_orders(
429-
self, symbol, side=None, tag=None, exchange_order_ids=None,
469+
self, symbol, side=None, tag=None, active=None, exchange_order_ids=None,
430470
dependencies: typing.Optional[commons_signals.SignalDependencies] = None
431-
) -> tuple[bool, commons_signals.SignalDependencies]:
471+
) -> tuple[bool, typing.Optional[commons_signals.SignalDependencies]]:
432472
"""
433473
Cancel all symbol open orders
434474
"""
435-
cancel_loaded_orders = self.get_should_cancel_loaded_orders()
436-
cancelled = False
437-
failed_to_cancel = False
438-
cancelled_dependencies = commons_signals.SignalDependencies()
439475
if self.exchange_manager.trader.is_enabled:
440-
for order in self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(
441-
symbol=symbol, tag=tag
476+
return await self._cancel_orders(
477+
self._get_to_cancel_orders(
478+
symbol=symbol, side=side, tag=tag, active=active, exchange_order_ids=exchange_order_ids
479+
), dependencies
480+
)
481+
return False, None
482+
483+
async def apply_cancel_policies(
484+
self, symbol=None, side=None, tag=None, exchange_order_ids=None, active=None,
485+
dependencies: typing.Optional[commons_signals.SignalDependencies] = None
486+
) -> tuple[bool, typing.Optional[commons_signals.SignalDependencies]]:
487+
"""
488+
Cancel all orders that should be according to their cancel policies
489+
"""
490+
if self.exchange_manager.trader.is_enabled:
491+
if to_cancel_orders := self.exchange_manager.exchange_personal_data.orders_manager.get_orders_to_cancel_from_policies(
492+
self._get_to_cancel_orders(
493+
symbol=symbol, side=side, tag=tag, active=active, exchange_order_ids=exchange_order_ids
494+
)
442495
):
443-
if (
444-
not (order.is_cancelled() or order.is_closed())
445-
and (cancel_loaded_orders or order.is_from_this_octobot)
446-
and (side is None or (side is order.side))
447-
and (exchange_order_ids is None or (order.exchange_order_id in exchange_order_ids))
448-
):
449-
try:
450-
is_cancelled, new_dependencies = await self.trading_mode.cancel_order(
451-
order, dependencies=dependencies
452-
)
453-
if is_cancelled:
454-
cancelled = True
455-
cancelled_dependencies.extend(new_dependencies)
456-
else:
457-
failed_to_cancel = True
458-
except errors.OctoBotExchangeError as err:
459-
# do not propagate exchange error when canceling order
460-
self.logger.exception(err, True, f"Error when cancelling order [{order}]: {err}")
461-
failed_to_cancel = True
462-
return (cancelled and not failed_to_cancel), cancelled_dependencies or None
496+
self.logger.info(f"Cancelling {len(to_cancel_orders)} orders from cancel policies")
497+
return await self._cancel_orders(to_cancel_orders, dependencies)
498+
return False, None
463499

464500
def all_databases(self):
465501
provider = databases.RunDatabasesProvider.instance()

octobot_trading/personal_data/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@
135135
TakeProfitLimitOrder,
136136
TrailingStopOrder,
137137
TrailingStopLimitOrder,
138+
create_cancel_policy,
139+
OrderCancelPolicy,
140+
ExpirationTimeOrderCancelPolicy,
141+
ChainedOrderFillingPriceOrderCancelPolicy,
138142
)
139143
from octobot_trading.personal_data import portfolios
140144
from octobot_trading.personal_data.portfolios import (
@@ -478,4 +482,8 @@
478482
"create_transfer_transaction",
479483
"SubPortfolioData",
480484
"ResolvedOrdersPortoflioDelta",
485+
"create_cancel_policy",
486+
"OrderCancelPolicy",
487+
"ExpirationTimeOrderCancelPolicy",
488+
"ChainedOrderFillingPriceOrderCancelPolicy",
481489
]

octobot_trading/personal_data/orders/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@
4545
StopFirstActiveOrderSwapStrategy,
4646
TakeProfitFirstActiveOrderSwapStrategy,
4747
)
48+
from octobot_trading.personal_data.orders import cancel_policies
49+
from octobot_trading.personal_data.orders.cancel_policies import (
50+
create_cancel_policy,
51+
OrderCancelPolicy,
52+
ExpirationTimeOrderCancelPolicy,
53+
ChainedOrderFillingPriceOrderCancelPolicy,
54+
)
4855
from octobot_trading.personal_data.orders import triggers
4956
from octobot_trading.personal_data.orders.triggers import (
5057
BaseTrigger,
@@ -208,6 +215,7 @@
208215
"is_stop_trade_order_type",
209216
"is_take_profit_order",
210217
"ensure_orders_limit",
218+
"create_cancel_policy",
211219
"create_as_chained_order",
212220
"ensure_orders_relevancy",
213221
"get_order_quantity_currency",
@@ -234,6 +242,9 @@
234242
"ActiveOrderSwapStrategy",
235243
"StopFirstActiveOrderSwapStrategy",
236244
"TakeProfitFirstActiveOrderSwapStrategy",
245+
"OrderCancelPolicy",
246+
"ExpirationTimeOrderCancelPolicy",
247+
"ChainedOrderFillingPriceOrderCancelPolicy",
237248
"BaseTrigger",
238249
"PriceTrigger",
239250
"OrdersUpdater",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
from octobot_trading.personal_data.orders.cancel_policies import cancel_policy_factory
18+
from octobot_trading.personal_data.orders.cancel_policies.cancel_policy_factory import (
19+
create_cancel_policy,
20+
)
21+
22+
from octobot_trading.personal_data.orders.cancel_policies import order_cancel_policy
23+
from octobot_trading.personal_data.orders.cancel_policies.order_cancel_policy import (
24+
OrderCancelPolicy,
25+
)
26+
27+
from octobot_trading.personal_data.orders.cancel_policies import expiration_time_order_cancel_policy
28+
from octobot_trading.personal_data.orders.cancel_policies.expiration_time_order_cancel_policy import (
29+
ExpirationTimeOrderCancelPolicy,
30+
)
31+
32+
from octobot_trading.personal_data.orders.cancel_policies import chained_order_filling_price_order_cancel_policy
33+
from octobot_trading.personal_data.orders.cancel_policies.chained_order_filling_price_order_cancel_policy import (
34+
ChainedOrderFillingPriceOrderCancelPolicy,
35+
)
36+
37+
__all__ = [
38+
"create_cancel_policy",
39+
"OrderCancelPolicy",
40+
"ExpirationTimeOrderCancelPolicy",
41+
"ChainedOrderFillingPriceOrderCancelPolicy",
42+
]
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
import typing
17+
import octobot_trading.personal_data.orders.cancel_policies.order_cancel_policy as order_cancel_policy_import
18+
import octobot_trading.personal_data.orders.cancel_policies.expiration_time_order_cancel_policy as expiration_time_order_cancel_policy_import
19+
import octobot_trading.personal_data.orders.cancel_policies.chained_order_filling_price_order_cancel_policy as chained_order_filling_price_order_cancel_policy_import
20+
import octobot_trading.errors as errors
21+
22+
23+
24+
def create_cancel_policy(
25+
policy_class_name: str,
26+
kwargs: typing.Optional[dict] = None
27+
) -> order_cancel_policy_import.OrderCancelPolicy:
28+
"""
29+
Create a cancel policy instance from its class name and kwargs.
30+
31+
:param policy_class_name: The name of the cancel policy class
32+
:param kwargs: Optional dictionary of keyword arguments to pass to the policy constructor
33+
:return: An instance of the cancel policy, or None if the class name is not recognized
34+
"""
35+
kwargs = kwargs or {}
36+
try:
37+
if policy_class_name == expiration_time_order_cancel_policy_import.ExpirationTimeOrderCancelPolicy.__name__:
38+
return expiration_time_order_cancel_policy_import.ExpirationTimeOrderCancelPolicy(**kwargs)
39+
elif policy_class_name == chained_order_filling_price_order_cancel_policy_import.ChainedOrderFillingPriceOrderCancelPolicy.__name__:
40+
return chained_order_filling_price_order_cancel_policy_import.ChainedOrderFillingPriceOrderCancelPolicy(**kwargs)
41+
except TypeError as err:
42+
raise errors.InvalidCancelPolicyError(f"Invalid kwargs for {policy_class_name}: {err}") from err
43+
raise NotImplementedError(f"Unsupported cancel policy class name: {policy_class_name}")
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
import dataclasses
17+
import octobot_trading.personal_data.orders.order_util as order_util
18+
import octobot_trading.personal_data.orders.cancel_policies.order_cancel_policy as order_cancel_policy_import
19+
20+
21+
@dataclasses.dataclass
22+
class ChainedOrderFillingPriceOrderCancelPolicy(order_cancel_policy_import.OrderCancelPolicy):
23+
"""
24+
Will cancel the order if at least one of the chained orders filling price is reached.
25+
"""
26+
27+
def should_cancel(self, order) -> bool:
28+
if order.is_cleared():
29+
self.get_logger().error(
30+
f"Ignored cancel policy: order {str(order)} has been cleared"
31+
)
32+
return False
33+
if not order.chained_orders:
34+
self.get_logger().error(
35+
f"Ignored cancel policy: order {str(order)} has no chained orders"
36+
)
37+
return False
38+
current_price, up_to_date = order_util.get_potentially_outdated_price(
39+
order.trader.exchange_manager, order.symbol
40+
)
41+
if not up_to_date:
42+
self.get_logger().error(
43+
f"Ignored cancel policy: order {str(order)} mark price: {current_price} is outdated"
44+
)
45+
return False
46+
for chained_order in order.chained_orders:
47+
if chained_order.trigger_above and current_price > chained_order.get_filling_price():
48+
# this chained order price is reached
49+
return True
50+
if not chained_order.trigger_above and current_price < chained_order.get_filling_price():
51+
# this chained order price is reached
52+
return True
53+
# no chained order price is reached
54+
return False

0 commit comments

Comments
 (0)