Merge pull request #9847 from freqtrade/fix/bybit_unified

Bump ccxt to 4.2.47
This commit is contained in:
Matthias 2024-02-20 08:26:55 +01:00 committed by GitHub
commit bfb0b535b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 122 additions and 11 deletions

View File

@ -2,7 +2,7 @@ import asyncio
import logging import logging
import time import time
from functools import wraps from functools import wraps
from typing import Any, Callable, Optional, TypeVar, cast, overload from typing import Any, Callable, Dict, List, Optional, TypeVar, cast, overload
from freqtrade.constants import ExchangeConfig from freqtrade.constants import ExchangeConfig
from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError
@ -60,16 +60,17 @@ SUPPORTED_EXCHANGES = [
'okx', 'okx',
] ]
EXCHANGE_HAS_REQUIRED = [ # either the main, or replacement methods (array) is required
EXCHANGE_HAS_REQUIRED: Dict[str, List[str]] = {
# Required / private # Required / private
'fetchOrder', 'fetchOrder': ['fetchOpenOrder', 'fetchClosedOrder'],
'cancelOrder', 'cancelOrder': [],
'createOrder', 'createOrder': [],
'fetchBalance', 'fetchBalance': [],
# Public endpoints # Public endpoints
'fetchOHLCV', 'fetchOHLCV': [],
] }
EXCHANGE_HAS_OPTIONAL = [ EXCHANGE_HAS_OPTIONAL = [
# Private # Private

View File

@ -1260,11 +1260,43 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def fetch_order_emulated(self, order_id: str, pair: str, params: Dict) -> Dict:
"""
Emulated fetch_order if the exchange doesn't support fetch_order, but requires separate
calls for open and closed orders.
"""
try:
order = self._api.fetch_open_order(order_id, pair, params=params)
self._log_exchange_response('fetch_open_order', order)
order = self._order_contracts_to_amount(order)
return order
except ccxt.OrderNotFound:
try:
order = self._api.fetch_closed_order(order_id, pair, params=params)
self._log_exchange_response('fetch_closed_order', order)
order = self._order_contracts_to_amount(order)
return order
except ccxt.OrderNotFound as e:
raise RetryableOrderError(
f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e
except ccxt.InvalidOrder as e:
raise InvalidOrderException(
f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT) @retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
def fetch_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: def fetch_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
if self._config['dry_run']: if self._config['dry_run']:
return self.fetch_dry_run_order(order_id) return self.fetch_dry_run_order(order_id)
try: try:
if not self.exchange_has('fetchOrder'):
return self.fetch_order_emulated(order_id, pair, params)
order = self._api.fetch_order(order_id, pair, params=params) order = self._api.fetch_order(order_id, pair, params=params)
self._log_exchange_response('fetch_order', order) self._log_exchange_response('fetch_order', order)
order = self._order_contracts_to_amount(order) order = self._order_contracts_to_amount(order)

View File

@ -49,7 +49,11 @@ def validate_exchange(exchange: str) -> Tuple[bool, str]:
reason = '' reason = ''
if not ex_mod or not ex_mod.has: if not ex_mod or not ex_mod.has:
return False, '' return False, ''
missing = [k for k in EXCHANGE_HAS_REQUIRED if ex_mod.has.get(k) is not True] missing = [
k for k, v in EXCHANGE_HAS_REQUIRED.items()
if ex_mod.has.get(k) is not True
and not (all(ex_mod.has.get(x) for x in v))
]
if missing: if missing:
result = False result = False
reason += f"missing: {', '.join(missing)}" reason += f"missing: {', '.join(missing)}"

View File

@ -2,7 +2,7 @@ numpy==1.26.4
pandas==2.1.4 pandas==2.1.4
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==4.2.42 ccxt==4.2.47
cryptography==42.0.3 cryptography==42.0.3
aiohttp==3.9.3 aiohttp==3.9.3
SQLAlchemy==2.0.27 SQLAlchemy==2.0.27

View File

@ -70,7 +70,7 @@ setup(
], ],
install_requires=[ install_requires=[
# from requirements.txt # from requirements.txt
'ccxt>=4.2.15', 'ccxt>=4.2.47',
'SQLAlchemy>=2.0.6', 'SQLAlchemy>=2.0.6',
'python-telegram-bot>=20.1', 'python-telegram-bot>=20.1',
'arrow>=1.0.0', 'arrow>=1.0.0',

View File

@ -131,6 +131,7 @@ def test_bybit_fetch_order_canceled_empty(default_conf_usdt, mocker):
'amount': 20.0, 'amount': 20.0,
}) })
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, id='bybit') exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, id='bybit')
res = exchange.fetch_order('123', 'BTC/USDT') res = exchange.fetch_order('123', 'BTC/USDT')

