Skip to content
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
52 changes: 45 additions & 7 deletions Trading/Exchange/binance/binance_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import ccxt

import octobot_commons.constants as commons_constants
import octobot_commons.symbols as symbols

import octobot_trading.enums as trading_enums
import octobot_trading.exchanges as exchanges
Expand Down Expand Up @@ -145,7 +146,6 @@ def supports_native_edit_order(self, order_type: trading_enums.TraderOrderType)
if self.exchange_manager.is_future:
# replace not supported in futures stop orders
return not is_stop
return True

async def get_account_id(self, **kwargs: dict) -> str:
try:
Expand Down Expand Up @@ -286,6 +286,36 @@ def get_order_additional_params(self, order) -> dict:
params["reduceOnly"] = order.reduce_only
return params

def order_request_kwargs_factory(
self,
exchange_order_id: str,
order_type: typing.Optional[trading_enums.TraderOrderType] = None,
**kwargs
) -> dict:
params = kwargs or {}
try:
if "stop" not in params:
order_type = (
order_type or
self.exchange_manager.exchange_personal_data.orders_manager.get_order(
None, exchange_order_id=exchange_order_id
).order_type
)
params["stop"] = (
personal_data.is_stop_order(order_type)
or personal_data.is_take_profit_order(order_type)
)
except KeyError as err:
self.logger.warning(
f"Order {exchange_order_id} not found in order manager: considering it a regular (no stop/take profit) order {err}"
)
return params

def fetch_stop_order_in_different_request(self, symbol: str) -> bool:
# Override in tentacles when stop orders need to be fetched in a separate request from CCXT
# Binance futures uses the algo orders endpoint for stop orders (but not for inverse orders)
return self.exchange_manager.is_future and not symbols.parse_symbol(symbol).is_inverse()

async def _create_market_sell_order(
self, symbol, quantity, price=None, reduce_only: bool = False, params=None
) -> dict:
Expand Down Expand Up @@ -365,11 +395,12 @@ def fix_order(self, raw, symbol=None, **kwargs):
return fixed

def _adapt_order_type(self, fixed):
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)
order_info = fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.INFO.value, {})
info_order_type = (order_info.get("type", {}) or order_info.get("orderType", None) or "").lower()
is_stop = info_order_type in self.STOP_ORDERS
is_tp = info_order_type in self.TAKE_PROFITS_ORDERS
if is_stop or is_tp:
if trigger_price := fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.TRIGGER_PRICE.value, None):
selling = (
fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.SIDE.value, None)
== trading_enums.TradeOrderSide.SELL.value
Expand All @@ -378,13 +409,15 @@ def _adapt_order_type(self, fixed):
trigger_above = False
if is_stop:
updated_type = trading_enums.TradeOrderType.STOP_LOSS.value
# force price to trigger price
fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = trigger_price
trigger_above = not selling # sell stop loss triggers when price is lower than target
elif 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
fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = trigger_price # waiting for TP handling
trigger_above = selling # sell take profit triggers when price is higher than target
else:
self.logger.error(
Expand All @@ -393,6 +426,11 @@ def _adapt_order_type(self, 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
else:
self.logger.error(
f"Unknown [{self.connector.exchange_manager.exchange_name}] order: stop order "
f"with no trigger price, order: {fixed}"
)
return fixed

def fix_trades(self, raw, **kwargs):
Expand Down
40 changes: 39 additions & 1 deletion Trading/Exchange/bitmart/bitmart_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library.
import typing
import ccxt.async_support

import octobot_trading.exchanges as exchanges
import octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants
import octobot_trading.enums as trading_enums
Expand All @@ -27,7 +29,43 @@ def _client_factory(
force_unauth,
keys_adapter: typing.Callable[[exchanges.ExchangeCredentialsData], exchanges.ExchangeCredentialsData]=None
) -> tuple:
return super()._client_factory(force_unauth, keys_adapter=self._keys_adapter)
client, is_authenticated = super()._client_factory(force_unauth, keys_adapter=self._keys_adapter)
if client:
client.handle_errors = self._patched_handle_errors_factory(client)
return client, is_authenticated

def _patched_handle_errors_factory(self, client: ccxt.async_support.Exchange):
self = client # set self to the client to use the client methods
def _patched_handle_errors(code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
# temporary patch waiting for CCXT fix (issue in ccxt 4.5.28)
if response is None:
return None
#
# spot
#
# {"message":"Bad Request [to is empty]","code":50000,"trace":"f9d46e1b-4edb-4d07-a06e-4895fb2fc8fc","data":{}}
# {"message":"Bad Request [from is empty]","code":50000,"trace":"579986f7-c93a-4559-926b-06ba9fa79d76","data":{}}
# {"message":"Kline size over 500","code":50004,"trace":"d625caa8-e8ca-4bd2-b77c-958776965819","data":{}}
# {"message":"Balance not enough","code":50020,"trace":"7c709d6a-3292-462c-98c5-32362540aeef","data":{}}
# {"code":40012,"message":"You contract account available balance not enough.","trace":"..."}
#
# contract
#
# {"errno":"OK","message":"INVALID_PARAMETER","code":49998,"trace":"eb5ebb54-23cd-4de2-9064-e090b6c3b2e3","data":null}
#
message = self.safe_string_lower(response, 'message') # PATCH
isErrorMessage = (message is not None) and (message != 'ok') and (message != 'success')
errorCode = self.safe_string(response, 'code')
isErrorCode = (errorCode is not None) and (errorCode != '1000')
if isErrorCode or isErrorMessage:
feedback = self.id + ' ' + body
self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback)
self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback)
self.throw_broadly_matched_exception(self.exceptions['broad'], errorCode, feedback)
raise ccxt.ExchangeError(feedback) # unknown message
return None
return _patched_handle_errors

def _keys_adapter(self, creds: exchanges.ExchangeCredentialsData) -> exchanges.ExchangeCredentialsData:
# use password as uid
Expand Down
8 changes: 7 additions & 1 deletion Trading/Exchange/bybit/bybit_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,13 @@ async def get_open_orders(self, symbol: str = None, since: int = None,
orders += await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs)
return orders

async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs: dict) -> dict:
async def get_order(
self,
exchange_order_id: str,
symbol: typing.Optional[str] = None,
order_type: typing.Optional[trading_enums.TraderOrderType] = None,
**kwargs: dict
) -> dict:
# regular get order is not supported
return await self.get_order_from_open_and_closed_orders(exchange_order_id, symbol=symbol, **kwargs)

