Merge pull request #6231 from freqtrade/funding_rate_backtest

Funding rate backtest
This commit is contained in:
Matthias 2022-01-27 17:01:28 +01:00 committed by GitHub
commit 108018b30b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 312 additions and 82 deletions

View File

@ -248,7 +248,7 @@ class IDataHandler(ABC):
timerange=timerange_startup, timerange=timerange_startup,
candle_type=candle_type 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 return pairdf
else: else:
enddate = pairdf.iloc[-1]['date'] enddate = pairdf.iloc[-1]['date']
@ -256,7 +256,7 @@ class IDataHandler(ABC):
if timerange_startup: if timerange_startup:
self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup) self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup)
pairdf = trim_dataframe(pairdf, 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 return pairdf
# incomplete candles should only be dropped if we didn't trim the end beforehand. # 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, fill_missing=fill_missing,
drop_incomplete=(drop_incomplete and drop_incomplete=(drop_incomplete and
enddate == pairdf.iloc[-1]['date'])) 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 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 Warn on empty dataframe
""" """
if pairdf.empty: if pairdf.empty:
if warn_no_data: if warn_no_data:
logger.warning( logger.warning(
f'No history data for pair: "{pair}", timeframe: {timeframe}. ' f"No history for {pair}, {candle_type}, {timeframe} found. "
'Use `freqtrade download-data` to download the data' "Use `freqtrade download-data` to download the data"
) )
return True return True
return False return False

View File

@ -1830,25 +1830,6 @@ class Exchange:
else: else:
return 1.0 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 @retrier
def _set_leverage( def _set_leverage(
self, self,
@ -1901,18 +1882,21 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def _calculate_funding_fees( def _fetch_and_calculate_funding_fees(
self, self,
pair: str, pair: str,
amount: float, amount: float,
is_short: bool,
open_date: datetime, open_date: datetime,
close_date: Optional[datetime] = None close_date: Optional[datetime] = None
) -> float: ) -> 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. 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 pair: The quote/base pair of the trade
:param amount: The quantity 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 open_date: The date and time that the trade started
:param close_date: The date and time that the trade ended :param close_date: The date and time that the trade ended
""" """
@ -1924,7 +1908,6 @@ class Exchange:
self._ft_has['mark_ohlcv_timeframe']) self._ft_has['mark_ohlcv_timeframe'])
open_date = timeframe_to_prev_date(timeframe, open_date) open_date = timeframe_to_prev_date(timeframe, open_date)
fees: float = 0
if not close_date: if not close_date:
close_date = datetime.now(timezone.utc) close_date = datetime.now(timezone.utc)
open_timestamp = int(open_date.timestamp()) * 1000 open_timestamp = int(open_date.timestamp()) * 1000
@ -1942,25 +1925,69 @@ class Exchange:
) )
funding_rates = candle_histories[funding_comb] funding_rates = candle_histories[funding_comb]
mark_rates = candle_histories[mark_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: if not df.empty:
df = df[(df['date'] >= open_date) & (df['date'] <= close_date)] df = df[(df['date'] >= open_date) & (df['date'] <= close_date)]
fees = sum(df['open_fund'] * df['open_mark'] * amount) 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 Fetch funding fees, either from the exchange (live) or calculates them
based on funding rate/mark price history based on funding rate/mark price history
:param pair: The quote/base pair of the trade :param pair: The quote/base pair of the trade
:param is_short: trade direction
:param amount: Trade amount :param amount: Trade amount
:param open_date: Open date of the trade :param open_date: Open date of the trade
""" """
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
if self._config['dry_run']: 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: else:
funding_fees = self._get_funding_fees_from_exchange(pair, open_date) funding_fees = self._get_funding_fees_from_exchange(pair, open_date)
return funding_fees return funding_fees

View File

@ -1,8 +1,10 @@
""" Kraken exchange subclass """ """ Kraken exchange subclass """
import logging import logging
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
import ccxt import ccxt
from pandas import DataFrame
from freqtrade.enums import Collateral, TradingMode from freqtrade.enums import Collateral, TradingMode
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
@ -157,11 +159,13 @@ class Kraken(Exchange):
params['leverage'] = leverage params['leverage'] = leverage
return params return params
def _get_funding_fee( def calculate_funding_fees(
self, self,
size: float, df: DataFrame,
funding_rate: float, amount: float,
mark_price: float, is_short: bool,
open_date: datetime,
close_date: Optional[datetime] = None,
time_in_ratio: Optional[float] = None time_in_ratio: Optional[float] = None
) -> float: ) -> float:
""" """
@ -169,16 +173,22 @@ class Kraken(Exchange):
# ! passed to _get_funding_fee. For kraken futures to work in dry run and backtesting # ! 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 # ! functionality must be added that passes the parameter time_in_ratio to
# ! _get_funding_fee when using Kraken # ! _get_funding_fee when using Kraken
Calculates a single funding fee calculates the sum of all funding fees that occurred for a pair during a futures trade
:param size: contract size * number of contracts :param df: Dataframe containing combined funding and mark rates
:param mark_price: The price of the asset that the contract is based off of as `open_fund` and `open_mark`.
:param funding_rate: the interest rate and the premium :param amount: The quantity of the trade
- interest rate: :param is_short: trade direction
- premium: varies by price difference between the perpetual contract and mark price :param open_date: The date and time that the trade started
:param time_in_ratio: time elapsed within funding period without position alteration :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: if not time_in_ratio:
raise OperationalException( raise OperationalException(
f"time_in_ratio is required for {self.name}._get_funding_fee") f"time_in_ratio is required for {self.name}._get_funding_fee")
nominal_value = mark_price * size fees: float = 0
return nominal_value * funding_rate * time_in_ratio
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

View File

@ -273,9 +273,10 @@ class FreqtradeBot(LoggingMixin):
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
for trade in trades: for trade in trades:
funding_fees = self.exchange.get_funding_fees( funding_fees = self.exchange.get_funding_fees(
trade.pair, pair=trade.pair,
trade.amount, amount=trade.amount,
trade.open_date is_short=trade.is_short,
open_date=trade.open_date
) )
trade.funding_fees = funding_fees trade.funding_fees = funding_fees
else: else:
@ -741,7 +742,8 @@ class FreqtradeBot(LoggingMixin):
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
open_date = datetime.now(timezone.utc) 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 # This is a new trade
if trade is None: if trade is None:
trade = Trade( trade = Trade(
@ -1379,9 +1381,10 @@ class FreqtradeBot(LoggingMixin):
:return: True if it succeeds (supported) False (not supported) :return: True if it succeeds (supported) False (not supported)
""" """
trade.funding_fees = self.exchange.get_funding_fees( trade.funding_fees = self.exchange.get_funding_fees(
trade.pair, pair=trade.pair,
trade.amount, amount=trade.amount,
trade.open_date is_short=trade.is_short,
open_date=trade.open_date,
) )
exit_type = 'sell' exit_type = 'sell'
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):

View File

@ -154,6 +154,7 @@ class Backtesting:
else: else:
self.timeframe_detail_min = 0 self.timeframe_detail_min = 0
self.detail_data: Dict[str, DataFrame] = {} self.detail_data: Dict[str, DataFrame] = {}
self.futures_data: Dict[str, DataFrame] = {}
def init_backtest(self): def init_backtest(self):
@ -233,6 +234,37 @@ class Backtesting:
) )
else: else:
self.detail_data = {} 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): def prepare_backtest(self, enable_protections):
""" """
@ -399,15 +431,12 @@ class Backtesting:
def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, def _get_sell_trade_entry_for_candle(self, trade: LocalTrade,
sell_row: Tuple) -> Optional[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 # Check if we need to adjust our current positions
if self.strategy.position_adjustment_enable: if self.strategy.position_adjustment_enable:
trade = self._get_adjust_trade_entry_for_candle(trade, sell_row) 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] 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] exit_ = sell_row[ESHORT_IDX] if trade.is_short else sell_row[ELONG_IDX]
sell = self.strategy.should_exit( sell = self.strategy.should_exit(
@ -460,8 +489,19 @@ class Backtesting:
return None return None
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: 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: 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) sell_candle_end = sell_candle_time + timedelta(minutes=self.timeframe_min)
detail_data = self.detail_data[trade.pair] detail_data = self.detail_data[trade.pair]
@ -549,7 +589,7 @@ class Backtesting:
return None return None
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): 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: if trade is None:
# Enter trade # Enter trade
has_buy_tag = len(row) >= ENTER_TAG_IDX + 1 has_buy_tag = len(row) >= ENTER_TAG_IDX + 1
@ -558,13 +598,14 @@ class Backtesting:
open_rate=propose_rate, open_rate=propose_rate,
open_date=current_time, open_date=current_time,
stake_amount=stake_amount, stake_amount=stake_amount,
amount=round((stake_amount / propose_rate) * leverage, 8), amount=amount,
fee_open=self.fee, fee_open=self.fee,
fee_close=self.fee, fee_close=self.fee,
is_open=True, is_open=True,
enter_tag=row[ENTER_TAG_IDX] if has_buy_tag else None, enter_tag=row[ENTER_TAG_IDX] if has_buy_tag else None,
exchange=self._exchange_name, exchange=self._exchange_name,
is_short=(direction == 'short'), is_short=(direction == 'short'),
trading_mode=self.trading_mode,
leverage=leverage, leverage=leverage,
orders=[] orders=[]
) )

