diff --git a/tests/exchange/test_baseexchange.py b/tests/exchange/test_baseexchange.py new file mode 100644 index 000000000..b8556e8d4 --- /dev/null +++ b/tests/exchange/test_baseexchange.py @@ -0,0 +1,1592 @@ +# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement +# pragma pylint: disable=protected-access +import copy +import logging +from datetime import datetime, timezone +from random import randint +from unittest.mock import MagicMock, Mock, PropertyMock + +import arrow +import ccxt +import pytest +from pandas import DataFrame + +from freqtrade import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError) +from freqtrade.exchange import Binance, Exchange, Kraken +from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes, + timeframe_to_msecs, + timeframe_to_next_date, + timeframe_to_prev_date, + timeframe_to_seconds) +from freqtrade.resolvers.exchange_resolver import ExchangeResolver +from tests.conftest import get_patched_exchange, log_has, log_has_re + +# Make sure to always keep one exchange here which is NOT subclassed!! +EXCHANGES = ['bittrex', 'binance', 'kraken', ] + + +# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines +def get_mock_coro(return_value): + async def mock_coro(*args, **kwargs): + return return_value + + return Mock(wraps=mock_coro) + + +def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + fun, mock_ccxt_fun, **kwargs): + with pytest.raises(TemporaryError): + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + getattr(exchange, fun)(**kwargs) + assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 + + with pytest.raises(OperationalException): + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + getattr(exchange, fun)(**kwargs) + assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 + + +async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): + with pytest.raises(TemporaryError): + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + await getattr(exchange, fun)(**kwargs) + assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 + + with pytest.raises(OperationalException): + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + await getattr(exchange, fun)(**kwargs) + assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 + + +def test_init(default_conf, mocker, caplog): + caplog.set_level(logging.INFO) + get_patched_exchange(mocker, default_conf) + assert log_has('Instance is running with dry_run enabled', caplog) + + +def test_init_ccxt_kwargs(default_conf, mocker, caplog): + mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) + caplog.set_level(logging.INFO) + conf = copy.deepcopy(default_conf) + conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True} + ex = Exchange(conf) + assert log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog) + assert ex._api_async.aiohttp_trust_env + assert not ex._api.aiohttp_trust_env + + # Reset logging and config + caplog.clear() + conf = copy.deepcopy(default_conf) + conf['exchange']['ccxt_config'] = {'TestKWARG': 11} + ex = Exchange(conf) + assert not log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog) + assert not ex._api_async.aiohttp_trust_env + assert hasattr(ex._api, 'TestKWARG') + assert ex._api.TestKWARG == 11 + assert not hasattr(ex._api_async, 'TestKWARG') + assert log_has("Applying additional ccxt config: {'TestKWARG': 11}", caplog) + + +def test_destroy(default_conf, mocker, caplog): + caplog.set_level(logging.DEBUG) + get_patched_exchange(mocker, default_conf) + assert log_has('Exchange object destroyed, closing async loop', caplog) + + +def test_init_exception(default_conf, mocker): + default_conf['exchange']['name'] = 'wrong_exchange_name' + + with pytest.raises(OperationalException, + match=f"Exchange {default_conf['exchange']['name']} is not supported"): + Exchange(default_conf) + + default_conf['exchange']['name'] = 'binance' + with pytest.raises(OperationalException, + match=f"Exchange {default_conf['exchange']['name']} is not supported"): + mocker.patch("ccxt.binance", MagicMock(side_effect=AttributeError)) + Exchange(default_conf) + + with pytest.raises(OperationalException, + match=r"Initialization of ccxt failed. Reason: DeadBeef"): + mocker.patch("ccxt.binance", MagicMock(side_effect=ccxt.BaseError("DeadBeef"))) + Exchange(default_conf) + + +def test_exchange_resolver(default_conf, mocker, caplog): + mocker.patch.multiple('freqtrade.exchange.BaseExchange', + _init_ccxt=MagicMock(return_value=MagicMock()), + validate_timeframes=MagicMock()) + mocker.patch.multiple('freqtrade.exchange.Exchange', + _load_async_markets=MagicMock(), + validate_timeframe=MagicMock(), + validate_pairs=MagicMock()) + exchange = ExchangeResolver('Bittrex', default_conf).exchange + assert isinstance(exchange, Exchange) + assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) + caplog.clear() + + exchange = ExchangeResolver('kraken', default_conf).exchange + assert isinstance(exchange, Exchange) + assert isinstance(exchange, Kraken) + assert not isinstance(exchange, Binance) + assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", + caplog) + + exchange = ExchangeResolver('binance', default_conf).exchange + assert isinstance(exchange, Exchange) + assert isinstance(exchange, Binance) + assert not isinstance(exchange, Kraken) + + assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", + caplog) + + +def test_validate_order_time_in_force(default_conf, mocker, caplog): + caplog.set_level(logging.INFO) + # explicitly test bittrex, exchanges implementing other policies need seperate tests + ex = get_patched_exchange(mocker, default_conf, id="bittrex") + tif = { + "buy": "gtc", + "sell": "gtc", + } + + ex.validate_order_time_in_force(tif) + tif2 = { + "buy": "fok", + "sell": "ioc", + } + with pytest.raises(OperationalException, match=r"Time in force.*not supported for .*"): + ex.validate_order_time_in_force(tif2) + + # Patch to see if this will pass if the values are in the ft dict + ex._ft_has.update({"order_time_in_force": ["gtc", "fok", "ioc"]}) + ex.validate_order_time_in_force(tif2) + + +def test_symbol_amount_prec(default_conf, mocker): + ''' + Test rounds down to 4 Decimal places + ''' + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value={ + 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' + }) + mocker.patch('freqtrade.exchange.BaseExchange.name', PropertyMock(return_value='binance')) + + markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': 4}}}) + type(api_mock).markets = markets + + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + amount = 2.34559 + pair = 'ETH/BTC' + amount = exchange.symbol_amount_prec(pair, amount) + assert amount == 2.3455 + + +def test_symbol_price_prec(default_conf, mocker): + ''' + Test rounds up to 4 decimal places + ''' + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value={ + 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' + }) + mocker.patch('freqtrade.exchange.BaseExchange.name', PropertyMock(return_value='binance')) + + markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': 4}}}) + type(api_mock).markets = markets + + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + price = 2.34559 + pair = 'ETH/BTC' + price = exchange.symbol_price_prec(pair, price) + assert price == 2.3456 + + +def test_set_sandbox(default_conf, mocker): + """ + Test working scenario + """ + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value={ + 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' + }) + url_mock = PropertyMock(return_value={'test': "api-public.sandbox.gdax.com", + 'api': 'https://api.gdax.com'}) + type(api_mock).urls = url_mock + exchange = get_patched_exchange(mocker, default_conf, api_mock) + liveurl = exchange._api.urls['api'] + default_conf['exchange']['sandbox'] = True + exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') + assert exchange._api.urls['api'] != liveurl + + +def test_set_sandbox_exception(default_conf, mocker): + """ + Test Fail scenario + """ + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value={ + 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' + }) + url_mock = PropertyMock(return_value={'api': 'https://api.gdax.com'}) + type(api_mock).urls = url_mock + + with pytest.raises(OperationalException, match=r'does not provide a sandbox api'): + exchange = get_patched_exchange(mocker, default_conf, api_mock) + default_conf['exchange']['sandbox'] = True + exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') + + +def test__load_async_markets(default_conf, mocker, caplog): + exchange = get_patched_exchange(mocker, default_conf) + exchange._api_async.load_markets = get_mock_coro(None) + exchange._load_async_markets() + assert exchange._api_async.load_markets.call_count == 1 + caplog.set_level(logging.DEBUG) + + exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef")) + exchange._load_async_markets() + + assert log_has('Could not load async markets. Reason: deadbeef', caplog) + + +def test__load_markets(default_conf, mocker, caplog): + caplog.set_level(logging.INFO) + api_mock = MagicMock() + api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError")) + mocker.patch.multiple('freqtrade.exchange.BaseExchange', + _init_ccxt=MagicMock(return_value=api_mock), + validate_timeframes=MagicMock()) + mocker.patch.multiple('freqtrade.exchange.Exchange', + _load_async_markets=MagicMock(), + validate_timeframe=MagicMock(), + validate_pairs=MagicMock()) + Exchange(default_conf) + assert log_has('Unable to initialize markets. Reason: SomeError', caplog) + + expected_return = {'ETH/BTC': 'available'} + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value=expected_return) + type(api_mock).markets = expected_return + default_conf['exchange']['pair_whitelist'] = ['ETH/BTC'] + ex = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + assert ex.markets == expected_return + + +def test__reload_markets(default_conf, mocker, caplog): + caplog.set_level(logging.DEBUG) + initial_markets = {'ETH/BTC': {}} + + def load_markets(*args, **kwargs): + exchange._api.markets = updated_markets + + api_mock = MagicMock() + api_mock.load_markets = load_markets + type(api_mock).markets = initial_markets + default_conf['exchange']['markets_refresh_interval'] = 10 + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange._last_markets_refresh = arrow.utcnow().timestamp + updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}} + + assert exchange.markets == initial_markets + + # less than 10 minutes have passed, no reload + exchange._reload_markets() + assert exchange.markets == initial_markets + + # more than 10 minutes have passed, reload is executed + exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60 + exchange._reload_markets() + assert exchange.markets == updated_markets + assert log_has('Performing scheduled market reload..', caplog) + + +def test__reload_markets_exception(default_conf, mocker, caplog): + caplog.set_level(logging.DEBUG) + + api_mock = MagicMock() + api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError("LoadError")) + default_conf['exchange']['markets_refresh_interval'] = 10 + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + + # less than 10 minutes have passed, no reload + exchange._reload_markets() + assert exchange._last_markets_refresh == 0 + assert log_has_re(r"Could not reload markets.*", caplog) + + +def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly + api_mock = MagicMock() + type(api_mock).markets = PropertyMock(return_value={ + 'ETH/BTC': {}, 'LTC/BTC': {}, 'XRP/BTC': {}, 'NEO/BTC': {} + }) + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + + mocker.patch.multiple('freqtrade.exchange.BaseExchange', + _init_ccxt=MagicMock(return_value=api_mock), + validate_timeframes=MagicMock()) + mocker.patch.multiple('freqtrade.exchange.Exchange', + _load_async_markets=MagicMock(), + validate_timeframe=MagicMock()) + Exchange(default_conf) + + +def test_validate_pairs_not_available(default_conf, mocker): + api_mock = MagicMock() + type(api_mock).markets = PropertyMock(return_value={ + 'XRP/BTC': {'inactive': True} + }) + mocker.patch.multiple('freqtrade.exchange.BaseExchange', + _init_ccxt=MagicMock(return_value=api_mock), + validate_timeframes=MagicMock()) + mocker.patch.multiple('freqtrade.exchange.Exchange', + _load_async_markets=MagicMock(), + validate_timeframe=MagicMock()) + + with pytest.raises(OperationalException, match=r'not available'): + Exchange(default_conf) + + +def test_validate_pairs_exception(default_conf, mocker, caplog): + caplog.set_level(logging.INFO) + api_mock = MagicMock() + mocker.patch('freqtrade.exchange.BaseExchange.name', PropertyMock(return_value='Binance')) + + type(api_mock).markets = PropertyMock(return_value={}) + mocker.patch.multiple('freqtrade.exchange.BaseExchange', + _init_ccxt=api_mock, + validate_timeframes=MagicMock()) + mocker.patch.multiple('freqtrade.exchange.Exchange', + _load_async_markets=MagicMock(), + validate_timeframe=MagicMock()) + + with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'): + Exchange(default_conf) + + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})) + Exchange(default_conf) + assert log_has('Unable to validate pairs (assuming they are correct).', caplog) + + +def test_validate_pairs_restricted(default_conf, mocker, caplog): + api_mock = MagicMock() + type(api_mock).markets = PropertyMock(return_value={ + 'ETH/BTC': {}, 'LTC/BTC': {}, 'NEO/BTC': {}, + 'XRP/BTC': {'info': {'IsRestricted': True}} + }) + mocker.patch.multiple('freqtrade.exchange.BaseExchange', + _init_ccxt=MagicMock(return_value=api_mock), + validate_timeframes=MagicMock()) + mocker.patch.multiple('freqtrade.exchange.Exchange', + _load_async_markets=MagicMock(), + validate_timeframe=MagicMock()) + + Exchange(default_conf) + assert log_has(f"Pair XRP/BTC is restricted for some users on this exchange." + f"Please check if you are impacted by this restriction " + f"on the exchange and eventually remove XRP/BTC from your whitelist.", caplog) + + +def test_validate_timeframes(default_conf, mocker): + default_conf["ticker_interval"] = "5m" + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + timeframes = PropertyMock(return_value={'1m': '1m', + '5m': '5m', + '15m': '15m', + '1h': '1h'}) + type(api_mock).timeframes = timeframes + + mocker.patch.multiple('freqtrade.exchange.BaseExchange', + _init_ccxt=MagicMock(return_value=api_mock)) + mocker.patch.multiple('freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + validate_pairs=MagicMock()) + Exchange(default_conf) + + +def test_validate_timeframes_failed(default_conf, mocker): + default_conf["ticker_interval"] = "3m" + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + timeframes = PropertyMock(return_value={'1m': '1m', + '5m': '5m', + '15m': '15m', + '1h': '1h'}) + type(api_mock).timeframes = timeframes + + mocker.patch.multiple('freqtrade.exchange.BaseExchange', + _init_ccxt=MagicMock(return_value=api_mock)) + mocker.patch.multiple('freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + validate_pairs=MagicMock()) + with pytest.raises(OperationalException, + match=r"Invalid ticker interval '3m'. This exchange supports.*"): + Exchange(default_conf) + + +def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker): + default_conf["ticker_interval"] = "3m" + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + + # delete timeframes so magicmock does not autocreate it + del api_mock.timeframes + + mocker.patch.multiple('freqtrade.exchange.BaseExchange', + _init_ccxt=MagicMock(return_value=api_mock)) + mocker.patch.multiple('freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + validate_pairs=MagicMock()) + with pytest.raises(OperationalException, + match=r'The ccxt library does not provide the list of timeframes ' + r'for the exchange ".*" and this exchange ' + r'is therefore not supported. *'): + Exchange(default_conf) + + +def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker): + default_conf["ticker_interval"] = "3m" + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + + # delete timeframes so magicmock does not autocreate it + del api_mock.timeframes + + mocker.patch.multiple('freqtrade.exchange.BaseExchange', + _init_ccxt=MagicMock(return_value=api_mock)) + mocker.patch.multiple('freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={'timeframes': None}), + validate_pairs=MagicMock()) + with pytest.raises(OperationalException, + match=r'The ccxt library does not provide the list of timeframes ' + r'for the exchange ".*" and this exchange ' + r'is therefore not supported. *'): + Exchange(default_conf) + + +def test_validate_timeframes_not_in_config(default_conf, mocker): + del default_conf["ticker_interval"] + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + timeframes = PropertyMock(return_value={'1m': '1m', + '5m': '5m', + '15m': '15m', + '1h': '1h'}) + type(api_mock).timeframes = timeframes + + mocker.patch.multiple('freqtrade.exchange.BaseExchange', + _init_ccxt=MagicMock(return_value=api_mock)) + mocker.patch.multiple('freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + validate_pairs=MagicMock()) + Exchange(default_conf) + + +def test_validate_order_types(default_conf, mocker): + api_mock = MagicMock() + type(api_mock).has = PropertyMock(return_value={'createMarketOrder': True}) + mocker.patch.multiple('freqtrade.exchange.BaseExchange', + _init_ccxt=MagicMock(return_value=api_mock), + validate_timeframes=MagicMock(), + name='Bittrex') + mocker.patch.multiple('freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + validate_pairs=MagicMock(), + validate_timeframe=MagicMock()) + default_conf['order_types'] = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'market', + 'stoploss_on_exchange': False + } + + Exchange(default_conf) + + type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False}) + mocker.patch('freqtrade.exchange.BaseExchange._init_ccxt', MagicMock(return_value=api_mock)) + + default_conf['order_types'] = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'market', + 'stoploss_on_exchange': 'false' + } + + with pytest.raises(OperationalException, + match=r'Exchange .* does not support market orders.'): + Exchange(default_conf) + + default_conf['order_types'] = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': True + } + + with pytest.raises(OperationalException, + match=r'On exchange stoploss is not supported for .*'): + Exchange(default_conf) + + +def test_validate_order_types_not_in_config(default_conf, mocker): + mocker.patch.multiple('freqtrade.exchange.BaseExchange', + _init_ccxt=MagicMock(return_value=MagicMock()), + validate_timeframes=MagicMock()) + mocker.patch.multiple('freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + validate_pairs=MagicMock(), + validate_timeframe=MagicMock()) + + conf = copy.deepcopy(default_conf) + Exchange(conf) + + +def test_exchange_has(default_conf, mocker): + exchange = get_patched_exchange(mocker, default_conf) + assert not exchange.exchange_has('ASDFASDF') + api_mock = MagicMock() + + type(api_mock).has = PropertyMock(return_value={'deadbeef': True}) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + assert exchange.exchange_has("deadbeef") + + type(api_mock).has = PropertyMock(return_value={'deadbeef': False}) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + assert not exchange.exchange_has("deadbeef") + + +@pytest.mark.parametrize("side", [ + ("buy"), + ("sell") +]) +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_dry_run_order(default_conf, mocker, side, exchange_name): + default_conf['dry_run'] = True + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + + order = exchange.dry_run_order( + pair='ETH/BTC', ordertype='limit', side=side, amount=1, rate=200) + assert 'id' in order + assert f'dry_run_{side}_' in order["id"] + assert order["side"] == side + assert order["type"] == "limit" + assert order["pair"] == "ETH/BTC" + + +@pytest.mark.parametrize("side", [ + ("buy"), + ("sell") +]) +@pytest.mark.parametrize("ordertype,rate,marketprice", [ + ("market", None, None), + ("market", 200, True), + ("limit", 200, None), + ("stop_loss_limit", 200, None) +]) +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, exchange_name): + api_mock = MagicMock() + order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6)) + api_mock.options = {} if not marketprice else {"createMarketBuyOrderRequiresPrice": True} + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + default_conf['dry_run'] = False + mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + + order = exchange.create_order( + pair='ETH/BTC', ordertype=ordertype, side=side, amount=1, rate=200) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == ordertype + assert api_mock.create_order.call_args[0][2] == side + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] is rate + + +def test_buy_dry_run(default_conf, mocker): + default_conf['dry_run'] = True + exchange = get_patched_exchange(mocker, default_conf) + + order = exchange.buy(pair='ETH/BTC', ordertype='limit', + amount=1, rate=200, time_in_force='gtc') + assert 'id' in order + assert 'dry_run_buy_' in order['id'] + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_buy_prod(default_conf, mocker, exchange_name): + api_mock = MagicMock() + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_type = 'market' + time_in_force = 'gtc' + api_mock.options = {} + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + default_conf['dry_run'] = False + mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + + order = exchange.buy(pair='ETH/BTC', ordertype=order_type, + amount=1, rate=200, time_in_force=time_in_force) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == order_type + assert api_mock.create_order.call_args[0][2] == 'buy' + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] is None + + api_mock.create_order.reset_mock() + order_type = 'limit' + order = exchange.buy( + pair='ETH/BTC', + ordertype=order_type, + amount=1, + rate=200, + time_in_force=time_in_force) + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == order_type + assert api_mock.create_order.call_args[0][2] == 'buy' + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] == 200 + + # test exception handling + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("Not enough funds")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.buy(pair='ETH/BTC', ordertype=order_type, + amount=1, rate=200, time_in_force=time_in_force) + + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.buy(pair='ETH/BTC', ordertype='limit', + amount=1, rate=200, time_in_force=time_in_force) + + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.buy(pair='ETH/BTC', ordertype='market', + amount=1, rate=200, time_in_force=time_in_force) + + with pytest.raises(TemporaryError): + api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("Network disconnect")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.buy(pair='ETH/BTC', ordertype=order_type, + amount=1, rate=200, time_in_force=time_in_force) + + with pytest.raises(OperationalException): + api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("Unknown error")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.buy(pair='ETH/BTC', ordertype=order_type, + amount=1, rate=200, time_in_force=time_in_force) + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): + api_mock = MagicMock() + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + api_mock.options = {} + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + default_conf['dry_run'] = False + mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + + order_type = 'limit' + time_in_force = 'ioc' + + order = exchange.buy(pair='ETH/BTC', ordertype=order_type, + amount=1, rate=200, time_in_force=time_in_force) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == order_type + assert api_mock.create_order.call_args[0][2] == 'buy' + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] == 200 + assert "timeInForce" in api_mock.create_order.call_args[0][5] + assert api_mock.create_order.call_args[0][5]["timeInForce"] == time_in_force + + order_type = 'market' + time_in_force = 'ioc' + + order = exchange.buy(pair='ETH/BTC', ordertype=order_type, + amount=1, rate=200, time_in_force=time_in_force) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == order_type + assert api_mock.create_order.call_args[0][2] == 'buy' + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] is None + # Market orders should not send timeInForce!! + assert "timeInForce" not in api_mock.create_order.call_args[0][5] + + +def test_sell_dry_run(default_conf, mocker): + default_conf['dry_run'] = True + exchange = get_patched_exchange(mocker, default_conf) + + order = exchange.sell(pair='ETH/BTC', ordertype='limit', amount=1, rate=200) + assert 'id' in order + assert 'dry_run_sell_' in order['id'] + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_sell_prod(default_conf, mocker, exchange_name): + api_mock = MagicMock() + order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) + order_type = 'market' + api_mock.options = {} + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + default_conf['dry_run'] = False + + mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + + order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == order_type + assert api_mock.create_order.call_args[0][2] == 'sell' + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] is None + + api_mock.create_order.reset_mock() + order_type = 'limit' + order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == order_type + assert api_mock.create_order.call_args[0][2] == 'sell' + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] == 200 + + # test exception handling + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) + + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.sell(pair='ETH/BTC', ordertype='limit', amount=1, rate=200) + + # Market orders don't require price, so the behaviour is slightly different + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.sell(pair='ETH/BTC', ordertype='market', amount=1, rate=200) + + with pytest.raises(TemporaryError): + api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No Connection")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) + + with pytest.raises(OperationalException): + api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): + api_mock = MagicMock() + order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + api_mock.options = {} + default_conf['dry_run'] = False + mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + + order_type = 'limit' + time_in_force = 'ioc' + + order = exchange.sell(pair='ETH/BTC', ordertype=order_type, + amount=1, rate=200, time_in_force=time_in_force) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == order_type + assert api_mock.create_order.call_args[0][2] == 'sell' + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] == 200 + assert "timeInForce" in api_mock.create_order.call_args[0][5] + assert api_mock.create_order.call_args[0][5]["timeInForce"] == time_in_force + + order_type = 'market' + time_in_force = 'ioc' + order = exchange.sell(pair='ETH/BTC', ordertype=order_type, + amount=1, rate=200, time_in_force=time_in_force) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == order_type + assert api_mock.create_order.call_args[0][2] == 'sell' + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] is None + # Market orders should not send timeInForce!! + assert "timeInForce" not in api_mock.create_order.call_args[0][5] + + +def test_get_balance_dry_run(default_conf, mocker): + default_conf['dry_run'] = True + + exchange = get_patched_exchange(mocker, default_conf) + assert exchange.get_balance(currency='BTC') == 999.9 + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_balance_prod(default_conf, mocker, exchange_name): + api_mock = MagicMock() + api_mock.fetch_balance = MagicMock(return_value={'BTC': {'free': 123.4, 'total': 123.4}}) + default_conf['dry_run'] = False + + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + + assert exchange.get_balance(currency='BTC') == 123.4 + + with pytest.raises(OperationalException): + api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError("Unknown error")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + + exchange.get_balance(currency='BTC') + + with pytest.raises(TemporaryError, match=r'.*balance due to malformed exchange response:.*'): + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + mocker.patch('freqtrade.exchange.Exchange.get_balances', MagicMock(return_value={})) + mocker.patch('freqtrade.exchange.Kraken.get_balances', MagicMock(return_value={})) + exchange.get_balance(currency='BTC') + + +def test_get_balances_dry_run(default_conf, mocker): + default_conf['dry_run'] = True + exchange = get_patched_exchange(mocker, default_conf) + assert exchange.get_balances() == {} + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_balances_prod(default_conf, mocker, exchange_name): + balance_item = { + 'free': 10.0, + 'total': 10.0, + 'used': 0.0 + } + + api_mock = MagicMock() + api_mock.fetch_balance = MagicMock(return_value={ + '1ST': balance_item, + '2ST': balance_item, + '3ST': balance_item + }) + default_conf['dry_run'] = False + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + assert len(exchange.get_balances()) == 3 + assert exchange.get_balances()['1ST']['free'] == 10.0 + assert exchange.get_balances()['1ST']['total'] == 10.0 + assert exchange.get_balances()['1ST']['used'] == 0.0 + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + "get_balances", "fetch_balance") + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_tickers(default_conf, mocker, exchange_name): + api_mock = MagicMock() + tick = {'ETH/BTC': { + 'symbol': 'ETH/BTC', + 'bid': 0.5, + 'ask': 1, + 'last': 42, + }, 'BCH/BTC': { + 'symbol': 'BCH/BTC', + 'bid': 0.6, + 'ask': 0.5, + 'last': 41, + } + } + api_mock.fetch_tickers = MagicMock(return_value=tick) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + # retrieve original ticker + tickers = exchange.get_tickers() + + assert 'ETH/BTC' in tickers + assert 'BCH/BTC' in tickers + assert tickers['ETH/BTC']['bid'] == 0.5 + assert tickers['ETH/BTC']['ask'] == 1 + assert tickers['BCH/BTC']['bid'] == 0.6 + assert tickers['BCH/BTC']['ask'] == 0.5 + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + "get_tickers", "fetch_tickers") + + with pytest.raises(OperationalException): + api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported("DeadBeef")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.get_tickers() + + api_mock.fetch_tickers = MagicMock(return_value={}) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.get_tickers() + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_ticker(default_conf, mocker, exchange_name): + api_mock = MagicMock() + tick = { + 'symbol': 'ETH/BTC', + 'bid': 0.00001098, + 'ask': 0.00001099, + 'last': 0.0001, + } + api_mock.fetch_ticker = MagicMock(return_value=tick) + api_mock.markets = {'ETH/BTC': {'active': True}} + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + # retrieve original ticker + ticker = exchange.get_ticker(pair='ETH/BTC') + + assert ticker['bid'] == 0.00001098 + assert ticker['ask'] == 0.00001099 + + # change the ticker + tick = { + 'symbol': 'ETH/BTC', + 'bid': 0.5, + 'ask': 1, + 'last': 42, + } + api_mock.fetch_ticker = MagicMock(return_value=tick) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + + # if not caching the result we should get the same ticker + # if not fetching a new result we should get the cached ticker + ticker = exchange.get_ticker(pair='ETH/BTC') + + assert api_mock.fetch_ticker.call_count == 1 + assert ticker['bid'] == 0.5 + assert ticker['ask'] == 1 + + assert 'ETH/BTC' in exchange._cached_ticker + assert exchange._cached_ticker['ETH/BTC']['bid'] == 0.5 + assert exchange._cached_ticker['ETH/BTC']['ask'] == 1 + + # Test caching + api_mock.fetch_ticker = MagicMock() + exchange.get_ticker(pair='ETH/BTC', refresh=False) + assert api_mock.fetch_ticker.call_count == 0 + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + "get_ticker", "fetch_ticker", + pair='ETH/BTC', refresh=True) + + api_mock.fetch_ticker = MagicMock(return_value={}) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.get_ticker(pair='ETH/BTC', refresh=True) + + with pytest.raises(DependencyException, match=r'Pair XRP/ETH not available'): + exchange.get_ticker(pair='XRP/ETH', refresh=True) + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + tick = [ + [ + arrow.utcnow().timestamp * 1000, # unix timestamp ms + 1, # open + 2, # high + 3, # low + 4, # close + 5, # volume (in quote currency) + ] + ] + pair = 'ETH/BTC' + + async def mock_candle_hist(pair, ticker_interval, since_ms): + return pair, ticker_interval, tick + + exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) + # one_call calculation * 1.8 should do 2 calls + since = 5 * 60 * 500 * 1.8 + ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) + + assert exchange._async_get_candle_history.call_count == 2 + # Returns twice the above tick + assert len(ret) == 2 + + +def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: + tick = [ + [ + (arrow.utcnow().timestamp - 1) * 1000, # unix timestamp ms + 1, # open + 2, # high + 3, # low + 4, # close + 5, # volume (in quote currency) + ], + [ + arrow.utcnow().timestamp * 1000, # unix timestamp ms + 3, # open + 1, # high + 4, # low + 6, # close + 5, # volume (in quote currency) + ] + ] + + caplog.set_level(logging.DEBUG) + exchange = get_patched_exchange(mocker, default_conf) + exchange._api_async.fetch_ohlcv = get_mock_coro(tick) + + pairs = [('IOTA/ETH', '5m'), ('XRP/ETH', '5m')] + # empty dicts + assert not exchange._klines + exchange.refresh_latest_ohlcv(pairs) + + assert log_has(f'Refreshing ohlcv data for {len(pairs)} pairs', caplog) + assert exchange._klines + assert exchange._api_async.fetch_ohlcv.call_count == 2 + for pair in pairs: + assert isinstance(exchange.klines(pair), DataFrame) + assert len(exchange.klines(pair)) > 0 + + # klines function should return a different object on each call + # if copy is "True" + assert exchange.klines(pair) is not exchange.klines(pair) + assert exchange.klines(pair) is not exchange.klines(pair, copy=True) + assert exchange.klines(pair, copy=True) is not exchange.klines(pair, copy=True) + assert exchange.klines(pair, copy=False) is exchange.klines(pair, copy=False) + + # test caching + exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]) + + assert exchange._api_async.fetch_ohlcv.call_count == 2 + assert log_has(f"Using cached ohlcv data for pair {pairs[0][0]}, interval {pairs[0][1]} ...", + caplog) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("exchange_name", EXCHANGES) +async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name): + tick = [ + [ + arrow.utcnow().timestamp * 1000, # unix timestamp ms + 1, # open + 2, # high + 3, # low + 4, # close + 5, # volume (in quote currency) + ] + ] + + caplog.set_level(logging.DEBUG) + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + # Monkey-patch async function + exchange._api_async.fetch_ohlcv = get_mock_coro(tick) + + pair = 'ETH/BTC' + res = await exchange._async_get_candle_history(pair, "5m") + assert type(res) is tuple + assert len(res) == 3 + assert res[0] == pair + assert res[1] == "5m" + assert res[2] == tick + assert exchange._api_async.fetch_ohlcv.call_count == 1 + assert not log_has(f"Using cached ohlcv data for {pair} ...", caplog) + + # exchange = Exchange(default_conf) + await async_ccxt_exception(mocker, default_conf, MagicMock(), + "_async_get_candle_history", "fetch_ohlcv", + pair='ABCD/BTC', ticker_interval=default_conf['ticker_interval']) + + api_mock = MagicMock() + with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'): + api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + await exchange._async_get_candle_history(pair, "5m", + (arrow.utcnow().timestamp - 2000) * 1000) + + +@pytest.mark.asyncio +async def test__async_get_candle_history_empty(default_conf, mocker, caplog): + """ Test empty exchange result """ + tick = [] + + caplog.set_level(logging.DEBUG) + exchange = get_patched_exchange(mocker, default_conf) + # Monkey-patch async function + exchange._api_async.fetch_ohlcv = get_mock_coro([]) + + exchange = Exchange(default_conf) + pair = 'ETH/BTC' + res = await exchange._async_get_candle_history(pair, "5m") + assert type(res) is tuple + assert len(res) == 3 + assert res[0] == pair + assert res[1] == "5m" + assert res[2] == tick + assert exchange._api_async.fetch_ohlcv.call_count == 1 + + +def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog): + + async def mock_get_candle_hist(pair, *args, **kwargs): + if pair == 'ETH/BTC': + return [[]] + else: + raise TypeError() + + exchange = get_patched_exchange(mocker, default_conf) + + # Monkey-patch async function with empty result + exchange._api_async.fetch_ohlcv = MagicMock(side_effect=mock_get_candle_hist) + + pairs = [("ETH/BTC", "5m"), ("XRP/BTC", "5m")] + res = exchange.refresh_latest_ohlcv(pairs) + assert exchange._klines + assert exchange._api_async.fetch_ohlcv.call_count == 2 + + assert type(res) is list + assert len(res) == 2 + # Test that each is in list at least once as order is not guaranteed + assert type(res[0]) is tuple or type(res[1]) is tuple + assert type(res[0]) is TypeError or type(res[1]) is TypeError + assert log_has("Error loading ETH/BTC. Result was [[]].", caplog) + assert log_has("Async code raised an exception: TypeError", caplog) + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_order_book(default_conf, mocker, order_book_l2, exchange_name): + default_conf['exchange']['name'] = exchange_name + api_mock = MagicMock() + + api_mock.fetch_l2_order_book = order_book_l2 + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + order_book = exchange.get_order_book(pair='ETH/BTC', limit=10) + assert 'bids' in order_book + assert 'asks' in order_book + assert len(order_book['bids']) == 10 + assert len(order_book['asks']) == 10 + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_order_book_exception(default_conf, mocker, exchange_name): + api_mock = MagicMock() + with pytest.raises(OperationalException): + api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported("Not supported")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.get_order_book(pair='ETH/BTC', limit=50) + with pytest.raises(TemporaryError): + api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.get_order_book(pair='ETH/BTC', limit=50) + with pytest.raises(OperationalException): + api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.get_order_book(pair='ETH/BTC', limit=50) + + +def make_fetch_ohlcv_mock(data): + def fetch_ohlcv_mock(pair, timeframe, since): + if since: + assert since > data[-1][0] + return [] + return data + return fetch_ohlcv_mock + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +@pytest.mark.asyncio +async def test___async_get_candle_history_sort(default_conf, mocker, exchange_name): + def sort_data(data, key): + return sorted(data, key=key) + + # GDAX use-case (real data from GDAX) + # This ticker history is ordered DESC (newest first, oldest last) + tick = [ + [1527833100000, 0.07666, 0.07671, 0.07666, 0.07668, 16.65244264], + [1527832800000, 0.07662, 0.07666, 0.07662, 0.07666, 1.30051526], + [1527832500000, 0.07656, 0.07661, 0.07656, 0.07661, 12.034778840000001], + [1527832200000, 0.07658, 0.07658, 0.07655, 0.07656, 0.59780186], + [1527831900000, 0.07658, 0.07658, 0.07658, 0.07658, 1.76278136], + [1527831600000, 0.07658, 0.07658, 0.07658, 0.07658, 2.22646521], + [1527831300000, 0.07655, 0.07657, 0.07655, 0.07657, 1.1753], + [1527831000000, 0.07654, 0.07654, 0.07651, 0.07651, 0.8073060299999999], + [1527830700000, 0.07652, 0.07652, 0.07651, 0.07652, 10.04822687], + [1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867] + ] + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange._api_async.fetch_ohlcv = get_mock_coro(tick) + sort_mock = mocker.patch('freqtrade.exchange.exchange.sorted', MagicMock(side_effect=sort_data)) + # Test the ticker history sort + res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval']) + assert res[0] == 'ETH/BTC' + ticks = res[2] + + assert sort_mock.call_count == 1 + assert ticks[0][0] == 1527830400000 + assert ticks[0][1] == 0.07649 + assert ticks[0][2] == 0.07651 + assert ticks[0][3] == 0.07649 + assert ticks[0][4] == 0.07651 + assert ticks[0][5] == 2.5734867 + + assert ticks[9][0] == 1527833100000 + assert ticks[9][1] == 0.07666 + assert ticks[9][2] == 0.07671 + assert ticks[9][3] == 0.07666 + assert ticks[9][4] == 0.07668 + assert ticks[9][5] == 16.65244264 + + # Bittrex use-case (real data from Bittrex) + # This ticker history is ordered ASC (oldest first, newest last) + tick = [ + [1527827700000, 0.07659999, 0.0766, 0.07627, 0.07657998, 1.85216924], + [1527828000000, 0.07657995, 0.07657995, 0.0763, 0.0763, 26.04051037], + [1527828300000, 0.0763, 0.07659998, 0.0763, 0.0764, 10.36434124], + [1527828600000, 0.0764, 0.0766, 0.0764, 0.0766, 5.71044773], + [1527828900000, 0.0764, 0.07666998, 0.0764, 0.07666998, 47.48888565], + [1527829200000, 0.0765, 0.07672999, 0.0765, 0.07672999, 3.37640326], + [1527829500000, 0.0766, 0.07675, 0.0765, 0.07675, 8.36203831], + [1527829800000, 0.07675, 0.07677999, 0.07620002, 0.076695, 119.22963884], + [1527830100000, 0.076695, 0.07671, 0.07624171, 0.07671, 1.80689244], + [1527830400000, 0.07671, 0.07674399, 0.07629216, 0.07655213, 2.31452783] + ] + exchange._api_async.fetch_ohlcv = get_mock_coro(tick) + # Reset sort mock + sort_mock = mocker.patch('freqtrade.exchange.sorted', MagicMock(side_effect=sort_data)) + # Test the ticker history sort + res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval']) + assert res[0] == 'ETH/BTC' + assert res[1] == default_conf['ticker_interval'] + ticks = res[2] + # Sorted not called again - data is already in order + assert sort_mock.call_count == 0 + assert ticks[0][0] == 1527827700000 + assert ticks[0][1] == 0.07659999 + assert ticks[0][2] == 0.0766 + assert ticks[0][3] == 0.07627 + assert ticks[0][4] == 0.07657998 + assert ticks[0][5] == 1.85216924 + + assert ticks[9][0] == 1527830400000 + assert ticks[9][1] == 0.07671 + assert ticks[9][2] == 0.07674399 + assert ticks[9][3] == 0.07629216 + assert ticks[9][4] == 0.07655213 + assert ticks[9][5] == 2.31452783 + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_cancel_order_dry_run(default_conf, mocker, exchange_name): + default_conf['dry_run'] = True + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + assert exchange.cancel_order(order_id='123', pair='TKN/BTC') is None + + +# Ensure that if not dry_run, we should call API +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_cancel_order(default_conf, mocker, exchange_name): + default_conf['dry_run'] = False + api_mock = MagicMock() + api_mock.cancel_order = MagicMock(return_value=123) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123 + + with pytest.raises(InvalidOrderException): + api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.cancel_order(order_id='_', pair='TKN/BTC') + assert api_mock.cancel_order.call_count == 1 + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + "cancel_order", "cancel_order", + order_id='_', pair='TKN/BTC') + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_order(default_conf, mocker, exchange_name): + default_conf['dry_run'] = True + order = MagicMock() + order.myid = 123 + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange._dry_run_open_orders['X'] = order + assert exchange.get_order('X', 'TKN/BTC').myid == 123 + + with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): + exchange.get_order('Y', 'TKN/BTC') + + default_conf['dry_run'] = False + api_mock = MagicMock() + api_mock.fetch_order = MagicMock(return_value=456) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + assert exchange.get_order('X', 'TKN/BTC') == 456 + + with pytest.raises(InvalidOrderException): + api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.get_order(order_id='_', pair='TKN/BTC') + assert api_mock.fetch_order.call_count == 1 + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + 'get_order', 'fetch_order', + order_id='_', pair='TKN/BTC') + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_name(default_conf, mocker, exchange_name): + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + + assert exchange.name == exchange_name.title() + assert exchange.id == exchange_name + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_trades_for_order(default_conf, mocker, exchange_name): + order_id = 'ABCD-ABCD' + since = datetime(2018, 5, 5, tzinfo=timezone.utc) + default_conf["dry_run"] = False + mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + api_mock = MagicMock() + + api_mock.fetch_my_trades = MagicMock(return_value=[{'id': 'TTR67E-3PFBD-76IISV', + 'order': 'ABCD-ABCD', + 'info': {'pair': 'XLTCZBTC', + 'time': 1519860024.4388, + 'type': 'buy', + 'ordertype': 'limit', + 'price': '20.00000', + 'cost': '38.62000', + 'fee': '0.06179', + 'vol': '5', + 'id': 'ABCD-ABCD'}, + 'timestamp': 1519860024438, + 'datetime': '2018-02-28T23:20:24.438Z', + 'symbol': 'LTC/BTC', + 'type': 'limit', + 'side': 'buy', + 'price': 165.0, + 'amount': 0.2340606, + 'fee': {'cost': 0.06179, 'currency': 'BTC'} + }]) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + + orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since) + assert len(orders) == 1 + assert orders[0]['price'] == 165 + assert api_mock.fetch_my_trades.call_count == 1 + # since argument should be + assert isinstance(api_mock.fetch_my_trades.call_args[0][1], int) + assert api_mock.fetch_my_trades.call_args[0][0] == 'LTC/BTC' + # Same test twice, hardcoded number and doing the same calculation + assert api_mock.fetch_my_trades.call_args[0][1] == 1525478395000 + assert api_mock.fetch_my_trades.call_args[0][1] == int(since.timestamp() - 5) * 1000 + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + 'get_trades_for_order', 'fetch_my_trades', + order_id=order_id, pair='LTC/BTC', since=since) + + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False)) + assert exchange.get_trades_for_order(order_id, 'LTC/BTC', since) == [] + + +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_fee(default_conf, mocker, exchange_name): + api_mock = MagicMock() + api_mock.calculate_fee = MagicMock(return_value={ + 'type': 'taker', + 'currency': 'BTC', + 'rate': 0.025, + 'cost': 0.05 + }) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + + assert exchange.get_fee() == 0.025 + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + 'get_fee', 'calculate_fee') + + +def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker): + exchange = get_patched_exchange(mocker, default_conf, 'bittrex') + with pytest.raises(OperationalException, match=r"stoploss_limit is not implemented .*"): + exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + + +def test_merge_ft_has_dict(default_conf, mocker): + mocker.patch.multiple('freqtrade.exchange.BaseExchange', + _init_ccxt=MagicMock(return_value=MagicMock()), + validate_timeframes=MagicMock()) + mocker.patch.multiple('freqtrade.exchange.Exchange', + _load_async_markets=MagicMock(), + validate_pairs=MagicMock(), + validate_timeframe=MagicMock()) + ex = Exchange(default_conf) + assert ex._ft_has == Exchange._ft_has_default + + ex = Kraken(default_conf) + assert ex._ft_has == Exchange._ft_has_default + + # Binance defines different values + ex = Binance(default_conf) + assert ex._ft_has != Exchange._ft_has_default + assert ex._ft_has['stoploss_on_exchange'] + assert ex._ft_has['order_time_in_force'] == ['gtc', 'fok', 'ioc'] + + conf = copy.deepcopy(default_conf) + conf['exchange']['_ft_has_params'] = {"DeadBeef": 20, + "stoploss_on_exchange": False} + # Use settings from configuration (overriding stoploss_on_exchange) + ex = Binance(conf) + assert ex._ft_has != Exchange._ft_has_default + assert not ex._ft_has['stoploss_on_exchange'] + assert ex._ft_has['DeadBeef'] == 20 + + +def test_get_valid_pair_combination(default_conf, mocker, markets): + mocker.patch.multiple('freqtrade.exchange.BaseExchange', + _init_ccxt=MagicMock(return_value=MagicMock()), + validate_timeframes=MagicMock()) + mocker.patch.multiple('freqtrade.exchange.Exchange', + _load_async_markets=MagicMock(), + validate_pairs=MagicMock(), + validate_timeframe=MagicMock(), + markets=PropertyMock(return_value=markets)) + ex = Exchange(default_conf) + + assert ex.get_valid_pair_combination("ETH", "BTC") == "ETH/BTC" + assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC" + with pytest.raises(DependencyException, match=r"Could not combine.* to get a valid pair."): + ex.get_valid_pair_combination("NOPAIR", "ETH") + + +def test_timeframe_to_minutes(): + assert timeframe_to_minutes("5m") == 5 + assert timeframe_to_minutes("10m") == 10 + assert timeframe_to_minutes("1h") == 60 + assert timeframe_to_minutes("1d") == 1440 + + +def test_timeframe_to_seconds(): + assert timeframe_to_seconds("5m") == 300 + assert timeframe_to_seconds("10m") == 600 + assert timeframe_to_seconds("1h") == 3600 + assert timeframe_to_seconds("1d") == 86400 + + +def test_timeframe_to_msecs(): + assert timeframe_to_msecs("5m") == 300000 + assert timeframe_to_msecs("10m") == 600000 + assert timeframe_to_msecs("1h") == 3600000 + assert timeframe_to_msecs("1d") == 86400000 + + +def test_timeframe_to_prev_date(): + # 2019-08-12 13:22:08 + date = datetime.fromtimestamp(1565616128, tz=timezone.utc) + + tf_list = [ + # 5m -> 2019-08-12 13:20:00 + ("5m", datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)), + # 10m -> 2019-08-12 13:20:00 + ("10m", datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)), + # 1h -> 2019-08-12 13:00:00 + ("1h", datetime(2019, 8, 12, 13, 00, 0, tzinfo=timezone.utc)), + # 2h -> 2019-08-12 12:00:00 + ("2h", datetime(2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc)), + # 4h -> 2019-08-12 12:00:00 + ("4h", datetime(2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc)), + # 1d -> 2019-08-12 00:00:00 + ("1d", datetime(2019, 8, 12, 00, 00, 0, tzinfo=timezone.utc)), + ] + for interval, result in tf_list: + assert timeframe_to_prev_date(interval, date) == result + + date = datetime.now(tz=timezone.utc) + assert timeframe_to_prev_date("5m") < date + + +def test_timeframe_to_next_date(): + # 2019-08-12 13:22:08 + date = datetime.fromtimestamp(1565616128, tz=timezone.utc) + tf_list = [ + # 5m -> 2019-08-12 13:25:00 + ("5m", datetime(2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc)), + # 10m -> 2019-08-12 13:30:00 + ("10m", datetime(2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc)), + # 1h -> 2019-08-12 14:00:00 + ("1h", datetime(2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc)), + # 2h -> 2019-08-12 14:00:00 + ("2h", datetime(2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc)), + # 4h -> 2019-08-12 14:00:00 + ("4h", datetime(2019, 8, 12, 16, 00, 0, tzinfo=timezone.utc)), + # 1d -> 2019-08-13 00:00:00 + ("1d", datetime(2019, 8, 13, 0, 0, 0, tzinfo=timezone.utc)), + ] + + for interval, result in tf_list: + assert timeframe_to_next_date(interval, date) == result + + date = datetime.now(tz=timezone.utc) + assert timeframe_to_next_date("5m") > date