diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index b87912080..31d768a5f 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -248,7 +248,7 @@ class IDataHandler(ABC): timerange=timerange_startup, candle_type=candle_type ) - if self._check_empty_df(pairdf, pair, timeframe, warn_no_data): + if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data): return pairdf else: enddate = pairdf.iloc[-1]['date'] @@ -256,7 +256,7 @@ class IDataHandler(ABC): if timerange_startup: self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup) pairdf = trim_dataframe(pairdf, timerange_startup) - if self._check_empty_df(pairdf, pair, timeframe, warn_no_data): + if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data): return pairdf # incomplete candles should only be dropped if we didn't trim the end beforehand. @@ -265,18 +265,19 @@ class IDataHandler(ABC): fill_missing=fill_missing, drop_incomplete=(drop_incomplete and enddate == pairdf.iloc[-1]['date'])) - self._check_empty_df(pairdf, pair, timeframe, warn_no_data) + self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data) return pairdf - def _check_empty_df(self, pairdf: DataFrame, pair: str, timeframe: str, warn_no_data: bool): + def _check_empty_df(self, pairdf: DataFrame, pair: str, timeframe: str, + candle_type: CandleType, warn_no_data: bool): """ Warn on empty dataframe """ if pairdf.empty: if warn_no_data: logger.warning( - f'No history data for pair: "{pair}", timeframe: {timeframe}. ' - 'Use `freqtrade download-data` to download the data' + f"No history for {pair}, {candle_type}, {timeframe} found. " + "Use `freqtrade download-data` to download the data" ) return True return False diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fd4dee465..0e0d8c51e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1830,25 +1830,6 @@ class Exchange: else: return 1.0 - def _get_funding_fee( - self, - size: float, - funding_rate: float, - mark_price: float, - time_in_ratio: Optional[float] = None - ) -> float: - """ - Calculates a single funding fee - :param size: contract size * number of contracts - :param mark_price: The price of the asset that the contract is based off of - :param funding_rate: the interest rate and the premium - - interest rate: - - premium: varies by price difference between the perpetual contract and mark price - :param time_in_ratio: Not used by most exchange classes - """ - nominal_value = mark_price * size - return nominal_value * funding_rate - @retrier def _set_leverage( self, @@ -1901,18 +1882,21 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def _calculate_funding_fees( + def _fetch_and_calculate_funding_fees( self, pair: str, amount: float, + is_short: bool, open_date: datetime, close_date: Optional[datetime] = None ) -> float: """ - calculates the sum of all funding fees that occurred for a pair during a futures trade + Fetches and calculates the sum of all funding fees that occurred for a pair + during a futures trade. Only used during dry-run or if the exchange does not provide a funding_rates endpoint. :param pair: The quote/base pair of the trade :param amount: The quantity of the trade + :param is_short: trade direction :param open_date: The date and time that the trade started :param close_date: The date and time that the trade ended """ @@ -1924,7 +1908,6 @@ class Exchange: 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 @@ -1942,25 +1925,69 @@ class Exchange: ) funding_rates = candle_histories[funding_comb] mark_rates = candle_histories[mark_comb] + funding_mark_rates = self.combine_funding_and_mark( + funding_rates=funding_rates, mark_rates=mark_rates) + + return self.calculate_funding_fees( + funding_mark_rates, + amount=amount, + is_short=is_short, + open_date=open_date, + close_date=close_date + ) + + @staticmethod + def combine_funding_and_mark(funding_rates: DataFrame, mark_rates: DataFrame) -> DataFrame: + """ + Combine funding-rates and mark-rates dataframes + :param funding_rates: Dataframe containing Funding rates (Type FUNDING_RATE) + :param mark_rates: Dataframe containing Mark rates (Type mark_ohlcv_price) + """ + + return funding_rates.merge(mark_rates, on='date', how="inner", suffixes=["_fund", "_mark"]) + + def calculate_funding_fees( + self, + df: DataFrame, + amount: float, + is_short: bool, + open_date: datetime, + close_date: Optional[datetime] = None, + time_in_ratio: Optional[float] = None + ) -> float: + """ + calculates the sum of all funding fees that occurred for a pair during a futures trade + :param df: Dataframe containing combined funding and mark rates + as `open_fund` and `open_mark`. + :param amount: The quantity of the trade + :param is_short: trade direction + :param open_date: The date and time that the trade started + :param close_date: The date and time that the trade ended + :param time_in_ratio: Not used by most exchange classes + """ + fees: float = 0 - df = funding_rates.merge(mark_rates, on='date', how="inner", suffixes=["_fund", "_mark"]) if not df.empty: df = df[(df['date'] >= open_date) & (df['date'] <= close_date)] fees = sum(df['open_fund'] * df['open_mark'] * amount) - return fees + # Negate fees for longs as funding_fees expects it this way based on live endpoints. + return fees if is_short else -fees - def get_funding_fees(self, pair: str, amount: float, open_date: datetime) -> float: + def get_funding_fees( + self, pair: str, amount: float, is_short: bool, open_date: datetime) -> float: """ Fetch funding fees, either from the exchange (live) or calculates them based on funding rate/mark price history :param pair: The quote/base pair of the trade + :param is_short: trade direction :param amount: Trade amount :param open_date: Open date of the trade """ if self.trading_mode == TradingMode.FUTURES: if self._config['dry_run']: - funding_fees = self._calculate_funding_fees(pair, amount, open_date) + funding_fees = self._fetch_and_calculate_funding_fees( + pair, amount, is_short, open_date) else: funding_fees = self._get_funding_fees_from_exchange(pair, open_date) return funding_fees diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index f2e5e4476..7c67d870d 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -1,8 +1,10 @@ """ Kraken exchange subclass """ import logging +from datetime import datetime from typing import Any, Dict, List, Optional, Tuple import ccxt +from pandas import DataFrame from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, @@ -157,11 +159,13 @@ class Kraken(Exchange): params['leverage'] = leverage return params - def _get_funding_fee( + def calculate_funding_fees( self, - size: float, - funding_rate: float, - mark_price: float, + df: DataFrame, + amount: float, + is_short: bool, + open_date: datetime, + close_date: Optional[datetime] = None, time_in_ratio: Optional[float] = None ) -> float: """ @@ -169,16 +173,22 @@ class Kraken(Exchange): # ! passed to _get_funding_fee. For kraken futures to work in dry run and backtesting # ! functionality must be added that passes the parameter time_in_ratio to # ! _get_funding_fee when using Kraken - Calculates a single funding fee - :param size: contract size * number of contracts - :param mark_price: The price of the asset that the contract is based off of - :param funding_rate: the interest rate and the premium - - interest rate: - - premium: varies by price difference between the perpetual contract and mark price - :param time_in_ratio: time elapsed within funding period without position alteration + calculates the sum of all funding fees that occurred for a pair during a futures trade + :param df: Dataframe containing combined funding and mark rates + as `open_fund` and `open_mark`. + :param amount: The quantity of the trade + :param is_short: trade direction + :param open_date: The date and time that the trade started + :param close_date: The date and time that the trade ended + :param time_in_ratio: Not used by most exchange classes """ if not time_in_ratio: raise OperationalException( f"time_in_ratio is required for {self.name}._get_funding_fee") - nominal_value = mark_price * size - return nominal_value * funding_rate * time_in_ratio + fees: float = 0 + + if not df.empty: + df = df[(df['date'] >= open_date) & (df['date'] <= close_date)] + fees = sum(df['open_fund'] * df['open_mark'] * amount * time_in_ratio) + + return fees if is_short else -fees diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ce9255854..7b7508854 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -273,9 +273,10 @@ class FreqtradeBot(LoggingMixin): trades = Trade.get_open_trades() for trade in trades: funding_fees = self.exchange.get_funding_fees( - trade.pair, - trade.amount, - trade.open_date + pair=trade.pair, + amount=trade.amount, + is_short=trade.is_short, + open_date=trade.open_date ) trade.funding_fees = funding_fees else: @@ -741,7 +742,8 @@ class FreqtradeBot(LoggingMixin): # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') open_date = datetime.now(timezone.utc) - funding_fees = self.exchange.get_funding_fees(pair, amount, open_date) + funding_fees = self.exchange.get_funding_fees( + pair=pair, amount=amount, is_short=is_short, open_date=open_date) # This is a new trade if trade is None: trade = Trade( @@ -1379,9 +1381,10 @@ class FreqtradeBot(LoggingMixin): :return: True if it succeeds (supported) False (not supported) """ trade.funding_fees = self.exchange.get_funding_fees( - trade.pair, - trade.amount, - trade.open_date + pair=trade.pair, + amount=trade.amount, + is_short=trade.is_short, + open_date=trade.open_date, ) exit_type = 'sell' if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 852bf10e8..7e6aa3ce5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -154,6 +154,7 @@ class Backtesting: else: self.timeframe_detail_min = 0 self.detail_data: Dict[str, DataFrame] = {} + self.futures_data: Dict[str, DataFrame] = {} def init_backtest(self): @@ -233,6 +234,37 @@ class Backtesting: ) else: self.detail_data = {} + if self.trading_mode == TradingMode.FUTURES: + # Load additional futures data. + funding_rates_dict = history.load_data( + datadir=self.config['datadir'], + pairs=self.pairlists.whitelist, + timeframe=self.exchange._ft_has['mark_ohlcv_timeframe'], + timerange=self.timerange, + startup_candles=0, + fail_without_data=True, + data_format=self.config.get('dataformat_ohlcv', 'json'), + candle_type=CandleType.FUNDING_RATE + ) + + # For simplicity, assign to CandleType.Mark (might contian index candles!) + mark_rates_dict = history.load_data( + datadir=self.config['datadir'], + pairs=self.pairlists.whitelist, + timeframe=self.exchange._ft_has['mark_ohlcv_timeframe'], + timerange=self.timerange, + startup_candles=0, + fail_without_data=True, + data_format=self.config.get('dataformat_ohlcv', 'json'), + candle_type=CandleType.from_string(self.exchange._ft_has["mark_ohlcv_price"]) + ) + # Combine data to avoid combining the data per trade. + for pair in self.pairlists.whitelist: + self.futures_data[pair] = funding_rates_dict[pair].merge( + mark_rates_dict[pair], on='date', how="inner", suffixes=["_fund", "_mark"]) + + else: + self.futures_data = {} def prepare_backtest(self, enable_protections): """ @@ -399,15 +431,12 @@ class Backtesting: def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: - # TODO-lev: add interest / funding fees to trade object -> - # Must be done either here, or one level higher -> - # (if we don't want to do it at "detail" level) # Check if we need to adjust our current positions if self.strategy.position_adjustment_enable: trade = self._get_adjust_trade_entry_for_candle(trade, sell_row) - sell_candle_time = sell_row[DATE_IDX].to_pydatetime() + sell_candle_time: datetime = sell_row[DATE_IDX].to_pydatetime() enter = sell_row[SHORT_IDX] if trade.is_short else sell_row[LONG_IDX] exit_ = sell_row[ESHORT_IDX] if trade.is_short else sell_row[ELONG_IDX] sell = self.strategy.should_exit( @@ -460,8 +489,19 @@ class Backtesting: return None def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: + sell_candle_time: datetime = sell_row[DATE_IDX].to_pydatetime() + + if self.trading_mode == TradingMode.FUTURES: + # TODO-lev: Other fees / liquidation price? + trade.funding_fees = self.exchange.calculate_funding_fees( + self.futures_data[trade.pair], + amount=trade.amount, + is_short=trade.is_short, + open_date=trade.open_date_utc, + close_date=sell_candle_time, + ) + if self.timeframe_detail and trade.pair in self.detail_data: - sell_candle_time = sell_row[DATE_IDX].to_pydatetime() sell_candle_end = sell_candle_time + timedelta(minutes=self.timeframe_min) detail_data = self.detail_data[trade.pair] @@ -549,7 +589,7 @@ class Backtesting: return None if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): - amount = round(stake_amount / propose_rate, 8) + amount = round((stake_amount / propose_rate) * leverage, 8) if trade is None: # Enter trade has_buy_tag = len(row) >= ENTER_TAG_IDX + 1 @@ -558,13 +598,14 @@ class Backtesting: open_rate=propose_rate, open_date=current_time, stake_amount=stake_amount, - amount=round((stake_amount / propose_rate) * leverage, 8), + amount=amount, fee_open=self.fee, fee_close=self.fee, is_open=True, enter_tag=row[ENTER_TAG_IDX] if has_buy_tag else None, exchange=self._exchange_name, is_short=(direction == 'short'), + trading_mode=self.trading_mode, leverage=leverage, orders=[] ) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 2b5504324..180d11486 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1372,10 +1372,10 @@ def test_start_list_data(testdatadir, capsys): start_list_data(pargs) captured = capsys.readouterr() - assert "Found 3 pair / timeframe combinations." in captured.out - assert "\n| Pair | Timeframe | Type |\n" in captured.out - assert "\n| XRP/USDT | 1h | futures |\n" in captured.out - assert "\n| XRP/USDT | 1h | mark |\n" in captured.out + assert "Found 5 pair / timeframe combinations." in captured.out + assert "\n| Pair | Timeframe | Type |\n" in captured.out + assert "\n| XRP/USDT | 1h | futures |\n" in captured.out + assert "\n| XRP/USDT | 1h, 8h | mark |\n" in captured.out @pytest.mark.usefixtures("init_persistence") diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 349deaa22..ad388a2c8 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -81,7 +81,7 @@ def test_load_data_7min_timeframe(mocker, caplog, default_conf, testdatadir) -> assert isinstance(ld, DataFrame) assert ld.empty assert log_has( - 'No history data for pair: "UNITTEST/BTC", timeframe: 7m. ' + 'No history for UNITTEST/BTC, spot, 7m found. ' 'Use `freqtrade download-data` to download the data', caplog ) @@ -138,8 +138,8 @@ def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog, load_pair_history(datadir=tmpdir1, timeframe='1m', pair='MEME/BTC', candle_type=candle_type) assert not file.is_file() assert log_has( - 'No history data for pair: "MEME/BTC", timeframe: 1m. ' - 'Use `freqtrade download-data` to download the data', caplog + f"No history for MEME/BTC, {candle_type}, 1m found. " + "Use `freqtrade download-data` to download the data", caplog ) # download a new pair if refresh_pairs is set @@ -744,6 +744,8 @@ def test_datahandler_ohlcv_get_available_data(testdatadir): ('UNITTEST/USDT', '1h', 'mark'), ('XRP/USDT', '1h', 'futures'), ('XRP/USDT', '1h', 'mark'), + ('XRP/USDT', '8h', 'mark'), + ('XRP/USDT', '8h', 'funding_rate'), } paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir, 'spot') diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index b3ebfd747..a799bc302 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -280,7 +280,8 @@ class TestCCXTExchange(): 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) + funding_fee = exchange._fetch_and_calculate_funding_fees( + pair, 20, is_short=False, open_date=since) assert isinstance(funding_fee, float) # assert funding_fee > 0 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e1f93f62d..72c6d4c72 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3540,7 +3540,7 @@ def test_get_max_leverage(default_conf, mocker, pair, nominal_value, max_lev): (10, 0.0002, 2.0, 0.01, 0.004, 0.00004), (10, 0.0002, 2.5, None, 0.005, None), ]) -def test__get_funding_fee( +def test_calculate_funding_fees( default_conf, mocker, size, @@ -3552,14 +3552,47 @@ def test__get_funding_fee( ): exchange = get_patched_exchange(mocker, default_conf) kraken = get_patched_exchange(mocker, default_conf, id="kraken") + prior_date = timeframe_to_prev_date('1h', datetime.now(timezone.utc) - timedelta(hours=1)) + trade_date = timeframe_to_prev_date('1h', datetime.now(timezone.utc)) + funding_rates = DataFrame([ + {'date': prior_date, 'open': funding_rate}, # Line not used. + {'date': trade_date, 'open': funding_rate}, + ]) + mark_rates = DataFrame([ + {'date': prior_date, 'open': mark_price}, + {'date': trade_date, 'open': mark_price}, + ]) + df = exchange.combine_funding_and_mark(funding_rates, mark_rates) - assert exchange._get_funding_fee(size, funding_rate, mark_price, time_in_ratio) == funding_fee + assert exchange.calculate_funding_fees( + df, + amount=size, + is_short=True, + open_date=trade_date, + close_date=trade_date, + time_in_ratio=time_in_ratio, + ) == funding_fee if (kraken_fee is None): with pytest.raises(OperationalException): - kraken._get_funding_fee(size, funding_rate, mark_price, time_in_ratio) + kraken.calculate_funding_fees( + df, + amount=size, + is_short=True, + open_date=trade_date, + close_date=trade_date, + time_in_ratio=time_in_ratio, + ) + else: - assert kraken._get_funding_fee(size, funding_rate, mark_price, time_in_ratio) == kraken_fee + assert kraken.calculate_funding_fees( + df, + amount=size, + is_short=True, + open_date=trade_date, + close_date=trade_date, + time_in_ratio=time_in_ratio, + ) == kraken_fee @pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [ @@ -3588,7 +3621,7 @@ def test__get_funding_fee( # ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0024895), ('ftx', 0, 9, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, 0.0016680000000000002), ]) -def test__calculate_funding_fees( +def test__fetch_and_calculate_funding_fees( mocker, default_conf, funding_rate_history_hourly, @@ -3651,15 +3684,20 @@ def test__calculate_funding_fees( 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) + funding_fees = exchange._fetch_and_calculate_funding_fees( + pair='ADA/USDT', amount=amount, is_short=True, open_date=d1, close_date=d2) assert pytest.approx(funding_fees) == expected_fees + # Fees for Longs are inverted + funding_fees = exchange._fetch_and_calculate_funding_fees( + pair='ADA/USDT', amount=amount, is_short=False, open_date=d1, close_date=d2) + assert pytest.approx(funding_fees) == -expected_fees @pytest.mark.parametrize('exchange,expected_fees', [ ('binance', -0.0009140999999999999), ('gateio', -0.0009140999999999999), ]) -def test__calculate_funding_fees_datetime_called( +def test__fetch_and_calculate_funding_fees_datetime_called( mocker, default_conf, funding_rate_history_octohourly, @@ -3679,7 +3717,8 @@ def test__calculate_funding_fees_datetime_called( d1 = datetime.strptime("2021-09-01 00:00:00 +0000", '%Y-%m-%d %H:%M:%S %z') time_machine.move_to("2021-09-01 08:00:00 +00:00") - funding_fees = exchange._calculate_funding_fees('ADA/USDT', 30.0, d1) + # TODO-lev: test this for longs + funding_fees = exchange._fetch_and_calculate_funding_fees('ADA/USDT', 30.0, True, d1) assert funding_fees == expected_fees diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index cfeb4a23a..540e963eb 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -1169,6 +1169,108 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat assert 'STRATEGY SUMMARY' in captured.out +@pytest.mark.filterwarnings("ignore:deprecated") +def test_backtest_start_nomock_futures(default_conf_usdt, mocker, + caplog, testdatadir, capsys): + # Tests detail-data loading + default_conf_usdt.update({ + "trading_mode": "futures", + "collateral": "isolated", + "use_sell_signal": True, + "sell_profit_only": False, + "sell_profit_offset": 0.0, + "ignore_roi_if_buy_signal": False, + "strategy": CURRENT_TEST_STRATEGY, + }) + patch_exchange(mocker) + result1 = pd.DataFrame({'pair': ['XRP/USDT', 'XRP/USDT'], + 'profit_ratio': [0.0, 0.0], + 'profit_abs': [0.0, 0.0], + 'open_date': pd.to_datetime(['2021-11-18 18:00:00', + '2021-11-18 03:00:00', ], utc=True + ), + 'close_date': pd.to_datetime(['2021-11-18 20:00:00', + '2021-11-18 05:00:00', ], utc=True), + 'trade_duration': [235, 40], + 'is_open': [False, False], + 'is_short': [False, False], + 'stake_amount': [0.01, 0.01], + 'open_rate': [0.104445, 0.10302485], + 'close_rate': [0.104969, 0.103541], + 'sell_reason': [SellType.ROI, SellType.ROI] + }) + result2 = pd.DataFrame({'pair': ['XRP/USDT', 'XRP/USDT', 'XRP/USDT'], + 'profit_ratio': [0.03, 0.01, 0.1], + 'profit_abs': [0.01, 0.02, 0.2], + 'open_date': pd.to_datetime(['2021-11-19 18:00:00', + '2021-11-19 03:00:00', + '2021-11-19 05:00:00'], utc=True + ), + 'close_date': pd.to_datetime(['2021-11-19 20:00:00', + '2021-11-19 05:00:00', + '2021-11-19 08:00:00'], utc=True), + 'trade_duration': [47, 40, 20], + 'is_open': [False, False, False], + 'is_short': [False, False, False], + 'stake_amount': [0.01, 0.01, 0.01], + 'open_rate': [0.104445, 0.10302485, 0.122541], + 'close_rate': [0.104969, 0.103541, 0.123541], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + }) + backtestmock = MagicMock(side_effect=[ + { + 'results': result1, + 'config': default_conf_usdt, + 'locks': [], + 'rejected_signals': 20, + 'final_balance': 1000, + }, + { + 'results': result2, + 'config': default_conf_usdt, + 'locks': [], + 'rejected_signals': 20, + 'final_balance': 1000, + } + ]) + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', + PropertyMock(return_value=['XRP/USDT'])) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) + + patched_configuration_load_config_file(mocker, default_conf_usdt) + + args = [ + 'backtesting', + '--config', 'config.json', + '--datadir', str(testdatadir), + '--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'), + '--timeframe', '1h', + ] + args = get_args(args) + start_backtesting(args) + + # check the logs, that will contain the backtest result + exists = [ + 'Parameter -i/--timeframe detected ... Using timeframe: 1h ...', + f'Using data directory: {testdatadir} ...', + 'Loading data from 2021-11-17 01:00:00 ' + 'up to 2021-11-21 03:00:00 (4 days).', + 'Backtesting with data from 2021-11-17 21:00:00 ' + 'up to 2021-11-21 03:00:00 (3 days).', + 'XRP/USDT, funding_rate, 8h, data starts at 2021-11-18 00:00:00', + 'XRP/USDT, mark, 8h, data starts at 2021-11-18 00:00:00', + f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', + ] + + for line in exists: + assert log_has(line, caplog) + + captured = capsys.readouterr() + assert 'BACKTESTING REPORT' in captured.out + assert 'SELL REASON STATS' in captured.out + assert 'LEFT OPEN TRADES REPORT' in captured.out + + @pytest.mark.filterwarnings("ignore:deprecated") def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker, caplog, testdatadir, capsys): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 9dcae292b..f036c3538 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4901,17 +4901,17 @@ def test_update_funding_fees( freqtrade = get_patched_freqtradebot(mocker, default_conf) # initial funding fees, - freqtrade.execute_entry('ETH/BTC', 123) - freqtrade.execute_entry('LTC/BTC', 2.0) - freqtrade.execute_entry('XRP/BTC', 123) - + freqtrade.execute_entry('ETH/BTC', 123, is_short=is_short) + freqtrade.execute_entry('LTC/BTC', 2.0, is_short=is_short) + freqtrade.execute_entry('XRP/BTC', 123, is_short=is_short) + multipl = 1 if is_short else -1 trades = Trade.get_open_trades() assert len(trades) == 3 for trade in trades: assert pytest.approx(trade.funding_fees) == ( trade.amount * mark_prices[trade.pair].iloc[0]['open'] * - funding_rates[trade.pair].iloc[0]['open'] + funding_rates[trade.pair].iloc[0]['open'] * multipl ) mocker.patch('freqtrade.exchange.Exchange.create_order', return_value=open_exit_order) time_machine.move_to("2021-09-01 08:00:00 +00:00") @@ -4926,7 +4926,7 @@ def test_update_funding_fees( 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'] + funding_rates[trade.pair].iloc[0:2]['open'] * multipl )) else: @@ -4936,7 +4936,9 @@ def test_update_funding_fees( for trade in trades: 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'] + mark_prices[trade.pair].iloc[0:2]['open'] * + funding_rates[trade.pair].iloc[0:2]['open'] * + multipl )) diff --git a/tests/testdata/futures/XRP_USDT-8h-funding_rate.json b/tests/testdata/futures/XRP_USDT-8h-funding_rate.json new file mode 100644 index 000000000..494da4efc --- /dev/null +++ b/tests/testdata/futures/XRP_USDT-8h-funding_rate.json @@ -0,0 +1 @@ +[[1637193600017,0.0001,0.0,0.0,0.0,0.0],[1637222400007,0.0001,0.0,0.0,0.0,0.0],[1637251200011,0.0001,0.0,0.0,0.0,0.0],[1637280000000,0.0001,0.0,0.0,0.0,0.0],[1637308800000,0.0001,0.0,0.0,0.0,0.0],[1637337600005,0.0001,0.0,0.0,0.0,0.0],[1637366400012,0.00013046,0.0,0.0,0.0,0.0],[1637395200000,0.0001,0.0,0.0,0.0,0.0],[1637424000007,0.0001,0.0,0.0,0.0,0.0],[1637452800000,0.00013862,0.0,0.0,0.0,0.0],[1637481600006,0.0001,0.0,0.0,0.0,0.0],[1637510400000,0.00019881,0.0,0.0,0.0,0.0],[1637539200004,0.00013991,0.0,0.0,0.0,0.0],[1637568000000,0.0001,0.0,0.0,0.0,0.0],[1637596800000,0.0001,0.0,0.0,0.0,0.0],[1637625600004,0.0001,0.0,0.0,0.0,0.0],[1637654400010,0.0001,0.0,0.0,0.0,0.0],[1637683200005,0.00017402,0.0,0.0,0.0,0.0],[1637712000001,0.00016775,0.0,0.0,0.0,0.0],[1637740800003,0.00033523,0.0,0.0,0.0,0.0],[1637769600010,0.0001,0.0,0.0,0.0,0.0],[1637798400000,0.00020066,0.0,0.0,0.0,0.0],[1637827200010,0.00034381,0.0,0.0,0.0,0.0],[1637856000000,0.00032096,0.0,0.0,0.0,0.0],[1637884800000,0.00058316,0.0,0.0,0.0,0.0],[1637913600000,0.0001646,0.0,0.0,0.0,0.0],[1637942400016,0.0001,0.0,0.0,0.0,0.0],[1637971200005,0.0001,0.0,0.0,0.0,0.0],[1638000000008,0.0001,0.0,0.0,0.0,0.0],[1638028800007,0.0001,0.0,0.0,0.0,0.0],[1638057600018,0.0001,0.0,0.0,0.0,0.0],[1638086400000,0.0001,0.0,0.0,0.0,0.0],[1638115200004,0.0001,0.0,0.0,0.0,0.0],[1638144000002,0.0001,0.0,0.0,0.0,0.0],[1638172800004,0.0001,0.0,0.0,0.0,0.0],[1638201600000,0.0001,0.0,0.0,0.0,0.0],[1638230400000,0.0001,0.0,0.0,0.0,0.0],[1638259200006,0.0001,0.0,0.0,0.0,0.0],[1638288000000,0.0001,0.0,0.0,0.0,0.0],[1638316800000,0.0001,0.0,0.0,0.0,0.0],[1638345600000,0.0001,0.0,0.0,0.0,0.0],[1638374400001,0.0001,0.0,0.0,0.0,0.0],[1638403200000,0.0001,0.0,0.0,0.0,0.0],[1638432000007,0.0001,0.0,0.0,0.0,0.0],[1638460800008,0.0001,0.0,0.0,0.0,0.0],[1638489600004,0.0001,0.0,0.0,0.0,0.0],[1638518400002,0.0001,0.0,0.0,0.0,0.0],[1638547200006,0.0001,0.0,0.0,0.0,0.0],[1638576000006,0.0001,0.0,0.0,0.0,0.0],[1638604800004,-0.00219334,0.0,0.0,0.0,0.0],[1638633600000,0.0001,0.0,0.0,0.0,0.0],[1638662400003,0.00006147,0.0,0.0,0.0,0.0],[1638691200008,0.0001,0.0,0.0,0.0,0.0],[1638720000007,0.0001,0.0,0.0,0.0,0.0],[1638748800009,0.0001,0.0,0.0,0.0,0.0],[1638777600001,0.0001,0.0,0.0,0.0,0.0],[1638806400000,0.0001,0.0,0.0,0.0,0.0],[1638835200018,0.0001,0.0,0.0,0.0,0.0],[1638864000000,0.0001,0.0,0.0,0.0,0.0],[1638892800000,0.0001,0.0,0.0,0.0,0.0],[1638921600000,0.0001,0.0,0.0,0.0,0.0],[1638950400018,0.0001,0.0,0.0,0.0,0.0],[1638979200010,0.0001,0.0,0.0,0.0,0.0],[1639008000010,0.0001,0.0,0.0,0.0,0.0],[1639036800000,0.0001,0.0,0.0,0.0,0.0],[1639065600000,0.0001,0.0,0.0,0.0,0.0],[1639094400000,0.0001,0.0,0.0,0.0,0.0],[1639123200008,0.0001,0.0,0.0,0.0,0.0],[1639152000012,0.00008995,0.0,0.0,0.0,0.0],[1639180800009,0.0001,0.0,0.0,0.0,0.0],[1639209600008,-0.00002574,0.0,0.0,0.0,0.0],[1639238400000,-0.00002024,0.0,0.0,0.0,0.0],[1639267200001,-0.00008282,0.0,0.0,0.0,0.0],[1639296000015,0.0001,0.0,0.0,0.0,0.0],[1639324800011,0.00008752,0.0,0.0,0.0,0.0],[1639353600006,0.0001,0.0,0.0,0.0,0.0],[1639382400019,0.0001,0.0,0.0,0.0,0.0],[1639411200000,0.0001,0.0,0.0,0.0,0.0],[1639440000004,0.00007825,0.0,0.0,0.0,0.0],[1639468800000,0.00007108,0.0,0.0,0.0,0.0],[1639497600015,0.0001,0.0,0.0,0.0,0.0],[1639526400000,0.0001,0.0,0.0,0.0,0.0],[1639555200008,0.0001,0.0,0.0,0.0,0.0],[1639584000005,0.0001,0.0,0.0,0.0,0.0],[1639612800006,0.0001,0.0,0.0,0.0,0.0],[1639641600009,0.0001,0.0,0.0,0.0,0.0],[1639670400000,0.0001,0.0,0.0,0.0,0.0],[1639699200000,0.0001,0.0,0.0,0.0,0.0],[1639728000005,0.0001,0.0,0.0,0.0,0.0],[1639756800006,0.0001,0.0,0.0,0.0,0.0],[1639785600014,0.0001,0.0,0.0,0.0,0.0]] \ No newline at end of file diff --git a/tests/testdata/futures/XRP_USDT-8h-mark.json b/tests/testdata/futures/XRP_USDT-8h-mark.json new file mode 100644 index 000000000..63dad259b --- /dev/null +++ b/tests/testdata/futures/XRP_USDT-8h-mark.json @@ -0,0 +1 @@ +[[1637193600000,1.0959,1.162,1.0907,1.1074,523374743.8000000119],[1637222400000,1.1075,1.1104,1.045,1.0563,429699821.3999999762],[1637251200000,1.0564,1.0635,1.0145,1.041,417701240.6000000238],[1637280000000,1.0411,1.0572,1.0179,1.0421,262751968.599999994],[1637308800000,1.042,1.1034,1.0418,1.0891,322658150.1999999881],[1637337600000,1.0891,1.099,1.0748,1.0903,176970752.400000006],[1637366400000,1.0903,1.1005,1.0821,1.0856,125726657.400000006],[1637395200000,1.0857,1.1024,1.06,1.0657,193947922.5],[1637424000000,1.0656,1.0987,1.0619,1.0976,165812883.599999994],[1637452800000,1.0975,1.0988,1.0732,1.0803,103157439.799999997],[1637481600000,1.0804,1.0818,1.0638,1.0788,139946704.400000006],[1637510400000,1.0787,1.0867,1.055,1.0581,155236087.3000000119],[1637539200000,1.0582,1.0604,1.026,1.0433,245459370.400000006],[1637568000000,1.0434,1.072,1.0373,1.0577,214156908.400000006],[1637596800000,1.0577,1.0598,1.0284,1.0366,171637007.0],[1637625600000,1.0365,1.0569,1.0311,1.0368,133990133.0],[1637654400000,1.0367,1.0623,1.02,1.0474,300886007.5],[1637683200000,1.0472,1.0725,1.043,1.067,164993866.900000006],[1637712000000,1.0671,1.0741,1.0328,1.0398,162787182.099999994],[1637740800000,1.0397,1.0496,1.005,1.0287,263357085.900000006],[1637769600000,1.0287,1.0343,1.0142,1.0329,142076018.1999999881],[1637798400000,1.0329,1.0525,1.0266,1.0332,151346926.0],[1637827200000,1.0333,1.0597,1.023,1.0529,210738649.0],[1637856000000,1.053,1.0663,1.041,1.0447,169577266.5],[1637884800000,1.0448,1.0479,1.0,1.0145,243945720.900000006],[1637913600000,1.0144,1.0146,0.8836,0.9465,1033033518.6000000238],[1637942400000,0.9467,0.9608,0.9333,0.9392,185904492.1999999881],[1637971200000,0.9392,0.9614,0.9354,0.947,133557450.400000006],[1638000000000,0.947,0.9659,0.9466,0.9563,130188025.599999994],[1638028800000,0.9562,0.9615,0.9338,0.9455,143028245.599999994],[1638057600000,0.9455,0.947,0.8779,0.93,306498284.8999999762],[1638086400000,0.93,0.9415,0.9177,0.9257,126269337.799999997],[1638115200000,0.9256,0.9693,0.8855,0.9686,298275834.3000000119],[1638144000000,0.9686,0.9954,0.9661,0.99,178517855.1999999881],[1638172800000,0.99,0.9926,0.9632,0.9772,199170626.8000000119],[1638201600000,0.9772,1.0024,0.973,0.9901,226187446.0],[1638230400000,0.9901,1.0154,0.9718,0.9833,239524176.3000000119],[1638259200000,0.9834,1.0301,0.97,1.0065,296499649.0],[1638288000000,1.0064,1.0138,0.9845,0.9989,232078115.8000000119],[1638316800000,0.9989,1.0182,0.9934,1.0143,118435865.599999994],[1638345600000,1.0143,1.017,0.9966,1.0119,169147098.3000000119],[1638374400000,1.0118,1.0182,0.98,0.9906,181653125.0],[1638403200000,0.9906,0.9909,0.9545,0.9746,168864095.6999999881],[1638432000000,0.9745,0.9844,0.9629,0.9748,153223080.3000000119],[1638460800000,0.9748,0.9843,0.9645,0.9722,110476722.5],[1638489600000,0.9722,0.981,0.9583,0.9779,132304244.599999994],[1638518400000,0.978,0.984,0.9569,0.9615,147970618.1999999881],[1638547200000,0.9614,0.9614,0.8854,0.9213,403564589.8000000119],[1638576000000,0.9212,0.9246,0.5764,0.7497,1544746782.7000000477],[1638604800000,0.7497,0.8066,0.7405,0.792,741292824.2999999523],[1638633600000,0.792,0.8574,0.7855,0.8449,360411800.1999999881],[1638662400000,0.8449,0.8623,0.8131,0.8382,270770494.1000000238],[1638691200000,0.8381,0.8437,0.7674,0.7897,459358701.5],[1638720000000,0.7897,0.8099,0.7816,0.8041,355021022.1999999881],[1638748800000,0.8041,0.8068,0.7619,0.7747,268906281.8000000119],[1638777600000,0.7748,0.7939,0.7475,0.7934,511486538.1000000238],[1638806400000,0.7934,0.8299,0.786,0.8251,246683461.0],[1638835200000,0.8252,0.8459,0.8209,0.8303,176572777.6999999881],[1638864000000,0.8303,0.844,0.8133,0.8368,248783345.1999999881],[1638892800000,0.8368,0.8386,0.8037,0.8154,188883508.6999999881],[1638921600000,0.8155,0.8437,0.8037,0.8404,168332179.400000006],[1638950400000,0.8404,0.8796,0.8022,0.8669,452844121.1999999881],[1638979200000,0.8668,0.8842,0.85,0.862,291431732.3000000119],[1639008000000,0.8619,0.8718,0.842,0.8602,203577851.900000006],[1639036800000,0.8603,0.934,0.8315,0.8911,1062305914.5],[1639065600000,0.8913,0.9045,0.8433,0.8582,451002103.3999999762],[1639094400000,0.8582,0.8829,0.8252,0.8333,426994850.3999999762],[1639123200000,0.8333,0.8677,0.8155,0.8234,472180388.3000000119],[1639152000000,0.8234,0.8365,0.791,0.7985,293320072.3999999762],[1639180800000,0.7984,0.8369,0.7837,0.826,287918666.5],[1639209600000,0.8261,0.8433,0.8228,0.8376,194875683.0],[1639238400000,0.8376,0.8442,0.8201,0.8388,171763908.1999999881],[1639267200000,0.8388,0.842,0.8151,0.8244,165764217.599999994],[1639296000000,0.8243,0.83,0.8081,0.822,154383486.900000006],[1639324800000,0.8221,0.857,0.8201,0.8393,188392105.5],[1639353600000,0.8393,0.8444,0.8165,0.8272,174739478.8000000119],[1639382400000,0.8271,0.8381,0.7923,0.7991,303947463.0],[1639411200000,0.7992,0.807,0.76,0.7815,426182302.3000000119],[1639440000000,0.7815,0.7966,0.7716,0.7891,237419158.400000006],[1639468800000,0.7891,0.8178,0.7787,0.7927,307108359.8999999762],[1639497600000,0.7926,0.8212,0.7883,0.8101,235803972.599999994],[1639526400000,0.8101,0.8151,0.8009,0.8102,155175275.900000006],[1639555200000,0.8102,0.8176,0.7767,0.7781,222914476.5],[1639584000000,0.778,0.8396,0.7768,0.8259,439618329.1000000238],[1639612800000,0.826,0.8361,0.8181,0.8238,133573251.099999994],[1639641600000,0.8238,0.8335,0.8191,0.8263,140555140.3000000119],[1639670400000,0.8263,0.8274,0.8025,0.8045,155672857.0],[1639699200000,0.8047,0.8323,0.8013,0.8124,173377367.599999994],[1639728000000,0.8124,0.8151,0.7749,0.7953,243494249.400000006],[1639756800000,0.7953,0.8109,0.7871,0.7963,186657940.1999999881],[1639785600000,0.7963,0.8159,0.7904,0.8124,144712394.5]] \ No newline at end of file