From 78d1a267f08a9fc68dd97a4f9a51bbb0e5d7c27b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 21 Dec 2021 15:45:16 -0600 Subject: [PATCH] contract-sizes tests --- freqtrade/exchange/exchange.py | 33 ++- tests/conftest.py | 57 ++++- tests/exchange/test_exchange.py | 426 +++++++++++++++++++++++++++----- 3 files changed, 443 insertions(+), 73 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a671a33ca..8a87a2c7f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -372,13 +372,17 @@ class Exchange: def _get_contract_size(self, pair: str) -> int: if self.trading_mode == TradingMode.FUTURES: - return self.markets[pair]['contract_size'] + 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: - contract_size = self._get_contract_size(trades[0]['pair']) + contract_size = self._get_contract_size(trades[0]['symbol']) if contract_size != 1: for trade in trades: trade['amount'] = trade['amount'] * contract_size @@ -387,10 +391,10 @@ class Exchange: return trades def _order_contracts_to_amount(self, order: Dict) -> Dict: - contract_size = self._get_contract_size(order['pair']) + contract_size = self._get_contract_size(order['symbol']) if contract_size != 1: for prop in ['amount', 'cost', 'filled', 'remaining']: - if prop in order: + if prop in order and order[prop] is not None: order[prop] = order[prop] * contract_size return order @@ -611,7 +615,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_contract_size(pair, amount) + 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'], @@ -670,12 +674,17 @@ 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( - self._contract_size_to_amount( + self._contracts_to_amount( pair, limits['amount']['min'] * price ) @@ -715,7 +724,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._contract_size_to_amount(pair, 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, @@ -865,19 +874,19 @@ class Exchange: params.update({param: time_in_force}) return params - def _amount_to_contract_size(self, pair: str, amount: float): + def _amount_to_contracts(self, pair: str, amount: float): if ('contractSize' in self.markets[pair]): return amount / self.markets[pair]['contractSize'] else: return amount - def _contract_size_to_amount(self, pair: str, amount: float): + def _contracts_to_amount(self, pair: str, num_contracts: float): if ('contractSize' in self.markets[pair]): - return amount * self.markets[pair]['contractSize'] + return num_contracts * self.markets[pair]['contractSize'] else: - return amount + return num_contracts def create_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, leverage: float = 1.0, time_in_force: str = 'gtc') -> Dict: 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_exchange.py b/tests/exchange/test_exchange.py index 5e59e7251..e253f82b1 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -20,6 +20,7 @@ from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_CO from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) +from freqtrade.persistence.models import Trade from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has_re, num_log_has_re @@ -224,28 +225,34 @@ 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", [ + (2.34559, 2, 4, 1, 2.3455), + (2.34559, 2, 5, 1, 2.34559), + (2.34559, 2, 3, 1, 2.345), + (2.9999, 2, 3, 1, 2.999), + (2.9909, 2, 3, 1, 2.990), # 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), + (2.34559, 4, 0.00001, 1, 2.34559), + (2.34559, 4, 0.001, 1, 2.345), + (2.9999, 4, 0.001, 1, 2.999), + (2.9909, 4, 0.001, 1, 2.990), + (2.9909, 4, 0.005, 0.01, 0.025), + (2.9999, 4, 0.005, 10, 29.995), ]) -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): """ Test rounds down """ - # TODO-lev: Test for contract sizes of 0.01 and 10 - markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': precision}}}) + markets = PropertyMock(return_value={ + 'ETH/BTC': { + 'contractSize': contract_size, + 'precision': { + 'amount': precision + } + } + }) exchange = get_patched_exchange(mocker, default_conf, id="binance") # digits counting mode @@ -324,12 +331,11 @@ def test_price_get_one_pip(default_conf, mocker, price, precision_mode, precisio assert pytest.approx(exchange.price_get_one_pip(pair, price)) == expected -def test_get_min_pair_stake_amount(mocker, default_conf) -> None: - # TODO-lev: Test for contract sizes of 0.01 and 10 +def test_get_min_pair_stake_amount(mocker, default_conf, markets) -> None: exchange = get_patched_exchange(mocker, default_conf, id="binance") stoploss = -0.05 - markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} + markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}, } # no pair found mocker.patch( @@ -467,6 +473,25 @@ 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 + 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") @@ -1006,7 +1031,6 @@ def test_exchange_has(default_conf, mocker): ]) @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_create_dry_run_order(default_conf, mocker, side, exchange_name): - # TODO-lev: Test for contract sizes of 0.01 and 10 default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) @@ -1023,6 +1047,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", [ @@ -1123,7 +1148,6 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou ]) @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, exchange_name): - # TODO-lev: Test for contract sizes of 0.01 and 10 api_mock = MagicMock() order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6)) api_mock.options = {} if not marketprice else {"createMarketBuyOrderRequiresPrice": True} @@ -1131,9 +1155,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) @@ -1141,7 +1168,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, @@ -1152,7 +1179,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 @@ -1162,7 +1190,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, @@ -1172,6 +1200,8 @@ 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 api_mock.create_order.call_args[0][3] == 100 + assert order['amount'] == 0.01 def test_buy_dry_run(default_conf, mocker): @@ -2291,6 +2321,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, @@ -2515,24 +2582,31 @@ def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, cap # Ensure that if not dry_run, we should call API +@pytest.mark.parametrize("trading_mode,amount", [ + ('spot', 2), + ('futures', 20), +]) @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_cancel_order(default_conf, mocker, exchange_name): - # TODO-lev: Test for contract sizes of 0.01 and 10 +def test_cancel_order(default_conf, mocker, exchange_name, trading_mode, amount): default_conf['dry_run'] = False + default_conf['trading_mode'] = trading_mode + default_conf['collateral'] = 'isolated' api_mock = MagicMock() - api_mock.cancel_order = MagicMock(return_value={'id': '123'}) + api_mock.cancel_order = MagicMock( + return_value={'id': '123', 'amount': 2, 'symbol': 'ETH/USDT:USDT'}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == {'id': '123'} + assert exchange.cancel_order( + order_id='_', pair='ETH/USDT:USDT') == {'id': '123', 'amount': amount, 'symbol': 'ETH/USDT:USDT'} 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') + exchange.cancel_order(order_id='_', pair='ETH/USDT:USDT') 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') + order_id='_', pair='ETH/USDT:USDT') @pytest.mark.parametrize("exchange_name", EXCHANGES) @@ -2594,13 +2668,17 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name): exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=123) +@pytest.mark.parametrize("trading_mode,amount", [ + ('spot', 2), + ('futures', 20), +]) @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_fetch_order(default_conf, mocker, exchange_name, caplog): - # TODO-lev: Test for contract sizes of 0.01 and 10 +def test_fetch_order(default_conf, mocker, exchange_name, caplog, trading_mode, amount): default_conf['dry_run'] = True default_conf['exchange']['log_responses'] = 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.fetch_order('X', 'TKN/BTC').myid == 123 @@ -2609,11 +2687,20 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog): exchange.fetch_order('Y', 'TKN/BTC') default_conf['dry_run'] = False + default_conf['trading_mode'] = trading_mode + default_conf['collateral'] = 'isolated' api_mock = MagicMock() - api_mock.fetch_order = MagicMock(return_value=456) + api_mock.fetch_order = MagicMock( + return_value={'id': '123', 'amount': 2, 'symbol': 'ETH/USDT:USDT'}) 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': amount, 'symbol': 'ETH/USDT:USDT'} + assert log_has( + ("API fetch_order: {\'id\': \'123\', \'amount\': " + + str(amount) + ", \'symbol\': \'ETH/USDT:USDT\'}" + ), + caplog + ) with pytest.raises(InvalidOrderException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) @@ -2706,12 +2793,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): - # TODO-lev: Test for contract sizes of 0.01 and 10 +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() @@ -2728,22 +2820,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 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( @@ -2751,10 +2845,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) @@ -3462,6 +3556,81 @@ def test__get_funding_fee( assert kraken._get_funding_fee(size, funding_rate, mark_price, time_in_ratio) == kraken_fee +def test__get_mark_price_history(mocker, default_conf, mark_ohlcv): + api_mock = MagicMock() + api_mock.fetch_ohlcv = MagicMock(return_value=mark_ohlcv) + type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) + + # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + mark_prices = exchange._get_mark_price_history("ADA/USDT", 1630454400000) + assert mark_prices == { + 1630454400000: 2.77, + 1630458000000: 2.73, + 1630461600000: 2.74, + 1630465200000: 2.76, + 1630468800000: 2.76, + 1630472400000: 2.77, + 1630476000000: 2.78, + 1630479600000: 2.78, + 1630483200000: 2.77, + 1630486800000: 2.77, + 1630490400000: 2.84, + 1630494000000: 2.81, + 1630497600000: 2.81, + 1630501200000: 2.82, + } + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "binance", + "_get_mark_price_history", + "fetch_ohlcv", + pair="ADA/USDT", + since=1635580800001 + ) + + +def test_get_funding_rate_history(mocker, default_conf, funding_rate_history_hourly): + api_mock = MagicMock() + api_mock.fetch_funding_rate_history = MagicMock(return_value=funding_rate_history_hourly) + type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True}) + + # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + funding_rates = exchange.get_funding_rate_history('ADA/USDT', 1635580800001) + + assert funding_rates == { + 1630454400000: -0.000008, + 1630458000000: -0.000004, + 1630461600000: 0.000012, + 1630465200000: -0.000003, + 1630468800000: -0.000007, + 1630472400000: 0.000003, + 1630476000000: 0.000019, + 1630479600000: 0.000003, + 1630483200000: -0.000003, + 1630486800000: 0, + 1630490400000: 0.000013, + 1630494000000: 0.000077, + 1630497600000: 0.000072, + 1630501200000: 0.000097, + } + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "binance", + "get_funding_rate_history", + "fetch_funding_rate_history", + pair="ADA/USDT", + since=1630454400000 + ) + + @pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [ ('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), ('binance', 0, 2, "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), @@ -3583,26 +3752,163 @@ def test__calculate_funding_fees_datetime_called( assert funding_fees == expected_fees -def test__get_contract_size(): - # TODO-lev - return +@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, markets, pair, expected_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) + size = exchange._get_contract_size(pair) + assert expected_size == size -def test__trades_contracts_to_amount(): - # TODO-lev - return +@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 -def test__order_contracts_to_amount(): - # TODO-lev - return +@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 -def test__amount_to_contract_size(): - # TODO-lev - return - - -def test__contract_size_to_amount(): - # TODO-lev - return +@pytest.mark.parametrize('pair,param_amount,param_size', [ + ('XLTCUSDT', 40, 4000), + ('LTC/ETH', 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'] = 'futures' + default_conf['collateral'] = 'isolated' + mocker.patch('freqtrade.exchange.Exchange.markets', markets) + 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