commit
90c565006b
@ -283,17 +283,20 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
|
|||||||
# Predefined candletype (and timeframe) depending on exchange
|
# Predefined candletype (and timeframe) depending on exchange
|
||||||
# Downloads what is necessary to backtest based on futures data.
|
# Downloads what is necessary to backtest based on futures data.
|
||||||
timeframe = exchange._ft_has['mark_ohlcv_timeframe']
|
timeframe = exchange._ft_has['mark_ohlcv_timeframe']
|
||||||
candle_type = CandleType.from_string(exchange._ft_has['mark_ohlcv_price'])
|
fr_candle_type = CandleType.from_string(exchange._ft_has['mark_ohlcv_price'])
|
||||||
|
# All exchanges need FundingRate for futures trading.
|
||||||
# TODO: this could be in most parts to the above.
|
# The timeframe is aligned to the mark-price timeframe.
|
||||||
if erase:
|
for candle_type in (CandleType.FUNDING_RATE, fr_candle_type):
|
||||||
if data_handler.ohlcv_purge(pair, timeframe, candle_type=candle_type):
|
# TODO: this could be in most parts to the above.
|
||||||
logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.')
|
if erase:
|
||||||
_download_pair_history(pair=pair, process=process,
|
if data_handler.ohlcv_purge(pair, timeframe, candle_type=candle_type):
|
||||||
datadir=datadir, exchange=exchange,
|
logger.info(
|
||||||
timerange=timerange, data_handler=data_handler,
|
f'Deleting existing data for pair {pair}, interval {timeframe}.')
|
||||||
timeframe=str(timeframe), new_pairs_days=new_pairs_days,
|
_download_pair_history(pair=pair, process=process,
|
||||||
candle_type=candle_type)
|
datadir=datadir, exchange=exchange,
|
||||||
|
timerange=timerange, data_handler=data_handler,
|
||||||
|
timeframe=str(timeframe), new_pairs_days=new_pairs_days,
|
||||||
|
candle_type=candle_type)
|
||||||
|
|
||||||
return pairs_not_available
|
return pairs_not_available
|
||||||
|
|
||||||
|
@ -1389,7 +1389,8 @@ class Exchange:
|
|||||||
return pair, timeframe, candle_type, data
|
return pair, timeframe, candle_type, data
|
||||||
|
|
||||||
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
||||||
since_ms: Optional[int] = None, cache: bool = True
|
since_ms: Optional[int] = None, cache: bool = True,
|
||||||
|
drop_incomplete: bool = None
|
||||||
) -> Dict[PairWithTimeframe, DataFrame]:
|
) -> Dict[PairWithTimeframe, DataFrame]:
|
||||||
"""
|
"""
|
||||||
Refresh in-memory OHLCV asynchronously and set `_klines` with the result
|
Refresh in-memory OHLCV asynchronously and set `_klines` with the result
|
||||||
@ -1398,10 +1399,13 @@ class Exchange:
|
|||||||
:param pair_list: List of 2 element tuples containing pair, interval to refresh
|
:param pair_list: List of 2 element tuples containing pair, interval to refresh
|
||||||
:param since_ms: time since when to download, in milliseconds
|
:param since_ms: time since when to download, in milliseconds
|
||||||
:param cache: Assign result to _klines. Usefull for one-off downloads like for pairlists
|
:param cache: Assign result to _klines. Usefull for one-off downloads like for pairlists
|
||||||
|
:param drop_incomplete: Control candle dropping.
|
||||||
|
Specifying None defaults to _ohlcv_partial_candle
|
||||||
:return: Dict of [{(pair, timeframe): Dataframe}]
|
:return: Dict of [{(pair, timeframe): Dataframe}]
|
||||||
"""
|
"""
|
||||||
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
|
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
|
||||||
|
# TODO-lev: maybe depend this on candle type?
|
||||||
|
drop_incomplete = self._ohlcv_partial_candle if drop_incomplete is None else drop_incomplete
|
||||||
input_coroutines = []
|
input_coroutines = []
|
||||||
cached_pairs = []
|
cached_pairs = []
|
||||||
# Gather coroutines to run
|
# Gather coroutines to run
|
||||||
@ -1447,7 +1451,7 @@ class Exchange:
|
|||||||
# keeping parsed dataframe in cache
|
# keeping parsed dataframe in cache
|
||||||
ohlcv_df = ohlcv_to_dataframe(
|
ohlcv_df = ohlcv_to_dataframe(
|
||||||
ticks, timeframe, pair=pair, fill_missing=True,
|
ticks, timeframe, pair=pair, fill_missing=True,
|
||||||
drop_incomplete=self._ohlcv_partial_candle)
|
drop_incomplete=drop_incomplete)
|
||||||
results_df[(pair, timeframe, c_type)] = ohlcv_df
|
results_df[(pair, timeframe, c_type)] = ohlcv_df
|
||||||
if cache:
|
if cache:
|
||||||
self._klines[(pair, timeframe, c_type)] = ohlcv_df
|
self._klines[(pair, timeframe, c_type)] = ohlcv_df
|
||||||
@ -1494,11 +1498,17 @@ class Exchange:
|
|||||||
params = deepcopy(self._ft_has.get('ohlcv_params', {}))
|
params = deepcopy(self._ft_has.get('ohlcv_params', {}))
|
||||||
if candle_type != CandleType.SPOT:
|
if candle_type != CandleType.SPOT:
|
||||||
params.update({'price': candle_type})
|
params.update({'price': candle_type})
|
||||||
data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe,
|
if candle_type != CandleType.FUNDING_RATE:
|
||||||
since=since_ms,
|
data = await self._api_async.fetch_ohlcv(
|
||||||
limit=self.ohlcv_candle_limit(timeframe),
|
pair, timeframe=timeframe, since=since_ms,
|
||||||
params=params)
|
limit=self.ohlcv_candle_limit(timeframe), params=params)
|
||||||
|
else:
|
||||||
|
# Funding rate
|
||||||
|
data = await self._api_async.fetch_funding_rate_history(
|
||||||
|
pair, since=since_ms,
|
||||||
|
limit=self.ohlcv_candle_limit(timeframe))
|
||||||
|
# Convert funding rate to candle pattern
|
||||||
|
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
|
||||||
# Some exchanges sort OHLCV in ASC order and others in DESC.
|
# Some exchanges sort OHLCV in ASC order and others in DESC.
|
||||||
# Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
|
# Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
|
||||||
# while GDAX returns the list of OHLCV in DESC order (newest first, oldest last)
|
# while GDAX returns the list of OHLCV in DESC order (newest first, oldest last)
|
||||||
@ -1812,46 +1822,6 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier
|
|
||||||
def _get_mark_price_history(self, pair: str, since: int) -> Dict:
|
|
||||||
"""
|
|
||||||
Get's the mark price history for a pair
|
|
||||||
:param pair: The quote/base pair of the trade
|
|
||||||
:param since: The earliest time to start downloading candles, in ms.
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
candles = self._api.fetch_ohlcv(
|
|
||||||
pair,
|
|
||||||
timeframe="1h",
|
|
||||||
since=since,
|
|
||||||
params={
|
|
||||||
'price': self._ft_has["mark_ohlcv_price"]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
history = {}
|
|
||||||
for candle in candles:
|
|
||||||
d = datetime.fromtimestamp(int(candle[0] / 1000), timezone.utc)
|
|
||||||
# Round down to the nearest hour, in case of a delayed timestamp
|
|
||||||
# The millisecond timestamps can be delayed ~20ms
|
|
||||||
time = timeframe_to_prev_date('1h', d).timestamp() * 1000
|
|
||||||
opening_mark_price = candle[1]
|
|
||||||
history[time] = opening_mark_price
|
|
||||||
return history
|
|
||||||
except ccxt.NotSupported as e:
|
|
||||||
raise OperationalException(
|
|
||||||
f'Exchange {self._api.name} does not support fetching historical '
|
|
||||||
f'mark price candle (OHLCV) data. Message: {e}') from e
|
|
||||||
except ccxt.DDoSProtection as e:
|
|
||||||
raise DDosProtection(e) from e
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
|
||||||
raise TemporaryError(f'Could not fetch historical mark price candle (OHLCV) data '
|
|
||||||
f'for pair {pair} due to {e.__class__.__name__}. '
|
|
||||||
f'Message: {e}') from e
|
|
||||||
except ccxt.BaseError as e:
|
|
||||||
raise OperationalException(f'Could not fetch historical mark price candle (OHLCV) data '
|
|
||||||
f'for pair {pair}. Message: {e}') from e
|
|
||||||
|
|
||||||
def _calculate_funding_fees(
|
def _calculate_funding_fees(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
@ -1870,36 +1840,33 @@ class Exchange:
|
|||||||
|
|
||||||
if self.funding_fee_cutoff(open_date):
|
if self.funding_fee_cutoff(open_date):
|
||||||
open_date += timedelta(hours=1)
|
open_date += timedelta(hours=1)
|
||||||
|
timeframe = self._ft_has['mark_ohlcv_timeframe']
|
||||||
open_date = timeframe_to_prev_date('1h', open_date)
|
timeframe_ff = self._ft_has.get('funding_fee_timeframe',
|
||||||
|
self._ft_has['mark_ohlcv_timeframe'])
|
||||||
|
open_date = timeframe_to_prev_date(timeframe, open_date)
|
||||||
|
|
||||||
fees: float = 0
|
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
|
||||||
# close_timestamp = int(close_date.timestamp()) * 1000
|
# close_timestamp = int(close_date.timestamp()) * 1000
|
||||||
funding_rate_history = self.get_funding_rate_history(
|
|
||||||
pair,
|
mark_comb: PairWithTimeframe = (
|
||||||
open_timestamp
|
pair, timeframe, CandleType.from_string(self._ft_has["mark_ohlcv_price"]))
|
||||||
|
|
||||||
|
funding_comb: PairWithTimeframe = (pair, timeframe_ff, CandleType.FUNDING_RATE)
|
||||||
|
candle_histories = self.refresh_latest_ohlcv(
|
||||||
|
[mark_comb, funding_comb],
|
||||||
|
since_ms=open_timestamp,
|
||||||
|
cache=False,
|
||||||
|
drop_incomplete=False,
|
||||||
)
|
)
|
||||||
mark_price_history = self._get_mark_price_history(
|
funding_rates = candle_histories[funding_comb]
|
||||||
pair,
|
mark_rates = candle_histories[mark_comb]
|
||||||
open_timestamp
|
|
||||||
)
|
df = funding_rates.merge(mark_rates, on='date', how="inner", suffixes=["_fund", "_mark"])
|
||||||
for timestamp in funding_rate_history.keys():
|
df = df[(df['date'] >= open_date) & (df['date'] <= close_date)]
|
||||||
funding_rate = funding_rate_history[timestamp]
|
fees = sum(df['open_fund'] * df['open_mark'] * amount)
|
||||||
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
|
return fees
|
||||||
|
|
||||||
@ -1920,42 +1887,6 @@ class Exchange:
|
|||||||
else:
|
else:
|
||||||
return 0.0
|
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:
|
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
|
||||||
return exchange_name in ccxt_exchanges(ccxt_module)
|
return exchange_name in ccxt_exchanges(ccxt_module)
|
||||||
|
@ -16,6 +16,8 @@ class Okex(Exchange):
|
|||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 100,
|
"ohlcv_candle_limit": 100,
|
||||||
|
"mark_ohlcv_timeframe": "4h",
|
||||||
|
"funding_fee_timeframe": "8h",
|
||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
|
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
|
||||||
|
@ -490,7 +490,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No
|
|||||||
@pytest.mark.parametrize('trademode,callcount', [
|
@pytest.mark.parametrize('trademode,callcount', [
|
||||||
('spot', 4),
|
('spot', 4),
|
||||||
('margin', 4),
|
('margin', 4),
|
||||||
('futures', 6),
|
('futures', 8), # Called 8 times - 4 normal, 2 funding and 2 mark/index calls
|
||||||
])
|
])
|
||||||
def test_refresh_backtest_ohlcv_data(
|
def test_refresh_backtest_ohlcv_data(
|
||||||
mocker, default_conf, markets, caplog, testdatadir, trademode, callcount):
|
mocker, default_conf, markets, caplog, testdatadir, trademode, callcount):
|
||||||
|
@ -91,7 +91,7 @@ def exchange_futures(request, exchange_conf, class_mocker):
|
|||||||
exchange_conf['exchange']['name'] = request.param
|
exchange_conf['exchange']['name'] = request.param
|
||||||
exchange_conf['trading_mode'] = 'futures'
|
exchange_conf['trading_mode'] = 'futures'
|
||||||
exchange_conf['collateral'] = 'cross'
|
exchange_conf['collateral'] = 'cross'
|
||||||
# TODO-lev This mock should no longer be necessary once futures are enabled.
|
# TODO-lev: This mock should no longer be necessary once futures are enabled.
|
||||||
class_mocker.patch(
|
class_mocker.patch(
|
||||||
'freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_collateral')
|
'freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_collateral')
|
||||||
class_mocker.patch(
|
class_mocker.patch(
|
||||||
@ -195,7 +195,6 @@ class TestCCXTExchange():
|
|||||||
assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
|
assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
|
||||||
|
|
||||||
def test_ccxt_fetch_funding_rate_history(self, exchange_futures):
|
def test_ccxt_fetch_funding_rate_history(self, exchange_futures):
|
||||||
# TODO-lev: enable this test once Futures mode is enabled.
|
|
||||||
exchange, exchangename = exchange_futures
|
exchange, exchangename = exchange_futures
|
||||||
if not exchange:
|
if not exchange:
|
||||||
# exchange_futures only returns values for supported exchanges
|
# exchange_futures only returns values for supported exchanges
|
||||||
@ -203,34 +202,59 @@ class TestCCXTExchange():
|
|||||||
|
|
||||||
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
||||||
since = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000)
|
since = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000)
|
||||||
|
timeframe_ff = exchange._ft_has.get('funding_fee_timeframe',
|
||||||
|
exchange._ft_has['mark_ohlcv_timeframe'])
|
||||||
|
pair_tf = (pair, timeframe_ff, CandleType.FUNDING_RATE)
|
||||||
|
|
||||||
rate = exchange.get_funding_rate_history(pair, since)
|
funding_ohlcv = exchange.refresh_latest_ohlcv(
|
||||||
assert isinstance(rate, dict)
|
[pair_tf],
|
||||||
|
since_ms=since,
|
||||||
|
drop_incomplete=False)
|
||||||
|
|
||||||
expected_tf = exchange._ft_has['mark_ohlcv_timeframe']
|
assert isinstance(funding_ohlcv, dict)
|
||||||
this_hour = timeframe_to_prev_date(expected_tf)
|
rate = funding_ohlcv[pair_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
|
|
||||||
|
|
||||||
@pytest.mark.skip("No futures support yet")
|
this_hour = timeframe_to_prev_date(timeframe_ff)
|
||||||
def test_fetch_mark_price_history(self, exchange_futures):
|
prev_hour = timeframe_to_prev_date(timeframe_ff, this_hour - timedelta(minutes=1))
|
||||||
|
assert rate[rate['date'] == this_hour].iloc[0]['open'] != 0.0
|
||||||
|
assert rate[rate['date'] == prev_hour].iloc[0]['open'] != 0.0
|
||||||
|
|
||||||
|
def test_ccxt_fetch_mark_price_history(self, exchange_futures):
|
||||||
exchange, exchangename = exchange_futures
|
exchange, exchangename = exchange_futures
|
||||||
if not exchange:
|
if not exchange:
|
||||||
# exchange_futures only returns values for supported exchanges
|
# exchange_futures only returns values for supported exchanges
|
||||||
return
|
return
|
||||||
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
||||||
since = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000)
|
since = int((datetime.now(timezone.utc) - timedelta(days=5)).timestamp() * 1000)
|
||||||
|
pair_tf = (pair, '1h', CandleType.MARK)
|
||||||
|
|
||||||
mark_candles = exchange._get_mark_price_history(pair, since)
|
mark_ohlcv = exchange.refresh_latest_ohlcv(
|
||||||
|
[pair_tf],
|
||||||
|
since_ms=since,
|
||||||
|
drop_incomplete=False)
|
||||||
|
|
||||||
assert isinstance(mark_candles, dict)
|
assert isinstance(mark_ohlcv, dict)
|
||||||
expected_tf = '1h'
|
expected_tf = '1h'
|
||||||
|
mark_candles = mark_ohlcv[pair_tf]
|
||||||
|
|
||||||
this_hour = timeframe_to_prev_date(expected_tf)
|
this_hour = timeframe_to_prev_date(expected_tf)
|
||||||
prev_tick = timeframe_to_prev_date(expected_tf, this_hour - timedelta(minutes=1))
|
prev_hour = timeframe_to_prev_date(expected_tf, this_hour - timedelta(minutes=1))
|
||||||
assert mark_candles[int(this_hour.timestamp() * 1000)] != 0.0
|
|
||||||
assert mark_candles[int(prev_tick.timestamp() * 1000)] != 0.0
|
assert mark_candles[mark_candles['date'] == prev_hour].iloc[0]['open'] != 0.0
|
||||||
|
assert mark_candles[mark_candles['date'] == this_hour].iloc[0]['open'] != 0.0
|
||||||
|
|
||||||
|
def test_ccxt__calculate_funding_fees(self, exchange_futures):
|
||||||
|
exchange, exchangename = exchange_futures
|
||||||
|
if not exchange:
|
||||||
|
# exchange_futures only returns values for supported exchanges
|
||||||
|
return
|
||||||
|
pair = EXCHANGES[exchangename].get('futures_pair', EXCHANGES[exchangename]['pair'])
|
||||||
|
since = datetime.now(timezone.utc) - timedelta(days=5)
|
||||||
|
|
||||||
|
funding_fee = exchange._calculate_funding_fees(pair, 20, open_date=since)
|
||||||
|
|
||||||
|
assert isinstance(funding_fee, float)
|
||||||
|
# assert funding_fee > 0
|
||||||
|
|
||||||
# TODO: tests fetch_trades (?)
|
# TODO: tests fetch_trades (?)
|
||||||
|
|
||||||
|
@ -3408,81 +3408,6 @@ def test__get_funding_fee(
|
|||||||
assert kraken._get_funding_fee(size, funding_rate, mark_price, time_in_ratio) == kraken_fee
|
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', [
|
@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: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', 0, 2, "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999),
|
||||||
@ -3566,14 +3491,14 @@ def test__calculate_funding_fees(
|
|||||||
'gateio': funding_rate_history_octohourly,
|
'gateio': funding_rate_history_octohourly,
|
||||||
}[exchange][rate_start:rate_end]
|
}[exchange][rate_start:rate_end]
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.fetch_funding_rate_history = MagicMock(return_value=funding_rate_history)
|
api_mock.fetch_funding_rate_history = get_mock_coro(return_value=funding_rate_history)
|
||||||
api_mock.fetch_ohlcv = MagicMock(return_value=mark_ohlcv)
|
api_mock.fetch_ohlcv = get_mock_coro(return_value=mark_ohlcv)
|
||||||
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
|
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
|
||||||
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._calculate_funding_fees('ADA/USDT', amount, d1, d2)
|
||||||
assert funding_fees == expected_fees
|
assert pytest.approx(funding_fees) == expected_fees
|
||||||
|
|
||||||
|
|
||||||
@ pytest.mark.parametrize('exchange,expected_fees', [
|
@ pytest.mark.parametrize('exchange,expected_fees', [
|
||||||
@ -3590,8 +3515,9 @@ def test__calculate_funding_fees_datetime_called(
|
|||||||
expected_fees
|
expected_fees
|
||||||
):
|
):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.fetch_ohlcv = MagicMock(return_value=mark_ohlcv)
|
api_mock.fetch_ohlcv = get_mock_coro(return_value=mark_ohlcv)
|
||||||
api_mock.fetch_funding_rate_history = MagicMock(return_value=funding_rate_history_octohourly)
|
api_mock.fetch_funding_rate_history = get_mock_coro(
|
||||||
|
return_value=funding_rate_history_octohourly)
|
||||||
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
|
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
|
||||||
type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True})
|
type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True})
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ from unittest.mock import ANY, MagicMock, PropertyMock
|
|||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT
|
from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT
|
||||||
from freqtrade.enums import CandleType, RPCMessageType, RunMode, SellType, SignalDirection, State
|
from freqtrade.enums import CandleType, RPCMessageType, RunMode, SellType, SignalDirection, State
|
||||||
@ -4802,60 +4803,67 @@ def test_update_funding_fees(
|
|||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
default_conf['trading_mode'] = 'futures'
|
default_conf['trading_mode'] = 'futures'
|
||||||
default_conf['collateral'] = 'isolated'
|
default_conf['collateral'] = 'isolated'
|
||||||
default_conf['dry_run'] = True
|
|
||||||
timestamp_midnight = 1630454400000
|
|
||||||
timestamp_eight = 1630483200000
|
|
||||||
funding_rates_midnight = {
|
|
||||||
"LTC/BTC": {
|
|
||||||
timestamp_midnight: 0.00032583,
|
|
||||||
},
|
|
||||||
"ETH/BTC": {
|
|
||||||
timestamp_midnight: 0.0001,
|
|
||||||
},
|
|
||||||
"XRP/BTC": {
|
|
||||||
timestamp_midnight: 0.00049426,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
funding_rates_eight = {
|
date_midnight = arrow.get('2021-09-01 00:00:00')
|
||||||
"LTC/BTC": {
|
date_eight = arrow.get('2021-09-01 08:00:00')
|
||||||
timestamp_midnight: 0.00032583,
|
date_sixteen = arrow.get('2021-09-01 16:00:00')
|
||||||
timestamp_eight: 0.00024472,
|
columns = ['date', 'open', 'high', 'low', 'close', 'volume']
|
||||||
},
|
# 16:00 entry is actually never used
|
||||||
"ETH/BTC": {
|
# But should be kept in the test to ensure we're filtering correctly.
|
||||||
timestamp_midnight: 0.0001,
|
funding_rates = {
|
||||||
timestamp_eight: 0.0001,
|
"LTC/BTC":
|
||||||
},
|
DataFrame([
|
||||||
"XRP/BTC": {
|
[date_midnight, 0.00032583, 0, 0, 0, 0],
|
||||||
timestamp_midnight: 0.00049426,
|
[date_eight, 0.00024472, 0, 0, 0, 0],
|
||||||
timestamp_eight: 0.00032715,
|
[date_sixteen, 0.00024472, 0, 0, 0, 0],
|
||||||
}
|
], columns=columns),
|
||||||
|
"ETH/BTC":
|
||||||
|
DataFrame([
|
||||||
|
[date_midnight, 0.0001, 0, 0, 0, 0],
|
||||||
|
[date_eight, 0.0001, 0, 0, 0, 0],
|
||||||
|
[date_sixteen, 0.0001, 0, 0, 0, 0],
|
||||||
|
], columns=columns),
|
||||||
|
"XRP/BTC":
|
||||||
|
DataFrame([
|
||||||
|
[date_midnight, 0.00049426, 0, 0, 0, 0],
|
||||||
|
[date_eight, 0.00032715, 0, 0, 0, 0],
|
||||||
|
[date_sixteen, 0.00032715, 0, 0, 0, 0],
|
||||||
|
], columns=columns)
|
||||||
}
|
}
|
||||||
|
|
||||||
mark_prices = {
|
mark_prices = {
|
||||||
"LTC/BTC": {
|
"LTC/BTC":
|
||||||
timestamp_midnight: 3.3,
|
DataFrame([
|
||||||
timestamp_eight: 3.2,
|
[date_midnight, 3.3, 0, 0, 0, 0],
|
||||||
},
|
[date_eight, 3.2, 0, 0, 0, 0],
|
||||||
"ETH/BTC": {
|
[date_sixteen, 3.2, 0, 0, 0, 0],
|
||||||
timestamp_midnight: 2.4,
|
], columns=columns),
|
||||||
timestamp_eight: 2.5,
|
"ETH/BTC":
|
||||||
},
|
DataFrame([
|
||||||
"XRP/BTC": {
|
[date_midnight, 2.4, 0, 0, 0, 0],
|
||||||
timestamp_midnight: 1.2,
|
[date_eight, 2.5, 0, 0, 0, 0],
|
||||||
timestamp_eight: 1.2,
|
[date_sixteen, 2.5, 0, 0, 0, 0],
|
||||||
}
|
], columns=columns),
|
||||||
|
"XRP/BTC":
|
||||||
|
DataFrame([
|
||||||
|
[date_midnight, 1.2, 0, 0, 0, 0],
|
||||||
|
[date_eight, 1.2, 0, 0, 0, 0],
|
||||||
|
[date_sixteen, 1.2, 0, 0, 0, 0],
|
||||||
|
], columns=columns)
|
||||||
}
|
}
|
||||||
|
|
||||||
mocker.patch(
|
def refresh_latest_ohlcv_mock(pairlist, **kwargs):
|
||||||
'freqtrade.exchange.Exchange._get_mark_price_history',
|
ret = {}
|
||||||
side_effect=lambda pair, since: mark_prices[pair]
|
for p, tf, ct in pairlist:
|
||||||
)
|
if ct == CandleType.MARK:
|
||||||
|
ret[(p, tf, ct)] = mark_prices[p]
|
||||||
|
else:
|
||||||
|
ret[(p, tf, ct)] = funding_rates[p]
|
||||||
|
|
||||||
mocker.patch(
|
return ret
|
||||||
'freqtrade.exchange.Exchange.get_funding_rate_history',
|
|
||||||
side_effect=lambda pair, since: funding_rates_midnight[pair]
|
mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv',
|
||||||
)
|
side_effect=refresh_latest_ohlcv_mock)
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -4880,38 +4888,33 @@ def test_update_funding_fees(
|
|||||||
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 trade.funding_fees == (
|
assert pytest.approx(trade.funding_fees) == (
|
||||||
trade.amount *
|
trade.amount *
|
||||||
mark_prices[trade.pair][timestamp_midnight] *
|
mark_prices[trade.pair].iloc[0]['open'] *
|
||||||
funding_rates_midnight[trade.pair][timestamp_midnight]
|
funding_rates[trade.pair].iloc[0]['open']
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.create_order', return_value=open_exit_order)
|
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")
|
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:
|
if schedule_off:
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
assert trade.funding_fees == (
|
|
||||||
trade.amount *
|
|
||||||
mark_prices[trade.pair][timestamp_midnight] *
|
|
||||||
funding_rates_eight[trade.pair][timestamp_midnight]
|
|
||||||
)
|
|
||||||
freqtrade.execute_trade_exit(
|
freqtrade.execute_trade_exit(
|
||||||
trade=trade,
|
trade=trade,
|
||||||
# The values of the next 2 params are irrelevant for this test
|
# The values of the next 2 params are irrelevant for this test
|
||||||
limit=ticker_usdt_sell_up()['bid'],
|
limit=ticker_usdt_sell_up()['bid'],
|
||||||
sell_reason=SellCheckTuple(sell_type=SellType.ROI)
|
sell_reason=SellCheckTuple(sell_type=SellType.ROI)
|
||||||
)
|
)
|
||||||
|
assert trade.funding_fees == pytest.approx(sum(
|
||||||
|
trade.amount *
|
||||||
|
mark_prices[trade.pair].iloc[0:2]['open'] *
|
||||||
|
funding_rates[trade.pair].iloc[0:2]['open']
|
||||||
|
))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
freqtrade._schedule.run_pending()
|
freqtrade._schedule.run_pending()
|
||||||
|
|
||||||
# Funding fees for 00:00 and 08:00
|
# Funding fees for 00:00 and 08:00
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
assert trade.funding_fees == sum([
|
assert trade.funding_fees == pytest.approx(sum(
|
||||||
trade.amount *
|
trade.amount *
|
||||||
mark_prices[trade.pair][time] *
|
mark_prices[trade.pair].iloc[0:2]['open'] * funding_rates[trade.pair].iloc[0:2]['open']
|
||||||
funding_rates_eight[trade.pair][time] for time in mark_prices[trade.pair].keys()
|
))
|
||||||
])
|
|
||||||
|
Loading…
Reference in New Issue
Block a user