Skip to content

Commit 2a0f2cf

Browse files
committed
[Orders] handle inactive orders swap
1 parent 30ababa commit 2a0f2cf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2074
-157
lines changed

octobot_trading/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040

4141
# Order creation
4242
ORDER_DATA_FETCHING_TIMEOUT = 5 * commons_constants.MINUTE_TO_SECONDS
43+
ACTIVE_ORDER_STRATEGY_SWAP_TIMEOUT = 2 * commons_constants.MINUTE_TO_SECONDS
4344
CHAINED_ORDER_PRICE_FETCHING_TIMEOUT = 1 # should be instant or ignored
4445
CHAINED_ORDERS_OUTDATED_PRICE_ALLOWANCE = decimal.Decimal("0.005") # allows 0.5% outdated price error
4546
# create instantly filled limit orders 0.5% beyond market

octobot_trading/enums.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,9 @@ class ExchangeConstantsOrderColumns(enum.Enum):
334334
ENTRIES = "entries"
335335
VOLUME = "volume"
336336
BROKER_APPLIED = "broker_applied"
337+
IS_ACTIVE = "is_active"
338+
ACTIVE_TRIGGER_PRICE = "active_trigger_price"
339+
ACTIVE_TRIGGER_ABOVE = "active_trigger_above"
337340

338341

339342
class TradeExtraConstants(enum.Enum):
@@ -552,9 +555,15 @@ class TradingSignalOrdersAttrs(enum.Enum):
552555
POST_ONLY = "post_only"
553556
GROUP_ID = "group_id"
554557
GROUP_TYPE = "group_type"
558+
ACTIVE_SWAP_STRATEGY_TYPE = "active_swap_strategy_type"
559+
ACTIVE_SWAP_STRATEGY_TIMEOUT = "active_swap_strategy_timeout"
560+
ACTIVE_SWAP_STRATEGY_TRIGGER_CONFIG = "active_swap_strategy_trigger_config"
555561
TAG = "tag"
556562
ORDER_ID = "order_id"
557563
TRAILING_PROFILE_TYPE = "trailing_profile_type"
564+
IS_ACTIVE = "is_active"
565+
ACTIVE_TRIGGER_PRICE = "active_trigger_price"
566+
ACTIVE_TRIGGER_ABOVE = "active_trigger_above"
558567
TRAILING_PROFILE = "trailing_profile"
559568
BUNDLED_WITH = "bundled_with"
560569
CHAINED_TO = "chained_to"
@@ -587,6 +596,10 @@ class StoredOrdersAttr(enum.Enum):
587596
GROUP = "gr"
588597
GROUP_ID = "gi"
589598
GROUP_TYPE = "gt"
599+
ORDER_SWAP_STRATEGY = "oss"
600+
STRATEGY_TYPE = "sty"
601+
STRATEGY_TIMEOUT = "sti"
602+
STRATEGY_TRIGGER_CONFIG = "stc"
590603
CHAINED_ORDERS = "co"
591604
TRAILING_PROFILE = "tp"
592605
TRAILING_PROFILE_TYPE = "tpt"
@@ -620,3 +633,7 @@ class TradingModeActivityType(enum.Enum):
620633
CREATED_ORDERS = "created_orders"
621634
NOTHING_TO_DO = "nothing_to_do"
622635
NO_ACTIVITY = None
636+
637+
638+
class ActiveOrderSwapTriggerPriceConfiguration(enum.Enum):
639+
FILLING_PRICE = "filling_price"

octobot_trading/exchanges/traders/trader.py

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def __init__(self, config, exchange_manager):
5858
if not hasattr(self, 'simulate'):
5959
self.simulate = False
6060
self.is_enabled = self.__class__.enabled(self.config)
61+
self.enable_inactive_orders = not self.simulate
6162

6263
async def initialize_impl(self):
6364
self.is_enabled = self.is_enabled and self.exchange_manager.is_trading
@@ -72,6 +73,9 @@ def clear(self):
7273
def enabled(cls, config):
7374
return util.is_trader_enabled(config)
7475