Expand Down
10 changes: 8 additions & 2 deletions Trading/Exchange/coinbase/coinbase_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,9 +496,15 @@ async def get_open_orders(self, symbol=None, since=None, limit=None, **kwargs) -
return await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs)

@_coinbase_retrier
async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs: dict) -> dict:
async def get_order(
self,
exchange_order_id: str,
symbol: typing.Optional[str] = None,
order_type: typing.Optional[trading_enums.TraderOrderType] = None,
**kwargs: dict
) -> dict:
# override for retrier
return await super().get_order(exchange_order_id, symbol=symbol, **kwargs)
return await super().get_order(exchange_order_id, symbol=symbol, order_type=order_type, **kwargs)

async def _create_market_stop_loss_order(self, symbol, quantity, price, side, current_price, params=None) -> dict:
# warning coinbase only supports stop limit orders, stop markets are not available
Expand Down
23 changes: 14 additions & 9 deletions Trading/Exchange/kucoin/kucoin_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,23 +393,28 @@ async def get_balance(self, **kwargs: dict):
return balance
return await super().get_balance(**kwargs)

def fetch_stop_order_in_different_request(self, symbol: str) -> bool:
# Override in tentacles when stop orders need to be fetched in a separate request from CCXT
# Kucoin uses the algo orders endpoint for all stop orders
return True

@_kucoin_retrier
async def get_open_orders(self, symbol=None, since=None, limit=None, **kwargs) -> list:
if limit is None:
# default is 50, The maximum cannot exceed 1000
# https://www.kucoin.com/docs/rest/futures-trading/orders/get-order-list
limit = 200
regular_orders = await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs)
stop_orders = []
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)
return regular_orders + stop_orders
return await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs)

@_kucoin_retrier
async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs: dict) -> dict:
return await super().get_order(exchange_order_id, symbol=symbol, **kwargs)
async def get_order(
self,
exchange_order_id: str,
symbol: typing.Optional[str] = None,
order_type: typing.Optional[trading_enums.TraderOrderType] = None,
**kwargs: dict
) -> dict:
return await super().get_order(exchange_order_id, symbol=symbol, order_type=order_type, **kwargs)

