Skip to content

Commit a084846

Browse files
authored
Merge pull request #1671 from Drakkar-Software/dev
Master merge
2 parents be4e317 + c8e23dc commit a084846

File tree

25 files changed

+11135
-52
lines changed

25 files changed

+11135
-52
lines changed

Trading/Exchange/binance/binance_exchange.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import ccxt
2121

2222
import octobot_commons.constants as commons_constants
23+
import octobot_commons.symbols as symbols
2324

2425
import octobot_trading.enums as trading_enums
2526
import octobot_trading.exchanges as exchanges
@@ -145,7 +146,6 @@ def supports_native_edit_order(self, order_type: trading_enums.TraderOrderType)
145146
if self.exchange_manager.is_future:
146147
# replace not supported in futures stop orders
147148
return not is_stop
148-
return True
149149

150150
async def get_account_id(self, **kwargs: dict) -> str:
151151
try:
@@ -286,6 +286,36 @@ def get_order_additional_params(self, order) -> dict:
286286
params["reduceOnly"] = order.reduce_only
287287
return params
288288

289+
def order_request_kwargs_factory(
290+
self,
291+
exchange_order_id: str,
292+
order_type: typing.Optional[trading_enums.TraderOrderType] = None,
293+
**kwargs
294+
) -> dict:
295+
params = kwargs or {}
296+
try:
297+
if "stop" not in params:
298+
order_type = (
299+
order_type or
300+
self.exchange_manager.exchange_personal_data.orders_manager.get_order(
301+
None, exchange_order_id=exchange_order_id
302+
).order_type
303+
)
304+
params["stop"] = (
305+
personal_data.is_stop_order(order_type)
306+
or personal_data.is_take_profit_order(order_type)
307+
)
308+
except KeyError as err:
309+
self.logger.warning(
310+
f"Order {exchange_order_id} not found in order manager: considering it a regular (no stop/take profit) order {err}"
311+
)
312+
return params
313+
314+
def fetch_stop_order_in_different_request(self, symbol: str) -> bool:
315+
# Override in tentacles when stop orders need to be fetched in a separate request from CCXT
316+
# Binance futures uses the algo orders endpoint for stop orders (but not for inverse orders)
317+
return self.exchange_manager.is_future and not symbols.parse_symbol(symbol).is_inverse()
318+
289319
async def _create_market_sell_order(
290320
self, symbol, quantity, price=None, reduce_only: bool = False, params=None
291321
) -> dict:
@@ -365,11 +395,12 @@ def fix_order(self, raw, symbol=None, **kwargs):
365395
return fixed
366396

