| @@ -283,17 +283,20 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes | ||||
|             # Predefined candletype (and timeframe) depending on exchange | ||||
|             # Downloads what is necessary to backtest based on futures data. | ||||
|             timeframe = exchange._ft_has['mark_ohlcv_timeframe'] | ||||
|             candle_type = CandleType.from_string(exchange._ft_has['mark_ohlcv_price']) | ||||
|  | ||||
|             # TODO: this could be in most parts to the above. | ||||
|             if erase: | ||||
|                 if data_handler.ohlcv_purge(pair, timeframe, candle_type=candle_type): | ||||
|                     logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.') | ||||
|             _download_pair_history(pair=pair, process=process, | ||||
|                                    datadir=datadir, exchange=exchange, | ||||
|                                    timerange=timerange, data_handler=data_handler, | ||||
|                                    timeframe=str(timeframe), new_pairs_days=new_pairs_days, | ||||
|                                    candle_type=candle_type) | ||||
|             fr_candle_type = CandleType.from_string(exchange._ft_has['mark_ohlcv_price']) | ||||
|             # All exchanges need FundingRate for futures trading. | ||||
|             # The timeframe is aligned to the mark-price timeframe. | ||||
|             for candle_type in (CandleType.FUNDING_RATE, fr_candle_type): | ||||
|                 # TODO: this could be in most parts to the above. | ||||
|                 if erase: | ||||
|                     if data_handler.ohlcv_purge(pair, timeframe, candle_type=candle_type): | ||||
|                         logger.info( | ||||
|                             f'Deleting existing data for pair {pair}, interval {timeframe}.') | ||||
|                 _download_pair_history(pair=pair, process=process, | ||||
|                                        datadir=datadir, exchange=exchange, | ||||
|                                        timerange=timerange, data_handler=data_handler, | ||||
|                                        timeframe=str(timeframe), new_pairs_days=new_pairs_days, | ||||
|                                        candle_type=candle_type) | ||||
|  | ||||
|     return pairs_not_available | ||||
|  | ||||
|   | ||||
| @@ -1389,7 +1389,8 @@ class Exchange: | ||||
|         return pair, timeframe, candle_type, data | ||||
|  | ||||
|     def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *, | ||||
|                              since_ms: Optional[int] = None, cache: bool = True | ||||
|                              since_ms: Optional[int] = None, cache: bool = True, | ||||
|                              drop_incomplete: bool = None | ||||
|                              ) -> Dict[PairWithTimeframe, DataFrame]: | ||||
|         """ | ||||
|         Refresh in-memory OHLCV asynchronously and set `_klines` with the result | ||||
| @@ -1398,10 +1399,13 @@ class Exchange: | ||||
|         :param pair_list: List of 2 element tuples containing pair, interval to refresh | ||||
|         :param since_ms: time since when to download, in milliseconds | ||||
|         :param cache: Assign result to _klines. Usefull for one-off downloads like for pairlists | ||||
|         :param drop_incomplete: Control candle dropping. | ||||
|             Specifying None defaults to _ohlcv_partial_candle | ||||
|         :return: Dict of [{(pair, timeframe): Dataframe}] | ||||
|         """ | ||||
|         logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list)) | ||||
|  | ||||
|         # TODO-lev: maybe depend this on candle type? | ||||
|         drop_incomplete = self._ohlcv_partial_candle if drop_incomplete is None else drop_incomplete | ||||
|         input_coroutines = [] | ||||
|         cached_pairs = [] | ||||
|         # Gather coroutines to run | ||||
| @@ -1447,7 +1451,7 @@ class Exchange: | ||||
|                 # keeping parsed dataframe in cache | ||||
|                 ohlcv_df = ohlcv_to_dataframe( | ||||
|                     ticks, timeframe, pair=pair, fill_missing=True, | ||||
|                     drop_incomplete=self._ohlcv_partial_candle) | ||||
|                     drop_incomplete=drop_incomplete) | ||||
|                 results_df[(pair, timeframe, c_type)] = ohlcv_df | ||||
|                 if cache: | ||||
|                     self._klines[(pair, timeframe, c_type)] = ohlcv_df | ||||
| @@ -1494,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) | ||||
| @@ -1812,46 +1822,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, | ||||
| @@ -1870,36 +1840,33 @@ class Exchange: | ||||
|  | ||||
|         if self.funding_fee_cutoff(open_date): | ||||
|             open_date += timedelta(hours=1) | ||||
|  | ||||
|         open_date = timeframe_to_prev_date('1h', open_date) | ||||
|         timeframe = self._ft_has['mark_ohlcv_timeframe'] | ||||
|         timeframe_ff = self._ft_has.get('funding_fee_timeframe', | ||||
|                                         self._ft_has['mark_ohlcv_timeframe']) | ||||
|         open_date = timeframe_to_prev_date(timeframe, open_date) | ||||
|  | ||||
|         fees: float = 0 | ||||
|         if not close_date: | ||||
|             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, timeframe, CandleType.from_string(self._ft_has["mark_ohlcv_price"])) | ||||
|  | ||||
|         funding_comb: PairWithTimeframe = (pair, timeframe_ff, 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"]) | ||||
|         df = df[(df['date'] >= open_date) & (df['date'] <= close_date)] | ||||
|         fees = sum(df['open_fund'] * df['open_mark'] * amount) | ||||
|  | ||||
|         return fees | ||||
|  | ||||
| @@ -1920,42 +1887,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) | ||||
|   | ||||
| @@ -16,6 +16,8 @@ class Okex(Exchange): | ||||
|  | ||||
|     _ft_has: Dict = { | ||||
|         "ohlcv_candle_limit": 100, | ||||
|         "mark_ohlcv_timeframe": "4h", | ||||
|         "funding_fee_timeframe": "8h", | ||||
|     } | ||||
|  | ||||
|     _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ | ||||
|   | ||||
| @@ -490,7 +490,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No | ||||
| @pytest.mark.parametrize('trademode,callcount', [ | ||||
|     ('spot', 4), | ||||
|     ('margin', 4), | ||||
|     ('futures', 6), | ||||
|     ('futures', 8),  # Called 8 times - 4 normal, 2 funding and 2 mark/index calls | ||||
| ]) | ||||
| def test_refresh_backtest_ohlcv_data( | ||||
|         mocker, default_conf, markets, caplog, testdatadir, trademode, callcount): | ||||
|   | ||||
| @@ -91,7 +91,7 @@ def exchange_futures(request, exchange_conf, class_mocker): | ||||
|         exchange_conf['exchange']['name'] = request.param | ||||
|         exchange_conf['trading_mode'] = 'futures' | ||||
|         exchange_conf['collateral'] = 'cross' | ||||
|         # TODO-lev This mock should no longer be necessary once futures are enabled. | ||||
|         # TODO-lev: This mock should no longer be necessary once futures are enabled. | ||||
|         class_mocker.patch( | ||||
|             'freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_collateral') | ||||
|         class_mocker.patch( | ||||
| @@ -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,34 +202,59 @@ class TestCCXTExchange(): | ||||
|  | ||||
|         pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair']) | ||||
|         since = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000) | ||||
|         timeframe_ff = exchange._ft_has.get('funding_fee_timeframe', | ||||
|                                             exchange._ft_has['mark_ohlcv_timeframe']) | ||||
|         pair_tf = (pair, timeframe_ff, 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) | ||||
|  | ||||
|         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 | ||||
|         assert isinstance(funding_ohlcv, dict) | ||||
|         rate = funding_ohlcv[pair_tf] | ||||
|  | ||||
|     @pytest.mark.skip("No futures support yet") | ||||
|     def test_fetch_mark_price_history(self, exchange_futures): | ||||
|         this_hour = timeframe_to_prev_date(timeframe_ff) | ||||
|         prev_hour = timeframe_to_prev_date(timeframe_ff, this_hour - timedelta(minutes=1)) | ||||
|         assert rate[rate['date'] == this_hour].iloc[0]['open'] != 0.0 | ||||
|         assert rate[rate['date'] == prev_hour].iloc[0]['open'] != 0.0 | ||||
|  | ||||
|     def test_ccxt_fetch_mark_price_history(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 = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000) | ||||
|         pair_tf = (pair, '1h', CandleType.MARK) | ||||
|  | ||||
|         mark_candles = exchange._get_mark_price_history(pair, since) | ||||
|         mark_ohlcv = exchange.refresh_latest_ohlcv( | ||||
|             [pair_tf], | ||||
|             since_ms=since, | ||||
|             drop_incomplete=False) | ||||
|  | ||||
|         assert isinstance(mark_candles, dict) | ||||
|         assert isinstance(mark_ohlcv, dict) | ||||
|         expected_tf = '1h' | ||||
|         mark_candles = mark_ohlcv[pair_tf] | ||||
|  | ||||
|         this_hour = timeframe_to_prev_date(expected_tf) | ||||
|         prev_tick = timeframe_to_prev_date(expected_tf, this_hour - timedelta(minutes=1)) | ||||
|         assert mark_candles[int(this_hour.timestamp() * 1000)] != 0.0 | ||||
|         assert mark_candles[int(prev_tick.timestamp() * 1000)] != 0.0 | ||||
|         prev_hour = timeframe_to_prev_date(expected_tf, this_hour - timedelta(minutes=1)) | ||||
|  | ||||
|         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 (?) | ||||
|  | ||||
|   | ||||
| @@ -3408,81 +3408,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), | ||||
| @@ -3566,14 +3491,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 +3515,9 @@ 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}) | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,7 @@ from unittest.mock import ANY, MagicMock, PropertyMock | ||||
|  | ||||
| import arrow | ||||
| import pytest | ||||
| from pandas import DataFrame | ||||
|  | ||||
| from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT | ||||
| from freqtrade.enums import CandleType, RPCMessageType, RunMode, SellType, SignalDirection, State | ||||
| @@ -4802,60 +4803,67 @@ def test_update_funding_fees( | ||||
|     patch_exchange(mocker) | ||||
|     default_conf['trading_mode'] = 'futures' | ||||
|     default_conf['collateral'] = 'isolated' | ||||
|     default_conf['dry_run'] = True | ||||
|     timestamp_midnight = 1630454400000 | ||||
|     timestamp_eight = 1630483200000 | ||||
|     funding_rates_midnight = { | ||||
|         "LTC/BTC": { | ||||
|             timestamp_midnight: 0.00032583, | ||||
|         }, | ||||
|         "ETH/BTC": { | ||||
|             timestamp_midnight: 0.0001, | ||||
|         }, | ||||
|         "XRP/BTC": { | ||||
|             timestamp_midnight: 0.00049426, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     funding_rates_eight = { | ||||
|         "LTC/BTC": { | ||||
|             timestamp_midnight: 0.00032583, | ||||
|             timestamp_eight: 0.00024472, | ||||
|         }, | ||||
|         "ETH/BTC": { | ||||
|             timestamp_midnight: 0.0001, | ||||
|             timestamp_eight: 0.0001, | ||||
|         }, | ||||
|         "XRP/BTC": { | ||||
|             timestamp_midnight: 0.00049426, | ||||
|             timestamp_eight: 0.00032715, | ||||
|         } | ||||
|     date_midnight = arrow.get('2021-09-01 00:00:00') | ||||
|     date_eight = arrow.get('2021-09-01 08:00:00') | ||||
|     date_sixteen = arrow.get('2021-09-01 16:00:00') | ||||
|     columns = ['date', 'open', 'high', 'low', 'close', 'volume'] | ||||
|     # 16:00 entry is actually never used | ||||
|     # But should be kept in the test to ensure we're filtering correctly. | ||||
|     funding_rates = { | ||||
|         "LTC/BTC": | ||||
|             DataFrame([ | ||||
|                 [date_midnight, 0.00032583, 0, 0, 0, 0], | ||||
|                 [date_eight, 0.00024472, 0, 0, 0, 0], | ||||
|                 [date_sixteen, 0.00024472, 0, 0, 0, 0], | ||||
|             ], columns=columns), | ||||
|         "ETH/BTC": | ||||
|             DataFrame([ | ||||
|                 [date_midnight, 0.0001, 0, 0, 0, 0], | ||||
|                 [date_eight, 0.0001, 0, 0, 0, 0], | ||||
|                 [date_sixteen, 0.0001, 0, 0, 0, 0], | ||||
|             ], columns=columns), | ||||
|         "XRP/BTC": | ||||
|             DataFrame([ | ||||
|                 [date_midnight, 0.00049426, 0, 0, 0, 0], | ||||
|                 [date_eight, 0.00032715, 0, 0, 0, 0], | ||||
|                 [date_sixteen, 0.00032715, 0, 0, 0, 0], | ||||
|             ], columns=columns) | ||||
|     } | ||||
|  | ||||
|     mark_prices = { | ||||
|         "LTC/BTC": { | ||||
|             timestamp_midnight: 3.3, | ||||
|             timestamp_eight: 3.2, | ||||
|         }, | ||||
|         "ETH/BTC": { | ||||
|             timestamp_midnight: 2.4, | ||||
|             timestamp_eight: 2.5, | ||||
|         }, | ||||
|         "XRP/BTC": { | ||||
|             timestamp_midnight: 1.2, | ||||
|             timestamp_eight: 1.2, | ||||
|         } | ||||
|         "LTC/BTC": | ||||
|             DataFrame([ | ||||
|                 [date_midnight, 3.3, 0, 0, 0, 0], | ||||
|                 [date_eight, 3.2, 0, 0, 0, 0], | ||||
|                 [date_sixteen, 3.2, 0, 0, 0, 0], | ||||
|             ], columns=columns), | ||||
|         "ETH/BTC": | ||||
|             DataFrame([ | ||||
|                 [date_midnight, 2.4, 0, 0, 0, 0], | ||||
|                 [date_eight, 2.5, 0, 0, 0, 0], | ||||
|                 [date_sixteen, 2.5, 0, 0, 0, 0], | ||||
|             ], columns=columns), | ||||
|         "XRP/BTC": | ||||
|             DataFrame([ | ||||
|                 [date_midnight, 1.2, 0, 0, 0, 0], | ||||
|                 [date_eight, 1.2, 0, 0, 0, 0], | ||||
|                 [date_sixteen, 1.2, 0, 0, 0, 0], | ||||
|             ], columns=columns) | ||||
|     } | ||||
|  | ||||
|     mocker.patch( | ||||
|         'freqtrade.exchange.Exchange._get_mark_price_history', | ||||
|         side_effect=lambda pair, since: mark_prices[pair] | ||||
|     ) | ||||
|     def refresh_latest_ohlcv_mock(pairlist, **kwargs): | ||||
|         ret = {} | ||||
|         for p, tf, ct in pairlist: | ||||
|             if ct == CandleType.MARK: | ||||
|                 ret[(p, tf, ct)] = mark_prices[p] | ||||
|             else: | ||||
|                 ret[(p, tf, ct)] = funding_rates[p] | ||||
|  | ||||
|     mocker.patch( | ||||
|         'freqtrade.exchange.Exchange.get_funding_rate_history', | ||||
|         side_effect=lambda pair, since: funding_rates_midnight[pair] | ||||
|     ) | ||||
|         return ret | ||||
|  | ||||
|     mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', | ||||
|                  side_effect=refresh_latest_ohlcv_mock) | ||||
|  | ||||
|     mocker.patch.multiple( | ||||
|         'freqtrade.exchange.Exchange', | ||||
| @@ -4880,38 +4888,33 @@ def test_update_funding_fees( | ||||
|     trades = Trade.get_open_trades() | ||||
|     assert len(trades) == 3 | ||||
|     for trade in trades: | ||||
|         assert trade.funding_fees == ( | ||||
|         assert pytest.approx(trade.funding_fees) == ( | ||||
|             trade.amount * | ||||
|             mark_prices[trade.pair][timestamp_midnight] * | ||||
|             funding_rates_midnight[trade.pair][timestamp_midnight] | ||||
|             mark_prices[trade.pair].iloc[0]['open'] * | ||||
|             funding_rates[trade.pair].iloc[0]['open'] | ||||
|         ) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.create_order', return_value=open_exit_order) | ||||
|     # create_mock_trades(fee, False) | ||||
|     time_machine.move_to("2021-09-01 08:00:00 +00:00") | ||||
|     mocker.patch( | ||||
|         'freqtrade.exchange.Exchange.get_funding_rate_history', | ||||
|         side_effect=lambda pair, since: funding_rates_eight[pair] | ||||
|     ) | ||||
|     if schedule_off: | ||||
|         for trade in trades: | ||||
|             assert trade.funding_fees == ( | ||||
|                 trade.amount * | ||||
|                 mark_prices[trade.pair][timestamp_midnight] * | ||||
|                 funding_rates_eight[trade.pair][timestamp_midnight] | ||||
|             ) | ||||
|             freqtrade.execute_trade_exit( | ||||
|                 trade=trade, | ||||
|                 # The values of the next 2 params are irrelevant for this test | ||||
|                 limit=ticker_usdt_sell_up()['bid'], | ||||
|                 sell_reason=SellCheckTuple(sell_type=SellType.ROI) | ||||
|             ) | ||||
|             assert trade.funding_fees == pytest.approx(sum( | ||||
|                 trade.amount * | ||||
|                 mark_prices[trade.pair].iloc[0:2]['open'] * | ||||
|                 funding_rates[trade.pair].iloc[0:2]['open'] | ||||
|             )) | ||||
|  | ||||
|     else: | ||||
|         freqtrade._schedule.run_pending() | ||||
|  | ||||
|     # Funding fees for 00:00 and 08:00 | ||||
|     for trade in trades: | ||||
|         assert trade.funding_fees == sum([ | ||||
|         assert trade.funding_fees == pytest.approx(sum( | ||||
|             trade.amount * | ||||
|             mark_prices[trade.pair][time] * | ||||
|             funding_rates_eight[trade.pair][time] for time in mark_prices[trade.pair].keys() | ||||
|         ]) | ||||
|             mark_prices[trade.pair].iloc[0:2]['open'] * funding_rates[trade.pair].iloc[0:2]['open'] | ||||
|             )) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user