diff --git a/Trading/Exchange/binance/binance_exchange.py b/Trading/Exchange/binance/binance_exchange.py index b7f1fd955..dac088347 100644 --- a/Trading/Exchange/binance/binance_exchange.py +++ b/Trading/Exchange/binance/binance_exchange.py @@ -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 @@ -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: @@ -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: @@ -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 @@ -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( @@ -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): diff --git a/Trading/Exchange/bitmart/bitmart_exchange.py b/Trading/Exchange/bitmart/bitmart_exchange.py index ae7a223b7..bdc1812c7 100644 --- a/Trading/Exchange/bitmart/bitmart_exchange.py +++ b/Trading/Exchange/bitmart/bitmart_exchange.py @@ -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 @@ -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 diff --git a/Trading/Exchange/bybit/bybit_exchange.py b/Trading/Exchange/bybit/bybit_exchange.py index be282df76..4b8799f94 100644 --- a/Trading/Exchange/bybit/bybit_exchange.py +++ b/Trading/Exchange/bybit/bybit_exchange.py @@ -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) diff --git a/Trading/Exchange/coinbase/coinbase_exchange.py b/Trading/Exchange/coinbase/coinbase_exchange.py index 68c58fb2e..a39707ae4 100644 --- a/Trading/Exchange/coinbase/coinbase_exchange.py +++ b/Trading/Exchange/coinbase/coinbase_exchange.py @@ -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 diff --git a/Trading/Exchange/kucoin/kucoin_exchange.py b/Trading/Exchange/kucoin/kucoin_exchange.py index 060d6b56f..da98edcff 100644 --- a/Trading/Exchange/kucoin/kucoin_exchange.py +++ b/Trading/Exchange/kucoin/kucoin_exchange.py @@ -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, diff --git a/Trading/Exchange/mexc/mexc_exchange.py b/Trading/Exchange/mexc/mexc_exchange.py index 83edf8b26..21ab6226b 100644 --- a/Trading/Exchange/mexc/mexc_exchange.py +++ b/Trading/Exchange/mexc/mexc_exchange.py @@ -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): diff --git a/Trading/Exchange/okx/okx_exchange.py b/Trading/Exchange/okx/okx_exchange.py index bac46eb26..a7023cc4d 100644 --- a/Trading/Exchange/okx/okx_exchange.py +++ b/Trading/Exchange/okx/okx_exchange.py @@ -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: @@ -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): @@ -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 {}) diff --git a/Trading/Exchange/phemex/phemex_exchange.py b/Trading/Exchange/phemex/phemex_exchange.py index 2423d1515..79fb65c33 100644 --- a/Trading/Exchange/phemex/phemex_exchange.py +++ b/Trading/Exchange/phemex/phemex_exchange.py @@ -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):