Skip to content
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

trigger above parsing #1409

Merged
merged 3 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 38 additions & 11 deletions Trading/Exchange/binance/binance_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ class Binance(exchanges.RestExchange):
# binance {"code":-2021,"msg":"Order would immediately trigger."}
("order would immediately trigger", )
]
# text content of errors due to an order that can't be cancelled on exchange (because filled or already cancelled)
EXCHANGE_ORDER_UNCANCELLABLE_ERRORS: typing.List[typing.Iterable[str]] = [
('Unknown order sent', )
]

BUY_STR = "BUY"
SELL_STR = "SELL"
Expand Down Expand Up @@ -274,8 +278,14 @@ async def set_symbol_margin_type(self, symbol: str, isolated: bool, **kwargs: di


class BinanceCCXTAdapter(exchanges.CCXTAdapter):
STOP_MARKET = 'stop_market'
STOP_ORDERS = [STOP_MARKET]
STOP_ORDERS = [
"stop_market", "stop", # futures
"stop_loss", "stop_loss_limit" # spot
]
TAKE_PROFITS_ORDERS = [
"take_profit_market", "take_profit_limit", # futures
"take_profit" # spot
]
BINANCE_DEFAULT_FUNDING_TIME = 8 * commons_constants.HOURS_TO_SECONDS

def fix_order(self, raw, symbol=None, **kwargs):
Expand All @@ -287,15 +297,32 @@ def fix_order(self, raw, symbol=None, **kwargs):
return fixed

def _adapt_order_type(self, fixed):
if fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.TYPE.value, None) in self.STOP_ORDERS:
stop_price = fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.STOP_PRICE.value, None)
updated_type = trading_enums.TradeOrderType.UNKNOWN.value
if stop_price is not None:
updated_type = trading_enums.TradeOrderType.STOP_LOSS.value
else:
self.logger.error(f"Unknown order type, order: {fixed}")
# stop loss and take profits are not tagged as such by ccxt, force it
fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = updated_type
if order_type := fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.TYPE.value, None):
is_stop = order_type.lower() in self.STOP_ORDERS
is_tp = order_type.lower() in self.TAKE_PROFITS_ORDERS
if is_stop or is_tp:
stop_price = fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.STOP_PRICE.value, None)
selling = (
fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.SIDE.value, None)
== trading_enums.TradeOrderSide.SELL.value
)
updated_type = trading_enums.TradeOrderType.UNKNOWN.value
trigger_above = False
if is_stop:
updated_type = trading_enums.TradeOrderType.STOP_LOSS.value
trigger_above = not selling # sell stop loss triggers when price is lower than target
if is_tp:
# updated_type = trading_enums.TradeOrderType.TAKE_PROFIT.value
# take profits are not yet handled as such: consider them as limit orders
updated_type = trading_enums.TradeOrderType.LIMIT.value # waiting for TP handling
if not fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]:
fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = stop_price # waiting for TP handling
trigger_above = selling # sell take profit triggers when price is higher than target
else:
self.logger.error(f"Unknown order type, order: {fixed}")
# stop loss and take profits are not tagged as such by ccxt, force it
fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = updated_type
fixed[trading_enums.ExchangeConstantsOrderColumns.TRIGGER_ABOVE.value] = trigger_above
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

return fixed

def fix_trades(self, raw, **kwargs):
Expand Down
36 changes: 31 additions & 5 deletions Trading/Exchange/bingx/bingx_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,19 @@ class Bingx(exchanges.RestExchange):
# bingx {"code":100404,"msg":"the order you want to cancel is FILLED or CANCELLED already, or is not a valid
# order id ,please verify","debugMsg":""}
("the order you want to cancel is filled or cancelled already", ),
# bingx {"code":100404,"msg":"the order is FILLED or CANCELLED already before, or is not a valid
# order id ,please verify","debugMsg":""}
("the order is filled or cancelled already before", ),
]
# text content of errors due to unhandled authentication issues
EXCHANGE_AUTHENTICATION_ERRORS: typing.List[typing.Iterable[str]] = [
# 'bingx {'code': '100413', 'msg': 'Incorrect apiKey', 'timestamp': '1725195218082'}'
("incorrect apikey",),
]
# text content of errors due to an order that can't be cancelled on exchange (because filled or already cancelled)
EXCHANGE_ORDER_UNCANCELLABLE_ERRORS: typing.List[typing.Iterable[str]] = [
('the order is filled or cancelled', )
]