367397
def _adapt_order_type(self, fixed):
368-
if order_type := fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.TYPE.value, None):
369-
is_stop = order_type.lower() in self.STOP_ORDERS
370-
is_tp = order_type.lower() in self.TAKE_PROFITS_ORDERS
371-
if is_stop or is_tp:
372-
stop_price = fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.STOP_PRICE.value, None)
398+
order_info = fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.INFO.value, {})
399+
info_order_type = (order_info.get("type", {}) or order_info.get("orderType", None) or "").lower()
400+
is_stop = info_order_type in self.STOP_ORDERS
401+
is_tp = info_order_type in self.TAKE_PROFITS_ORDERS
402+
if is_stop or is_tp:
403+
if trigger_price := fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.TRIGGER_PRICE.value, None):
373404
selling = (
374405
fixed.get(ccxt_enums.ExchangeOrderCCXTColumns.SIDE.value, None)
375406
== trading_enums.TradeOrderSide.SELL.value
@@ -378,13 +409,15 @@ def _adapt_order_type(self, fixed):
378409
trigger_above = False
379410
if is_stop:
380411
updated_type = trading_enums.TradeOrderType.STOP_LOSS.value
412+
# force price to trigger price
413+
fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = trigger_price
381414
trigger_above = not selling # sell stop loss triggers when price is lower than target
382415
elif is_tp:
383416
# updated_type = trading_enums.TradeOrderType.TAKE_PROFIT.value
384417
# take profits are not yet handled as such: consider them as limit orders
385418
updated_type = trading_enums.TradeOrderType.LIMIT.value # waiting for TP handling
386419
if not fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value]:
387-
fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = stop_price # waiting for TP handling
420+
fixed[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = trigger_price # waiting for TP handling
388421
trigger_above = selling # sell take profit triggers when price is higher than target
389422
else:
390423
self.logger.error(
@@ -393,6 +426,11 @@ def _adapt_order_type(self, fixed):
393426
# stop loss and take profits are not tagged as such by ccxt, force it
394427
fixed[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = updated_type
395428
fixed[trading_enums.ExchangeConstantsOrderColumns.TRIGGER_ABOVE.value] = trigger_above
429+
else:
430+
self.logger.error(
431+
f"Unknown [{self.connector.exchange_manager.exchange_name}] order: stop order "
432+
f"with no trigger price, order: {fixed}"
433+
)
396434
return fixed
397435

398436
def fix_trades(self, raw, **kwargs):

Trading/Exchange/bitmart/bitmart_exchange.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
# You should have received a copy of the GNU Lesser General Public
1515
# License along with this library.
1616
import typing
17+
import ccxt.async_support
18+
1719
import octobot_trading.exchanges as exchanges
1820
import octobot_trading.exchanges.connectors.ccxt.constants as ccxt_constants
1921
import octobot_trading.enums as trading_enums
@@ -27,7 +29,43 @@ def _client_factory(
2729
force_unauth,
2830
keys_adapter: typing.Callable[[exchanges.ExchangeCredentialsData], exchanges.ExchangeCredentialsData]=None
2931
) -> tuple:
30-
return super()._client_factory(force_unauth, keys_adapter=self._keys_adapter)
32+
client, is_authenticated = super()._client_factory(force_unauth, keys_adapter=self._keys_adapter)
33+
if client:
34+
client.handle_errors = self._patched_handle_errors_factory(client)
35+
return client, is_authenticated
36+
37+
def _patched_handle_errors_factory(self, client: ccxt.async_support.Exchange):
38+
self = client # set self to the client to use the client methods
39+
def _patched_handle_errors(code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
40+
# temporary patch waiting for CCXT fix (issue in ccxt 4.5.28)
41+
if response is None:
42+
return None
43+
#
44+
# spot
45+
#
46+
# {"message":"Bad Request [to is empty]","code":50000,"trace":"f9d46e1b-4edb-4d07-a06e-4895fb2fc8fc","data":{}}
47+
# {"message":"Bad Request [from is empty]","code":50000,"trace":"579986f7-c93a-4559-926b-06ba9fa79d76","data":{}}
48+
# {"message":"Kline size over 500","code":50004,"trace":"d625caa8-e8ca-4bd2-b77c-958776965819","data":{}}
49+
# {"message":"Balance not enough","code":50020,"trace":"7c709d6a-3292-462c-98c5-32362540aeef","data":{}}
50+
# {"code":40012,"message":"You contract account available balance not enough.","trace":"..."}
51+
#
52+
# contract
53+
#
54+
# {"errno":"OK","message":"INVALID_PARAMETER","code":49998,"trace":"eb5ebb54-23cd-4de2-9064-e090b6c3b2e3","data":null}
55+
#
56+
message = self.safe_string_lower(response, 'message') # PATCH
57+
isErrorMessage = (message is not None) and (message != 'ok') and (message != 'success')
58+
errorCode = self.safe_string(response, 'code')
59+
isErrorCode = (errorCode is not None) and (errorCode != '1000')
60+
if isErrorCode or isErrorMessage:
61+
feedback = self.id + ' ' + body
62+
self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback)
63+
self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
64+
self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback)
65+
self.throw_broadly_matched_exception(self.exceptions['broad'], errorCode, feedback)
66+
raise ccxt.ExchangeError(feedback) # unknown message
67+
return None
68+
return _patched_handle_errors
3169

3270
def _keys_adapter(self, creds: exchanges.ExchangeCredentialsData) -> exchanges.ExchangeCredentialsData:
3371
# use password as uid

Trading/Exchange/bybit/bybit_exchange.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,13 @@ async def get_open_orders(self, symbol: str = None, since: int = None,
168168
orders += await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs)
169169
return orders
170170

171-
async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs: dict) -> dict:
171+
async def get_order(
172+
self,
173+
exchange_order_id: str,
174+
symbol: typing.Optional[str] = None,
175+
order_type: typing.Optional[trading_enums.TraderOrderType] = None,
176+
**kwargs: dict
177+
) -> dict:
172178
# regular get order is not supported
173179
return await self.get_order_from_open_and_closed_orders(exchange_order_id, symbol=symbol, **kwargs)
174180

Trading/Exchange/coinbase/coinbase_exchange.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -496,9 +496,15 @@ async def get_open_orders(self, symbol=None, since=None, limit=None, **kwargs) -
496496
return await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs)
497497