View File

@ -1372,10 +1372,10 @@ def test_start_list_data(testdatadir, capsys):
start_list_data(pargs) start_list_data(pargs)
captured = capsys.readouterr() captured = capsys.readouterr()
assert "Found 3 pair / timeframe combinations." in captured.out assert "Found 5 pair / timeframe combinations." in captured.out
assert "\n| Pair | Timeframe | Type |\n" 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 | futures |\n" in captured.out
assert "\n| XRP/USDT | 1h | mark |\n" in captured.out assert "\n| XRP/USDT | 1h, 8h | mark |\n" in captured.out
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")

View File

@ -81,7 +81,7 @@ def test_load_data_7min_timeframe(mocker, caplog, default_conf, testdatadir) ->
assert isinstance(ld, DataFrame) assert isinstance(ld, DataFrame)
assert ld.empty assert ld.empty
assert log_has( 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 '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) load_pair_history(datadir=tmpdir1, timeframe='1m', pair='MEME/BTC', candle_type=candle_type)
assert not file.is_file() assert not file.is_file()
assert log_has( assert log_has(
'No history data for pair: "MEME/BTC", timeframe: 1m. ' f"No history for MEME/BTC, {candle_type}, 1m found. "
'Use `freqtrade download-data` to download the data', caplog "Use `freqtrade download-data` to download the data", caplog
) )
# download a new pair if refresh_pairs is set # 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'), ('UNITTEST/USDT', '1h', 'mark'),
('XRP/USDT', '1h', 'futures'), ('XRP/USDT', '1h', 'futures'),
('XRP/USDT', '1h', 'mark'), ('XRP/USDT', '1h', 'mark'),
('XRP/USDT', '8h', 'mark'),
('XRP/USDT', '8h', 'funding_rate'),
} }
paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir, 'spot') paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir, 'spot')