# Set True when get_open_order() can return outdated orders (cancelled or not yet created)
CAN_HAVE_DELAYED_CANCELLED_ORDERS = True
Expand Down Expand Up @@ -117,17 +124,36 @@ def _update_stop_order_or_trade_type_and_price(self, order_or_trade: dict):
trading_enums.ExchangeConstantsOrderColumns.PRICE.value
)
)
is_selling = (
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.SIDE.value]
== trading_enums.TradeOrderSide.SELL.value
)
stop_price = float(stop_price)
# use stop price as order price to parse it properly
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = stop_price
# type is TAKE_STOP_LIMIT (not unified)
if order_or_trade.get(trading_enums.ExchangeConstantsOrderColumns.TYPE.value) not in (
trading_enums.TradeOrderType.STOP_LOSS.value, trading_enums.TradeOrderType.TAKE_PROFIT.value
):
if order_or_trade.get(trading_enums.ExchangeConstantsOrderColumns.TYPE.value) == "take_stop_limit":
# unsupported: no way to figure out if this order is a stop loss or a take profit
# (trigger above or bellow)
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = (
trading_enums.TradeOrderType.UNSUPPORTED.value)
self.logger.info(f"Unsupported order fetched: {order_or_trade}")
else:
if stop_price <= order_creation_price:
order_type = trading_enums.TradeOrderType.STOP_LOSS.value
trigger_above = False
if is_selling:
order_type = trading_enums.TradeOrderType.STOP_LOSS.value
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.STOP_PRICE.value] = stop_price
else:
order_type = trading_enums.TradeOrderType.LIMIT.value
else:
order_type = trading_enums.TradeOrderType.TAKE_PROFIT.value
trigger_above = True
if is_selling:
order_type = trading_enums.TradeOrderType.LIMIT.value
else:
order_type = trading_enums.TradeOrderType.STOP_LOSS.value
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.STOP_PRICE.value] = stop_price
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.TRIGGER_ABOVE.value] = trigger_above
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = order_type

def fix_order(self, raw, **kwargs):
Expand Down
36 changes: 33 additions & 3 deletions Trading/Exchange/coinbase/coinbase_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ class Coinbase(exchanges.RestExchange):
EXCHANGE_MISSING_FUNDS_ERRORS: typing.List[typing.Iterable[str]] = [
("insufficient balance in source account", )
]
# text content of errors due to an order that can't be cancelled on exchange (because filled or already cancelled)
EXCHANGE_ORDER_UNCANCELLABLE_ERRORS: typing.List[typing.Iterable[str]] = [
('cancelorders() has failed, check your arguments and parameters', )
]

# should be overridden locally to match exchange support
SUPPORTED_ELEMENTS = {
Expand Down Expand Up @@ -375,9 +379,35 @@ def _update_stop_order_or_trade_type_and_price(self, order_or_trade: dict):
trading_enums.TradeOrderType.STOP_LOSS.value, trading_enums.TradeOrderType.TAKE_PROFIT.value
):
# Force stop loss. Add order direction parsing logic to handle take profits if necessary
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = (
trading_enums.TradeOrderType.STOP_LOSS.value # simulate market stop loss
)
order_type = trading_enums.TradeOrderType.STOP_LOSS.value
trigger_above = False
try:
order_config = order_or_trade.get(ccxt_constants.CCXT_INFO, {}).get("order_configuration", {})
stop_config = order_config.get("stop_limit_stop_limit_gtc") or order_config.get("stop_limit_stop_limit_gtd")
stop_direction = stop_config.get("stop_direction", "")
if "down" in stop_direction.lower():
trigger_above = False
elif "up" in stop_direction.lower():
trigger_above = True
else:
self.logger.error(f"Unknown order direction: {stop_direction} ({order_or_trade})")
side = order_or_trade[trading_enums.ExchangeConstantsOrderColumns.SIDE.value]
if side == trading_enums.TradeOrderSide.SELL.value:
if trigger_above:
# take profits are not yet handled as such: consider them as limit orders
order_type = trading_enums.TradeOrderType.LIMIT.value # waiting for TP handling
else:
order_type = trading_enums.TradeOrderType.STOP_LOSS.value
elif side == trading_enums.TradeOrderSide.BUY.value:
if trigger_above:
order_type = trading_enums.TradeOrderType.STOP_LOSS.value
else:
# take profits are not yet handled as such: consider them as limit orders
order_type = trading_enums.TradeOrderType.LIMIT.value # waiting for TP handling
except (KeyError, TypeError) as err:
self.logger.error(f"missing expected coinbase order config: {err}, {order_or_trade}")
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = order_type
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.TRIGGER_ABOVE.value] = trigger_above