498498
@_coinbase_retrier
499-
async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs: dict) -> dict:
499+
async def get_order(
500+
self,
501+
exchange_order_id: str,
502+
symbol: typing.Optional[str] = None,
503+
order_type: typing.Optional[trading_enums.TraderOrderType] = None,
504+
**kwargs: dict
505+
) -> dict:
500506
# override for retrier
501-
return await super().get_order(exchange_order_id, symbol=symbol, **kwargs)
507+
return await super().get_order(exchange_order_id, symbol=symbol, order_type=order_type, **kwargs)
502508

503509
async def _create_market_stop_loss_order(self, symbol, quantity, price, side, current_price, params=None) -> dict:
504510
# warning coinbase only supports stop limit orders, stop markets are not available

Trading/Exchange/kucoin/kucoin_exchange.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -393,23 +393,28 @@ async def get_balance(self, **kwargs: dict):
393393
return balance
394394
return await super().get_balance(**kwargs)
395395

396+
def fetch_stop_order_in_different_request(self, symbol: str) -> bool:
397+
# Override in tentacles when stop orders need to be fetched in a separate request from CCXT
398+
# Kucoin uses the algo orders endpoint for all stop orders
399+
return True
400+
396401
@_kucoin_retrier
397402
async def get_open_orders(self, symbol=None, since=None, limit=None, **kwargs) -> list:
398403
if limit is None:
399404
# default is 50, The maximum cannot exceed 1000
400405
# https://www.kucoin.com/docs/rest/futures-trading/orders/get-order-list
401406
limit = 200
402-
regular_orders = await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs)
403-
stop_orders = []
404-
if "stop" not in kwargs:
405-
# add untriggered stop orders (different api endpoint)
406-
kwargs["stop"] = True
407-
stop_orders = await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs)
408-
return regular_orders + stop_orders
407+
return await super().get_open_orders(symbol=symbol, since=since, limit=limit, **kwargs)
409408

410409
@_kucoin_retrier
411-
async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs: dict) -> dict:
412-
return await super().get_order(exchange_order_id, symbol=symbol, **kwargs)
410+
async def get_order(
411+
self,
412+
exchange_order_id: str,
413+
symbol: typing.Optional[str] = None,
414+
order_type: typing.Optional[trading_enums.TraderOrderType] = None,
415+
**kwargs: dict
416+
) -> dict:
417+
return await super().get_order(exchange_order_id, symbol=symbol, order_type=order_type, **kwargs)
413418

414419
async def create_order(self, order_type: trading_enums.TraderOrderType, symbol: str, quantity: decimal.Decimal,
415420
price: decimal.Decimal = None, stop_price: decimal.Decimal = None,

Trading/Exchange/mexc/mexc_exchange.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,10 +245,16 @@ async def get_closed_orders(self, symbol: str = None, since: int = None, limit:
245245
False
246246
)
247247

248-
async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs: dict) -> dict:
248+
async def get_order(
249+
self,
250+
exchange_order_id: str,
251+
symbol: typing.Optional[str] = None,
252+
order_type: typing.Optional[trading_enums.TraderOrderType] = None,
253+
**kwargs: dict
254+
) -> dict:
249255
try:
250256
return await super().get_order(
251-
exchange_order_id, symbol=symbol, **kwargs
257+
exchange_order_id, symbol=symbol, order_type=order_type, **kwargs
252258
)
253259
except octobot_trading.errors.FailedRequest as err:
254260
if "Order does not exist" in str(err):

Trading/Exchange/okx/okx_exchange.py

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ def get_bundled_order_parameters(self, order, stop_loss_price=None, take_profit_
267267
return params
268268

269269
async def _get_all_typed_orders(self, method, symbol=None, since=None, limit=None, **kwargs) -> list:
270+
# todo replace by settings fetch_stop_order_in_different_request method when OKX will be stable again
270271
limit = self._fix_limit(limit)
271272
is_stop_order = kwargs.get("stop", False)
272273
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)
294295
super().get_closed_orders, symbol=symbol, since=since, limit=limit, **kwargs
295296
)
296297