View File

@ -3237,6 +3237,7 @@ def test_is_cancel_order_result_suitable(mocker, default_conf, exchange_name, or
def test_cancel_order_with_result(default_conf, mocker, exchange_name, corder, def test_cancel_order_with_result(default_conf, mocker, exchange_name, corder,
call_corder, call_forder): call_corder, call_forder):
default_conf['dry_run'] = False default_conf['dry_run'] = False
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
api_mock = MagicMock() api_mock = MagicMock()
api_mock.cancel_order = MagicMock(return_value=corder) api_mock.cancel_order = MagicMock(return_value=corder)
api_mock.fetch_order = MagicMock(return_value={}) api_mock.fetch_order = MagicMock(return_value={})
@ -3250,6 +3251,7 @@ def test_cancel_order_with_result(default_conf, mocker, exchange_name, corder,
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, caplog): def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, caplog):
default_conf['dry_run'] = False default_conf['dry_run'] = False
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
api_mock = MagicMock() api_mock = MagicMock()
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
@ -3347,6 +3349,7 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog):
order.myid = 123 order.myid = 123
order.symbol = 'TKN/BTC' order.symbol = 'TKN/BTC'
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
exchange._dry_run_open_orders['X'] = order exchange._dry_run_open_orders['X'] = order
assert exchange.fetch_order('X', 'TKN/BTC').myid == 123 assert exchange.fetch_order('X', 'TKN/BTC').myid == 123
@ -3391,10 +3394,80 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog):
order_id='_', pair='TKN/BTC') order_id='_', pair='TKN/BTC')
@pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_fetch_order_emulated(default_conf, mocker, exchange_name, caplog):
default_conf['dry_run'] = True
default_conf['exchange']['log_responses'] = True
order = MagicMock()
order.myid = 123
order.symbol = 'TKN/BTC'
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
mocker.patch(f'{EXMS}.exchange_has', return_value=False)
exchange._dry_run_open_orders['X'] = order
# Dry run - regular fetch_order behavior
assert exchange.fetch_order('X', 'TKN/BTC').myid == 123
with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
exchange.fetch_order('Y', 'TKN/BTC')
default_conf['dry_run'] = False
mocker.patch(f'{EXMS}.exchange_has', return_value=False)
api_mock = MagicMock()
api_mock.fetch_open_order = MagicMock(
return_value={'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'})
api_mock.fetch_closed_order = MagicMock(
return_value={'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
assert exchange.fetch_order(
'X', 'TKN/BTC') == {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}
assert log_has(
("API fetch_open_order: {\'id\': \'123\', \'amount\': 2, \'symbol\': \'TKN/BTC\'}"
),
caplog
)
assert api_mock.fetch_open_order.call_count == 1
assert api_mock.fetch_closed_order.call_count == 0
caplog.clear()
# open_order doesn't find order
api_mock.fetch_open_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found"))
api_mock.fetch_closed_order = MagicMock(
return_value={'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
assert exchange.fetch_order(
'X', 'TKN/BTC') == {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}
assert log_has(
("API fetch_closed_order: {\'id\': \'123\', \'amount\': 2, \'symbol\': \'TKN/BTC\'}"
),
caplog
)
assert api_mock.fetch_open_order.call_count == 1
assert api_mock.fetch_closed_order.call_count == 1
caplog.clear()
with pytest.raises(InvalidOrderException):
api_mock.fetch_open_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
api_mock.fetch_closed_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.fetch_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_open_order.call_count == 1
api_mock.fetch_open_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
'fetch_order_emulated', 'fetch_open_order',
retries=1,
order_id='_', pair='TKN/BTC', params={})
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_fetch_stoploss_order(default_conf, mocker, exchange_name): def test_fetch_stoploss_order(default_conf, mocker, exchange_name):
default_conf['dry_run'] = True default_conf['dry_run'] = True
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
order = MagicMock() order = MagicMock()
order.myid = 123 order.myid = 123
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)