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

[Exchanges] support spot stop losses #1396

Merged
merged 2 commits into from
Dec 8, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
22 changes: 10 additions & 12 deletions Trading/Exchange/binance/binance_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class Binance(exchanges.RestExchange):
trading_enums.ExchangeTypes.SPOT.value: {
# order that should be self-managed by OctoBot
trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [
trading_enums.TraderOrderType.STOP_LOSS,
# trading_enums.TraderOrderType.STOP_LOSS, # supported on spot
trading_enums.TraderOrderType.STOP_LOSS_LIMIT,
trading_enums.TraderOrderType.TAKE_PROFIT,
trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,
Expand Down Expand Up @@ -209,17 +209,15 @@ async def set_symbol_partial_take_profit_stop_loss(self, symbol: str, inverse: b
"""

async def _create_market_stop_loss_order(self, symbol, quantity, price, side, current_price, params=None) -> dict:
if self.exchange_manager.is_future:
params = params or {}
params["stopLossPrice"] = price # make ccxt understand that it's a stop loss
order = self.connector.adapter.adapt_order(
await self.connector.client.create_order(
symbol, trading_enums.TradeOrderType.MARKET.value, side, quantity, params=params
),
symbol=symbol, quantity=quantity
)
return order
return await super()._create_market_stop_loss_order(symbol, quantity, price, side, current_price, params=params)
params = params or {}
params["stopLossPrice"] = price # make ccxt understand that it's a stop loss
order = self.connector.adapter.adapt_order(
await self.connector.client.create_order(
symbol, trading_enums.TradeOrderType.MARKET.value, side, quantity, params=params
),
symbol=symbol, quantity=quantity
)
return order

async def get_positions(self, symbols=None, **kwargs: dict) -> list:
positions = []
Expand Down
71 changes: 71 additions & 0 deletions Trading/Exchange/bingx/bingx_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,37 @@ class Bingx(exchanges.RestExchange):
# Set True when get_open_order() can return outdated orders (cancelled or not yet created)
CAN_HAVE_DELAYED_CANCELLED_ORDERS = True

# should be overridden locally to match exchange support
SUPPORTED_ELEMENTS = {
trading_enums.ExchangeTypes.FUTURE.value: {
# order that should be self-managed by OctoBot
trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [
trading_enums.TraderOrderType.STOP_LOSS,
trading_enums.TraderOrderType.STOP_LOSS_LIMIT,
trading_enums.TraderOrderType.TAKE_PROFIT,
trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,
trading_enums.TraderOrderType.TRAILING_STOP,
trading_enums.TraderOrderType.TRAILING_STOP_LIMIT
],
# order that can be bundled together to create them all in one request
# not supported or need custom mechanics with batch orders
trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},
},
trading_enums.ExchangeTypes.SPOT.value: {
# order that should be self-managed by OctoBot
trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [
# trading_enums.TraderOrderType.STOP_LOSS, # supported on spot
trading_enums.TraderOrderType.STOP_LOSS_LIMIT,
trading_enums.TraderOrderType.TAKE_PROFIT,
trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,
trading_enums.TraderOrderType.TRAILING_STOP,
trading_enums.TraderOrderType.TRAILING_STOP_LIMIT
],
# order that can be bundled together to create them all in one request
trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},
}
}

def get_adapter_class(self):
return BingxCCXTAdapter

Expand All @@ -64,17 +95,57 @@ def is_authenticated_request(self, url: str, method: str, headers: dict, body) -
and signature_identifier in url
)

async def _create_market_stop_loss_order(self, symbol, quantity, price, side, current_price, params=None) -> dict:
params = params or {}
params["stopLossPrice"] = price # make ccxt understand that it's a stop loss
order = self.connector.adapter.adapt_order(
await self.connector.client.create_order(
symbol, trading_enums.TradeOrderType.MARKET.value, side, quantity, params=params
),
symbol=symbol, quantity=quantity
)
return order

class BingxCCXTAdapter(exchanges.CCXTAdapter):

def _update_stop_order_or_trade_type_and_price(self, order_or_trade: dict):
info = order_or_trade.get(ccxt_constants.CCXT_INFO, {})
if stop_price := order_or_trade.get(trading_enums.ExchangeConstantsOrderColumns.STOP_LOSS_PRICE.value):
# from https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Current%20Open%20Orders
order_creation_price = float(
info.get("price") or order_or_trade.get(
trading_enums.ExchangeConstantsOrderColumns.PRICE.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 stop_price <= order_creation_price:
order_type = trading_enums.TradeOrderType.STOP_LOSS.value
else:
order_type = trading_enums.TradeOrderType.TAKE_PROFIT.value
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = order_type

def fix_order(self, raw, **kwargs):
fixed = super().fix_order(raw, **kwargs)
self._update_stop_order_or_trade_type_and_price(fixed)
try:
info = fixed[ccxt_constants.CCXT_INFO]
fixed[trading_enums.ExchangeConstantsOrderColumns.EXCHANGE_ID.value] = info["orderId"]
except KeyError:
pass
return fixed

def fix_trades(self, raw, **kwargs):
fixed = super().fix_trades(raw, **kwargs)
for trade in fixed:
self._update_stop_order_or_trade_type_and_price(trade)
return fixed

def fix_market_status(self, raw, remove_price_limits=False, **kwargs):
fixed = super().fix_market_status(raw, remove_price_limits=remove_price_limits, **kwargs)
if not fixed:
Expand Down
65 changes: 65 additions & 0 deletions Trading/Exchange/coinbase/coinbase_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,39 @@ class Coinbase(exchanges.RestExchange):
("insufficient balance in source account", )
]

# should be overridden locally to match exchange support
SUPPORTED_ELEMENTS = {
trading_enums.ExchangeTypes.FUTURE.value: {
# order that should be self-managed by OctoBot
trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [
trading_enums.TraderOrderType.STOP_LOSS,
trading_enums.TraderOrderType.STOP_LOSS_LIMIT,
trading_enums.TraderOrderType.TAKE_PROFIT,
trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,
trading_enums.TraderOrderType.TRAILING_STOP,
trading_enums.TraderOrderType.TRAILING_STOP_LIMIT
],
# order that can be bundled together to create them all in one request
# not supported or need custom mechanics with batch orders
trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},
},
trading_enums.ExchangeTypes.SPOT.value: {
# order that should be self-managed by OctoBot
trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [
# trading_enums.TraderOrderType.STOP_LOSS, # supported on spot (as spot limit)
Copy link
Member

Choose a reason for hiding this comment

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

should we also allow stop loss limit as it is supported?

trading_enums.TraderOrderType.STOP_LOSS_LIMIT,
trading_enums.TraderOrderType.TAKE_PROFIT,
trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,
trading_enums.TraderOrderType.TRAILING_STOP,
trading_enums.TraderOrderType.TRAILING_STOP_LIMIT
],
# order that can be bundled together to create them all in one request
trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},
}
}
# stop limit price is 2% bellow trigger price to ensure instant fill
STOP_LIMIT_ORDER_INSTANT_FILL_PRICE_RATIO = decimal.Decimal("0.98")

@classmethod
def get_name(cls):
return 'coinbase'
Expand Down Expand Up @@ -253,6 +286,21 @@ async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs:
# override for retrier
return await super().get_order(exchange_order_id, symbol=symbol, **kwargs)

@_coinbase_retrier
async def _create_market_stop_loss_order(self, symbol, quantity, price, side, current_price, params=None) -> dict:
params = params or {}
# warning coinbase only supports stop limit orders, stop markets are not available
if "stopLossPrice" not in params:
params["stopLossPrice"] = price # make ccxt understand that it's a stop loss
price = float(decimal.Decimal(str(price)) * self.STOP_LIMIT_ORDER_INSTANT_FILL_PRICE_RATIO)
order = self.connector.adapter.adapt_order(
await self.connector.client.create_order(
symbol, trading_enums.TradeOrderType.LIMIT.value, side, quantity, price, params=params
),
symbol=symbol, quantity=quantity
)
return order

def _get_ohlcv_params(self, time_frame, input_limit, **kwargs):
limit = input_limit
if not input_limit or input_limit > self.MAX_PAGINATION_LIMIT:
Expand Down Expand Up @@ -311,6 +359,21 @@ def _register_exchange_fees(self, order_or_trade):
except (KeyError, TypeError):
pass

def _update_stop_order_or_trade_type_and_price(self, order_or_trade: dict):
if stop_price := order_or_trade.get(trading_enums.ExchangeConstantsOrderColumns.STOP_PRICE.value):
# from https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Current%20Open%20Orders
limit_price = order_or_trade.get(trading_enums.ExchangeConstantsOrderColumns.PRICE.value)
# 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
):
# 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
)

def fix_order(self, raw, **kwargs):
"""
Handle 'order_type': 'UNKNOWN_ORDER_TYPE in coinbase order response (translated into None in ccxt order type)
Expand All @@ -336,6 +399,7 @@ def fix_order(self, raw, **kwargs):
'takeProfitPrice': None, 'stopLossPrice': None, 'exchange_id': 'd7471b4e-960e-4c92-bdbf-755cb92e176b'}
"""
fixed = super().fix_order(raw, **kwargs)
self._update_stop_order_or_trade_type_and_price(fixed)
if fixed[ccxt_enums.ExchangeOrderCCXTColumns.TYPE.value] is None:
if fixed[ccxt_enums.ExchangeOrderCCXTColumns.STOP_PRICE.value] is not None:
# stop price set: stop order
Expand All @@ -361,6 +425,7 @@ def fix_order(self, raw, **kwargs):
def fix_trades(self, raw, **kwargs):
raw = super().fix_trades(raw, **kwargs)
for trade in raw:
self._update_stop_order_or_trade_type_and_price(trade)
trade[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] = trading_enums.OrderStatus.CLOSED.value
try:
if trade[trading_enums.ExchangeConstantsOrderColumns.AMOUNT.value] is None and \
Expand Down
16 changes: 8 additions & 8 deletions Trading/Exchange/kucoin/kucoin_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class Kucoin(exchanges.RestExchange):
trading_enums.ExchangeTypes.SPOT.value: {
# order that should be self-managed by OctoBot
trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [
trading_enums.TraderOrderType.STOP_LOSS,
# trading_enums.TraderOrderType.STOP_LOSS, # supported on spot
trading_enums.TraderOrderType.STOP_LOSS_LIMIT,
trading_enums.TraderOrderType.TAKE_PROFIT,
trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,
Expand Down Expand Up @@ -333,8 +333,7 @@ async def get_open_orders(self, symbol=None, since=None, limit=None, **kwargs) -
limit = 200
regular_orders = await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs)
stop_orders = []
if self.exchange_manager.is_future:
# stop ordes are futures only for now
if "stop" not in kwargs:
# add untriggered stop orders (different api endpoint)
kwargs["stop"] = True
stop_orders = await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs)
Expand Down Expand Up @@ -494,6 +493,7 @@ def fix_order(self, raw, symbol=None, **kwargs):
def fix_trades(self, raw, **kwargs):
fixed = super().fix_trades(raw, **kwargs)
for trade in fixed:
self._adapt_order_type(trade)
self._ensure_fees(trade)
return fixed

Expand All @@ -506,16 +506,16 @@ def _adapt_order_type(self, fixed):
down: Triggers when the price reaches or goes below the stopPrice.
up: Triggers when the price reaches or goes above the stopPrice.
"""
side = fixed[trading_enums.ExchangeConstantsOrderColumns.SIDE.value]
side = fixed.get(trading_enums.ExchangeConstantsOrderColumns.SIDE.value)
if side == trading_enums.TradeOrderSide.BUY.value:
if trigger_direction == "up":
if trigger_direction in ("up", "loss"):
updated_type = trading_enums.TradeOrderType.STOP_LOSS.value
elif trigger_direction == "down":
elif trigger_direction in ("down", "entry"):
updated_type = trading_enums.TradeOrderType.TAKE_PROFIT.value
else:
if trigger_direction == "up":
if trigger_direction in ("up", "entry"):
updated_type = trading_enums.TradeOrderType.TAKE_PROFIT.value
elif trigger_direction == "down":
elif trigger_direction in ("down", "loss"):
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
Expand Down
Loading