def fix_order(self, raw, **kwargs):
"""
Expand Down
33 changes: 27 additions & 6 deletions Trading/Exchange/kucoin/kucoin_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ class Kucoin(exchanges.RestExchange):
EXCHANGE_ORDER_IMMEDIATELY_TRIGGER_ERRORS: typing.List[typing.Iterable[str]] = [
# doesn't seem to happen on kucoin
]
# text content of errors due to an order that can't be cancelled on exchange (because filled or already cancelled)
EXCHANGE_ORDER_UNCANCELLABLE_ERRORS: typing.List[typing.Iterable[str]] = [
('order cannot be canceled', ),
('order_not_exist_or_not_allow_to_cancel', )
]

DEFAULT_BALANCE_CURRENCIES_TO_FETCH = ["USDT"]

Expand Down Expand Up @@ -511,18 +516,34 @@ def _adapt_order_type(self, fixed):
up: Triggers when the price reaches or goes above the stopPrice.
"""
side = fixed.get(trading_enums.ExchangeConstantsOrderColumns.SIDE.value)
trigger_above = False
if trigger_direction in ("up", "loss"):
trigger_above = True
elif trigger_direction in ("down", "loss"):
trigger_above = False
else:
self.logger.error(f"Unknown trigger direction {trigger_direction} ({fixed})")
stop_price = fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.STOP_PRICE.value, None)
if side == trading_enums.TradeOrderSide.BUY.value:
if trigger_direction in ("up", "loss"):
if trigger_above:
updated_type = trading_enums.TradeOrderType.STOP_LOSS.value
elif trigger_direction in ("down", "entry"):
updated_type = trading_enums.TradeOrderType.TAKE_PROFIT.value
else:
# take profits are not yet handled as such: consider them as limit orders
updated_type = trading_enums.TradeOrderType.LIMIT.value # waiting for TP handling
if not fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]:
fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = stop_price # waiting for TP handling
else:
if trigger_direction in ("up", "entry"):
updated_type = trading_enums.TradeOrderType.TAKE_PROFIT.value
elif trigger_direction in ("down", "loss"):
# selling
if trigger_above:
# take profits are not yet handled as such: consider them as limit orders
updated_type = trading_enums.TradeOrderType.LIMIT.value # waiting for TP handling
if not fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]:
fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = stop_price # waiting for TP handling
else:
updated_type = trading_enums.TradeOrderType.STOP_LOSS.value
# stop loss are not tagged as such by ccxt, force it
fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = updated_type
fixed[trading_enums.ExchangeConstantsOrderColumns.TRIGGER_ABOVE.value] = trigger_above
return fixed

