Skip to content

Commit

Permalink
[Exchanges] support spot stop losses
Browse files Browse the repository at this point in the history
  • Loading branch information
GuillaumeDSM committed Dec 8, 2024
1 parent 7bee003 commit 7bd9486
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 20 deletions.
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)
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
17 changes: 9 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,8 @@ 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:
# stop orders are futures only for now
# 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 +494,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 +507,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

0 comments on commit 7bd9486

Please sign in to comment.