76+
def set_enable_inactive_orders(self, enabled: bool):
77+
self.enable_inactive_orders = enabled
78+
7579
def set_risk(self, risk):
7680
min_risk = decimal.Decimal(str(octobot_commons.constants.CONFIG_TRADER_RISK_MIN))
7781
max_risk = decimal.Decimal(str(octobot_commons.constants.CONFIG_TRADER_RISK_MAX))
@@ -116,8 +120,7 @@ async def create_order(
116120
try:
117121
params = params or {}
118122
self.logger.info(f"Creating order: {created_order}")
119-
created_order = await self._create_new_order(order, params, wait_for_creation=wait_for_creation,
120-
creation_timeout=creation_timeout)
123+
created_order = await self._create_new_order(order, params, wait_for_creation, creation_timeout)
121124
if created_order is None:
122125
self.logger.warning(f"Order not created on {self.exchange_manager.exchange_name} "
123126
f"(failed attempt to create: {order}). This is likely due to "
@@ -186,7 +189,7 @@ async def edit_order(self, order,
186189
disabled_state_updater = self.exchange_manager.exchange_personal_data \
187190
.orders_manager.enable_order_auto_synchronization is False
188191
# now that we got the lock, ensure we can edit the order
189-
if not self.simulate and not order.is_self_managed() and (
192+
if not self.simulate and order.is_active and not order.is_self_managed() and (
190193
order.state is not None or disabled_state_updater
191194
):
192195
if disabled_state_updater:
@@ -264,16 +267,18 @@ async def _edit_order_on_exchange(
264267
await self.exchange_manager.exchange_personal_data.handle_portfolio_and_position_update_from_order(order)
265268
return changed
266269

267-
async def _create_new_order(self, new_order, params: dict,
268-
wait_for_creation=True,
269-
creation_timeout=octobot_trading.constants.INDIVIDUAL_ORDER_SYNC_TIMEOUT) -> object:
270+
async def _create_new_order(
271+
self, new_order, params: dict, wait_for_creation: bool, creation_timeout: float
272+
):
270273
"""
271274
Creates an exchange managed order, it might be a simulated or a real order.
272275
Portfolio will be updated by the created order state after order will be initialized
273276
"""
274277
updated_order = new_order
275278
is_pending_creation = False
276-
if not self.simulate and not new_order.is_self_managed():
279+
if not self.simulate and not new_order.is_self_managed() and (
280+
new_order.is_in_active_inactive_transition or new_order.is_active
281+
):
277282
order_params = self.exchange_manager.exchange.get_order_additional_params(new_order)
278283
order_params.update(new_order.exchange_creation_params)
279284
order_params.update(params)
@@ -313,18 +318,29 @@ async def _create_new_order(self, new_order, params: dict,
313318
updated_order.associated_entry_ids = new_order.associated_entry_ids
314319
updated_order.update_with_triggering_order_fees = new_order.update_with_triggering_order_fees
315320
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
323+
updated_order.is_in_active_inactive_transition = new_order.is_in_active_inactive_transition
316324

317325
if is_pending_creation:
318326
# register order as pending order, it will then be added to live orders in order manager once open
319327
self.exchange_manager.exchange_personal_data.orders_manager.register_pending_creation_order(
320328
updated_order
321329
)
322330

323-
await updated_order.initialize()
324-
if is_pending_creation and wait_for_creation \
325-
and updated_order.state is not None and updated_order.state.is_pending()\
326-
and self.exchange_manager.exchange_personal_data.orders_manager.enable_order_auto_synchronization:
327-
await updated_order.state.wait_for_terminate(creation_timeout)
331+
try:
332+
await updated_order.initialize()
333+
if is_pending_creation and wait_for_creation \
334+
and updated_order.state is not None and updated_order.state.is_pending()\
335+
and self.exchange_manager.exchange_personal_data.orders_manager.enable_order_auto_synchronization:
336+
await updated_order.state.wait_for_terminate(creation_timeout)
337+
if new_order.is_in_active_inactive_transition:
338+
# transition successful: new_order is now inactive
339+
await new_order.on_active_from_inactive()
340+
finally:
341+
if updated_order.is_in_active_inactive_transition:
342+
# transition completed: never leave is_in_active_inactive_transition to True after transition
343+
updated_order.is_in_active_inactive_transition = False
328344
return updated_order
329345

330346
def get_take_profit_order_type(self, base_order, order_type: enums.TraderOrderType) -> enums.TraderOrderType:
@@ -385,6 +401,36 @@ async def chain_order(self, order, chained_order, update_with_triggering_order_f
385401
order.add_chained_order(chained_order)
386402
self.logger.info(f"Added chained order [{chained_order}] to [{order}] order.")
387403

404+
async def update_order_as_inactive(
405+
self, order, ignored_order=None, wait_for_cancelling=True,
406+
cancelling_timeout=octobot_trading.constants.INDIVIDUAL_ORDER_SYNC_TIMEOUT
407+
) -> bool:
408+
if self.simulate:
409+
self.logger.error(f"Can't update order as inactive on simulated trading.")
410+
return False
411+
cancelled = False
412+
if order and order.is_open():
413+
with order.active_or_inactive_transition():
414+
cancelled = await self._handle_order_cancellation(
415+
order, ignored_order, wait_for_cancelling, cancelling_timeout
416+
)
417+
else:
418+
self.logger.error(f"Can't update order as inactive: {order} is not open on exchange.")
419+
return cancelled
420+
421+
async def update_order_as_active(
422+
self, order, params: dict = None, wait_for_creation=True, raise_all_creation_error=False,
423+
creation_timeout=octobot_trading.constants.INDIVIDUAL_ORDER_SYNC_TIMEOUT
424+
):
425+
if self.simulate:
426+
self.logger.error(f"Can't update order as active on simulated trading.")
427+
return order
428+
with order.active_or_inactive_transition():
429+
return await self.create_order(
430+
order, loaded=False, params=params, wait_for_creation=wait_for_creation,
431+
raise_all_creation_error=raise_all_creation_error, creation_timeout=creation_timeout
432+
)
433+
388434
async def cancel_order(self, order, ignored_order=None,
389435
wait_for_cancelling=True,
390436
cancelling_timeout=octobot_trading.constants.INDIVIDUAL_ORDER_SYNC_TIMEOUT) -> bool:
@@ -400,11 +446,12 @@ async def cancel_order(self, order, ignored_order=None,
400446
:param cancelling_timeout: time before raising a timeout error when waiting for an order cancel
401447
:return: None
402448
"""
403-
if order and order.is_open():
449+
if order and order.is_open() or not order.is_active:
404450
self.logger.info(f"Cancelling order: {order}")
405451
# always cancel this order first to avoid infinite loop followed by deadlock
406-
return await self._handle_order_cancellation(order, ignored_order,
407-
wait_for_cancelling, cancelling_timeout)
452+
return await self._handle_order_cancellation(
453+
order, ignored_order, wait_for_cancelling, cancelling_timeout
454+
)
408455
return False
409456

410457
async def _handle_order_cancellation(
@@ -417,7 +464,7 @@ async def _handle_order_cancellation(
417464
return success
418465
order_status = None
419466
# if real order: cancel on exchange
420-
if not self.simulate and not order.is_self_managed():
467+
if not self.simulate and order.is_active and not order.is_self_managed():
421468
try:
422469
async with order.lock:
423470
try:

octobot_trading/personal_data/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
is_associated_pending_order,
5959
apply_pending_order_from_created_order,
6060
get_up_to_date_price,
61+
create_as_active_order_using_strategy_if_any,
62+
create_as_active_order_on_exchange,
63+
update_order_as_inactive_on_exchange,
6164
get_potentially_outdated_price,
6265
get_pre_order_data,
6366
get_portfolio_amounts,
@@ -74,6 +77,8 @@
7477
TrailingProfileTypes,
7578
create_trailing_profile,
7679
create_filled_take_profit_trailing_profile,
80+
ActiveOrderSwapStrategy,
81+
StopFirstActiveOrderSwapStrategy,
7782
OrdersUpdater,
7883
adapt_price,
7984
get_minimal_order_amount,
@@ -296,6 +301,9 @@
296301
"is_associated_pending_order",
297302
"apply_pending_order_from_created_order",
298303
"get_up_to_date_price",
304+
"create_as_active_order_using_strategy_if_any",
305+
"create_as_active_order_on_exchange",
306+
"update_order_as_inactive_on_exchange",
299307
"get_potentially_outdated_price",
300308
"get_pre_order_data",
301309
"get_portfolio_amounts",
@@ -312,6 +320,8 @@
312320
"TrailingProfileTypes",
313321
"create_trailing_profile",
314322
"create_filled_take_profit_trailing_profile",
323+
"ActiveOrderSwapStrategy",
324+
"StopFirstActiveOrderSwapStrategy",
315325
"OrdersUpdater",
316326
"adapt_price",
317327
"get_minimal_order_amount",

octobot_trading/personal_data/exchange_personal_data.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# You should have received a copy of the GNU Lesser General Public
1515
# License along with this library.
1616
import asyncio
17+
import decimal
1718
import uuid
1819
import typing
1920

@@ -294,6 +295,17 @@ async def handle_closed_order_update(self, exchange_order_id, raw_order) -> bool
294295
self.logger.exception(e, True, f"Failed to update order : {e}")
295296
return False
296297

298+
async def check_and_update_inactive_orders_when_necessary(
299+
self, symbol: str, current_price: decimal.Decimal, price_time: float,
300+
strategy_timeout: typing.Optional[float], wait_for_fill_callback: typing.Optional[typing.Callable]
301+
):
302+
for order in self.orders_manager.get_inactive_orders(symbol=symbol):
303+
if order.should_become_active(price_time, current_price):
304+
try:
305+
await order.on_active_trigger(strategy_timeout, wait_for_fill_callback)
306+
except Exception as err:
307+
self.logger.exception(err, True, f"Failed order on_active_trigger {err} (order: {order})")
308+
297309
async def handle_trade_update(self, symbol, trade_id, trade,
298310
is_old_trade: bool = False, should_notify: bool = True):
299311
try:

octobot_trading/personal_data/orders/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
create_trailing_profile,
4040
create_filled_take_profit_trailing_profile,
4141
)
42+
from octobot_trading.personal_data.orders import active_order_swap_strategies
43+
from octobot_trading.personal_data.orders.active_order_swap_strategies import (
44+
ActiveOrderSwapStrategy,
45+
StopFirstActiveOrderSwapStrategy,
46+
)
4247
from octobot_trading.personal_data.orders import order
4348
from octobot_trading.personal_data.orders.order import (
4449
Order,
@@ -110,6 +115,9 @@
110115
is_stop_trade_order_type,
111116
is_take_profit_order,
112117
get_trade_order_type,
118+
create_as_active_order_using_strategy_if_any,
119+
create_as_active_order_on_exchange,
120+
update_order_as_inactive_on_exchange,
113121
create_as_chained_order,
114122
is_associated_pending_order,
115123
apply_pending_order_from_created_order,
@@ -180,6 +188,9 @@
180188
"parse_is_pending_cancel",
181189
"parse_is_open",
182190
"get_up_to_date_price",
191+
"create_as_active_order_using_strategy_if_any",
192+
"create_as_active_order_on_exchange",
193+
"update_order_as_inactive_on_exchange",
183194
"get_potentially_outdated_price",
184195
"get_pre_order_data",
185196
"get_portfolio_amounts",
@@ -209,6 +220,8 @@
209220
"TrailingProfileTypes",
210221
"create_trailing_profile",
211222
"create_filled_take_profit_trailing_profile",
223+
"ActiveOrderSwapStrategy",
224+
"StopFirstActiveOrderSwapStrategy",
212225
"OrdersUpdater",
213226
"adapt_price",
214227
"get_minimal_order_amount",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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.active_order_swap_strategies import active_order_swap_strategy
18+
from octobot_trading.personal_data.orders.active_order_swap_strategies.active_order_swap_strategy import (
19+
ActiveOrderSwapStrategy,
20+
)
21+
22+
from octobot_trading.personal_data.orders.active_order_swap_strategies import stop_first_active_order_swap_strategy
23+
from octobot_trading.personal_data.orders.active_order_swap_strategies.stop_first_active_order_swap_strategy import (
24+
StopFirstActiveOrderSwapStrategy,
25+
)
26+
27+
28+
__all__ = [
29+
"ActiveOrderSwapStrategy",
30+
"StopFirstActiveOrderSwapStrategy",
31+
]

0 commit comments

Comments
 (0)