def parse_funding_rate(self, fixed, from_ticker=False, **kwargs):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,13 +374,15 @@ async def _create_order(self, order_description, symbol, created_groups, fees_cu
or order_description[trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value]
adapted_price = personal_data.decimal_adapt_price(symbol_market, decimal.Decimal(f"{price}"))
order_type = trading_enums.TraderOrderType(
order_description[trading_enums.TradingSignalOrdersAttrs.TYPE.value]
)
order_description[trading_enums.TradingSignalOrdersAttrs.TYPE.value]
)
if order_type in (trading_enums.TraderOrderType.BUY_MARKET, trading_enums.TraderOrderType.SELL_MARKET):
# side param is not supported for these orders
side = None
associated_entries = order_description.get(trading_enums.TradingSignalOrdersAttrs.ASSOCIATED_ORDER_IDS.value,
None)
associated_entries = order_description.get(
trading_enums.TradingSignalOrdersAttrs.ASSOCIATED_ORDER_IDS.value, None
)
trigger_above = order_description.get(trading_enums.TradingSignalOrdersAttrs.TRIGGER_ABOVE.value, None)
order = personal_data.create_order_instance(
trader=self.exchange_manager.trader,
order_type=order_type,
Expand All @@ -389,6 +391,7 @@ async def _create_order(self, order_description, symbol, created_groups, fees_cu
quantity=adapted_quantity,
price=adapted_price,
side=side,
trigger_above=trigger_above,
tag=order_description[trading_enums.TradingSignalOrdersAttrs.TAG.value],
order_id=order_description[trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value],
group=group,
Expand Down
43 changes: 43 additions & 0 deletions Trading/Mode/remote_trading_signals_trading_mode/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def mocked_sell_limit_signal():
trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: 1000.69,
trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: 0.0,
trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: True,
trading_enums.TradingSignalOrdersAttrs.TRIGGER_ABOVE.value: True,
trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: False,
trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: "46a0b2de-5b8f-4a39-89a0-137504f83dfc",
trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value:
Expand Down Expand Up @@ -278,6 +279,48 @@ def mocked_bundle_stop_loss_in_sell_limit_in_market_signal(mocked_sell_limit_sig
return mocked_buy_market_signal


@pytest.fixture
def mocked_bundle_trigger_above_stop_loss_in_sell_limit_in_market_signal(mocked_sell_limit_signal, mocked_buy_market_signal):
mocked_sell_limit_signal.content[trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value].append(
{
trading_enums.TradingSignalCommonsAttrs.ACTION.value: trading_enums.TradingSignalOrdersActions.CREATE.value,
trading_enums.TradingSignalOrdersAttrs.SYMBOL.value: "BTC/USDT:USDT",
trading_enums.TradingSignalOrdersAttrs.EXCHANGE.value: "bybit",
trading_enums.TradingSignalOrdersAttrs.EXCHANGE_TYPE.value: trading_enums.ExchangeTypes.SPOT.value,
trading_enums.TradingSignalOrdersAttrs.SIDE.value: trading_enums.TradeOrderSide.SELL.value,
trading_enums.TradingSignalOrdersAttrs.TYPE.value: trading_enums.TraderOrderType.STOP_LOSS.value,
trading_enums.TradingSignalOrdersAttrs.QUANTITY.value: 0.004,
trading_enums.TradingSignalOrdersAttrs.TARGET_AMOUNT.value: "5.356892%",
trading_enums.TradingSignalOrdersAttrs.TARGET_POSITION.value: 0,
trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_AMOUNT.value: None,
trading_enums.TradingSignalOrdersAttrs.UPDATED_TARGET_POSITION.value: None,
trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value: 999999990.0,
trading_enums.TradingSignalOrdersAttrs.UPDATED_LIMIT_PRICE.value: 0.0,
trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value: 0.0,
trading_enums.TradingSignalOrdersAttrs.UPDATED_STOP_PRICE.value: 0.0,
trading_enums.TradingSignalOrdersAttrs.CURRENT_PRICE.value: 1000.69,
trading_enums.TradingSignalOrdersAttrs.UPDATED_CURRENT_PRICE.value: 0.0,
trading_enums.TradingSignalOrdersAttrs.REDUCE_ONLY.value: True,
trading_enums.TradingSignalOrdersAttrs.TRIGGER_ABOVE.value: True,
trading_enums.TradingSignalOrdersAttrs.POST_ONLY.value: False,
trading_enums.TradingSignalOrdersAttrs.GROUP_ID.value: "46a0b2de-5b8f-4a39-89a0-137504f83dfc",
trading_enums.TradingSignalOrdersAttrs.GROUP_TYPE.value:
trading_personal_data.BalancedTakeProfitAndStopOrderGroup.__name__,
trading_enums.TradingSignalOrdersAttrs.TAG.value: "managed_order long exit (id: 143968020)",
trading_enums.TradingSignalOrdersAttrs.ORDER_ID.value: "5ad2a999-5ac2-47f0-9b69-c75a36f3858a",
trading_enums.TradingSignalOrdersAttrs.BUNDLED_WITH.value: "adc24701-573b-40dd-b6c9-3666cd22f33e",
trading_enums.TradingSignalOrdersAttrs.CHAINED_TO.value: "adc24701-573b-40dd-b6c9-3666cd22f33e",
trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value: [],
trading_enums.TradingSignalOrdersAttrs.ASSOCIATED_ORDER_IDS.value: None,
trading_enums.TradingSignalOrdersAttrs.UPDATE_WITH_TRIGGERING_ORDER_FEES.value: False,
}
)
mocked_buy_market_signal.content[trading_enums.TradingSignalOrdersAttrs.ADDITIONAL_ORDERS.value].append(
mocked_sell_limit_signal.content
)
return mocked_buy_market_signal


@pytest.fixture
def mocked_buy_market_signal():
return signals.Signal(
Expand Down
Loading
Loading