Merge pull request #5779 from samgermain/funding-fee-dry-run

Funding fee dry run
This commit is contained in:
Matthias 2021-11-14 19:49:05 +01:00 committed by GitHub
commit da3ceb0904
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 795 additions and 43 deletions

View File

@ -1,6 +1,6 @@
""" Bibox exchange subclass """
import logging
from typing import Dict, List
from typing import Dict
from freqtrade.exchange import Exchange
@ -26,5 +26,3 @@ class Bibox(Exchange):
config = {"has": {"fetchCurrencies": False}}
config.update(super()._ccxt_config)
return config
funding_fee_times: List[int] = [0, 8, 16] # hours of the day

View File

@ -1,6 +1,7 @@
""" Binance exchange subclass """
import json
import logging
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple
@ -29,8 +30,6 @@ class Binance(Exchange):
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
"ccxt_futures_name": "future"
}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
# but the schedule won't check within this timeframe
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list
@ -211,3 +210,11 @@ class Binance(Exchange):
f"{arrow.get(since_ms // 1000).isoformat()}.")
return await super()._async_get_historic_ohlcv(
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair)
def funding_fee_cutoff(self, open_date: datetime):
"""
# TODO-lev: Double check that gateio, ftx, and kraken don't also have this
:param open_date: The open date for a trade
:return: The cutoff open time for when a funding fee is charged
"""
return open_date.minute > 0 or (open_date.minute == 0 and open_date.second > 15)

View File

@ -24,8 +24,6 @@ class Bybit(Exchange):
"ccxt_futures_name": "linear"
}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list
# TODO-lev: Uncomment once supported

View File

