From aabca85a5f7f4dc5b578670f25a26145ed84b221 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 10 Dec 2021 19:50:58 +0100 Subject: [PATCH] Update `_calculate_funding_fees` to reuse existing async infrastructure --- freqtrade/exchange/exchange.py | 53 +++++++++++++++--------------- tests/exchange/test_ccxt_compat.py | 30 +++++++++++++---- tests/exchange/test_exchange.py | 10 +++--- 3 files changed, 56 insertions(+), 37 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e09ed1c52..98014fa5f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1498,11 +1498,17 @@ class Exchange: params = deepcopy(self._ft_has.get('ohlcv_params', {})) if candle_type != CandleType.SPOT: params.update({'price': candle_type}) - data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, - since=since_ms, - limit=self.ohlcv_candle_limit(timeframe), - params=params) - + if candle_type != CandleType.FUNDING_RATE: + data = await self._api_async.fetch_ohlcv( + pair, timeframe=timeframe, since=since_ms, + limit=self.ohlcv_candle_limit(timeframe), params=params) + else: + # Funding rate + data = await self._api_async.fetch_funding_rate_history( + pair, since=since_ms, + limit=self.ohlcv_candle_limit(timeframe)) + # Convert funding rate to candle pattern + data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data] # Some exchanges sort OHLCV in ASC order and others in DESC. # Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last) # while GDAX returns the list of OHLCV in DESC order (newest first, oldest last) @@ -1882,28 +1888,23 @@ class Exchange: close_date = datetime.now(timezone.utc) open_timestamp = int(open_date.timestamp()) * 1000 # close_timestamp = int(close_date.timestamp()) * 1000 - funding_rate_history = self.get_funding_rate_history( - pair, - open_timestamp + + mark_comb: PairWithTimeframe = ( + pair, '1h', CandleType.from_string(self._ft_has["mark_ohlcv_price"])) + # TODO-lev: funding_rate downloading this way is not yet possible. + funding_comb: PairWithTimeframe = (pair, '1h', CandleType.FUNDING_RATE) + candle_histories = self.refresh_latest_ohlcv( + [mark_comb, funding_comb], + since_ms=open_timestamp, + cache=False, + drop_incomplete=False, ) - mark_price_history = self._get_mark_price_history( - pair, - open_timestamp - ) - for timestamp in funding_rate_history.keys(): - funding_rate = funding_rate_history[timestamp] - if timestamp in mark_price_history: - mark_price = mark_price_history[timestamp] - fees += self._get_funding_fee( - size=amount, - mark_price=mark_price, - funding_rate=funding_rate - ) - else: - logger.warning( - f"Mark price for {pair} at timestamp {timestamp} not found in " - f"funding_rate_history Funding fee calculation may be incorrect" - ) + funding_rates = candle_histories[funding_comb] + mark_rates = candle_histories[mark_comb] + + df = funding_rates.merge(mark_rates, on='date', how="inner", suffixes=["_fund", "_mark"]) + # TODO-lev: filter for relevant timeperiod? + fees = sum(df['open_fund'] * df['open_mark'] * amount) return fees diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index c9b2173b6..1da109cfb 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -195,7 +195,6 @@ class TestCCXTExchange(): assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now) def test_ccxt_fetch_funding_rate_history(self, exchange_futures): - # TODO-lev: enable this test once Futures mode is enabled. exchange, exchangename = exchange_futures if not exchange: # exchange_futures only returns values for supported exchanges @@ -203,15 +202,21 @@ class TestCCXTExchange(): pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair']) since = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000) + pair_tf = (pair, '1h', CandleType.FUNDING_RATE) - rate = exchange.get_funding_rate_history(pair, since) - assert isinstance(rate, dict) + funding_ohlcv = exchange.refresh_latest_ohlcv( + [pair_tf], + since_ms=since, + drop_incomplete=False) + + assert isinstance(funding_ohlcv, dict) + rate = funding_ohlcv[pair_tf] expected_tf = exchange._ft_has['mark_ohlcv_timeframe'] this_hour = timeframe_to_prev_date(expected_tf) - prev_tick = timeframe_to_prev_date(expected_tf, this_hour - timedelta(minutes=1)) - assert rate[int(this_hour.timestamp() * 1000)] != 0.0 - assert rate[int(prev_tick.timestamp() * 1000)] != 0.0 + prev_hour = timeframe_to_prev_date(expected_tf, this_hour - timedelta(minutes=1)) + assert rate[rate['date'] == this_hour].iloc[0]['open'] != 0.0 + assert rate[rate['date'] == prev_hour].iloc[0]['open'] != 0.0 def test_ccxt_fetch_mark_price_history(self, exchange_futures): exchange, exchangename = exchange_futures @@ -237,6 +242,19 @@ class TestCCXTExchange(): assert mark_candles[mark_candles['date'] == prev_hour].iloc[0]['open'] != 0.0 assert mark_candles[mark_candles['date'] == this_hour].iloc[0]['open'] != 0.0 + def test_ccxt__calculate_funding_fees(self, exchange_futures): + exchange, exchangename = exchange_futures + if not exchange: + # exchange_futures only returns values for supported exchanges + return + pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair']) + since = datetime.now(timezone.utc) - timedelta(days=5) + + funding_fee = exchange._calculate_funding_fees(pair, 20, open_date=since) + + assert isinstance(funding_fee, float) + # assert funding_fee > 0 + # TODO: tests fetch_trades (?) def test_ccxt_get_fee(self, exchange): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4632e6c56..23c0c6982 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3566,14 +3566,14 @@ def test__calculate_funding_fees( 'gateio': funding_rate_history_octohourly, }[exchange][rate_start:rate_end] api_mock = MagicMock() - api_mock.fetch_funding_rate_history = MagicMock(return_value=funding_rate_history) - api_mock.fetch_ohlcv = MagicMock(return_value=mark_ohlcv) + api_mock.fetch_funding_rate_history = get_mock_coro(return_value=funding_rate_history) + api_mock.fetch_ohlcv = get_mock_coro(return_value=mark_ohlcv) type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) funding_fees = exchange._calculate_funding_fees('ADA/USDT', amount, d1, d2) - assert funding_fees == expected_fees + assert pytest.approx(funding_fees, expected_fees) @ pytest.mark.parametrize('exchange,expected_fees', [ @@ -3590,8 +3590,8 @@ def test__calculate_funding_fees_datetime_called( expected_fees ): api_mock = MagicMock() - api_mock.fetch_ohlcv = MagicMock(return_value=mark_ohlcv) - api_mock.fetch_funding_rate_history = MagicMock(return_value=funding_rate_history_octohourly) + api_mock.fetch_ohlcv = get_mock_coro(return_value=mark_ohlcv) + api_mock.fetch_funding_rate_history = get_mock_coro(return_value=funding_rate_history_octohourly) type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True})