From 3f75531105f6a499d758628364930664e0ee74aa Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 13 Nov 2021 17:11:28 -0600 Subject: [PATCH 01/23] added methods _contract_size_to_amount and _amount_to_contract_size, added _amount_to_contract_size to create_order, added contract_size_to_amount to get_min_leverage --- freqtrade/exchange/exchange.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 48189938d..96d477567 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -667,7 +667,10 @@ class Exchange: # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. return self._get_stake_amount_considering_leverage( - max(min_stake_amounts) * amount_reserve_percent, + self._contract_size_to_amount( + pair, + max(min_stake_amounts) * amount_reserve_percent + ), leverage or 1.0 ) @@ -835,6 +838,20 @@ class Exchange: params.update({param: time_in_force}) return params + def _amount_to_contract_size(self, pair: str, amount: float): + + if ('contractSize' in self._api.markets[pair]): + return amount / self._api.markets[pair]['contractSize'] + else: + return amount + + def _contract_size_to_amount(self, pair: str, amount: float): + + if ('contractSize' in self._api.markets[pair]): + return amount * self._api.markets[pair]['contractSize'] + else: + return amount + def create_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, leverage: float = 1.0, time_in_force: str = 'gtc') -> Dict: # TODO-lev: remove default for leverage @@ -852,6 +869,7 @@ class Exchange: rate_for_order = self.price_to_precision(pair, rate) if needs_price else None self._lev_prep(pair, leverage) + amount = self._amount_to_contract_size(pair, amount) order = self._api.create_order( pair, ordertype, From ef6ad0e6d7393421a9c86b9753508dcab4a22674 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 13 Nov 2021 18:35:22 -0600 Subject: [PATCH 02/23] Removed leverage param from get_min_pair_stake_amount --- freqtrade/exchange/exchange.py | 34 +++++++++++----------------- tests/exchange/test_exchange.py | 39 --------------------------------- 2 files changed, 13 insertions(+), 60 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 96d477567..a2e9644e8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -631,8 +631,12 @@ class Exchange: else: return 1 / pow(10, precision) - def get_min_pair_stake_amount(self, pair: str, price: float, stoploss: float, - leverage: Optional[float] = 1.0) -> Optional[float]: + def get_min_pair_stake_amount( + self, + pair: str, + price: float, + stoploss: float + ) -> Optional[float]: try: market = self.markets[pair] except KeyError: @@ -666,23 +670,11 @@ class Exchange: # The value returned should satisfy both limits: for amount (base currency) and # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. - return self._get_stake_amount_considering_leverage( - self._contract_size_to_amount( - pair, - max(min_stake_amounts) * amount_reserve_percent - ), - leverage or 1.0 + return self._contract_size_to_amount( + pair, + max(min_stake_amounts) * amount_reserve_percent ) - def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float): - """ - Takes the minimum stake amount for a pair with no leverage and returns the minimum - stake amount when leverage is considered - :param stake_amount: The stake amount for a pair before leverage is considered - :param leverage: The amount of leverage being used on the current trade - """ - return stake_amount / leverage - # Dry-run methods def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, @@ -840,15 +832,15 @@ class Exchange: def _amount_to_contract_size(self, pair: str, amount: float): - if ('contractSize' in self._api.markets[pair]): - return amount / self._api.markets[pair]['contractSize'] + 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): - if ('contractSize' in self._api.markets[pair]): - return amount * self._api.markets[pair]['contractSize'] + if ('contractSize' in self.markets[pair]): + return amount * self.markets[pair]['contractSize'] else: return amount diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 9f0f272e1..c91cc29c8 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -398,9 +398,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) expected_result = 2 * (1+0.05) / (1-abs(stoploss)) assert isclose(result, expected_result) - # With Leverage - result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss, 3.0) - assert isclose(result, expected_result/3) # min amount is set markets["ETH/BTC"]["limits"] = { @@ -414,9 +411,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) expected_result = 2 * 2 * (1+0.05) / (1-abs(stoploss)) assert isclose(result, expected_result) - # With Leverage - result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0) - assert isclose(result, expected_result/5) # min amount and cost are set (cost is minimal) markets["ETH/BTC"]["limits"] = { @@ -430,9 +424,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) expected_result = max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss)) assert isclose(result, expected_result) - # With Leverage - result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10) - assert isclose(result, expected_result/10) # min amount and cost are set (amount is minial) markets["ETH/BTC"]["limits"] = { @@ -446,24 +437,15 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) expected_result = max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss)) assert isclose(result, expected_result) - # With Leverage - result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0) - assert isclose(result, expected_result/7.0) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) expected_result = max(8, 2 * 2) * 1.5 assert isclose(result, expected_result) - # With Leverage - result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0) - assert isclose(result, expected_result/8.0) # Really big stoploss result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) expected_result = max(8, 2 * 2) * 1.5 assert isclose(result, expected_result) - # With Leverage - result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) - assert isclose(result, expected_result/12) def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: @@ -483,8 +465,6 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) expected_result = max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)) assert round(result, 8) == round(expected_result, 8) - result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0) - assert round(result, 8) == round(expected_result/3, 8) def test_set_sandbox(default_conf, mocker): @@ -3263,25 +3243,6 @@ def test__get_funding_fees_from_exchange(default_conf, mocker, exchange_name): ) -@pytest.mark.parametrize('exchange', ['binance', 'kraken', 'ftx']) -@pytest.mark.parametrize('stake_amount,leverage,min_stake_with_lev', [ - (9.0, 3.0, 3.0), - (20.0, 5.0, 4.0), - (100.0, 100.0, 1.0) -]) -def test_get_stake_amount_considering_leverage( - exchange, - stake_amount, - leverage, - min_stake_with_lev, - mocker, - default_conf -): - exchange = get_patched_exchange(mocker, default_conf, id=exchange) - assert exchange._get_stake_amount_considering_leverage( - stake_amount, leverage) == min_stake_with_lev - - @pytest.mark.parametrize("exchange_name,trading_mode", [ ("binance", TradingMode.FUTURES), ("ftx", TradingMode.MARGIN), From 2df5993812c1e231c4f4c81df83b6355818a83a2 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 17 Nov 2021 07:00:53 -0600 Subject: [PATCH 03/23] _contract_size_to_amount only impacts limits.amount and not limits.cost, put _get_stake_amount_considering_leverage back in --- freqtrade/exchange/exchange.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a2e9644e8..07b7e2ecb 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -631,12 +631,8 @@ class Exchange: else: return 1 / pow(10, precision) - def get_min_pair_stake_amount( - self, - pair: str, - price: float, - stoploss: float - ) -> Optional[float]: + def get_min_pair_stake_amount(self, pair: str, price: float, stoploss: float, + leverage: Optional[float] = 1.0) -> Optional[float]: try: market = self.markets[pair] except KeyError: @@ -653,7 +649,7 @@ class Exchange: if ('amount' in limits and 'min' in limits['amount'] and limits['amount']['min'] is not None): - min_stake_amounts.append(limits['amount']['min'] * price) + self._contract_size_to_amount(pair, min_stake_amounts.append(limits['amount']['min'] * price)) if not min_stake_amounts: return None @@ -670,11 +666,20 @@ class Exchange: # The value returned should satisfy both limits: for amount (base currency) and # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. - return self._contract_size_to_amount( - pair, - max(min_stake_amounts) * amount_reserve_percent + return self._get_stake_amount_considering_leverage( + max(min_stake_amounts) * amount_reserve_percent, + leverage or 1.0 ) + def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float): + """ + Takes the minimum stake amount for a pair with no leverage and returns the minimum + stake amount when leverage is considered + :param stake_amount: The stake amount for a pair before leverage is considered + :param leverage: The amount of leverage being used on the current trade + """ + return stake_amount / leverage + # Dry-run methods def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, From ee63f12236ee2534330fa039db9a87f6967629f9 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 17 Nov 2021 07:17:27 -0600 Subject: [PATCH 04/23] Revert "Removed leverage param from get_min_pair_stake_amount" This reverts commit 096588550ca1de5e5edf63cf7214af037d7bc93b. --- freqtrade/exchange/exchange.py | 13 ++++++----- tests/exchange/test_exchange.py | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 07b7e2ecb..45b566bcf 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -649,7 +649,10 @@ class Exchange: if ('amount' in limits and 'min' in limits['amount'] and limits['amount']['min'] is not None): - self._contract_size_to_amount(pair, min_stake_amounts.append(limits['amount']['min'] * price)) + self._contract_size_to_amount( + pair, + min_stake_amounts.append(limits['amount']['min'] * price) + ) if not min_stake_amounts: return None @@ -837,15 +840,15 @@ class Exchange: def _amount_to_contract_size(self, pair: str, amount: float): - if ('contractSize' in self.markets[pair]): - return amount / self.markets[pair]['contractSize'] + if ('contractSize' in self._api.markets[pair]): + return amount / self._api.markets[pair]['contractSize'] else: return amount def _contract_size_to_amount(self, pair: str, amount: float): - if ('contractSize' in self.markets[pair]): - return amount * self.markets[pair]['contractSize'] + if ('contractSize' in self._api.markets[pair]): + return amount * self._api.markets[pair]['contractSize'] else: return amount diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index c91cc29c8..9f0f272e1 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -398,6 +398,9 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) expected_result = 2 * (1+0.05) / (1-abs(stoploss)) assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss, 3.0) + assert isclose(result, expected_result/3) # min amount is set markets["ETH/BTC"]["limits"] = { @@ -411,6 +414,9 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) expected_result = 2 * 2 * (1+0.05) / (1-abs(stoploss)) assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0) + assert isclose(result, expected_result/5) # min amount and cost are set (cost is minimal) markets["ETH/BTC"]["limits"] = { @@ -424,6 +430,9 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) expected_result = max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss)) assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10) + assert isclose(result, expected_result/10) # min amount and cost are set (amount is minial) markets["ETH/BTC"]["limits"] = { @@ -437,15 +446,24 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) expected_result = max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss)) assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0) + assert isclose(result, expected_result/7.0) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) expected_result = max(8, 2 * 2) * 1.5 assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0) + assert isclose(result, expected_result/8.0) # Really big stoploss result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) expected_result = max(8, 2 * 2) * 1.5 assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) + assert isclose(result, expected_result/12) def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: @@ -465,6 +483,8 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) expected_result = max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)) assert round(result, 8) == round(expected_result, 8) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0) + assert round(result, 8) == round(expected_result/3, 8) def test_set_sandbox(default_conf, mocker): @@ -3243,6 +3263,25 @@ def test__get_funding_fees_from_exchange(default_conf, mocker, exchange_name): ) +@pytest.mark.parametrize('exchange', ['binance', 'kraken', 'ftx']) +@pytest.mark.parametrize('stake_amount,leverage,min_stake_with_lev', [ + (9.0, 3.0, 3.0), + (20.0, 5.0, 4.0), + (100.0, 100.0, 1.0) +]) +def test_get_stake_amount_considering_leverage( + exchange, + stake_amount, + leverage, + min_stake_with_lev, + mocker, + default_conf +): + exchange = get_patched_exchange(mocker, default_conf, id=exchange) + assert exchange._get_stake_amount_considering_leverage( + stake_amount, leverage) == min_stake_with_lev + + @pytest.mark.parametrize("exchange_name,trading_mode", [ ("binance", TradingMode.FUTURES), ("ftx", TradingMode.MARGIN), From e10ceb2362959e6a36e53b54f45e2ef3efd3e971 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 17 Nov 2021 07:48:07 -0600 Subject: [PATCH 05/23] Amount to precision has _amount_to_contract_size in it --- freqtrade/exchange/exchange.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 45b566bcf..885d9769f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -587,6 +587,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) if self.markets[pair]['precision']['amount']: amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE, precision=self.markets[pair]['precision']['amount'], @@ -649,9 +650,11 @@ class Exchange: if ('amount' in limits and 'min' in limits['amount'] and limits['amount']['min'] is not None): - self._contract_size_to_amount( - pair, - min_stake_amounts.append(limits['amount']['min'] * price) + min_stake_amounts.append( + self._contract_size_to_amount( + pair, + limits['amount']['min'] * price + ) ) if not min_stake_amounts: @@ -688,7 +691,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._contract_size_to_amount(pair, self.amount_to_precision(pair, amount)) dry_order: Dict[str, Any] = { 'id': order_id, 'symbol': pair, @@ -840,15 +843,15 @@ class Exchange: def _amount_to_contract_size(self, pair: str, amount: float): - if ('contractSize' in self._api.markets[pair]): - return amount / self._api.markets[pair]['contractSize'] + 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): - if ('contractSize' in self._api.markets[pair]): - return amount * self._api.markets[pair]['contractSize'] + if ('contractSize' in self.markets[pair]): + return amount * self.markets[pair]['contractSize'] else: return amount @@ -869,7 +872,6 @@ class Exchange: rate_for_order = self.price_to_precision(pair, rate) if needs_price else None self._lev_prep(pair, leverage) - amount = self._amount_to_contract_size(pair, amount) order = self._api.create_order( pair, ordertype, @@ -1269,7 +1271,7 @@ class Exchange: # validate that markets are loaded before trying to get fee if self._api.markets is None or len(self._api.markets) == 0: self._api.load_markets() - + # TODO-lev: Convert this amount to contract size? return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount, price=price, takerOrMaker=taker_or_maker)['rate'] except ccxt.DDoSProtection as e: From 4f6203e45f9a86ba4c87717e5cff518c04dadf5a Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Dec 2021 00:56:34 -0600 Subject: [PATCH 06/23] Added conversions from contract size to amount for objects returned from api --- freqtrade/exchange/exchange.py | 146 ++++++++++++++++++++++++++++---- tests/exchange/test_exchange.py | 27 +++++- 2 files changed, 156 insertions(+), 17 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 885d9769f..e61ccd773 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -370,6 +370,30 @@ class Exchange: else: return DataFrame() + def _get_contract_size(self, pair: str) -> int: + if self.trading_mode == TradingMode.FUTURES: + return self.markets[pair]['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']) + if contract_size != 1: + for trade in trades: + trade['amount'] = trade['amount'] * contract_size + return trades + else: + return trades + + def _order_contracts_to_amount(self, order: Dict) -> Dict: + contract_size = self._get_contract_size(order['pair']) + if contract_size != 1: + for prop in ['amount', 'cost', 'filled', 'remaining']: + if prop in order: + order[prop] = order[prop] * contract_size + return order + def set_sandbox(self, api: ccxt.Exchange, exchange_config: dict, name: str) -> None: if exchange_config.get('sandbox'): if api.urls.get('test'): @@ -872,13 +896,15 @@ class Exchange: rate_for_order = self.price_to_precision(pair, rate) if needs_price else None self._lev_prep(pair, leverage) - order = self._api.create_order( - pair, - ordertype, - side, - amount, - rate_for_order, - params + order = self._order_contracts_to_amount( + self._api.create_order( + pair, + ordertype, + side, + amount, + rate_for_order, + params + ) ) self._log_exchange_response('create_order', order) return order @@ -927,7 +953,9 @@ class Exchange: if self._config['dry_run']: return self.fetch_dry_run_order(order_id) try: - order = self._api.fetch_order(order_id, pair) + order = self._order_contracts_to_amount( + self._api.fetch_order(order_id, pair) + ) self._log_exchange_response('fetch_order', order) return order except ccxt.OrderNotFound as e: @@ -981,7 +1009,9 @@ class Exchange: return {} try: - order = self._api.cancel_order(order_id, pair) + order = self._order_contracts_to_amount( + self._api.cancel_order(order_id, pair) + ) self._log_exchange_response('cancel_order', order) return order except ccxt.InvalidOrder as e: @@ -1245,9 +1275,13 @@ class Exchange: # since needs to be int in milliseconds _params = params if params else {} my_trades = self._api.fetch_my_trades( - pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000), - params=_params) - matched_trades = [trade for trade in my_trades if trade['order'] == order_id] + pair, + int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000), + params=_params + ) + matched_trades = self._trades_contracts_to_amount( + trades=[trade for trade in my_trades if trade['order'] == order_id] + ) self._log_exchange_response('get_trades_for_order', matched_trades) return matched_trades @@ -1584,14 +1618,18 @@ class Exchange: # fetch trades asynchronously if params: logger.debug("Fetching trades for pair %s, params: %s ", pair, params) - trades = await self._api_async.fetch_trades(pair, params=params, limit=1000) + trades = self._trades_contracts_to_amount( + trades=await self._api_async.fetch_trades(pair, params=params, limit=1000), + ) else: logger.debug( "Fetching trades for pair %s, since %s %s...", pair, since, '(' + 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=await self._api_async.fetch_trades(pair, since=since, limit=1000), + ) return trades_dict_to_list(trades) except ccxt.NotSupported as e: raise OperationalException( @@ -1727,7 +1765,7 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - @retrier + @ retrier def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float: """ Returns the sum of all funding fees that were exchanged for a pair within a timeframe @@ -1833,7 +1871,7 @@ class Exchange: """ return open_date.minute > 0 or open_date.second > 0 - @retrier + @ retrier def set_margin_mode(self, pair: str, collateral: Collateral, params: dict = {}): """ Set's the margin mode on the exchange to cross or isolated for a specific pair @@ -1853,6 +1891,46 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + @retrier + def _get_mark_price_history(self, pair: str, since: int) -> Dict: + """ + Get's the mark price history for a pair + :param pair: The quote/base pair of the trade + :param since: The earliest time to start downloading candles, in ms. + """ + + try: + candles = self._api.fetch_ohlcv( + pair, + timeframe="1h", + since=since, + params={ + 'price': self._ft_has["mark_ohlcv_price"] + } + ) + history = {} + for candle in candles: + d = datetime.fromtimestamp(int(candle[0] / 1000), timezone.utc) + # Round down to the nearest hour, in case of a delayed timestamp + # The millisecond timestamps can be delayed ~20ms + time = timeframe_to_prev_date('1h', d).timestamp() * 1000 + opening_mark_price = candle[1] + history[time] = opening_mark_price + return history + except ccxt.NotSupported as e: + raise OperationalException( + f'Exchange {self._api.name} does not support fetching historical ' + f'mark price candle (OHLCV) data. Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError(f'Could not fetch historical mark price candle (OHLCV) data ' + f'for pair {pair} due to {e.__class__.__name__}. ' + f'Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(f'Could not fetch historical mark price candle (OHLCV) data ' + f'for pair {pair}. Message: {e}') from e + def _calculate_funding_fees( self, pair: str, @@ -1919,6 +1997,42 @@ class Exchange: else: return 0.0 + @retrier + def get_funding_rate_history(self, pair: str, since: int) -> Dict: + """ + :param pair: quote/base currency pair + :param since: timestamp in ms of the beginning time + :param end: timestamp in ms of the end time + """ + if not self.exchange_has("fetchFundingRateHistory"): + raise ExchangeError( + f"fetch_funding_rate_history is not available using {self.name}" + ) + + # TODO-lev: Gateio has a max limit into the past of 333 days, okex has a limit of 3 months + try: + funding_history: Dict = {} + response = self._api.fetch_funding_rate_history( + pair, + limit=1000, + since=since + ) + for fund in response: + d = datetime.fromtimestamp(int(fund['timestamp'] / 1000), timezone.utc) + # Round down to the nearest hour, in case of a delayed timestamp + # The millisecond timestamps can be delayed ~20ms + time = int(timeframe_to_prev_date('1h', d).timestamp() * 1000) + + funding_history[time] = fund['fundingRate'] + return funding_history + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 9f0f272e1..8c046fd5b 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3549,7 +3549,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 +3575,28 @@ 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 + + +def test__get_contract_size(): + # TODO + return + + +def test__trades_contracts_to_amount(): + # TODO + return + + +def test__order_contracts_to_amount(): + # TODO + return + + +def test__amount_to_contract_size(): + # TODO + return + + +def test__contract_size_to_amount(): + # TODO + return From d0a300a2e10ddb235b2d3e390bd2d6870f9b5d4d Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Dec 2021 01:03:02 -0600 Subject: [PATCH 07/23] Added TODOs --- freqtrade/exchange/exchange.py | 4 ++-- tests/exchange/test_exchange.py | 20 +++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e61ccd773..a671a33ca 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1765,7 +1765,7 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - @ retrier + @retrier def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float: """ Returns the sum of all funding fees that were exchanged for a pair within a timeframe @@ -1871,7 +1871,7 @@ class Exchange: """ return open_date.minute > 0 or open_date.second > 0 - @ retrier + @retrier def set_margin_mode(self, pair: str, collateral: Collateral, params: dict = {}): """ Set's the margin mode on the exchange to cross or isolated for a specific pair diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8c046fd5b..5e59e7251 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -243,6 +243,7 @@ def test_amount_to_precision(default_conf, mocker, amount, precision_mode, preci """ Test rounds down """ + # TODO-lev: Test for contract sizes of 0.01 and 10 markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': precision}}}) @@ -324,6 +325,7 @@ def test_price_get_one_pip(default_conf, mocker, price, precision_mode, precisio def test_get_min_pair_stake_amount(mocker, default_conf) -> None: + # TODO-lev: Test for contract sizes of 0.01 and 10 exchange = get_patched_exchange(mocker, default_conf, id="binance") stoploss = -0.05 @@ -1004,6 +1006,7 @@ 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) @@ -1120,6 +1123,7 @@ 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} @@ -2243,7 +2247,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 @@ -2513,6 +2517,7 @@ 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("exchange_name", EXCHANGES) def test_cancel_order(default_conf, mocker, exchange_name): + # TODO-lev: Test for contract sizes of 0.01 and 10 default_conf['dry_run'] = False api_mock = MagicMock() api_mock.cancel_order = MagicMock(return_value={'id': '123'}) @@ -2591,6 +2596,7 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name): @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 default_conf['dry_run'] = True default_conf['exchange']['log_responses'] = True order = MagicMock() @@ -2702,7 +2708,7 @@ def test_name(default_conf, mocker, exchange_name): @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 order_id = 'ABCD-ABCD' since = datetime(2018, 5, 5, 0, 0, 0) default_conf["dry_run"] = False @@ -3578,25 +3584,25 @@ def test__calculate_funding_fees_datetime_called( def test__get_contract_size(): - # TODO + # TODO-lev return def test__trades_contracts_to_amount(): - # TODO + # TODO-lev return def test__order_contracts_to_amount(): - # TODO + # TODO-lev return def test__amount_to_contract_size(): - # TODO + # TODO-lev return def test__contract_size_to_amount(): - # TODO + # TODO-lev return From 78d1a267f08a9fc68dd97a4f9a51bbb0e5d7c27b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 21 Dec 2021 15:45:16 -0600 Subject: [PATCH 08/23] 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 From 49a6ebb4545e0ab5531d29f567c5680ea93793ab Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 31 Dec 2021 04:29:19 -0600 Subject: [PATCH 09/23] exchange class contract methods safe check for symbol --- freqtrade/exchange/exchange.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 8a87a2c7f..eaebd06d3 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -381,21 +381,20 @@ class Exchange: return 1 def _trades_contracts_to_amount(self, trades: List) -> List: - if len(trades) > 0: + 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 - else: - return trades + return trades def _order_contracts_to_amount(self, order: Dict) -> Dict: - 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 + 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 set_sandbox(self, api: ccxt.Exchange, exchange_config: dict, name: str) -> None: From 8da596f66d05ee393915d1bbf6dc64a76fe97d6a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Dec 2021 14:38:17 +0100 Subject: [PATCH 10/23] Implement PR feedback --- freqtrade/exchange/exchange.py | 41 +++++++++++++--------------- tests/exchange/test_exchange.py | 47 +++++++++++++-------------------- tests/exchange/test_kraken.py | 2 ++ 3 files changed, 39 insertions(+), 51 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index eaebd06d3..1f395b014 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -904,17 +904,16 @@ class Exchange: rate_for_order = self.price_to_precision(pair, rate) if needs_price else None self._lev_prep(pair, leverage) - order = self._order_contracts_to_amount( - self._api.create_order( - pair, - ordertype, - side, - amount, - rate_for_order, - params - ) + order = self._api.create_order( + pair, + ordertype, + side, + amount, + rate_for_order, + params ) self._log_exchange_response('create_order', order) + order = self._order_contracts_to_amount(order) return order except ccxt.InsufficientFunds as e: @@ -961,10 +960,9 @@ class Exchange: if self._config['dry_run']: return self.fetch_dry_run_order(order_id) try: - order = self._order_contracts_to_amount( - self._api.fetch_order(order_id, pair) - ) + 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( @@ -1017,10 +1015,9 @@ class Exchange: return {} try: - order = self._order_contracts_to_amount( - self._api.cancel_order(order_id, pair) - ) + 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( @@ -1292,6 +1289,9 @@ class Exchange: ) 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 @@ -1313,7 +1313,7 @@ class Exchange: # validate that markets are loaded before trying to get fee if self._api.markets is None or len(self._api.markets) == 0: self._api.load_markets() - # TODO-lev: Convert this amount to contract size? + return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount, price=price, takerOrMaker=taker_or_maker)['rate'] except ccxt.DDoSProtection as e: @@ -1626,18 +1626,15 @@ class Exchange: # fetch trades asynchronously if params: logger.debug("Fetching trades for pair %s, params: %s ", pair, params) - trades = self._trades_contracts_to_amount( - trades=await self._api_async.fetch_trades(pair, params=params, limit=1000), - ) + trades = await self._api_async.fetch_trades(pair, params=params, limit=1000) else: logger.debug( "Fetching trades for pair %s, since %s %s...", pair, since, '(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else '' ) - trades = self._trades_contracts_to_amount( - trades=await self._api_async.fetch_trades(pair, since=since, limit=1000), - ) + 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/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e253f82b1..02429f128 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -331,11 +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, markets) -> None: +def test_get_min_pair_stake_amount(mocker, default_conf) -> 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( @@ -1223,6 +1223,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' } @@ -1298,6 +1299,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' } @@ -1360,6 +1362,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' } @@ -1426,6 +1429,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' } @@ -2582,31 +2586,23 @@ 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, trading_mode, amount): +def test_cancel_order(default_conf, mocker, exchange_name): 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', 'amount': 2, 'symbol': 'ETH/USDT:USDT'}) + api_mock.cancel_order = MagicMock(return_value={'id': '123'}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.cancel_order( - order_id='_', pair='ETH/USDT:USDT') == {'id': '123', 'amount': amount, 'symbol': 'ETH/USDT:USDT'} + assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == {'id': '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='ETH/USDT:USDT') + 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='ETH/USDT:USDT') + order_id='_', pair='TKN/BTC') @pytest.mark.parametrize("exchange_name", EXCHANGES) @@ -2668,16 +2664,13 @@ 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, trading_mode, amount): +def test_fetch_order(default_conf, mocker, exchange_name, caplog): default_conf['dry_run'] = True default_conf['exchange']['log_responses'] = True order = MagicMock() order.myid = 123 + order.symbol = 'TKN/BTC' exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange._dry_run_open_orders['X'] = order @@ -2687,17 +2680,13 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog, trading_mode, 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={'id': '123', 'amount': 2, 'symbol': 'ETH/USDT:USDT'}) + 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') == {'id': '123', 'amount': amount, 'symbol': 'ETH/USDT:USDT'} + 'X', 'TKN/BTC') == {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'} assert log_has( - ("API fetch_order: {\'id\': \'123\', \'amount\': " - + str(amount) + ", \'symbol\': \'ETH/USDT:USDT\'}" + ("API fetch_order: {\'id\': \'123\', \'amount\': 2, \'symbol\': \'TKN/BTC\'}" ), caplog ) @@ -2744,9 +2733,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")) 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' } From a85566d6c3afaccf73f2d374195c9522745a74d3 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 31 Dec 2021 04:37:22 -0600 Subject: [PATCH 11/23] test_exchange.test_create_order removed # assert api_mock.create_order.call_args[0][3] == 100 --- tests/exchange/test_exchange.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 02429f128..32840d4d2 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1200,7 +1200,6 @@ 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 From d105bb764a1c058cd48195d741b4d0d2a126d3f0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 31 Dec 2021 04:50:55 -0600 Subject: [PATCH 12/23] test__get_contract_size creates its own markets instead of using the markets from conftest --- tests/exchange/test_exchange.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 32840d4d2..c467c9457 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3747,11 +3747,27 @@ def test__calculate_funding_fees_datetime_called( ('LTC/ETH', 1, 'futures'), ('ETH/USDT:USDT', 10, 'futures') ]) -def test__get_contract_size(mocker, default_conf, markets, pair, expected_size, trading_mode): +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', markets) + 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 From 6ab0e870c22b492e0f2d4ad8beddf22fee0c6c0f Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 31 Dec 2021 05:01:19 -0600 Subject: [PATCH 13/23] fixed breaking test test_amount_to_precision --- tests/exchange/test_exchange.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index c467c9457..429ba153f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -237,8 +237,8 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog): (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), + (2.9909, 4, 0.005, 0.01, 299.09), + (2.9999, 4, 0.005, 10, 0.295), ]) def test_amount_to_precision(default_conf, mocker, amount, precision_mode, precision, contract_size, expected): """ From f92d47a16bb61fc9631b0cac29cb63b68cab25f3 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 31 Dec 2021 05:28:25 -0600 Subject: [PATCH 14/23] exchange._contracts_to_amount and exchange._amount_to_contracts safe checks --- freqtrade/exchange/exchange.py | 12 ++++++++++-- tests/exchange/test_exchange.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 1f395b014..cdb6de52a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -875,15 +875,23 @@ class Exchange: def _amount_to_contracts(self, pair: str, amount: float): + contract_size = None if ('contractSize' in self.markets[pair]): - return amount / self.markets[pair]['contractSize'] + contract_size = self.markets[pair]['contractSize'] + + if (contract_size and self.trading_mode == TradingMode.FUTURES): + return amount / contract_size else: return amount def _contracts_to_amount(self, pair: str, num_contracts: float): + contract_size = None if ('contractSize' in self.markets[pair]): - return num_contracts * self.markets[pair]['contractSize'] + contract_size = self.markets[pair]['contractSize'] + + if (contract_size and self.trading_mode == TradingMode.FUTURES): + return num_contracts * contract_size else: return num_contracts diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 429ba153f..94aee0e14 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3897,6 +3897,7 @@ def test__trades_contracts_to_amount( @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( @@ -3908,9 +3909,32 @@ def test__amount_to_contracts( param_size ): api_mock = MagicMock() - default_conf['trading_mode'] = 'futures' + default_conf['trading_mode'] = 'spot' default_conf['collateral'] = 'isolated' - mocker.patch('freqtrade.exchange.Exchange.markets', markets) + 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 From 230dd15ee73500d9c9af27deb6d6c73d4b217933 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 31 Dec 2021 06:40:12 -0600 Subject: [PATCH 15/23] fixed test_amount_to_precision --- tests/exchange/test_exchange.py | 40 +++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 94aee0e14..268da5b69 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -225,22 +225,31 @@ 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,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), +@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, 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, 299.09), - (2.9999, 4, 0.005, 10, 0.295), + (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, contract_size, expected): +def test_amount_to_precision( + default_conf, + mocker, + amount, + precision_mode, + precision, + contract_size, + expected, + trading_mode +): """ Test rounds down """ @@ -254,6 +263,9 @@ def test_amount_to_precision(default_conf, mocker, amount, precision_mode, preci } }) + default_conf['trading_mode'] = trading_mode + default_conf['collateral'] = 'isolated' + exchange = get_patched_exchange(mocker, default_conf, id="binance") # digits counting mode # DECIMAL_PLACES = 2 From 48567a130165878f5b8a1641de8bb393bc747764 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 31 Dec 2021 07:00:47 -0600 Subject: [PATCH 16/23] fixe broken test_get_min_pair_stake_amount --- tests/exchange/test_exchange.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 268da5b69..4953539f7 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -486,6 +486,9 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: 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) From 3d4a5eab81da68bfbde6e6622107058ac3b681b4 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 31 Dec 2021 07:26:46 -0600 Subject: [PATCH 17/23] fixed flake8 error --- tests/exchange/test_exchange.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4953539f7..eddd1ed33 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -20,7 +20,6 @@ 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 From fcded264e6d04fe1f8553c70ac4ad6dc30697d39 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 1 Jan 2022 13:53:26 -0600 Subject: [PATCH 18/23] removed exchange._get_mark_price_history --- freqtrade/exchange/exchange.py | 40 ---------------------------------- 1 file changed, 40 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index cdb6de52a..4c9736bbe 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1904,46 +1904,6 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - @retrier - def _get_mark_price_history(self, pair: str, since: int) -> Dict: - """ - Get's the mark price history for a pair - :param pair: The quote/base pair of the trade - :param since: The earliest time to start downloading candles, in ms. - """ - - try: - candles = self._api.fetch_ohlcv( - pair, - timeframe="1h", - since=since, - params={ - 'price': self._ft_has["mark_ohlcv_price"] - } - ) - history = {} - for candle in candles: - d = datetime.fromtimestamp(int(candle[0] / 1000), timezone.utc) - # Round down to the nearest hour, in case of a delayed timestamp - # The millisecond timestamps can be delayed ~20ms - time = timeframe_to_prev_date('1h', d).timestamp() * 1000 - opening_mark_price = candle[1] - history[time] = opening_mark_price - return history - except ccxt.NotSupported as e: - raise OperationalException( - f'Exchange {self._api.name} does not support fetching historical ' - f'mark price candle (OHLCV) data. Message: {e}') from e - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError(f'Could not fetch historical mark price candle (OHLCV) data ' - f'for pair {pair} due to {e.__class__.__name__}. ' - f'Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(f'Could not fetch historical mark price candle (OHLCV) data ' - f'for pair {pair}. Message: {e}') from e - def _calculate_funding_fees( self, pair: str, From 3e4912979ada5db670bf5649dec738c55caa215d Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 1 Jan 2022 14:03:26 -0600 Subject: [PATCH 19/23] exchange.py: removed get funding rate history --- freqtrade/exchange/exchange.py | 36 ---------------------------------- 1 file changed, 36 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 4c9736bbe..9631757a9 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1970,42 +1970,6 @@ class Exchange: else: return 0.0 - @retrier - def get_funding_rate_history(self, pair: str, since: int) -> Dict: - """ - :param pair: quote/base currency pair - :param since: timestamp in ms of the beginning time - :param end: timestamp in ms of the end time - """ - if not self.exchange_has("fetchFundingRateHistory"): - raise ExchangeError( - f"fetch_funding_rate_history is not available using {self.name}" - ) - - # TODO-lev: Gateio has a max limit into the past of 333 days, okex has a limit of 3 months - try: - funding_history: Dict = {} - response = self._api.fetch_funding_rate_history( - pair, - limit=1000, - since=since - ) - for fund in response: - d = datetime.fromtimestamp(int(fund['timestamp'] / 1000), timezone.utc) - # Round down to the nearest hour, in case of a delayed timestamp - # The millisecond timestamps can be delayed ~20ms - time = int(timeframe_to_prev_date('1h', d).timestamp() * 1000) - - funding_history[time] = fund['fundingRate'] - return funding_history - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e - def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) From 14ae327459d4aa8476c39f72417983c26a45cea2 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 1 Jan 2022 14:08:10 -0600 Subject: [PATCH 20/23] grouped contract methods --- freqtrade/exchange/exchange.py | 44 +++++++++++++++++----------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9631757a9..5cadc242e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -397,6 +397,28 @@ class Exchange: order[prop] = order[prop] * contract_size return order + def _amount_to_contracts(self, pair: str, amount: float): + + contract_size = None + if ('contractSize' in self.markets[pair]): + contract_size = self.markets[pair]['contractSize'] + + if (contract_size and self.trading_mode == TradingMode.FUTURES): + return amount / contract_size + else: + return amount + + def _contracts_to_amount(self, pair: str, num_contracts: float): + + contract_size = None + if ('contractSize' in self.markets[pair]): + contract_size = self.markets[pair]['contractSize'] + + if (contract_size and self.trading_mode == TradingMode.FUTURES): + 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'): @@ -873,28 +895,6 @@ class Exchange: params.update({param: time_in_force}) return params - def _amount_to_contracts(self, pair: str, amount: float): - - contract_size = None - if ('contractSize' in self.markets[pair]): - contract_size = self.markets[pair]['contractSize'] - - if (contract_size and self.trading_mode == TradingMode.FUTURES): - return amount / contract_size - else: - return amount - - def _contracts_to_amount(self, pair: str, num_contracts: float): - - contract_size = None - if ('contractSize' in self.markets[pair]): - contract_size = self.markets[pair]['contractSize'] - - if (contract_size and self.trading_mode == TradingMode.FUTURES): - return num_contracts * contract_size - else: - 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: # TODO-lev: remove default for leverage From 67a57395012129c37960b6f53666b84fd3bfe863 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 1 Jan 2022 14:29:13 -0600 Subject: [PATCH 21/23] fixed test_get_trades_for_order for contracts --- tests/exchange/test_exchange.py | 79 +-------------------------------- 1 file changed, 2 insertions(+), 77 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index eddd1ed33..79de1e1b2 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2797,7 +2797,7 @@ def test_name(default_conf, mocker, exchange_name): @pytest.mark.parametrize("trading_mode,amount", [ ('spot', 0.2340606), - ('futures', 2.340606), + ('futures', 23.40606), ]) @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_trades_for_order(default_conf, mocker, exchange_name, trading_mode, amount): @@ -2835,7 +2835,7 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name, trading_mode, 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 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) @@ -3558,81 +3558,6 @@ 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), From 7f88f9bf27dacba038a382b54b009bbec6292b3e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Jan 2022 13:11:29 +0100 Subject: [PATCH 22/23] Revert unintended double-call of amount conversion --- freqtrade/exchange/exchange.py | 10 +++------- tests/exchange/test_exchange.py | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5cadc242e..1141b0fa6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1288,13 +1288,9 @@ class Exchange: # since needs to be int in milliseconds _params = params if params else {} my_trades = self._api.fetch_my_trades( - pair, - int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000), - params=_params - ) - matched_trades = self._trades_contracts_to_amount( - trades=[trade for trade in my_trades if trade['order'] == order_id] - ) + pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000), + params=_params) + matched_trades = [trade for trade in my_trades if trade['order'] == order_id] self._log_exchange_response('get_trades_for_order', matched_trades) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 79de1e1b2..7edc3c755 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2797,7 +2797,7 @@ def test_name(default_conf, mocker, exchange_name): @pytest.mark.parametrize("trading_mode,amount", [ ('spot', 0.2340606), - ('futures', 23.40606), + ('futures', 2.340606), ]) @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_trades_for_order(default_conf, mocker, exchange_name, trading_mode, amount): From d8cb61278f2ccdf6a9d4a5e8aa51fd6cb892e1c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Jan 2022 18:12:45 +0100 Subject: [PATCH 23/23] Simplify contract conversion code by reusing "get_contract_size" --- freqtrade/exchange/exchange.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 1141b0fa6..43ce37051 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -399,22 +399,16 @@ class Exchange: def _amount_to_contracts(self, pair: str, amount: float): - contract_size = None - if ('contractSize' in self.markets[pair]): - contract_size = self.markets[pair]['contractSize'] - - if (contract_size and self.trading_mode == TradingMode.FUTURES): + 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 = None - if ('contractSize' in self.markets[pair]): - contract_size = self.markets[pair]['contractSize'] - - if (contract_size and self.trading_mode == TradingMode.FUTURES): + contract_size = self._get_contract_size(pair) + if contract_size and contract_size != 1: return num_contracts * contract_size else: return num_contracts