diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 48189938d..43ce37051 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -370,6 +370,49 @@ class Exchange: else: return DataFrame() + def _get_contract_size(self, pair: str) -> int: + if self.trading_mode == TradingMode.FUTURES: + market = self.markets[pair] + contract_size = 1 + if 'contractSize' in market and market['contractSize'] is not None: + contract_size = market['contractSize'] + return contract_size + else: + return 1 + + def _trades_contracts_to_amount(self, trades: List) -> List: + if len(trades) > 0 and 'symbol' in trades[0]: + contract_size = self._get_contract_size(trades[0]['symbol']) + if contract_size != 1: + for trade in trades: + trade['amount'] = trade['amount'] * contract_size + return trades + + def _order_contracts_to_amount(self, order: Dict) -> Dict: + if 'symbol' in order: + contract_size = self._get_contract_size(order['symbol']) + if contract_size != 1: + for prop in ['amount', 'cost', 'filled', 'remaining']: + if prop in order and order[prop] is not None: + order[prop] = order[prop] * contract_size + return order + + def _amount_to_contracts(self, pair: str, amount: float): + + contract_size = self._get_contract_size(pair) + if contract_size and contract_size != 1: + return amount / contract_size + else: + return amount + + def _contracts_to_amount(self, pair: str, num_contracts: float): + + contract_size = self._get_contract_size(pair) + if contract_size and contract_size != 1: + return num_contracts * contract_size + else: + return num_contracts + def set_sandbox(self, api: ccxt.Exchange, exchange_config: dict, name: str) -> None: if exchange_config.get('sandbox'): if api.urls.get('test'): @@ -587,6 +630,7 @@ class Exchange: Re-implementation of ccxt internal methods - ensuring we can test the result is correct based on our definitions. """ + amount = self._amount_to_contracts(pair, amount) if self.markets[pair]['precision']['amount']: amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE, precision=self.markets[pair]['precision']['amount'], @@ -645,11 +689,21 @@ class Exchange: limits = market['limits'] if ('cost' in limits and 'min' in limits['cost'] and limits['cost']['min'] is not None): - min_stake_amounts.append(limits['cost']['min']) + min_stake_amounts.append( + self._contracts_to_amount( + pair, + limits['cost']['min'] + ) + ) if ('amount' in limits and 'min' in limits['amount'] and limits['amount']['min'] is not None): - min_stake_amounts.append(limits['amount']['min'] * price) + min_stake_amounts.append( + self._contracts_to_amount( + pair, + limits['amount']['min'] * price + ) + ) if not min_stake_amounts: return None @@ -685,7 +739,7 @@ class Exchange: def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' - _amount = self.amount_to_precision(pair, amount) + _amount = self._contracts_to_amount(pair, self.amount_to_precision(pair, amount)) dry_order: Dict[str, Any] = { 'id': order_id, 'symbol': pair, @@ -861,6 +915,7 @@ class Exchange: params ) self._log_exchange_response('create_order', order) + order = self._order_contracts_to_amount(order) return order except ccxt.InsufficientFunds as e: @@ -909,6 +964,7 @@ class Exchange: try: order = self._api.fetch_order(order_id, pair) self._log_exchange_response('fetch_order', order) + order = self._order_contracts_to_amount(order) return order except ccxt.OrderNotFound as e: raise RetryableOrderError( @@ -963,6 +1019,7 @@ class Exchange: try: order = self._api.cancel_order(order_id, pair) self._log_exchange_response('cancel_order', order) + order = self._order_contracts_to_amount(order) return order except ccxt.InvalidOrder as e: raise InvalidOrderException( @@ -1230,6 +1287,9 @@ class Exchange: matched_trades = [trade for trade in my_trades if trade['order'] == order_id] self._log_exchange_response('get_trades_for_order', matched_trades) + + matched_trades = self._trades_contracts_to_amount(matched_trades) + return matched_trades except ccxt.DDoSProtection as e: raise DDosProtection(e) from e @@ -1572,6 +1632,7 @@ class Exchange: '(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else '' ) trades = await self._api_async.fetch_trades(pair, since=since, limit=1000) + trades = self._trades_contracts_to_amount(trades) return trades_dict_to_list(trades) except ccxt.NotSupported as e: raise OperationalException( diff --git a/tests/conftest.py b/tests/conftest.py index d9b7aad86..d3836af59 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -914,6 +914,7 @@ def get_markets(): 'active': True, 'spot': True, 'type': 'spot', + 'contractSize': None, 'precision': { 'amount': 8, 'price': 8 @@ -937,7 +938,8 @@ def get_markets(): 'quote': 'USDT', 'active': True, 'spot': False, - 'type': 'SomethingElse', + 'type': 'swap', + 'contractSize': 0.01, 'precision': { 'amount': 8, 'price': 8 @@ -985,6 +987,59 @@ def get_markets(): 'info': { } }, + 'ETH/USDT:USDT': { + 'id': 'ETH_USDT', + 'symbol': 'ETH/USDT:USDT', + 'base': 'ETH', + 'quote': 'USDT', + 'settle': 'USDT', + 'baseId': 'ETH', + 'quoteId': 'USDT', + 'settleId': 'USDT', + 'type': 'swap', + 'spot': False, + 'margin': False, + 'swap': True, + 'futures': False, + 'option': False, + 'derivative': True, + 'contract': True, + 'linear': True, + 'inverse': False, + 'tierBased': False, + 'percentage': True, + 'taker': 0.0006, + 'maker': 0.0002, + 'contractSize': 10, + 'active': True, + 'expiry': None, + 'expiryDatetime': None, + 'strike': None, + 'optionType': None, + 'limits': { + 'leverage': { + 'min': 1, + 'max': 100 + }, + 'amount': { + 'min': 1, + 'max': 300000 + }, + 'price': { + 'min': None, + 'max': None, + }, + 'cost': { + 'min': None, + 'max': None, + } + }, + 'precision': { + 'price': 0.05, + 'amount': 1 + }, + 'info': {} + } } diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 122e6e37c..fff565152 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -215,9 +215,19 @@ class TestCCXTExchange(): rate = funding_ohlcv[pair_tf] this_hour = timeframe_to_prev_date(timeframe_ff) - prev_hour = timeframe_to_prev_date(timeframe_ff, this_hour - timedelta(minutes=1)) - assert rate[rate['date'] == this_hour].iloc[0]['open'] != 0.0 - assert rate[rate['date'] == prev_hour].iloc[0]['open'] != 0.0 + hour1 = timeframe_to_prev_date(timeframe_ff, this_hour - timedelta(minutes=1)) + hour2 = timeframe_to_prev_date(timeframe_ff, hour1 - timedelta(minutes=1)) + hour3 = timeframe_to_prev_date(timeframe_ff, hour2 - timedelta(minutes=1)) + val0 = rate[rate['date'] == this_hour].iloc[0]['open'] + val1 = rate[rate['date'] == hour1].iloc[0]['open'] + val2 = rate[rate['date'] == hour2].iloc[0]['open'] + val3 = rate[rate['date'] == hour3].iloc[0]['open'] + + # Test For last 4 hours + # Avoids random test-failure when funding-fees are 0 for a few hours. + assert val0 != 0.0 or val1 != 0.0 or val2 != 0.0 or val3 != 0.0 + assert rate['open'].max() != 0.0 + assert rate['open'].min() != rate['open'].max() def test_ccxt_fetch_mark_price_history(self, exchange_futures): exchange, exchangename = exchange_futures diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 9f0f272e1..7edc3c755 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -224,27 +224,46 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog): ex.validate_order_time_in_force(tif2) -@pytest.mark.parametrize("amount,precision_mode,precision,expected", [ - (2.34559, 2, 4, 2.3455), - (2.34559, 2, 5, 2.34559), - (2.34559, 2, 3, 2.345), - (2.9999, 2, 3, 2.999), - (2.9909, 2, 3, 2.990), +@pytest.mark.parametrize("amount,precision_mode,precision,contract_size,expected,trading_mode", [ + (2.34559, 2, 4, 1, 2.3455, 'spot'), + (2.34559, 2, 5, 1, 2.34559, 'spot'), + (2.34559, 2, 3, 1, 2.345, 'spot'), + (2.9999, 2, 3, 1, 2.999, 'spot'), + (2.9909, 2, 3, 1, 2.990, 'spot'), # Tests for Tick-size - (2.34559, 4, 0.0001, 2.3455), - (2.34559, 4, 0.00001, 2.34559), - (2.34559, 4, 0.001, 2.345), - (2.9999, 4, 0.001, 2.999), - (2.9909, 4, 0.001, 2.990), - (2.9909, 4, 0.005, 2.990), - (2.9999, 4, 0.005, 2.995), + (2.34559, 4, 0.0001, 1, 2.3455, 'spot'), + (2.34559, 4, 0.00001, 1, 2.34559, 'spot'), + (2.34559, 4, 0.001, 1, 2.345, 'spot'), + (2.9999, 4, 0.001, 1, 2.999, 'spot'), + (2.9909, 4, 0.001, 1, 2.990, 'spot'), + (2.9909, 4, 0.005, 0.01, 299.09, 'futures'), + (2.9999, 4, 0.005, 10, 0.295, 'futures'), ]) -def test_amount_to_precision(default_conf, mocker, amount, precision_mode, precision, expected): +def test_amount_to_precision( + default_conf, + mocker, + amount, + precision_mode, + precision, + contract_size, + expected, + trading_mode +): """ Test rounds down """ - markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': precision}}}) + markets = PropertyMock(return_value={ + 'ETH/BTC': { + 'contractSize': contract_size, + 'precision': { + 'amount': precision + } + } + }) + + default_conf['trading_mode'] = trading_mode + default_conf['collateral'] = 'isolated' exchange = get_patched_exchange(mocker, default_conf, id="binance") # digits counting mode @@ -465,6 +484,28 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) assert isclose(result, expected_result/12) + markets["ETH/BTC"]["contractSize"] = 0.01 + default_conf['trading_mode'] = 'futures' + default_conf['collateral'] = 'isolated' + exchange = get_patched_exchange(mocker, default_conf, id="binance") + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + + # Contract size 0.01 + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) + assert isclose(result, expected_result * 0.01) + + markets["ETH/BTC"]["contractSize"] = 10 + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + # With Leverage, Contract size 10 + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) + assert isclose(result, (expected_result/12) * 10.0) + def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: exchange = get_patched_exchange(mocker, default_conf, id="binance") @@ -1020,6 +1061,7 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name): assert order["side"] == side assert order["type"] == "limit" assert order["symbol"] == "ETH/BTC" + assert order["amount"] == 1 @pytest.mark.parametrize("side,startprice,endprice", [ @@ -1127,9 +1169,12 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, 'id': order_id, 'info': { 'foo': 'bar' - } + }, + 'symbol': 'XLTCUSDT', + 'amount': 1 }) default_conf['dry_run'] = False + default_conf['collateral'] = 'isolated' mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) @@ -1137,7 +1182,7 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, exchange.set_margin_mode = MagicMock() order = exchange.create_order( - pair='ETH/BTC', + pair='XLTCUSDT', ordertype=ordertype, side=side, amount=1, @@ -1148,7 +1193,8 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, 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 order['amount'] == 1 + assert api_mock.create_order.call_args[0][0] == 'XLTCUSDT' 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 @@ -1158,7 +1204,7 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, exchange.trading_mode = TradingMode.FUTURES order = exchange.create_order( - pair='ETH/BTC', + pair='XLTCUSDT', ordertype=ordertype, side=side, amount=1, @@ -1168,6 +1214,7 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, assert exchange._set_leverage.call_count == 1 assert exchange.set_margin_mode.call_count == 1 + assert order['amount'] == 0.01 def test_buy_dry_run(default_conf, mocker): @@ -1189,6 +1236,7 @@ def test_buy_prod(default_conf, mocker, exchange_name): api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, + 'symbol': 'ETH/BTC', 'info': { 'foo': 'bar' } @@ -1264,6 +1312,7 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, + 'symbol': 'ETH/BTC', 'info': { 'foo': 'bar' } @@ -1326,6 +1375,7 @@ def test_sell_prod(default_conf, mocker, exchange_name): api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, + 'symbol': 'ETH/BTC', 'info': { 'foo': 'bar' } @@ -1392,6 +1442,7 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) api_mock.create_order = MagicMock(return_value={ 'id': order_id, + 'symbol': 'ETH/BTC', 'info': { 'foo': 'bar' } @@ -2243,7 +2294,7 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na @pytest.mark.parametrize("exchange_name", EXCHANGES) async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name, fetch_trades_result): - + # TODO-lev: Test for contract sizes of 0.01 and 10 caplog.set_level(logging.DEBUG) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) # Monkey-patch async function @@ -2287,6 +2338,43 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name, await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000) +@pytest.mark.asyncio +@pytest.mark.parametrize("exchange_name", EXCHANGES) +async def test__async_fetch_trades_contract_size(default_conf, mocker, caplog, exchange_name, + fetch_trades_result): + caplog.set_level(logging.DEBUG) + default_conf['collateral'] = 'isolated' + default_conf['trading_mode'] = 'futures' + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + # Monkey-patch async function + exchange._api_async.fetch_trades = get_mock_coro([ + {'info': {'a': 126181333, + 'p': '0.01952600', + 'q': '0.01200000', + 'f': 138604158, + 'l': 138604158, + 'T': 1565798399872, + 'm': True, + 'M': True}, + 'timestamp': 1565798399872, + 'datetime': '2019-08-14T15:59:59.872Z', + 'symbol': 'ETH/USDT:USDT', + 'id': '126181383', + 'order': None, + 'type': None, + 'takerOrMaker': None, + 'side': 'sell', + 'price': 2.0, + 'amount': 30.0, + 'cost': 60.0, + 'fee': None}] + ) + + pair = 'ETH/USDT:USDT' + res = await exchange._async_fetch_trades(pair, since=None, params=None) + assert res[0][5] == 300 + + @pytest.mark.asyncio @pytest.mark.parametrize("exchange_name", EXCHANGES) async def test__async_get_trade_history_id(default_conf, mocker, exchange_name, @@ -2595,6 +2683,8 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog): 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) exchange._dry_run_open_orders['X'] = order assert exchange.fetch_order('X', 'TKN/BTC').myid == 123 @@ -2604,10 +2694,15 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog): default_conf['dry_run'] = False api_mock = MagicMock() - api_mock.fetch_order = MagicMock(return_value=456) + api_mock.fetch_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') == 456 - assert log_has("API fetch_order: 456", caplog) + assert exchange.fetch_order( + 'X', 'TKN/BTC') == {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'} + assert log_has( + ("API fetch_order: {\'id\': \'123\', \'amount\': 2, \'symbol\': \'TKN/BTC\'}" + ), + caplog + ) with pytest.raises(InvalidOrderException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) @@ -2651,9 +2746,9 @@ def test_fetch_stoploss_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = False api_mock = MagicMock() - api_mock.fetch_order = MagicMock(return_value=456) + api_mock.fetch_order = MagicMock(return_value={'id': '123', 'symbol': 'TKN/BTC'}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.fetch_stoploss_order('X', 'TKN/BTC') == 456 + assert exchange.fetch_stoploss_order('X', 'TKN/BTC') == {'id': '123', 'symbol': 'TKN/BTC'} with pytest.raises(InvalidOrderException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) @@ -2700,12 +2795,17 @@ def test_name(default_conf, mocker, exchange_name): assert exchange.id == exchange_name +@pytest.mark.parametrize("trading_mode,amount", [ + ('spot', 0.2340606), + ('futures', 2.340606), +]) @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_trades_for_order(default_conf, mocker, exchange_name): - +def test_get_trades_for_order(default_conf, mocker, exchange_name, trading_mode, amount): order_id = 'ABCD-ABCD' since = datetime(2018, 5, 5, 0, 0, 0) default_conf["dry_run"] = False + default_conf["trading_mode"] = trading_mode + default_conf["collateral"] = 'isolated' mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) api_mock = MagicMock() @@ -2722,22 +2822,24 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name): 'id': 'ABCD-ABCD'}, 'timestamp': 1519860024438, 'datetime': '2018-02-28T23:20:24.438Z', - 'symbol': 'LTC/BTC', + 'symbol': 'ETH/USDT:USDT', '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) + orders = exchange.get_trades_for_order(order_id, 'ETH/USDT:USDT', since) assert len(orders) == 1 assert orders[0]['price'] == 165 + assert isclose(orders[0]['amount'], amount) 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' + assert api_mock.fetch_my_trades.call_args[0][0] == 'ETH/USDT:USDT' # 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.replace( @@ -2745,10 +2847,10 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name): 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) + order_id=order_id, pair='ETH/USDT:USDT', since=since) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False)) - assert exchange.get_trades_for_order(order_id, 'LTC/BTC', since) == [] + assert exchange.get_trades_for_order(order_id, 'ETH/USDT:USDT', since) == [] @pytest.mark.parametrize("exchange_name", EXCHANGES) @@ -3549,7 +3651,7 @@ def test__calculate_funding_fees( assert pytest.approx(funding_fees) == expected_fees -@ pytest.mark.parametrize('exchange,expected_fees', [ +@pytest.mark.parametrize('exchange,expected_fees', [ ('binance', -0.0009140999999999999), ('gateio', -0.0009140999999999999), ]) @@ -3575,3 +3677,205 @@ def test__calculate_funding_fees_datetime_called( time_machine.move_to("2021-09-01 08:00:00 +00:00") funding_fees = exchange._calculate_funding_fees('ADA/USDT', 30.0, d1) assert funding_fees == expected_fees + + +@pytest.mark.parametrize('pair,expected_size,trading_mode', [ + ('XLTCUSDT', 1, 'spot'), + ('LTC/USD', 1, 'futures'), + ('XLTCUSDT', 0.01, 'futures'), + ('LTC/ETH', 1, 'futures'), + ('ETH/USDT:USDT', 10, 'futures') +]) +def test__get_contract_size(mocker, default_conf, pair, expected_size, trading_mode): + api_mock = MagicMock() + default_conf['trading_mode'] = trading_mode + default_conf['collateral'] = 'isolated' + mocker.patch('freqtrade.exchange.Exchange.markets', { + 'LTC/USD': { + 'symbol': 'LTC/USD', + 'contractSize': None, + }, + 'XLTCUSDT': { + 'symbol': 'XLTCUSDT', + 'contractSize': 0.01, + }, + 'LTC/ETH': { + 'symbol': 'LTC/ETH', + }, + 'ETH/USDT:USDT': { + 'symbol': 'ETH/USDT:USDT', + 'contractSize': 10, + } + }) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + size = exchange._get_contract_size(pair) + assert expected_size == size + + +@pytest.mark.parametrize('pair,contract_size,trading_mode', [ + ('XLTCUSDT', 1, 'spot'), + ('LTC/USD', 1, 'futures'), + ('XLTCUSDT', 0.01, 'futures'), + ('LTC/ETH', 1, 'futures'), + ('ETH/USDT:USDT', 10, 'futures'), +]) +def test__order_contracts_to_amount( + mocker, + default_conf, + markets, + pair, + contract_size, + trading_mode, +): + api_mock = MagicMock() + default_conf['trading_mode'] = trading_mode + default_conf['collateral'] = 'isolated' + mocker.patch('freqtrade.exchange.Exchange.markets', markets) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + orders = [ + { + 'id': '123456320', + 'clientOrderId': '12345632018', + 'timestamp': 1640124992000, + 'datetime': 'Tue 21 Dec 2021 22:16:32 UTC', + 'lastTradeTimestamp': 1640124911000, + 'status': 'active', + 'symbol': pair, + 'type': 'limit', + 'timeInForce': 'gtc', + 'postOnly': None, + 'side': 'buy', + 'price': 2.0, + 'stopPrice': None, + 'average': None, + 'amount': 30.0, + 'cost': 60.0, + 'filled': None, + 'remaining': 30.0, + 'fee': 0.06, + 'fees': [{ + 'currency': 'USDT', + 'cost': 0.06, + }], + 'trades': None, + 'info': {}, + }, + { + 'id': '123456380', + 'clientOrderId': '12345638203', + 'timestamp': 1640124992000, + 'datetime': 'Tue 21 Dec 2021 22:16:32 UTC', + 'lastTradeTimestamp': 1640124911000, + 'status': 'active', + 'symbol': pair, + 'type': 'limit', + 'timeInForce': 'gtc', + 'postOnly': None, + 'side': 'sell', + 'price': 2.2, + 'stopPrice': None, + 'average': None, + 'amount': 40.0, + 'cost': 80.0, + 'filled': None, + 'remaining': 40.0, + 'fee': 0.08, + 'fees': [{ + 'currency': 'USDT', + 'cost': 0.08, + }], + 'trades': None, + 'info': {}, + }, + ] + + order1 = exchange._order_contracts_to_amount(orders[0]) + order2 = exchange._order_contracts_to_amount(orders[1]) + assert order1['amount'] == 30.0 * contract_size + assert order2['amount'] == 40.0 * contract_size + + +@pytest.mark.parametrize('pair,contract_size,trading_mode', [ + ('XLTCUSDT', 1, 'spot'), + ('LTC/USD', 1, 'futures'), + ('XLTCUSDT', 0.01, 'futures'), + ('LTC/ETH', 1, 'futures'), + ('ETH/USDT:USDT', 10, 'futures'), +]) +def test__trades_contracts_to_amount( + mocker, + default_conf, + markets, + pair, + contract_size, + trading_mode, +): + api_mock = MagicMock() + default_conf['trading_mode'] = trading_mode + default_conf['collateral'] = 'isolated' + mocker.patch('freqtrade.exchange.Exchange.markets', markets) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + + trades = [ + { + 'symbol': pair, + 'amount': 30.0, + }, + { + 'symbol': pair, + 'amount': 40.0, + } + ] + + new_amount_trades = exchange._trades_contracts_to_amount(trades) + assert new_amount_trades[0]['amount'] == 30.0 * contract_size + assert new_amount_trades[1]['amount'] == 40.0 * contract_size + + +@pytest.mark.parametrize('pair,param_amount,param_size', [ + ('XLTCUSDT', 40, 4000), + ('LTC/ETH', 30, 30), + ('LTC/USD', 30, 30), + ('ETH/USDT:USDT', 10, 1), +]) +def test__amount_to_contracts( + mocker, + default_conf, + markets, + pair, + param_amount, + param_size +): + api_mock = MagicMock() + default_conf['trading_mode'] = 'spot' + default_conf['collateral'] = 'isolated' + mocker.patch('freqtrade.exchange.Exchange.markets', { + 'LTC/USD': { + 'symbol': 'LTC/USD', + 'contractSize': None, + }, + 'XLTCUSDT': { + 'symbol': 'XLTCUSDT', + 'contractSize': 0.01, + }, + 'LTC/ETH': { + 'symbol': 'LTC/ETH', + }, + 'ETH/USDT:USDT': { + 'symbol': 'ETH/USDT:USDT', + 'contractSize': 10, + } + }) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + result_size = exchange._amount_to_contracts(pair, param_amount) + assert result_size == param_amount + result_amount = exchange._contracts_to_amount(pair, param_size) + assert result_amount == param_size + + default_conf['trading_mode'] = 'futures' + exchange = get_patched_exchange(mocker, default_conf, api_mock) + result_size = exchange._amount_to_contracts(pair, param_amount) + assert result_size == param_size + result_amount = exchange._contracts_to_amount(pair, param_size) + assert result_amount == param_amount diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 2db3955ba..ff4200f8d 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -21,6 +21,7 @@ def test_buy_kraken_trading_agreement(default_conf, mocker): api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, + 'symbol': 'ETH/BTC', 'info': { 'foo': 'bar' } @@ -53,6 +54,7 @@ def test_sell_kraken_trading_agreement(default_conf, mocker): api_mock.options = {} api_mock.create_order = MagicMock(return_value={ 'id': order_id, + 'symbol': 'ETH/BTC', 'info': { 'foo': 'bar' }