mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge pull request #9847 from freqtrade/fix/bybit_unified
Bump ccxt to 4.2.47
This commit is contained in:
commit
bfb0b535b7
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)}"
|
||||||
|
|
|
@ -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
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -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',
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user