@ -7,7 +7,7 @@ import http
import inspect
import logging
from copy import deepcopy
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from math import ceil
from typing import Any, Dict, List, Optional, Tuple, Union
@ -69,14 +69,11 @@ class Exchange:
"trades_pagination_arg": "since",
"l2_limit_range": None,
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
"mark_ohlcv_price": "mark",
"ccxt_futures_name": "swap"
}
_ft_has: Dict = {}
# funding_fee_times is currently unused, but should ideally be used to properly
# schedule refresh times
funding_fee_times: List[int] = [] # hours of the day
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list
]
@ -90,6 +87,7 @@ class Exchange:
self._api: ccxt.Exchange = None
self._api_async: ccxt_async.Exchange = None
self._markets: Dict = {}
self._leverage_brackets: Dict = {}
self._config.update(config)
@ -180,7 +178,6 @@ class Exchange:
self.markets_refresh_interval: int = exchange_config.get(
"markets_refresh_interval", 60) * 60
self._leverage_brackets: Dict = {}
if self.trading_mode != TradingMode.SPOT:
self.fill_leverage_brackets()
@ -1619,17 +1616,18 @@ class Exchange:
until=until, from_id=from_id))
@retrier
def get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float:
def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float:
"""
Returns the sum of all funding fees that were exchanged for a pair within a timeframe
Dry-run handling happens as part of _calculate_funding_fees.
:param pair: (e.g. ADA/USDT)
:param since: The earliest time of consideration for calculating funding fees,
in unix time or as a datetime
"""
if not self.exchange_has("fetchFundingHistory"):
raise OperationalException(
f"fetch_funding_history() has not been implemented on ccxt.{self.name}")
f"fetch_funding_history() is not available using {self.name}"
)
if type(since) is datetime:
since = int(since.timestamp()) * 1000 # * 1000 for ms
@ -1672,6 +1670,25 @@ 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,
@ -1697,11 +1714,18 @@ class Exchange:
except ccxt.BaseError as e:
raise OperationalException(e) from e
def funding_fee_cutoff(self, open_date: datetime):
"""
:param open_date: The open date for a trade
:return: The cutoff open time for when a funding fee is charged
"""
return open_date.minute > 0 or open_date.second > 0
@retrier
def set_margin_mode(self, pair: str, collateral: Collateral, params: dict = {}):
"""
Set's the margin mode on the exchange to cross or isolated for a specific pair
:param symbol: base/quote currency pair (e.g. "ADA/USDT")
:param pair: base/quote currency pair (e.g. "ADA/USDT")
"""
if self._config['dry_run'] or not self.exchange_has("setMarginMode"):
# Some exchanges only support one collateral type
@ -1717,6 +1741,161 @@ class Exchange:
except ccxt.BaseError as e:
raise OperationalException(e) from e
@retrier
def _get_mark_price_history(self, pair: str, since: int) -> Dict:
"""
Get's the mark price history for a pair
:param pair: The quote/base pair of the trade
:param since: The earliest time to start downloading candles, in ms.
"""
try:
candles = self._api.fetch_ohlcv(
pair,
timeframe="1h",
since=since,
params={
'price': self._ft_has["mark_ohlcv_price"]
}
)
history = {}
for candle in candles:
d = datetime.fromtimestamp(int(candle[0] / 1000), timezone.utc)
# Round down to the nearest hour, in case of a delayed timestamp
# The millisecond timestamps can be delayed ~20ms
time = datetime(
d.year,
d.month,
d.day,
d.hour,
tzinfo=timezone.utc
).timestamp() * 1000
opening_mark_price = candle[1]
history[time] = opening_mark_price
return history
except ccxt.NotSupported as e:
raise OperationalException(
f'Exchange {self._api.name} does not support fetching historical '
f'mark price candle (OHLCV) data. Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(f'Could not fetch historical mark price candle (OHLCV) data '
f'for pair {pair} due to {e.__class__.__name__}. '
f'Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(f'Could not fetch historical mark price candle (OHLCV) data '
f'for pair {pair}. Message: {e}') from e
def _calculate_funding_fees(
self,
pair: str,
amount: float,
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
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 open_date: The date and time that the trade started
:param close_date: The date and time that the trade ended
"""
if self.funding_fee_cutoff(open_date):
open_date += timedelta(hours=1)
open_date = datetime(
open_date.year,
open_date.month,
open_date.day,
open_date.hour,
tzinfo=timezone.utc
)
fees: float = 0
if not close_date:
close_date = datetime.now(timezone.utc)
open_timestamp = int(open_date.timestamp()) * 1000
# close_timestamp = int(close_date.timestamp()) * 1000
funding_rate_history = self.get_funding_rate_history(
pair,
open_timestamp
)
mark_price_history = self._get_mark_price_history(
pair,
open_timestamp
)
for timestamp in funding_rate_history.keys():
funding_rate = funding_rate_history[timestamp]
if timestamp in mark_price_history:
mark_price = mark_price_history[timestamp]
fees += self._get_funding_fee(
size=amount,
mark_price=mark_price,
funding_rate=funding_rate
)
else:
logger.warning(
f"Mark price for {pair} at timestamp {timestamp} not found in "
f"funding_rate_history Funding fee calculation may be incorrect"
)
return fees
def get_funding_fees(self, pair: str, amount: float, 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 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)
else:
funding_fees = self._get_funding_fees_from_exchange(pair, open_date)
return funding_fees
else:
return 0.0
@retrier
def get_funding_rate_history(self, pair: str, since: int) -> Dict:
"""
:param pair: quote/base currency pair
:param since: timestamp in ms of the beginning time
:param end: timestamp in ms of the end time
"""
if not self.exchange_has("fetchFundingRateHistory"):
raise ExchangeError(
f"fetch_funding_rate_history is not available using {self.name}"
)
# TODO-lev: Gateio has a max limit into the past of 333 days, okex has a limit of 3 months
try:
funding_history: Dict = {}
response = self._api.fetch_funding_rate_history(
pair,
limit=1000,
since=since
)
for fund in response:
d = datetime.fromtimestamp(int(fund['timestamp'] / 1000), timezone.utc)
# Round down to the nearest hour, in case of a delayed timestamp
# The millisecond timestamps can be delayed ~20ms
time = int(timeframe_to_prev_date('1h', d).timestamp() * 1000)
funding_history[time] = fund['fundingRate']
return funding_history
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
return exchange_name in ccxt_exchanges(ccxt_module)

View File

@ -20,8 +20,8 @@ class Ftx(Exchange):
_ft_has: Dict = {
"stoploss_on_exchange": True,
"ohlcv_candle_limit": 1500,
"mark_ohlcv_price": "index"
}
funding_fee_times: List[int] = list(range(0, 24))
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list

View File

@ -26,8 +26,6 @@ class Gateio(Exchange):
_headers = {'X-Gate-Channel-Id': 'freqtrade'}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list
# TODO-lev: Uncomment once supported

View File

@ -1,5 +1,5 @@
import logging
from typing import Dict, List
from typing import Dict
from freqtrade.exchange import Exchange
@ -21,5 +21,3 @@ class Hitbtc(Exchange):
"ohlcv_candle_limit": 1000,
"ohlcv_params": {"sort": "DESC"}
}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day

View File

@ -23,7 +23,6 @@ class Kraken(Exchange):
"trades_pagination": "id",
"trades_pagination_arg": "since",
}
funding_fee_times: List[int] = [0, 4, 8, 12, 16, 20] # hours of the day
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list
@ -157,3 +156,29 @@ class Kraken(Exchange):
if leverage > 1.0:
params['leverage'] = leverage
return params
def _get_funding_fee(
self,
size: float,
funding_rate: float,
mark_price: float,
time_in_ratio: Optional[float] = None
) -> float:
"""
# ! This method will always error when run by Freqtrade because time_in_ratio is never
# ! 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
"""
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

View File

@ -1,6 +1,6 @@
""" Kucoin exchange subclass """
import logging
from typing import Dict, List
from typing import Dict
from freqtrade.exchange import Exchange
@ -24,5 +24,3 @@ class Kucoin(Exchange):
"order_time_in_force": ['gtc', 'fok', 'ioc'],
"time_in_force_parameter": "timeInForce",
}
funding_fee_times: List[int] = [4, 12, 20] # hours of the day

View File

@ -17,7 +17,6 @@ class Okex(Exchange):
_ft_has: Dict = {
"ohlcv_candle_limit": 100,
}
funding_fee_times: List[int] = [0, 8, 16] # hours of the day
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list

View File

@ -268,12 +268,16 @@ class FreqtradeBot(LoggingMixin):
def update_funding_fees(self):
if self.trading_mode == TradingMode.FUTURES:
for trade in Trade.get_open_trades():
funding_fees = self.exchange.get_funding_fees_from_exchange(
trades = Trade.get_open_trades()
for trade in trades:
funding_fees = self.exchange.get_funding_fees(
trade.pair,
trade.amount,
trade.open_date
)
trade.funding_fees = funding_fees
else:
return 0.0
def startup_update_open_orders(self):
"""
@ -703,10 +707,7 @@ 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)
if self.trading_mode == TradingMode.FUTURES:
funding_fees = self.exchange.get_funding_fees_from_exchange(pair, open_date)
else:
funding_fees = 0.0
funding_fees = self.exchange.get_funding_fees(pair, amount, open_date)
trade = Trade(
pair=pair,
@ -1260,6 +1261,11 @@ class FreqtradeBot(LoggingMixin):
:param sell_reason: Reason the sell was triggered
:return: True if it succeeds (supported) False (not supported)
"""
trade.funding_fees = self.exchange.get_funding_fees(
trade.pair,
trade.amount,
trade.open_date
)
exit_type = 'sell' # TODO-lev: Update to exit
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
exit_type = 'stoploss'

View File

@ -2366,3 +2366,131 @@ def limit_order_open(limit_buy_order_usdt_open, limit_sell_order_usdt_open):
'buy': limit_buy_order_usdt_open,
'sell': limit_sell_order_usdt_open
}
@pytest.fixture(scope='function')
def mark_ohlcv():
return [
[1630454400000, 2.77, 2.77, 2.73, 2.73, 0],
[1630458000000, 2.73, 2.76, 2.72, 2.74, 0],
[1630461600000, 2.74, 2.76, 2.74, 2.76, 0],
[1630465200000, 2.76, 2.76, 2.74, 2.76, 0],
[1630468800000, 2.76, 2.77, 2.75, 2.77, 0],
[1630472400000, 2.77, 2.79, 2.75, 2.78, 0],
[1630476000000, 2.78, 2.80, 2.77, 2.77, 0],
[1630479600000, 2.78, 2.79, 2.77, 2.77, 0],
[1630483200000, 2.77, 2.79, 2.77, 2.78, 0],
[1630486800000, 2.77, 2.84, 2.77, 2.84, 0],
[1630490400000, 2.84, 2.85, 2.81, 2.81, 0],
[1630494000000, 2.81, 2.83, 2.81, 2.81, 0],
[1630497600000, 2.81, 2.84, 2.81, 2.82, 0],
[1630501200000, 2.82, 2.83, 2.81, 2.81, 0],
]
@pytest.fixture(scope='function')
def funding_rate_history_hourly():
return [
{
"symbol": "ADA/USDT",
"fundingRate": -0.000008,
"timestamp": 1630454400000,
"datetime": "2021-09-01T00:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"fundingRate": -0.000004,
"timestamp": 1630458000000,
"datetime": "2021-09-01T01:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"fundingRate": 0.000012,
"timestamp": 1630461600000,
"datetime": "2021-09-01T02:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"fundingRate": -0.000003,
"timestamp": 1630465200000,
"datetime": "2021-09-01T03:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"fundingRate": -0.000007,
"timestamp": 1630468800000,
"datetime": "2021-09-01T04:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"fundingRate": 0.000003,
"timestamp": 1630472400000,
"datetime": "2021-09-01T05:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"fundingRate": 0.000019,
"timestamp": 1630476000000,
"datetime": "2021-09-01T06:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"fundingRate": 0.000003,
"timestamp": 1630479600000,
"datetime": "2021-09-01T07:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"fundingRate": -0.000003,
"timestamp": 1630483200000,
"datetime": "2021-09-01T08:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"fundingRate": 0,
"timestamp": 1630486800000,
"datetime": "2021-09-01T09:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"fundingRate": 0.000013,
"timestamp": 1630490400000,
"datetime": "2021-09-01T10:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"fundingRate": 0.000077,
"timestamp": 1630494000000,
"datetime": "2021-09-01T11:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"fundingRate": 0.000072,
"timestamp": 1630497600000,
"datetime": "2021-09-01T12:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"fundingRate": 0.000097,
"timestamp": 1630501200000,
"datetime": "2021-09-01T13:00:00.000Z"
},
]
@pytest.fixture(scope='function')
def funding_rate_history_octohourly():
return [
{
"symbol": "ADA/USDT",
"fundingRate": -0.000008,
"timestamp": 1630454400000,
"datetime": "2021-09-01T00:00:00.000Z"
},
{
"symbol": "ADA/USDT",
"fundingRate": -0.000003,
"timestamp": 1630483200000,
"datetime": "2021-09-01T08:00:00.000Z"
}
]

View File

@ -12,7 +12,7 @@ import pytest
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_default_conf
from tests.conftest import get_default_conf_usdt
# Exchanges that should be tested
@ -33,9 +33,11 @@ EXCHANGES = {
'timeframe': '5m',
},
'ftx': {
'pair': 'BTC/USDT',
'pair': 'BTC/USD',
'hasQuoteVolume': True,
'timeframe': '5m',
'futures_pair': 'BTC-PERP',
'futures': True,
},
'kucoin': {
'pair': 'BTC/USDT',
@ -46,18 +48,24 @@ EXCHANGES = {
'pair': 'BTC/USDT',
'hasQuoteVolume': True,
'timeframe': '5m',
'futures': True,
'futures_fundingrate_tf': '8h',
'futures_pair': 'BTC/USDT:USDT',
},
'okex': {
'pair': 'BTC/USDT',
'hasQuoteVolume': True,
'timeframe': '5m',
'futures_fundingrate_tf': '8h',
'futures_pair': 'BTC/USDT:USDT',
'futures': True,
},
}
@pytest.fixture(scope="class")
def exchange_conf():
config = get_default_conf((Path(__file__).parent / "testdata").resolve())
config = get_default_conf_usdt((Path(__file__).parent / "testdata").resolve())
config['exchange']['pair_whitelist'] = []
config['exchange']['key'] = ''
config['exchange']['secret'] = ''
@ -73,6 +81,19 @@ def exchange(request, exchange_conf):
yield exchange, request.param
@pytest.fixture(params=EXCHANGES, scope="class")
def exchange_futures(request, exchange_conf):
if not EXCHANGES[request.param].get('futures') is True:
yield None, request.param
else:
exchange_conf['exchange']['name'] = request.param
exchange_conf['trading_mode'] = 'futures'
exchange_conf['collateral'] = 'cross'
exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True)
yield exchange, request.param
@pytest.mark.longrun
class TestCCXTExchange():
@ -149,6 +170,25 @@ class TestCCXTExchange():
now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
@pytest.mark.skip("No futures support yet")
def test_ccxt_fetch_funding_rate_history(self, exchange_futures):
# TODO-lev: enable this test once Futures mode is enabled.
exchange, exchangename = exchange_futures
if not exchange:
# exchange_futures only returns values for supported exchanges
return
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
since = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000)
rate = exchange.get_funding_rate_history(pair, since)
assert isinstance(rate, dict)
expected_tf = EXCHANGES[exchangename].get('futures_fundingrate_tf', '1h')
this_hour = timeframe_to_prev_date(expected_tf)
prev_tick = timeframe_to_prev_date(expected_tf, this_hour - timedelta(minutes=1))
assert rate[int(this_hour.timestamp() * 1000)] != 0.0
assert rate[int(prev_tick.timestamp() * 1000)] != 0.0
# TODO: tests fetch_trades (?)
def test_ccxt_get_fee(self, exchange):

View File

@ -3058,7 +3058,7 @@ def test_calculate_backoff(retrycount, max_retries, expected):
@pytest.mark.parametrize("exchange_name", ['binance', 'ftx'])
def test_get_funding_fees_from_exchange(default_conf, mocker, exchange_name):
def test__get_funding_fees_from_exchange(default_conf, mocker, exchange_name):
api_mock = MagicMock()
api_mock.fetch_funding_history = MagicMock(return_value=[
{
@ -3101,11 +3101,11 @@ def test_get_funding_fees_from_exchange(default_conf, mocker, exchange_name):
date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ')
unix_time = int(date_time.timestamp())
expected_fees = -0.001 # 0.14542341 + -0.14642341
fees_from_datetime = exchange.get_funding_fees_from_exchange(
fees_from_datetime = exchange._get_funding_fees_from_exchange(
pair='XRP/USDT',
since=date_time
)
fees_from_unix_time = exchange.get_funding_fees_from_exchange(
fees_from_unix_time = exchange._get_funding_fees_from_exchange(
pair='XRP/USDT',
since=unix_time
)
@ -3118,7 +3118,7 @@ def test_get_funding_fees_from_exchange(default_conf, mocker, exchange_name):
default_conf,
api_mock,
exchange_name,
"get_funding_fees_from_exchange",
"_get_funding_fees_from_exchange",
"fetch_funding_history",
pair="XRP/USDT",
since=unix_time
@ -3288,3 +3288,226 @@ def test_get_max_leverage(default_conf, mocker, pair, nominal_value, max_lev):
# Binance has a different method of getting the max leverage
exchange = get_patched_exchange(mocker, default_conf, id="kraken")
assert exchange.get_max_leverage(pair, nominal_value) == max_lev
@pytest.mark.parametrize(
'size,funding_rate,mark_price,time_in_ratio,funding_fee,kraken_fee', [
(10, 0.0001, 2.0, 1.0, 0.002, 0.002),
(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(
default_conf,
mocker,
size,
funding_rate,
mark_price,
funding_fee,
kraken_fee,
time_in_ratio
):
exchange = get_patched_exchange(mocker, default_conf)
kraken = get_patched_exchange(mocker, default_conf, id="kraken")
assert exchange._get_funding_fee(size, funding_rate, mark_price, 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)
else:
assert kraken._get_funding_fee(size, funding_rate, mark_price, time_in_ratio) == kraken_fee
def test__get_mark_price_history(mocker, default_conf, mark_ohlcv):
api_mock = MagicMock()
api_mock.fetch_ohlcv = MagicMock(return_value=mark_ohlcv)
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
# mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
mark_prices = exchange._get_mark_price_history("ADA/USDT", 1630454400000)
assert mark_prices == {
1630454400000: 2.77,
1630458000000: 2.73,
1630461600000: 2.74,
1630465200000: 2.76,
1630468800000: 2.76,
1630472400000: 2.77,
1630476000000: 2.78,
1630479600000: 2.78,
1630483200000: 2.77,
1630486800000: 2.77,
1630490400000: 2.84,
1630494000000: 2.81,
1630497600000: 2.81,
1630501200000: 2.82,
}
ccxt_exceptionhandlers(
mocker,
default_conf,
api_mock,
"binance",
"_get_mark_price_history",
"fetch_ohlcv",
pair="ADA/USDT",
since=1635580800001
)
def test_get_funding_rate_history(mocker, default_conf, funding_rate_history_hourly):
api_mock = MagicMock()
api_mock.fetch_funding_rate_history = MagicMock(return_value=funding_rate_history_hourly)
type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True})
# mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
funding_rates = exchange.get_funding_rate_history('ADA/USDT', 1635580800001)
assert funding_rates == {
1630454400000: -0.000008,
1630458000000: -0.000004,
1630461600000: 0.000012,
1630465200000: -0.000003,
1630468800000: -0.000007,
1630472400000: 0.000003,
1630476000000: 0.000019,
1630479600000: 0.000003,
1630483200000: -0.000003,
1630486800000: 0,
1630490400000: 0.000013,
1630494000000: 0.000077,
1630497600000: 0.000072,
1630501200000: 0.000097,
}
ccxt_exceptionhandlers(
mocker,
default_conf,
api_mock,
"binance",
"get_funding_rate_history",
"fetch_funding_rate_history",
pair="ADA/USDT",
since=1630454400000
)
@pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [
('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999),
('binance', 0, 2, "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999),
('binance', 1, 2, "2021-09-01 01:00:14", "2021-09-01 08:00:00", 30.0, -0.0002493),
('binance', 1, 2, "2021-09-01 00:00:16", "2021-09-01 08:00:00", 30.0, -0.0002493),
('binance', 0, 1, "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0006647999999999999),
('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999),
('binance', 0, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999),
# TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._get_funding_fee
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0014937),
# ('kraken', "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0008289),
# ('kraken', "2021-09-01 01:00:14", "2021-09-01 08:00:00", 30.0, -0.0008289),
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0012443999999999999),
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0045759),
# ('kraken', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0008289),
('ftx', 0, 9, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, 0.0010008000000000003),
('ftx', 0, 13, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0146691),
('ftx', 1, 9, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, 0.0016656000000000002),
('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999),
('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999),
('gateio', 1, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493),
('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0015235000000000001),
# TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._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(
mocker,
default_conf,
funding_rate_history_hourly,
funding_rate_history_octohourly,
rate_start,
rate_end,
mark_ohlcv,
exchange,
d1,
d2,
amount,
expected_fees
):
'''
nominal_value = mark_price * size
funding_fee = nominal_value * funding_rate
size: 30
time: 0, mark: 2.77, nominal_value: 83.1, fundRate: -0.000008, fundFee: -0.0006648
time: 1, mark: 2.73, nominal_value: 81.9, fundRate: -0.000004, fundFee: -0.0003276
time: 2, mark: 2.74, nominal_value: 82.2, fundRate: 0.000012, fundFee: 0.0009864
time: 3, mark: 2.76, nominal_value: 82.8, fundRate: -0.000003, fundFee: -0.0002484
time: 4, mark: 2.76, nominal_value: 82.8, fundRate: -0.000007, fundFee: -0.0005796
time: 5, mark: 2.77, nominal_value: 83.1, fundRate: 0.000003, fundFee: 0.0002493
time: 6, mark: 2.78, nominal_value: 83.4, fundRate: 0.000019, fundFee: 0.0015846
time: 7, mark: 2.78, nominal_value: 83.4, fundRate: 0.000003, fundFee: 0.0002502
time: 8, mark: 2.77, nominal_value: 83.1, fundRate: -0.000003, fundFee: -0.0002493
time: 9, mark: 2.77, nominal_value: 83.1, fundRate: 0, fundFee: 0.0
time: 10, mark: 2.84, nominal_value: 85.2, fundRate: 0.000013, fundFee: 0.0011076
time: 11, mark: 2.81, nominal_value: 84.3, fundRate: 0.000077, fundFee: 0.0064911
time: 12, mark: 2.81, nominal_value: 84.3, fundRate: 0.000072, fundFee: 0.0060696
time: 13, mark: 2.82, nominal_value: 84.6, fundRate: 0.000097, fundFee: 0.0082062
size: 50
time: 0, mark: 2.77, nominal_value: 138.5, fundRate: -0.000008, fundFee: -0.001108
time: 1, mark: 2.73, nominal_value: 136.5, fundRate: -0.000004, fundFee: -0.000546
time: 2, mark: 2.74, nominal_value: 137.0, fundRate: 0.000012, fundFee: 0.001644
time: 3, mark: 2.76, nominal_value: 138.0, fundRate: -0.000003, fundFee: -0.000414
time: 4, mark: 2.76, nominal_value: 138.0, fundRate: -0.000007, fundFee: -0.000966
time: 5, mark: 2.77, nominal_value: 138.5, fundRate: 0.000003, fundFee: 0.0004155
time: 6, mark: 2.78, nominal_value: 139.0, fundRate: 0.000019, fundFee: 0.002641
time: 7, mark: 2.78, nominal_value: 139.0, fundRate: 0.000003, fundFee: 0.000417
time: 8, mark: 2.77, nominal_value: 138.5, fundRate: -0.000003, fundFee: -0.0004155
time: 9, mark: 2.77, nominal_value: 138.5, fundRate: 0, fundFee: 0.0
time: 10, mark: 2.84, nominal_value: 142.0, fundRate: 0.000013, fundFee: 0.001846
time: 11, mark: 2.81, nominal_value: 140.5, fundRate: 0.000077, fundFee: 0.0108185
time: 12, mark: 2.81, nominal_value: 140.5, fundRate: 0.000072, fundFee: 0.010116
time: 13, mark: 2.82, nominal_value: 141.0, fundRate: 0.000097, fundFee: 0.013677
'''
d1 = datetime.strptime(f"{d1} +0000", '%Y-%m-%d %H:%M:%S %z')
d2 = datetime.strptime(f"{d2} +0000", '%Y-%m-%d %H:%M:%S %z')
funding_rate_history = {
'binance': funding_rate_history_octohourly,
'ftx': funding_rate_history_hourly,
'gateio': funding_rate_history_octohourly,
}[exchange][rate_start:rate_end]
api_mock = MagicMock()
api_mock.fetch_funding_rate_history = MagicMock(return_value=funding_rate_history)
api_mock.fetch_ohlcv = MagicMock(return_value=mark_ohlcv)
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange)
funding_fees = exchange._calculate_funding_fees('ADA/USDT', amount, d1, d2)
assert funding_fees == expected_fees
@ pytest.mark.parametrize('exchange,expected_fees', [
('binance', -0.0009140999999999999),
('gateio', -0.0009140999999999999),
])
def test__calculate_funding_fees_datetime_called(
mocker,
default_conf,
funding_rate_history_octohourly,
mark_ohlcv,
exchange,
time_machine,
expected_fees
):
api_mock = MagicMock()
api_mock.fetch_ohlcv = MagicMock(return_value=mark_ohlcv)
api_mock.fetch_funding_rate_history = MagicMock(return_value=funding_rate_history_octohourly)
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange)
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)
assert funding_fees == expected_fees

View File

@ -4700,8 +4700,8 @@ def test_leverage_prep():
('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:07"),
('futures', 33, "2021-08-31 23:59:58", "2021-09-01 08:00:07"),
])
def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_machine,
t1, t2):
def test_update_funding_fees_schedule(mocker, default_conf, trading_mode, calls, time_machine,
t1, t2):
time_machine.move_to(f"{t1} +00:00")
patch_RPCManager(mocker)
@ -4716,3 +4716,158 @@ def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_mac
freqtrade._schedule.run_pending()
assert freqtrade.update_funding_fees.call_count == calls
@pytest.mark.parametrize('schedule_off', [False, True])
@pytest.mark.parametrize('is_short', [True, False])
def test_update_funding_fees(
mocker,
default_conf,
time_machine,
fee,
ticker_usdt_sell_up,
is_short,
limit_order_open,
schedule_off
):
'''
nominal_value = mark_price * size
funding_fee = nominal_value * funding_rate
size = 123
"LTC/BTC"
time: 0, mark: 3.3, fundRate: 0.00032583, nominal_value: 405.9, fundFee: 0.132254397
time: 8, mark: 3.2, fundRate: 0.00024472, nominal_value: 393.6, fundFee: 0.096321792
"ETH/BTC"
time: 0, mark: 2.4, fundRate: 0.0001, nominal_value: 295.2, fundFee: 0.02952
time: 8, mark: 2.5, fundRate: 0.0001, nominal_value: 307.5, fundFee: 0.03075
"ETC/BTC"
time: 0, mark: 4.3, fundRate: 0.00031077, nominal_value: 528.9, fundFee: 0.164366253
time: 8, mark: 4.1, fundRate: 0.00022655, nominal_value: 504.3, fundFee: 0.114249165
"XRP/BTC"
time: 0, mark: 1.2, fundRate: 0.00049426, nominal_value: 147.6, fundFee: 0.072952776
time: 8, mark: 1.2, fundRate: 0.00032715, nominal_value: 147.6, fundFee: 0.04828734
'''
# SETUP
time_machine.move_to("2021-09-01 00:00:00 +00:00")
open_order = limit_order_open[enter_side(is_short)]
open_exit_order = limit_order_open[exit_side(is_short)]
bid = 0.11
enter_rate_mock = MagicMock(return_value=bid)
enter_mm = MagicMock(return_value=open_order)
patch_RPCManager(mocker)
patch_exchange(mocker)
default_conf['trading_mode'] = 'futures'
default_conf['collateral'] = 'isolated'
default_conf['dry_run'] = True
funding_rates_midnight = {
"LTC/BTC": {
1630454400000: 0.00032583,
},
"ETH/BTC": {
1630454400000: 0.0001,
},
"XRP/BTC": {
1630454400000: 0.00049426,
}
}
funding_rates_eight = {
"LTC/BTC": {
1630454400000: 0.00032583,
1630483200000: 0.00024472,
},
"ETH/BTC": {
1630454400000: 0.0001,
1630483200000: 0.0001,
},
"XRP/BTC": {
1630454400000: 0.00049426,
1630483200000: 0.00032715,
}
}
mark_prices = {
"LTC/BTC": {
1630454400000: 3.3,
1630483200000: 3.2,
},
"ETH/BTC": {
1630454400000: 2.4,
1630483200000: 2.5,
},
"XRP/BTC": {
1630454400000: 1.2,
1630483200000: 1.2,
}
}
mocker.patch(
'freqtrade.exchange.Exchange._get_mark_price_history',
side_effect=lambda pair, since: mark_prices[pair]
)
mocker.patch(
'freqtrade.exchange.Exchange.get_funding_rate_history',
side_effect=lambda pair, since: funding_rates_midnight[pair]
)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_rate=enter_rate_mock,
fetch_ticker=MagicMock(return_value={
'bid': 1.9,
'ask': 2.2,
'last': 1.9
}),
create_order=enter_mm,
get_min_pair_stake_amount=MagicMock(return_value=1),
get_fee=fee,
)
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)
trades = Trade.get_open_trades()
assert len(trades) == 3
for trade in trades:
assert trade.funding_fees == (
trade.amount *
mark_prices[trade.pair][1630454400000] *
funding_rates_midnight[trade.pair][1630454400000]
)
mocker.patch('freqtrade.exchange.Exchange.create_order', return_value=open_exit_order)
# create_mock_trades(fee, False)
time_machine.move_to("2021-09-01 08:00:00 +00:00")
mocker.patch(
'freqtrade.exchange.Exchange.get_funding_rate_history',
side_effect=lambda pair, since: funding_rates_eight[pair]
)
if schedule_off:
for trade in trades:
assert trade.funding_fees == (
trade.amount *
mark_prices[trade.pair][1630454400000] *
funding_rates_eight[trade.pair][1630454400000]
)
freqtrade.execute_trade_exit(
trade=trade,
# The values of the next 2 params are irrelevant for this test
limit=ticker_usdt_sell_up()['bid'],
sell_reason=SellCheckTuple(sell_type=SellType.ROI)
)
else:
freqtrade._schedule.run_pending()
# Funding fees for 00:00 and 08:00
for trade in trades:
assert trade.funding_fees == sum([
trade.amount *
mark_prices[trade.pair][time] *
funding_rates_eight[trade.pair][time] for time in mark_prices[trade.pair].keys()
])