Skip to content

Inactive orders #1206

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions octobot_trading/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
)
from octobot_trading.api.orders import (
get_open_orders,
get_all_orders,
get_pending_creation_orders,
get_order_exchange_name,
order_to_dict,
Expand Down Expand Up @@ -377,6 +378,7 @@
"get_reference_market",
"get_initializing_currencies_prices",
"get_open_orders",
"get_all_orders",
"get_pending_creation_orders",
"get_order_exchange_name",
"order_to_dict",
Expand Down
8 changes: 6 additions & 2 deletions octobot_trading/api/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,12 @@ async def create_order(exchange_manager,
)


def get_open_orders(exchange_manager, symbol=None) -> list:
return exchange_manager.exchange_personal_data.orders_manager.get_open_orders(symbol=symbol)
def get_open_orders(exchange_manager, symbol=None, active=None) -> list:
return exchange_manager.exchange_personal_data.orders_manager.get_open_orders(symbol=symbol, active=active)


def get_all_orders(exchange_manager, symbol=None, active=None) -> list:
return exchange_manager.exchange_personal_data.orders_manager.get_all_orders(symbol=symbol, active=active)


def get_pending_creation_orders(exchange_manager) -> list:
Expand Down
1 change: 1 addition & 0 deletions octobot_trading/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

# Order creation
ORDER_DATA_FETCHING_TIMEOUT = 5 * commons_constants.MINUTE_TO_SECONDS
ACTIVE_ORDER_STRATEGY_SWAP_TIMEOUT = 2 * commons_constants.MINUTE_TO_SECONDS
CHAINED_ORDER_PRICE_FETCHING_TIMEOUT = 1 # should be instant or ignored
CHAINED_ORDERS_OUTDATED_PRICE_ALLOWANCE = decimal.Decimal("0.005") # allows 0.5% outdated price error
# create instantly filled limit orders 0.5% beyond market
Expand Down
19 changes: 19 additions & 0 deletions octobot_trading/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ class ExchangeConstantsOrderColumns(enum.Enum):
ENTRIES = "entries"
VOLUME = "volume"
BROKER_APPLIED = "broker_applied"
IS_ACTIVE = "is_active"


class TradeExtraConstants(enum.Enum):
Expand Down Expand Up @@ -552,9 +553,15 @@ class TradingSignalOrdersAttrs(enum.Enum):
POST_ONLY = "post_only"
GROUP_ID = "group_id"
GROUP_TYPE = "group_type"
ACTIVE_SWAP_STRATEGY_TYPE = "active_swap_strategy_type"
ACTIVE_SWAP_STRATEGY_TIMEOUT = "active_swap_strategy_timeout"
ACTIVE_SWAP_STRATEGY_TRIGGER_CONFIG = "active_swap_strategy_trigger_config"
TAG = "tag"
ORDER_ID = "order_id"
TRAILING_PROFILE_TYPE = "trailing_profile_type"
IS_ACTIVE = "is_active"
ACTIVE_TRIGGER_PRICE = "active_trigger_price"
ACTIVE_TRIGGER_ABOVE = "active_trigger_above"
TRAILING_PROFILE = "trailing_profile"
BUNDLED_WITH = "bundled_with"
CHAINED_TO = "chained_to"
Expand Down Expand Up @@ -587,7 +594,14 @@ class StoredOrdersAttr(enum.Enum):
GROUP = "gr"
GROUP_ID = "gi"
GROUP_TYPE = "gt"
ORDER_SWAP_STRATEGY = "oss"
STRATEGY_TYPE = "sty"
STRATEGY_TIMEOUT = "sti"
STRATEGY_TRIGGER_CONFIG = "stc"
CHAINED_ORDERS = "co"
ACTIVE_TRIGGER = "at"
ACTIVE_TRIGGER_PRICE = "atp"
ACTIVE_TRIGGER_ABOVE = "ata"
TRAILING_PROFILE = "tp"
TRAILING_PROFILE_TYPE = "tpt"
TRAILING_PROFILE_DETAILS = "tpd"
Expand Down Expand Up @@ -620,3 +634,8 @@ class TradingModeActivityType(enum.Enum):
CREATED_ORDERS = "created_orders"
NOTHING_TO_DO = "nothing_to_do"
NO_ACTIVITY = None


class ActiveOrderSwapTriggerPriceConfiguration(enum.Enum):
FILLING_PRICE = "filling_price"
ORDER_PARAMS_ONLY = "order_params_only"
81 changes: 65 additions & 16 deletions octobot_trading/exchanges/traders/trader.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def __init__(self, config, exchange_manager):
if not hasattr(self, 'simulate'):
self.simulate = False
self.is_enabled = self.__class__.enabled(self.config)
self.enable_inactive_orders = not self.simulate

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

def set_enable_inactive_orders(self, enabled: bool):
self.enable_inactive_orders = enabled

def set_risk(self, risk):
min_risk = decimal.Decimal(str(octobot_commons.constants.CONFIG_TRADER_RISK_MIN))
max_risk = decimal.Decimal(str(octobot_commons.constants.CONFIG_TRADER_RISK_MAX))
Expand Down Expand Up @@ -116,8 +120,7 @@ async def create_order(
try:
params = params or {}
self.logger.info(f"Creating order: {created_order}")
created_order = await self._create_new_order(order, params, wait_for_creation=wait_for_creation,
creation_timeout=creation_timeout)
created_order = await self._create_new_order(order, params, wait_for_creation, creation_timeout)
if created_order is None:
self.logger.warning(f"Order not created on {self.exchange_manager.exchange_name} "
f"(failed attempt to create: {order}). This is likely due to "
Expand Down Expand Up @@ -186,7 +189,7 @@ async def edit_order(self, order,
disabled_state_updater = self.exchange_manager.exchange_personal_data \
.orders_manager.enable_order_auto_synchronization is False
# now that we got the lock, ensure we can edit the order
if not self.simulate and not order.is_self_managed() and (
if not self.simulate and order.is_active and not order.is_self_managed() and (
order.state is not None or disabled_state_updater
):
if disabled_state_updater:
Expand Down Expand Up @@ -264,16 +267,18 @@ async def _edit_order_on_exchange(
await self.exchange_manager.exchange_personal_data.handle_portfolio_and_position_update_from_order(order)
return changed

async def _create_new_order(self, new_order, params: dict,
wait_for_creation=True,
creation_timeout=octobot_trading.constants.INDIVIDUAL_ORDER_SYNC_TIMEOUT) -> object:
async def _create_new_order(
self, new_order, params: dict, wait_for_creation: bool, creation_timeout: float
):
"""
Creates an exchange managed order, it might be a simulated or a real order.
Portfolio will be updated by the created order state after order will be initialized
"""
updated_order = new_order
is_pending_creation = False
if not self.simulate and not new_order.is_self_managed():
if not self.simulate and not new_order.is_self_managed() and (
new_order.is_in_active_inactive_transition or new_order.is_active
):
order_params = self.exchange_manager.exchange.get_order_additional_params(new_order)
order_params.update(new_order.exchange_creation_params)
order_params.update(params)
Expand Down Expand Up @@ -313,18 +318,31 @@ async def _create_new_order(self, new_order, params: dict,
updated_order.associated_entry_ids = new_order.associated_entry_ids
updated_order.update_with_triggering_order_fees = new_order.update_with_triggering_order_fees
updated_order.trailing_profile = new_order.trailing_profile
if new_order.active_trigger is not None:
updated_order.use_active_trigger(order_util.create_order_price_trigger(
updated_order, new_order.active_trigger.trigger_price, new_order.active_trigger.trigger_above
))
updated_order.is_in_active_inactive_transition = new_order.is_in_active_inactive_transition

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

await updated_order.initialize()
if is_pending_creation and wait_for_creation \
and updated_order.state is not None and updated_order.state.is_pending()\
and self.exchange_manager.exchange_personal_data.orders_manager.enable_order_auto_synchronization:
await updated_order.state.wait_for_terminate(creation_timeout)
try:
await updated_order.initialize()
if is_pending_creation and wait_for_creation \
and updated_order.state is not None and updated_order.state.is_pending()\
and self.exchange_manager.exchange_personal_data.orders_manager.enable_order_auto_synchronization:
await updated_order.state.wait_for_terminate(creation_timeout)
if new_order.is_in_active_inactive_transition:
# transition successful: new_order is now inactive
await new_order.on_active_from_inactive()
finally:
if updated_order.is_in_active_inactive_transition:
# transition completed: never leave is_in_active_inactive_transition to True after transition
updated_order.is_in_active_inactive_transition = False
return updated_order

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

async def update_order_as_inactive(
self, order, ignored_order=None, wait_for_cancelling=True,
cancelling_timeout=octobot_trading.constants.INDIVIDUAL_ORDER_SYNC_TIMEOUT
) -> bool:
if not self.enable_inactive_orders:
self.logger.error(f"Can't update order as inactive when {self.enable_inactive_orders=}.")
return False
cancelled = False
if order and order.is_open():
with order.active_or_inactive_transition():
cancelled = await self._handle_order_cancellation(
order, ignored_order, wait_for_cancelling, cancelling_timeout
)
else:
self.logger.error(f"Can't update order as inactive: {order} is not open on exchange.")
return cancelled

async def update_order_as_active(
self, order, params: dict = None, wait_for_creation=True, raise_all_creation_error=False,
creation_timeout=octobot_trading.constants.INDIVIDUAL_ORDER_SYNC_TIMEOUT
):
if not self.enable_inactive_orders:
self.logger.error(f"Can't update order as active when {self.enable_inactive_orders=}.")
return order
with order.active_or_inactive_transition():
return await self.create_order(
order, loaded=False, params=params, wait_for_creation=wait_for_creation,
raise_all_creation_error=raise_all_creation_error, creation_timeout=creation_timeout
)

async def cancel_order(self, order, ignored_order=None,
wait_for_cancelling=True,
cancelling_timeout=octobot_trading.constants.INDIVIDUAL_ORDER_SYNC_TIMEOUT) -> bool:
Expand All @@ -400,11 +448,12 @@ async def cancel_order(self, order, ignored_order=None,
:param cancelling_timeout: time before raising a timeout error when waiting for an order cancel
:return: None
"""
if order and order.is_open():
if order and order.is_open() or not order.is_active:
self.logger.info(f"Cancelling order: {order}")
# always cancel this order first to avoid infinite loop followed by deadlock
return await self._handle_order_cancellation(order, ignored_order,
wait_for_cancelling, cancelling_timeout)
return await self._handle_order_cancellation(
order, ignored_order, wait_for_cancelling, cancelling_timeout
)
return False

async def _handle_order_cancellation(
Expand All @@ -417,7 +466,7 @@ async def _handle_order_cancellation(
return success
order_status = None
# if real order: cancel on exchange
if not self.simulate and not order.is_self_managed():
if not self.simulate and order.is_active and not order.is_self_managed():
try:
async with order.lock:
try:
Expand Down
22 changes: 20 additions & 2 deletions octobot_trading/personal_data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@
wait_for_order_fill,
apply_order_storage_details_if_any,
create_orders_storage_related_elements,
create_missing_self_managed_orders_from_storage_order_groups,
create_missing_virtual_orders_from_storage_order_groups,
is_associated_pending_order,
apply_pending_order_from_created_order,
get_up_to_date_price,
create_order_price_trigger,
create_as_active_order_using_strategy_if_any,
create_as_active_order_on_exchange,
update_order_as_inactive_on_exchange,
get_potentially_outdated_price,
get_pre_order_data,
get_portfolio_amounts,
Expand All @@ -74,6 +78,11 @@
TrailingProfileTypes,
create_trailing_profile,
create_filled_take_profit_trailing_profile,
ActiveOrderSwapStrategy,
StopFirstActiveOrderSwapStrategy,
TakeProfitFirstActiveOrderSwapStrategy,
BaseTrigger,
PriceTrigger,
OrdersUpdater,
adapt_price,
get_minimal_order_amount,
Expand Down Expand Up @@ -292,10 +301,14 @@
"wait_for_order_fill",
"apply_order_storage_details_if_any",
"create_orders_storage_related_elements",
"create_missing_self_managed_orders_from_storage_order_groups",
"create_missing_virtual_orders_from_storage_order_groups",
"is_associated_pending_order",
"apply_pending_order_from_created_order",
"get_up_to_date_price",
"create_order_price_trigger",
"create_as_active_order_using_strategy_if_any",
"create_as_active_order_on_exchange",
"update_order_as_inactive_on_exchange",
"get_potentially_outdated_price",
"get_pre_order_data",
"get_portfolio_amounts",
Expand All @@ -312,6 +325,11 @@
"TrailingProfileTypes",
"create_trailing_profile",
"create_filled_take_profit_trailing_profile",
"ActiveOrderSwapStrategy",
"StopFirstActiveOrderSwapStrategy",
"TakeProfitFirstActiveOrderSwapStrategy",
"BaseTrigger",
"PriceTrigger",
"OrdersUpdater",
"adapt_price",
"get_minimal_order_amount",
Expand Down
19 changes: 19 additions & 0 deletions octobot_trading/personal_data/exchange_personal_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library.
import asyncio
import decimal
import uuid
import typing

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

async def check_and_update_inactive_orders_when_necessary(
self, symbol: str, current_price: decimal.Decimal, price_time: float,
strategy_timeout: typing.Optional[float], wait_for_fill_callback: typing.Optional[typing.Callable]
) -> int:
handled_orders_count = 0
sorted_inactive_orders = sorted(
self.orders_manager.get_all_orders(symbol=symbol, active=False),
key= lambda o: o.origin_price if o.trigger_above else -o.origin_price
)
for order in sorted_inactive_orders:
if order.should_become_active(price_time, current_price):
handled_orders_count += 1
try:
await order.on_active_trigger(strategy_timeout, wait_for_fill_callback)
except Exception as err:
self.logger.exception(err, True, f"Failed order on_active_trigger {err} (order: {order})")
return handled_orders_count

async def handle_trade_update(self, symbol, trade_id, trade,
is_old_trade: bool = False, should_notify: bool = True):
try:
Expand Down
Loading
Loading