async def create_order(self, order_type: trading_enums.TraderOrderType, symbol: str, quantity: decimal.Decimal,
price: decimal.Decimal = None, stop_price: decimal.Decimal = None,
Expand Down
10 changes: 8 additions & 2 deletions Trading/Exchange/mexc/mexc_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,16 @@ async def get_closed_orders(self, symbol: str = None, since: int = None, limit:
False
)

async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs: dict) -> dict:
async def get_order(
self,
exchange_order_id: str,
symbol: typing.Optional[str] = None,
order_type: typing.Optional[trading_enums.TraderOrderType] = None,
**kwargs: dict
) -> dict:
try:
return await super().get_order(
exchange_order_id, symbol=symbol, **kwargs
exchange_order_id, symbol=symbol, order_type=order_type, **kwargs
)
except octobot_trading.errors.FailedRequest as err:
if "Order does not exist" in str(err):
Expand Down
56 changes: 30 additions & 26 deletions Trading/Exchange/okx/okx_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ def get_bundled_order_parameters(self, order, stop_loss_price=None, take_profit_
return params

async def _get_all_typed_orders(self, method, symbol=None, since=None, limit=None, **kwargs) -> list:
# todo replace by settings fetch_stop_order_in_different_request method when OKX will be stable again
limit = self._fix_limit(limit)
is_stop_order = kwargs.get("stop", False)
if is_stop_order and self.connector.adapter.OKX_ORDER_TYPE not in kwargs:
Expand Down Expand Up @@ -294,10 +295,17 @@ async def get_closed_orders(self, symbol=None, since=None, limit=None, **kwargs)
super().get_closed_orders, symbol=symbol, since=since, limit=limit, **kwargs
)

async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs: dict) -> dict:
async def get_order(
self,
exchange_order_id: str,
symbol: typing.Optional[str] = None,
order_type: typing.Optional[trading_enums.TraderOrderType] = None,
**kwargs: dict
) -> dict:
try:
kwargs = self._get_okx_order_params(exchange_order_id, **kwargs)
order = await super().get_order(exchange_order_id, symbol=symbol, **kwargs)
order = await super().get_order(
exchange_order_id, symbol=symbol, order_type=order_type, **kwargs
)
return order
except trading_errors.NotSupported:
if kwargs.get("stop", False):
Expand All @@ -306,35 +314,31 @@ async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs:
return await self.get_order_from_open_and_closed_orders(exchange_order_id, symbol=symbol, **kwargs)
raise

async def cancel_order(
self, exchange_order_id: str, symbol: str, order_type: trading_enums.TraderOrderType, **kwargs: dict
) -> trading_enums.OrderStatus:
return await super().cancel_order(
exchange_order_id, symbol, order_type, **self._get_okx_order_params(exchange_order_id, order_type, **kwargs)
)

def _get_okx_order_params(self, exchange_order_id, order_type=None, **kwargs):
def order_request_kwargs_factory(
self,
exchange_order_id: str,
order_type: typing.Optional[trading_enums.TraderOrderType] = None,
**kwargs
) -> dict:
params = kwargs or {}
try:
if "stop" not in params:
order_type = order_type or \
self.exchange_manager.exchange_personal_data.orders_manager.get_order(
None, exchange_order_id=exchange_order_id
).order_type
params["stop"] = trading_personal_data.is_stop_order(order_type) \
order_type = (
order_type or
self.exchange_manager.exchange_personal_data.orders_manager.get_order(
None, exchange_order_id=exchange_order_id
).order_type
)
params["stop"] = (
trading_personal_data.is_stop_order(order_type)
or trading_personal_data.is_take_profit_order(order_type)
except KeyError:
pass
)
except KeyError as err:
self.logger.warning(
f"Order {exchange_order_id} not found in order manager: considering it a regular (no stop/take profit) order {err}"
)
return params

async def _verify_order(self, created_order, order_type, symbol, price, quantity, side, get_order_params=None):

if trading_personal_data.is_stop_order(order_type) or trading_personal_data.is_take_profit_order(order_type):
get_order_params = get_order_params or {}
get_order_params["stop"] = True
return await super()._verify_order(created_order, order_type, symbol, price, quantity, side,
get_order_params=get_order_params)

def _is_oco_order(self, params):
return all(
oco_order_param in (params or {})
Expand Down
12 changes: 10 additions & 2 deletions Trading/Exchange/phemex/phemex_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,16 @@ async def cancel_order(
order_status = trading_enums.OrderStatus.CANCELED
return order_status

async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs: dict) -> dict:
if order := await self.connector.get_order(symbol=symbol, exchange_order_id=exchange_order_id, **kwargs):
async def get_order(
self,
exchange_order_id: str,
symbol: typing.Optional[str] = None,
order_type: typing.Optional[trading_enums.TraderOrderType] = None,
**kwargs: dict
) -> dict:
if order := await self.connector.get_order(
symbol=symbol, exchange_order_id=exchange_order_id, order_type=order_type, **kwargs
):
return order
# try from closed orders (get_order is not returning filled or cancelled orders)
if order := await self.get_order_from_open_and_closed_orders(exchange_order_id, symbol):
Expand Down