297-
async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs: dict) -> dict:
298+
async def get_order(
299+
self,
300+
exchange_order_id: str,
301+
symbol: typing.Optional[str] = None,
302+
order_type: typing.Optional[trading_enums.TraderOrderType] = None,
303+
**kwargs: dict
304+
) -> dict:
298305
try:
299-
kwargs = self._get_okx_order_params(exchange_order_id, **kwargs)
300-
order = await super().get_order(exchange_order_id, symbol=symbol, **kwargs)
306+
order = await super().get_order(
307+
exchange_order_id, symbol=symbol, order_type=order_type, **kwargs
308+
)
301309
return order
302310
except trading_errors.NotSupported:
303311
if kwargs.get("stop", False):
@@ -306,35 +314,31 @@ async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs:
306314
return await self.get_order_from_open_and_closed_orders(exchange_order_id, symbol=symbol, **kwargs)
307315
raise
308316

309-
async def cancel_order(
310-
self, exchange_order_id: str, symbol: str, order_type: trading_enums.TraderOrderType, **kwargs: dict
311-
) -> trading_enums.OrderStatus:
312-
return await super().cancel_order(
313-
exchange_order_id, symbol, order_type, **self._get_okx_order_params(exchange_order_id, order_type, **kwargs)
314-
)
315-
316-
def _get_okx_order_params(self, exchange_order_id, order_type=None, **kwargs):
317+
def order_request_kwargs_factory(
318+
self,
319+
exchange_order_id: str,
320+
order_type: typing.Optional[trading_enums.TraderOrderType] = None,
321+
**kwargs
322+
) -> dict:
317323
params = kwargs or {}
318324
try:
319325
if "stop" not in params:
320-
order_type = order_type or \
321-
self.exchange_manager.exchange_personal_data.orders_manager.get_order(
322-
None, exchange_order_id=exchange_order_id
323-
).order_type
324-
params["stop"] = trading_personal_data.is_stop_order(order_type) \
326+
order_type = (
327+
order_type or
328+
self.exchange_manager.exchange_personal_data.orders_manager.get_order(
329+
None, exchange_order_id=exchange_order_id
330+
).order_type
331+
)
332+
params["stop"] = (
333+
trading_personal_data.is_stop_order(order_type)
325334
or trading_personal_data.is_take_profit_order(order_type)
326-
except KeyError:
327-
pass
335+
)
336+
except KeyError as err:
337+
self.logger.warning(
338+
f"Order {exchange_order_id} not found in order manager: considering it a regular (no stop/take profit) order {err}"
339+
)
328340
return params
329341

330-
async def _verify_order(self, created_order, order_type, symbol, price, quantity, side, get_order_params=None):
331-
332-
if trading_personal_data.is_stop_order(order_type) or trading_personal_data.is_take_profit_order(order_type):
333-
get_order_params = get_order_params or {}
334-
get_order_params["stop"] = True
335-
return await super()._verify_order(created_order, order_type, symbol, price, quantity, side,
336-
get_order_params=get_order_params)
337-
338342
def _is_oco_order(self, params):
339343
return all(
340344
oco_order_param in (params or {})

Trading/Exchange/phemex/phemex_exchange.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,16 @@ async def cancel_order(
8686
order_status = trading_enums.OrderStatus.CANCELED
8787
return order_status
8888

89-
async def get_order(self, exchange_order_id: str, symbol: str = None, **kwargs: dict) -> dict:
90-
if order := await self.connector.get_order(symbol=symbol, exchange_order_id=exchange_order_id, **kwargs):
89+
async def get_order(
90+
self,
91+
exchange_order_id: str,
92+
symbol: typing.Optional[str] = None,
93+
order_type: typing.Optional[trading_enums.TraderOrderType] = None,
94+
**kwargs: dict
95+
) -> dict:
96+
if order := await self.connector.get_order(
97+
symbol=symbol, exchange_order_id=exchange_order_id, order_type=order_type, **kwargs
98+
):
9199
return order
92100
# try from closed orders (get_order is not returning filled or cancelled orders)
93101
if order := await self.get_order_from_open_and_closed_orders(exchange_order_id, symbol):
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Drakkar-Software OctoBot-Tentacles
2+
# Copyright (c) Drakkar-Software, All rights reserved.
3+
#
4+
# This library is free software; you can redistribute it and/or
5+
# modify it under the terms of the GNU Lesser General Public
6+
# License as published by the Free Software Foundation; either
7+
# version 3.0 of the License, or (at your option) any later version.
8+
#
9+
# This library is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
# Lesser General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU Lesser General Public
15+
# License along with this library.
16+
17+
from .ccxt import CCXTPolymarketExchange, CCXTAsyncPolymarketExchange, CCXTProPolymarketExchange
18+
from .polymarket_exchange import Polymarket

0 commit comments

Comments
 (0)