View File

@ -280,7 +280,8 @@ class TestCCXTExchange():
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair']) pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
since = datetime.now(timezone.utc) - timedelta(days=5) 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 isinstance(funding_fee, float)
# assert funding_fee > 0 # assert funding_fee > 0

View File

@ -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.0, 0.01, 0.004, 0.00004),
(10, 0.0002, 2.5, None, 0.005, None), (10, 0.0002, 2.5, None, 0.005, None),
]) ])
def test__get_funding_fee( def test_calculate_funding_fees(
default_conf, default_conf,
mocker, mocker,
size, size,
@ -3552,14 +3552,47 @@ def test__get_funding_fee(
): ):
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
kraken = get_patched_exchange(mocker, default_conf, id="kraken") 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): if (kraken_fee is None):
with pytest.raises(OperationalException): 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: 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', [ @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), # ('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), ('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, mocker,
default_conf, default_conf,
funding_rate_history_hourly, funding_rate_history_hourly,
@ -3651,15 +3684,20 @@ def test__calculate_funding_fees(
type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True}) type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) 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 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', [ @pytest.mark.parametrize('exchange,expected_fees', [
('binance', -0.0009140999999999999), ('binance', -0.0009140999999999999),
('gateio', -0.0009140999999999999), ('gateio', -0.0009140999999999999),
]) ])
def test__calculate_funding_fees_datetime_called( def test__fetch_and_calculate_funding_fees_datetime_called(
mocker, mocker,
default_conf, default_conf,
funding_rate_history_octohourly, 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') 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") 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 assert funding_fees == expected_fees

View File

@ -1169,6 +1169,108 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
assert 'STRATEGY SUMMARY' in captured.out 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") @pytest.mark.filterwarnings("ignore:deprecated")
def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker, def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
caplog, testdatadir, capsys): caplog, testdatadir, capsys):

View File

@ -4901,17 +4901,17 @@ def test_update_funding_fees(
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
# initial funding fees, # initial funding fees,
freqtrade.execute_entry('ETH/BTC', 123) freqtrade.execute_entry('ETH/BTC', 123, is_short=is_short)
freqtrade.execute_entry('LTC/BTC', 2.0) freqtrade.execute_entry('LTC/BTC', 2.0, is_short=is_short)
freqtrade.execute_entry('XRP/BTC', 123) freqtrade.execute_entry('XRP/BTC', 123, is_short=is_short)
multipl = 1 if is_short else -1
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
assert len(trades) == 3 assert len(trades) == 3
for trade in trades: for trade in trades:
assert pytest.approx(trade.funding_fees) == ( assert pytest.approx(trade.funding_fees) == (
trade.amount * trade.amount *
mark_prices[trade.pair].iloc[0]['open'] * 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) mocker.patch('freqtrade.exchange.Exchange.create_order', return_value=open_exit_order)
time_machine.move_to("2021-09-01 08:00:00 +00:00") 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( assert trade.funding_fees == pytest.approx(sum(
trade.amount * trade.amount *
mark_prices[trade.pair].iloc[0:2]['open'] * 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: else:
@ -4936,7 +4936,9 @@ def test_update_funding_fees(
for trade in trades: for trade in trades:
assert trade.funding_fees == pytest.approx(sum( assert trade.funding_fees == pytest.approx(sum(
trade.amount * 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
)) ))

View File

@ -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]]

File diff suppressed because one or more lines are too long