Merge pull request #6231 from freqtrade/funding_rate_backtest
Funding rate backtest
This commit is contained in:
commit
108018b30b
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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=[]
|
||||
)
|
||||
|
@ -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")
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
))
|
||||
|
||||
|
||||
|
1
tests/testdata/futures/XRP_USDT-8h-funding_rate.json
vendored
Normal file
1
tests/testdata/futures/XRP_USDT-8h-funding_rate.json
vendored
Normal 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]]
|
1
tests/testdata/futures/XRP_USDT-8h-mark.json
vendored
Normal file
1
tests/testdata/futures/XRP_USDT-8h-mark.json
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user