From 2a26c6fbed747511834b1724ce2cad55ca1a6a9d Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 26 Sep 2021 04:11:35 -0600 Subject: [PATCH 01/53] Added backtesting methods back in --- freqtrade/exchange/binance.py | 56 +++++++++++++++++++++- freqtrade/exchange/exchange.py | 82 ++++++++++++++++++++++++++++++++- freqtrade/exchange/ftx.py | 40 +++++++++++++++- freqtrade/persistence/models.py | 14 ++++++ tests/exchange/test_binance.py | 8 ++++ tests/exchange/test_exchange.py | 12 +++++ tests/exchange/test_ftx.py | 33 +++++++++++++ 7 files changed, 241 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index d23f84e7b..5169a1625 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,8 +1,9 @@ """ Binance exchange subclass """ import json import logging +from datetime import datetime from pathlib import Path -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple import arrow import ccxt @@ -29,7 +30,13 @@ class Binance(Exchange): "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } funding_fee_times: List[int] = [0, 8, 16] # hours of the day - # but the schedule won't check within this timeframe + _funding_interest_rates: Dict = {} # TODO-lev: delete + + def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: + super().__init__(config, validate) + # TODO-lev: Uncomment once lev-exchange merged in + # if self.trading_mode == TradingMode.FUTURES: + # self._funding_interest_rates = self._get_funding_interest_rates() _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ # TradingMode.SPOT always supported and not required in this list @@ -211,6 +218,51 @@ class Binance(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e + def _get_premium_index(self, pair: str, date: datetime) -> float: + raise OperationalException(f'_get_premium_index has not been implemented on {self.name}') + + def _get_mark_price(self, pair: str, date: datetime) -> float: + raise OperationalException(f'_get_mark_price has not been implemented on {self.name}') + + def _get_funding_interest_rates(self): + rates = self._api.fetch_funding_rates() + interest_rates = {} + for pair, data in rates.items(): + interest_rates[pair] = data['interestRate'] + return interest_rates + + def _calculate_funding_rate(self, pair: str, premium_index: float) -> Optional[float]: + """ + Get's the funding_rate for a pair at a specific date and time in the past + """ + return ( + premium_index + + max(min(self._funding_interest_rates[pair] - premium_index, 0.0005), -0.0005) + ) + + def _get_funding_fee( + self, + pair: str, + contract_size: float, + mark_price: float, + premium_index: Optional[float], + ) -> float: + """ + Calculates a single funding fee + :param contract_size: The amount/quanity + :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: 0.03% daily, BNBUSDT, LINKUSDT, and LTCUSDT are 0% + - premium: varies by price difference between the perpetual contract and mark price + """ + if premium_index is None: + raise OperationalException("Premium index cannot be None for Binance._get_funding_fee") + nominal_value = mark_price * contract_size + funding_rate = self._calculate_funding_rate(pair, premium_index) + if funding_rate is None: + raise OperationalException("Funding rate should never be none on Binance") + return nominal_value * funding_rate + async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int, is_new_pair: bool ) -> List: diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index de9711ddd..bdb5ccd20 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -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 @@ -1604,6 +1604,14 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) + # https://www.binance.com/en/support/faq/360033525031 + def fetch_funding_rate(self, pair): + if not self.exchange_has("fetchFundingHistory"): + raise OperationalException( + f"fetch_funding_history() has not been implemented on ccxt.{self.name}") + + return self._api.fetch_funding_rates() + @retrier def get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float: """ @@ -1659,6 +1667,37 @@ class Exchange: else: return 1.0 + def _get_premium_index(self, pair: str, date: datetime) -> float: + raise OperationalException(f'_get_premium_index has not been implemented on {self.name}') + + def _get_mark_price(self, pair: str, date: datetime) -> float: + raise OperationalException(f'_get_mark_price has not been implemented on {self.name}') + + def _get_funding_rate(self, pair: str, when: datetime): + """ + Get's the funding_rate for a pair at a specific date and time in the past + """ + # TODO-lev: implement + raise OperationalException(f"get_funding_rate has not been implemented for {self.name}") + + def _get_funding_fee( + self, + pair: str, + contract_size: float, + mark_price: float, + premium_index: Optional[float], + # index_price: float, + # interest_rate: float) + ) -> float: + """ + Calculates a single funding fee + :param contract_size: The amount/quanity + :param mark_price: The price of the asset that the contract is based off of + :param funding_rate: the interest rate and the premium + - premium: varies by price difference between the perpetual contract and mark price + """ + raise OperationalException(f"Funding fee has not been implemented for {self.name}") + @retrier def _set_leverage( self, @@ -1684,6 +1723,19 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + def _get_funding_fee_dates(self, d1, d2): + d1 = datetime(d1.year, d1.month, d1.day, d1.hour) + d2 = datetime(d2.year, d2.month, d2.day, d2.hour) + + results = [] + d3 = d1 + while d3 < d2: + d3 += timedelta(hours=1) + if d3.hour in self.funding_fee_times: + results.append(d3) + + return results + @retrier def set_margin_mode(self, pair: str, collateral: Collateral, params: dict = {}): ''' @@ -1704,6 +1756,34 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + def calculate_funding_fees( + self, + pair: str, + amount: float, + open_date: datetime, + close_date: datetime + ) -> float: + """ + calculates the sum of all funding fees that occurred for a pair during a futures trade + :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 + """ + + fees: float = 0 + for date in self._get_funding_fee_dates(open_date, close_date): + premium_index = self._get_premium_index(pair, date) + mark_price = self._get_mark_price(pair, date) + fees += self._get_funding_fee( + pair=pair, + contract_size=amount, + mark_price=mark_price, + premium_index=premium_index + ) + + return fees + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 2acf32ba3..dcbe848b7 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,6 +1,7 @@ """ FTX exchange subclass """ import logging -from typing import Any, Dict, List, Tuple +from datetime import datetime +from typing import Any, Dict, List, Optional, Tuple import ccxt @@ -168,3 +169,40 @@ class Ftx(Exchange): if order['type'] == 'stop': return safe_value_fallback2(order, order, 'id_stop', 'id') return order['id'] + + def fill_leverage_brackets(self): + """ + FTX leverage is static across the account, and doesn't change from pair to pair, + so _leverage_brackets doesn't need to be set + """ + return + + def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: + """ + Returns the maximum leverage that a pair can be traded at, which is always 20 on ftx + :param pair: Here for super method, not used on FTX + :nominal_value: Here for super method, not used on FTX + """ + return 20.0 + + def _get_funding_rate(self, pair: str, when: datetime) -> Optional[float]: + """FTX doesn't use this""" + return None + + def _get_funding_fee( + self, + pair: str, + contract_size: float, + mark_price: float, + premium_index: Optional[float], + # index_price: float, + # interest_rate: float) + ) -> float: + """ + Calculates a single funding fee + Always paid in USD on FTX # TODO: How do we account for this + : param contract_size: The amount/quanity + : param mark_price: The price of the asset that the contract is based off of + : param funding_rate: Must be None on ftx + """ + return (contract_size * mark_price) / 24 diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 5496628f4..623dd74d3 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -707,6 +707,7 @@ class LocalTrade(): return float(self._calc_base_close(amount, rate, fee) - total_interest) elif (trading_mode == TradingMode.FUTURES): + self.add_funding_fees() funding_fees = self.funding_fees or 0.0 if self.is_short: return float(self._calc_base_close(amount, rate, fee)) - funding_fees @@ -788,6 +789,19 @@ class LocalTrade(): else: return None + def add_funding_fees(self): + if self.trading_mode == TradingMode.FUTURES: + # TODO-lev: Calculate this correctly and add it + # if self.config['runmode'].value in ('backtest', 'hyperopt'): + # self.funding_fees = getattr(Exchange, self.exchange).calculate_funding_fees( + # self.exchange, + # self.pair, + # self.amount, + # self.open_date_utc, + # self.close_date_utc + # ) + return + @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 0c3e86fdd..dc08a2025 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -342,6 +342,14 @@ def test__set_leverage_binance(mocker, default_conf): ) +def test_get_funding_rate(): + return + + +def test__get_funding_fee(): + return + + @pytest.mark.asyncio async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog): ohlcv = [ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index de1328f3e..a9b899276 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3280,3 +3280,15 @@ 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 + + +def test_get_mark_price(): + return + + +def test_get_funding_fee_dates(): + return + + +def test_calculate_funding_fees(): + return diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 97093bdcb..966a63a74 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -1,3 +1,4 @@ +from datetime import datetime, timedelta from random import randint from unittest.mock import MagicMock @@ -250,3 +251,35 @@ def test_get_order_id(mocker, default_conf): } } assert exchange.get_order_id_conditional(order) == '1111' + + +@pytest.mark.parametrize('pair,nominal_value,max_lev', [ + ("ADA/BTC", 0.0, 20.0), + ("BTC/EUR", 100.0, 20.0), + ("ZEC/USD", 173.31, 20.0), +]) +def test_get_max_leverage_ftx(default_conf, mocker, pair, nominal_value, max_lev): + exchange = get_patched_exchange(mocker, default_conf, id="ftx") + assert exchange.get_max_leverage(pair, nominal_value) == max_lev + + +def test_fill_leverage_brackets_ftx(default_conf, mocker): + # FTX only has one account wide leverage, so there's no leverage brackets + exchange = get_patched_exchange(mocker, default_conf, id="ftx") + exchange.fill_leverage_brackets() + assert exchange._leverage_brackets == {} + + +@pytest.mark.parametrize("pair,when", [ + ('XRP/USDT', datetime.utcnow()), + ('ADA/BTC', datetime.utcnow()), + ('XRP/USDT', datetime.utcnow() - timedelta(hours=30)), +]) +def test__get_funding_rate(default_conf, mocker, pair, when): + api_mock = MagicMock() + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="ftx") + assert exchange._get_funding_rate(pair, when) is None + + +def test__get_funding_fee(): + return From cba0a8cee6234d4f2fb7c1158e0a1a79d6e2b0de Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 5 Oct 2021 17:25:09 -0600 Subject: [PATCH 02/53] adjusted funding fee formula binance --- freqtrade/exchange/binance.py | 19 +++---------------- freqtrade/exchange/exchange.py | 21 ++++----------------- 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 5169a1625..0b0ad1a0f 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -218,34 +218,21 @@ class Binance(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e - def _get_premium_index(self, pair: str, date: datetime) -> float: - raise OperationalException(f'_get_premium_index has not been implemented on {self.name}') - def _get_mark_price(self, pair: str, date: datetime) -> float: raise OperationalException(f'_get_mark_price has not been implemented on {self.name}') - def _get_funding_interest_rates(self): - rates = self._api.fetch_funding_rates() - interest_rates = {} - for pair, data in rates.items(): - interest_rates[pair] = data['interestRate'] - return interest_rates - - def _calculate_funding_rate(self, pair: str, premium_index: float) -> Optional[float]: + def _get_funding_rate(self, pair: str, premium_index: float) -> Optional[float]: """ Get's the funding_rate for a pair at a specific date and time in the past """ - return ( - premium_index + - max(min(self._funding_interest_rates[pair] - premium_index, 0.0005), -0.0005) - ) + raise OperationalException(f'_get_mark_price has not been implemented on {self.name}') def _get_funding_fee( self, pair: str, contract_size: float, mark_price: float, - premium_index: Optional[float], + funding_rate: Optional[float], ) -> float: """ Calculates a single funding fee diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index bdb5ccd20..a6a54a0d6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1604,14 +1604,6 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - # https://www.binance.com/en/support/faq/360033525031 - def fetch_funding_rate(self, pair): - if not self.exchange_has("fetchFundingHistory"): - raise OperationalException( - f"fetch_funding_history() has not been implemented on ccxt.{self.name}") - - return self._api.fetch_funding_rates() - @retrier def get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float: """ @@ -1667,9 +1659,6 @@ class Exchange: else: return 1.0 - def _get_premium_index(self, pair: str, date: datetime) -> float: - raise OperationalException(f'_get_premium_index has not been implemented on {self.name}') - def _get_mark_price(self, pair: str, date: datetime) -> float: raise OperationalException(f'_get_mark_price has not been implemented on {self.name}') @@ -1685,9 +1674,7 @@ class Exchange: pair: str, contract_size: float, mark_price: float, - premium_index: Optional[float], - # index_price: float, - # interest_rate: float) + funding_rate: Optional[float] ) -> float: """ Calculates a single funding fee @@ -1740,7 +1727,7 @@ class Exchange: 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 @@ -1773,13 +1760,13 @@ class Exchange: fees: float = 0 for date in self._get_funding_fee_dates(open_date, close_date): - premium_index = self._get_premium_index(pair, date) + funding_rate = self._get_funding_rate(pair, date) mark_price = self._get_mark_price(pair, date) fees += self._get_funding_fee( pair=pair, contract_size=amount, mark_price=mark_price, - premium_index=premium_index + funding_rate=funding_rate ) return fees From badc0fa4458cabd8abc6fb062ffb79076cd1cff4 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 5 Oct 2021 17:25:31 -0600 Subject: [PATCH 03/53] Adjusted _get_funding_fee_method --- freqtrade/exchange/binance.py | 8 ++++---- freqtrade/exchange/exchange.py | 32 ++++---------------------------- 2 files changed, 8 insertions(+), 32 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 0b0ad1a0f..490961520 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -242,12 +242,12 @@ class Binance(Exchange): - interest rate: 0.03% daily, BNBUSDT, LINKUSDT, and LTCUSDT are 0% - premium: varies by price difference between the perpetual contract and mark price """ - if premium_index is None: - raise OperationalException("Premium index cannot be None for Binance._get_funding_fee") + if mark_price is None: + raise OperationalException("Mark price cannot be None for Binance._get_funding_fee") nominal_value = mark_price * contract_size - funding_rate = self._calculate_funding_rate(pair, premium_index) if funding_rate is None: - raise OperationalException("Funding rate should never be none on Binance") + raise OperationalException( + "Funding rate should never be none on Binance._get_funding_fee") return nominal_value * funding_rate async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a6a54a0d6..8a0d6c863 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -89,6 +89,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) @@ -157,6 +158,9 @@ class Exchange: self._api_async = self._init_ccxt( exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) + if self.trading_mode != TradingMode.SPOT: + self.fill_leverage_brackets() + logger.info('Using Exchange "%s"', self.name) if validate: @@ -179,10 +183,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() - def __del__(self): """ Destructor - clean up async stuff @@ -1635,30 +1635,6 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def fill_leverage_brackets(self): - """ - Assigns property _leverage_brackets to a dictionary of information about the leverage - allowed on each pair - Not used if the exchange has a static max leverage value for the account or each pair - """ - return - - def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: - """ - Returns the maximum leverage that a pair can be traded at - :param pair: The base/quote currency pair being traded - :nominal_value: The total value of the trade in quote currency (collateral + debt) - """ - market = self.markets[pair] - if ( - 'limits' in market and - 'leverage' in market['limits'] and - 'max' in market['limits']['leverage'] - ): - return market['limits']['leverage']['max'] - else: - return 1.0 - def _get_mark_price(self, pair: str, date: datetime) -> float: raise OperationalException(f'_get_mark_price has not been implemented on {self.name}') From ef8b617eb2425f662ad50fd76c99c0e632fdd922 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 9 Oct 2021 12:49:00 -0600 Subject: [PATCH 04/53] gateio, ftx and binance all use same funding fee formula --- freqtrade/exchange/binance.py | 43 ++------------------------------- freqtrade/exchange/exchange.py | 6 +++-- freqtrade/exchange/ftx.py | 23 ------------------ tests/exchange/test_binance.py | 8 ------ tests/exchange/test_exchange.py | 8 ++++++ tests/exchange/test_ftx.py | 16 ------------ 6 files changed, 14 insertions(+), 90 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 490961520..d23f84e7b 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,9 +1,8 @@ """ Binance exchange subclass """ import json import logging -from datetime import datetime from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Tuple import arrow import ccxt @@ -30,13 +29,7 @@ class Binance(Exchange): "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } funding_fee_times: List[int] = [0, 8, 16] # hours of the day - _funding_interest_rates: Dict = {} # TODO-lev: delete - - def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: - super().__init__(config, validate) - # TODO-lev: Uncomment once lev-exchange merged in - # if self.trading_mode == TradingMode.FUTURES: - # self._funding_interest_rates = self._get_funding_interest_rates() + # 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 @@ -218,38 +211,6 @@ class Binance(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e - def _get_mark_price(self, pair: str, date: datetime) -> float: - raise OperationalException(f'_get_mark_price has not been implemented on {self.name}') - - def _get_funding_rate(self, pair: str, premium_index: float) -> Optional[float]: - """ - Get's the funding_rate for a pair at a specific date and time in the past - """ - raise OperationalException(f'_get_mark_price has not been implemented on {self.name}') - - def _get_funding_fee( - self, - pair: str, - contract_size: float, - mark_price: float, - funding_rate: Optional[float], - ) -> float: - """ - Calculates a single funding fee - :param contract_size: The amount/quanity - :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: 0.03% daily, BNBUSDT, LINKUSDT, and LTCUSDT are 0% - - premium: varies by price difference between the perpetual contract and mark price - """ - if mark_price is None: - raise OperationalException("Mark price cannot be None for Binance._get_funding_fee") - nominal_value = mark_price * contract_size - if funding_rate is None: - raise OperationalException( - "Funding rate should never be none on Binance._get_funding_fee") - return nominal_value * funding_rate - async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int, is_new_pair: bool ) -> List: diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 8a0d6c863..70ed6f184 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1649,17 +1649,19 @@ class Exchange: self, pair: str, contract_size: float, + funding_rate: float, mark_price: float, - funding_rate: Optional[float] ) -> float: """ Calculates a single funding fee :param contract_size: The amount/quanity :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 """ - raise OperationalException(f"Funding fee has not been implemented for {self.name}") + nominal_value = mark_price * contract_size + return nominal_value * funding_rate @retrier def _set_leverage( diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index dcbe848b7..5072d653e 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,6 +1,5 @@ """ FTX exchange subclass """ import logging -from datetime import datetime from typing import Any, Dict, List, Optional, Tuple import ccxt @@ -184,25 +183,3 @@ class Ftx(Exchange): :nominal_value: Here for super method, not used on FTX """ return 20.0 - - def _get_funding_rate(self, pair: str, when: datetime) -> Optional[float]: - """FTX doesn't use this""" - return None - - def _get_funding_fee( - self, - pair: str, - contract_size: float, - mark_price: float, - premium_index: Optional[float], - # index_price: float, - # interest_rate: float) - ) -> float: - """ - Calculates a single funding fee - Always paid in USD on FTX # TODO: How do we account for this - : param contract_size: The amount/quanity - : param mark_price: The price of the asset that the contract is based off of - : param funding_rate: Must be None on ftx - """ - return (contract_size * mark_price) / 24 diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index dc08a2025..0c3e86fdd 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -342,14 +342,6 @@ def test__set_leverage_binance(mocker, default_conf): ) -def test_get_funding_rate(): - return - - -def test__get_funding_fee(): - return - - @pytest.mark.asyncio async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog): ohlcv = [ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a9b899276..d29698aa5 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3292,3 +3292,11 @@ def test_get_funding_fee_dates(): def test_calculate_funding_fees(): return + + +def test__get_funding_rate(default_conf, mocker): + return + + +def test__get_funding_fee(): + return diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 966a63a74..ca6b24d64 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -1,4 +1,3 @@ -from datetime import datetime, timedelta from random import randint from unittest.mock import MagicMock @@ -268,18 +267,3 @@ def test_fill_leverage_brackets_ftx(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id="ftx") exchange.fill_leverage_brackets() assert exchange._leverage_brackets == {} - - -@pytest.mark.parametrize("pair,when", [ - ('XRP/USDT', datetime.utcnow()), - ('ADA/BTC', datetime.utcnow()), - ('XRP/USDT', datetime.utcnow() - timedelta(hours=30)), -]) -def test__get_funding_rate(default_conf, mocker, pair, when): - api_mock = MagicMock() - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="ftx") - assert exchange._get_funding_rate(pair, when) is None - - -def test__get_funding_fee(): - return From 2533d3b42064037523a23d041eae88d235ea5651 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 18 Oct 2021 01:37:42 -0600 Subject: [PATCH 05/53] Added get_funding_rate_history method to exchange --- freqtrade/exchange/exchange.py | 46 ++++++++++++++++++++++++++++++- freqtrade/exchange/gateio.py | 12 ++++++++ freqtrade/optimize/backtesting.py | 6 ++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 70ed6f184..2192005b5 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -75,6 +75,7 @@ class Exchange: # 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 + funding_rate_history: Dict = {} _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ # TradingMode.SPOT always supported and not required in this list @@ -1636,13 +1637,17 @@ class Exchange: raise OperationalException(e) from e def _get_mark_price(self, pair: str, date: datetime) -> float: + """ + Get's the mark price for a pair at a specific date and time in the past + """ + # TODO-lev: Can maybe use self._api.fetchFundingRate, or get the most recent candlestick raise OperationalException(f'_get_mark_price has not been implemented on {self.name}') def _get_funding_rate(self, pair: str, when: datetime): """ Get's the funding_rate for a pair at a specific date and time in the past """ - # TODO-lev: implement + # TODO-lev: Maybe use self._api.fetchFundingRate or fetchFundingRateHistory with length 1 raise OperationalException(f"get_funding_rate has not been implemented for {self.name}") def _get_funding_fee( @@ -1749,6 +1754,45 @@ class Exchange: return fees + def get_funding_rate_history( + self, + start: int, + end: int + ) -> Dict: + ''' + :param start: 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"CCXT has not implemented fetchFundingRateHistory for {self.name}; " + f"therefore, backtesting for {self.name} is currently unavailable" + ) + + try: + funding_history: Dict = {} + for pair, market in self.markets.items(): + if market['swap']: + response = self._api.fetch_funding_rate_history( + pair, + limit=1000, + since=start, + params={ + 'endTime': end + } + ) + funding_history[pair] = {} + for fund in response: + funding_history[pair][fund['timestamp']] = fund['funding_rate'] + 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) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 33006d4a5..f025ed4dd 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -33,3 +33,15 @@ class Gateio(Exchange): if any(v == 'market' for k, v in order_types.items()): raise OperationalException( f'Exchange {self.name} does not support market orders.') + + def get_funding_rate_history( + self, + start: int, + end: int + ) -> Dict: + ''' + :param start: timestamp in ms of the beginning time + :param end: timestamp in ms of the end time + ''' + # TODO-lev: Has a max limit into the past of 333 days + return super().get_funding_rate_history(start, end) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index aaf875a94..a5f63c396 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -3,6 +3,7 @@ """ This module contains the backtesting logic """ +import ccxt import logging from collections import defaultdict from copy import deepcopy @@ -125,6 +126,11 @@ class Backtesting: self.progress = BTProgress() self.abort = False + + self.funding_rate_history = getattr(ccxt, self._exchange_name).load_funding_rate_history( + self.timerange.startts, + self.timerange.stopts + ) self.init_backtest() def __del__(self): From 3eda9455b98395aaee83c1c9cb1009963d585f96 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 22 Oct 2021 09:35:50 -0600 Subject: [PATCH 06/53] Added dry run capability to funding-fee --- freqtrade/exchange/exchange.py | 86 +++++++++++++++++++------------ freqtrade/exchange/ftx.py | 27 ++++++++++ freqtrade/exchange/gateio.py | 7 +-- freqtrade/freqtradebot.py | 15 ++++-- freqtrade/optimize/backtesting.py | 5 -- 5 files changed, 95 insertions(+), 45 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2192005b5..72049cc3a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1636,23 +1636,8 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def _get_mark_price(self, pair: str, date: datetime) -> float: - """ - Get's the mark price for a pair at a specific date and time in the past - """ - # TODO-lev: Can maybe use self._api.fetchFundingRate, or get the most recent candlestick - raise OperationalException(f'_get_mark_price has not been implemented on {self.name}') - - def _get_funding_rate(self, pair: str, when: datetime): - """ - Get's the funding_rate for a pair at a specific date and time in the past - """ - # TODO-lev: Maybe use self._api.fetchFundingRate or fetchFundingRateHistory with length 1 - raise OperationalException(f"get_funding_rate has not been implemented for {self.name}") - def _get_funding_fee( self, - pair: str, contract_size: float, funding_rate: float, mark_price: float, @@ -1726,12 +1711,39 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + def _get_mark_price_history( + self, + pair: str, + start: int, + end: Optional[int] + ) -> Dict: + """ + Get's the mark price history for a pair + """ + if end: + params = { + 'endTime': end + } + else: + params = {} + + candles = self._api.fetch_mark_ohlcv( + pair, + timeframe="1h", + since=start, + params=params + ) + history = {} + for candle in candles: + history[candle[0]] = candle[1] + return history + def calculate_funding_fees( self, pair: str, amount: float, open_date: datetime, - close_date: datetime + close_date: Optional[datetime] ) -> float: """ calculates the sum of all funding fees that occurred for a pair during a futures trade @@ -1742,11 +1754,22 @@ class Exchange: """ fees: float = 0 + if close_date: + close_date_timestamp: Optional[int] = int(close_date.timestamp()) + funding_rate_history = self.get_funding_rate_history( + pair, + int(open_date.timestamp()), + close_date_timestamp + ) + mark_price_history = self._get_mark_price_history( + pair, + int(open_date.timestamp()), + close_date_timestamp + ) for date in self._get_funding_fee_dates(open_date, close_date): - funding_rate = self._get_funding_rate(pair, date) - mark_price = self._get_mark_price(pair, date) + funding_rate = funding_rate_history[date.timestamp] + mark_price = mark_price_history[date.timestamp] fees += self._get_funding_fee( - pair=pair, contract_size=amount, mark_price=mark_price, funding_rate=funding_rate @@ -1756,10 +1779,12 @@ class Exchange: def get_funding_rate_history( self, + pair: str, start: int, - end: int + end: Optional[int] = None ) -> Dict: ''' + :param pair: quote/base currency pair :param start: timestamp in ms of the beginning time :param end: timestamp in ms of the end time ''' @@ -1771,19 +1796,14 @@ class Exchange: try: funding_history: Dict = {} - for pair, market in self.markets.items(): - if market['swap']: - response = self._api.fetch_funding_rate_history( - pair, - limit=1000, - since=start, - params={ - 'endTime': end - } - ) - funding_history[pair] = {} - for fund in response: - funding_history[pair][fund['timestamp']] = fund['funding_rate'] + response = self._api.fetch_funding_rate_history( + pair, + limit=1000, + start=start, + end=end + ) + for fund in response: + funding_history[fund['timestamp']] = fund['fundingRate'] return funding_history except ccxt.DDoSProtection as e: raise DDosProtection(e) from e diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 5072d653e..c668add2f 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -183,3 +183,30 @@ class Ftx(Exchange): :nominal_value: Here for super method, not used on FTX """ return 20.0 + + def _get_mark_price_history( + self, + pair: str, + start: int, + end: Optional[int] + ) -> Dict: + """ + Get's the mark price history for a pair + """ + if end: + params = { + 'endTime': end + } + else: + params = {} + + candles = self._api.fetch_index_ohlcv( + pair, + timeframe="1h", + since=start, + params=params + ) + history = {} + for candle in candles: + history[candle[0]] = candle[1] + return history diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index f025ed4dd..3c488a0a0 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -1,6 +1,6 @@ """ Gate.io exchange subclass """ import logging -from typing import Dict, List +from typing import Dict, List, Optional from freqtrade.exceptions import OperationalException from freqtrade.exchange import Exchange @@ -36,12 +36,13 @@ class Gateio(Exchange): def get_funding_rate_history( self, + pair: str, start: int, - end: int + end: Optional[int] = None ) -> Dict: ''' :param start: timestamp in ms of the beginning time :param end: timestamp in ms of the end time ''' # TODO-lev: Has a max limit into the past of 333 days - return super().get_funding_rate_history(start, end) + return super().get_funding_rate_history(pair, start, end) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index bb7e06e8a..cfac786c0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -269,10 +269,17 @@ 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( - trade.pair, - trade.open_date - ) + if self.config['dry_run']: + funding_fees = self.exchange.calculate_funding_fees( + trade.pair, + trade.amount, + trade.open_date + ) + else: + funding_fees = self.exchange.get_funding_fees_from_exchange( + trade.pair, + trade.open_date + ) trade.funding_fees = funding_fees def startup_update_open_orders(self): diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index a5f63c396..24a3e744a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -3,7 +3,6 @@ """ This module contains the backtesting logic """ -import ccxt import logging from collections import defaultdict from copy import deepcopy @@ -127,10 +126,6 @@ class Backtesting: self.progress = BTProgress() self.abort = False - self.funding_rate_history = getattr(ccxt, self._exchange_name).load_funding_rate_history( - self.timerange.startts, - self.timerange.stopts - ) self.init_backtest() def __del__(self): From d99e0dac7b56f43c6539430a3989264dd7744048 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 23 Oct 2021 01:13:59 -0600 Subject: [PATCH 07/53] Added name for futures market property --- freqtrade/exchange/binance.py | 1 + freqtrade/exchange/bybit.py | 1 + freqtrade/exchange/exchange.py | 1 + 3 files changed, 3 insertions(+) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index d23f84e7b..3aee67039 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -37,6 +37,7 @@ class Binance(Exchange): # (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported ] + name_for_futures_market = 'future' @property def _ccxt_config(self) -> Dict: diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index df19a671b..8cd37fbbc 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -30,3 +30,4 @@ class Bybit(Exchange): # (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported ] + name_for_futures_market = 'linear' diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 72049cc3a..0f7d6c07b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -80,6 +80,7 @@ class Exchange: _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ # TradingMode.SPOT always supported and not required in this list ] + name_for_futures_market = 'swap' def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: """ From 60478cb2135a2411f6b5bbbaeb9632a808f3a387 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 23 Oct 2021 22:01:44 -0600 Subject: [PATCH 08/53] Add fill_leverage_brackets and get_max_leverage back in --- freqtrade/exchange/exchange.py | 31 +++++++++++++++++++++++++++---- freqtrade/exchange/ftx.py | 15 --------------- freqtrade/optimize/backtesting.py | 1 - freqtrade/persistence/models.py | 14 -------------- tests/exchange/test_ftx.py | 17 ----------------- 5 files changed, 27 insertions(+), 51 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 0f7d6c07b..e29ef9df0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -75,7 +75,6 @@ class Exchange: # 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 - funding_rate_history: Dict = {} _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ # TradingMode.SPOT always supported and not required in this list @@ -160,9 +159,6 @@ class Exchange: self._api_async = self._init_ccxt( exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) - if self.trading_mode != TradingMode.SPOT: - self.fill_leverage_brackets() - logger.info('Using Exchange "%s"', self.name) if validate: @@ -185,6 +181,9 @@ class Exchange: self.markets_refresh_interval: int = exchange_config.get( "markets_refresh_interval", 60) * 60 + if self.trading_mode != TradingMode.SPOT: + self.fill_leverage_brackets() + def __del__(self): """ Destructor - clean up async stuff @@ -1637,6 +1636,30 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + def fill_leverage_brackets(self): + """ + Assigns property _leverage_brackets to a dictionary of information about the leverage + allowed on each pair + Not used if the exchange has a static max leverage value for the account or each pair + """ + return + + def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: + """ + Returns the maximum leverage that a pair can be traded at + :param pair: The base/quote currency pair being traded + :nominal_value: The total value of the trade in quote currency (collateral + debt) + """ + market = self.markets[pair] + if ( + 'limits' in market and + 'leverage' in market['limits'] and + 'max' in market['limits']['leverage'] + ): + return market['limits']['leverage']['max'] + else: + return 1.0 + def _get_funding_fee( self, contract_size: float, diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index c668add2f..e78c43872 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -169,21 +169,6 @@ class Ftx(Exchange): return safe_value_fallback2(order, order, 'id_stop', 'id') return order['id'] - def fill_leverage_brackets(self): - """ - FTX leverage is static across the account, and doesn't change from pair to pair, - so _leverage_brackets doesn't need to be set - """ - return - - def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: - """ - Returns the maximum leverage that a pair can be traded at, which is always 20 on ftx - :param pair: Here for super method, not used on FTX - :nominal_value: Here for super method, not used on FTX - """ - return 20.0 - def _get_mark_price_history( self, pair: str, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 24a3e744a..aaf875a94 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -125,7 +125,6 @@ class Backtesting: self.progress = BTProgress() self.abort = False - self.init_backtest() def __del__(self): diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 623dd74d3..5496628f4 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -707,7 +707,6 @@ class LocalTrade(): return float(self._calc_base_close(amount, rate, fee) - total_interest) elif (trading_mode == TradingMode.FUTURES): - self.add_funding_fees() funding_fees = self.funding_fees or 0.0 if self.is_short: return float(self._calc_base_close(amount, rate, fee)) - funding_fees @@ -789,19 +788,6 @@ class LocalTrade(): else: return None - def add_funding_fees(self): - if self.trading_mode == TradingMode.FUTURES: - # TODO-lev: Calculate this correctly and add it - # if self.config['runmode'].value in ('backtest', 'hyperopt'): - # self.funding_fees = getattr(Exchange, self.exchange).calculate_funding_fees( - # self.exchange, - # self.pair, - # self.amount, - # self.open_date_utc, - # self.close_date_utc - # ) - return - @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index ca6b24d64..97093bdcb 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -250,20 +250,3 @@ def test_get_order_id(mocker, default_conf): } } assert exchange.get_order_id_conditional(order) == '1111' - - -@pytest.mark.parametrize('pair,nominal_value,max_lev', [ - ("ADA/BTC", 0.0, 20.0), - ("BTC/EUR", 100.0, 20.0), - ("ZEC/USD", 173.31, 20.0), -]) -def test_get_max_leverage_ftx(default_conf, mocker, pair, nominal_value, max_lev): - exchange = get_patched_exchange(mocker, default_conf, id="ftx") - assert exchange.get_max_leverage(pair, nominal_value) == max_lev - - -def test_fill_leverage_brackets_ftx(default_conf, mocker): - # FTX only has one account wide leverage, so there's no leverage brackets - exchange = get_patched_exchange(mocker, default_conf, id="ftx") - exchange.fill_leverage_brackets() - assert exchange._leverage_brackets == {} From 956352f041aaf402a0933c97ba50fcee398b06ab Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 28 Oct 2021 07:19:46 -0600 Subject: [PATCH 09/53] Removed name_for_futures_market --- freqtrade/exchange/binance.py | 1 - freqtrade/exchange/bybit.py | 1 - freqtrade/exchange/exchange.py | 1 - 3 files changed, 3 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 3aee67039..d23f84e7b 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -37,7 +37,6 @@ class Binance(Exchange): # (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported ] - name_for_futures_market = 'future' @property def _ccxt_config(self) -> Dict: diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 8cd37fbbc..df19a671b 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -30,4 +30,3 @@ class Bybit(Exchange): # (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported ] - name_for_futures_market = 'linear' diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e29ef9df0..ac5abff01 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -79,7 +79,6 @@ class Exchange: _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ # TradingMode.SPOT always supported and not required in this list ] - name_for_futures_market = 'swap' def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: """ From 44d9a07acd8637478b359ad6f14fa1b1165f3715 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 28 Oct 2021 07:20:45 -0600 Subject: [PATCH 10/53] Fixed _get_funding_fee_dates method --- freqtrade/exchange/exchange.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ac5abff01..a0261dc83 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1702,7 +1702,8 @@ class Exchange: raise OperationalException(e) from e def _get_funding_fee_dates(self, d1, d2): - d1 = datetime(d1.year, d1.month, d1.day, d1.hour) + d1_hours = d1.hour + 1 if d1.minute > 0 or (d1.minute == 0 and d1.second > 15) else d1.hour + d1 = datetime(d1.year, d1.month, d1.day, d1_hours) d2 = datetime(d2.year, d2.month, d2.day, d2.hour) results = [] From 0b12107ef819be40686da17ebfc510baf77dff34 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 28 Oct 2021 07:22:47 -0600 Subject: [PATCH 11/53] Updated error message in fetchFundingRateHistory --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a0261dc83..a9be5bb14 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1815,7 +1815,7 @@ class Exchange: if not self.exchange_has("fetchFundingRateHistory"): raise ExchangeError( f"CCXT has not implemented fetchFundingRateHistory for {self.name}; " - f"therefore, backtesting for {self.name} is currently unavailable" + f"therefore, dry-run/backtesting for {self.name} is currently unavailable" ) try: From 02ab3b1697b8567eeab7fc6d07d9e3bd659b7778 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 28 Oct 2021 07:26:36 -0600 Subject: [PATCH 12/53] Switched mark_price endTime to until --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a9be5bb14..373d10269 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1746,7 +1746,7 @@ class Exchange: """ if end: params = { - 'endTime': end + 'until': end } else: params = {} From a4892654da036cd1bf194b70c08704809837c7e2 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 29 Oct 2021 19:37:02 -0600 Subject: [PATCH 13/53] Removed params from _get_mark_price_history --- freqtrade/exchange/exchange.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 373d10269..3069aea61 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1738,24 +1738,16 @@ class Exchange: def _get_mark_price_history( self, pair: str, - start: int, - end: Optional[int] + start: int ) -> Dict: """ Get's the mark price history for a pair """ - if end: - params = { - 'until': end - } - else: - params = {} candles = self._api.fetch_mark_ohlcv( pair, timeframe="1h", - since=start, - params=params + since=start ) history = {} for candle in candles: From 0ea8957cccc97a975960abe20ad089ffa56a3bc2 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 29 Oct 2021 20:07:24 -0600 Subject: [PATCH 14/53] removed ftx get_mark_price_history, added variable mark_ohlcv_price, used fetch_ohlcv instead of fetch_mark_ohlcv inside get_mark_price_history --- freqtrade/exchange/exchange.py | 40 ++++++++++++++++++++++++---------- freqtrade/exchange/ftx.py | 30 ++----------------------- 2 files changed, 31 insertions(+), 39 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3069aea61..ed21e57b9 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -80,6 +80,8 @@ class Exchange: # TradingMode.SPOT always supported and not required in this list ] + mark_ohlcv_price = 'mark' + def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: """ Initializes this module with the given config, @@ -1744,15 +1746,32 @@ class Exchange: Get's the mark price history for a pair """ - candles = self._api.fetch_mark_ohlcv( - pair, - timeframe="1h", - since=start - ) - history = {} - for candle in candles: - history[candle[0]] = candle[1] - return history + try: + candles = self._api.fetch_ohlcv( + pair, + timeframe="1h", + since=start, + params={ + 'price': self.mark_ohlcv_price + } + ) + history = {} + for candle in candles: + history[candle[0]] = candle[1] + 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, @@ -1779,8 +1798,7 @@ class Exchange: ) mark_price_history = self._get_mark_price_history( pair, - int(open_date.timestamp()), - close_date_timestamp + int(open_date.timestamp()) ) for date in self._get_funding_fee_dates(open_date, close_date): funding_rate = funding_rate_history[date.timestamp] diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index e78c43872..14045e302 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,6 +1,6 @@ """ FTX exchange subclass """ import logging -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Tuple import ccxt @@ -28,6 +28,7 @@ class Ftx(Exchange): # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported # (TradingMode.FUTURES, Collateral.CROSS) # TODO-lev: Uncomment once supported ] + mark_ohlcv_price = 'index' def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ @@ -168,30 +169,3 @@ class Ftx(Exchange): if order['type'] == 'stop': return safe_value_fallback2(order, order, 'id_stop', 'id') return order['id'] - - def _get_mark_price_history( - self, - pair: str, - start: int, - end: Optional[int] - ) -> Dict: - """ - Get's the mark price history for a pair - """ - if end: - params = { - 'endTime': end - } - else: - params = {} - - candles = self._api.fetch_index_ohlcv( - pair, - timeframe="1h", - since=start, - params=params - ) - history = {} - for candle in candles: - history[candle[0]] = candle[1] - return history From 2bfc812618d0e50cee8599038d010835edad410f Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 31 Oct 2021 00:53:36 -0600 Subject: [PATCH 15/53] moved mark_ohlcv_price in _ft_has --- freqtrade/exchange/exchange.py | 5 ++--- freqtrade/exchange/ftx.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ed21e57b9..5b9ebcbcd 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -69,6 +69,7 @@ class Exchange: "trades_pagination_arg": "since", "l2_limit_range": None, "l2_limit_range_required": True, # Allow Empty L2 limit (kucoin) + "mark_ohlcv_price": "mark" } _ft_has: Dict = {} @@ -80,8 +81,6 @@ class Exchange: # TradingMode.SPOT always supported and not required in this list ] - mark_ohlcv_price = 'mark' - def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: """ Initializes this module with the given config, @@ -1752,7 +1751,7 @@ class Exchange: timeframe="1h", since=start, params={ - 'price': self.mark_ohlcv_price + 'price': self._ft_has["mark_ohlcv_price"] } ) history = {} diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 14045e302..d84b3a5d4 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -20,6 +20,7 @@ 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)) @@ -28,7 +29,6 @@ class Ftx(Exchange): # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported # (TradingMode.FUTURES, Collateral.CROSS) # TODO-lev: Uncomment once supported ] - mark_ohlcv_price = 'index' def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ From f6924aca40cd0ed59d66e4a263dd24709cb7ba82 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 31 Oct 2021 01:24:02 -0600 Subject: [PATCH 16/53] removed get_funding_rate_history from gateio --- freqtrade/exchange/gateio.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 741df98d7..83abd1266 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -1,6 +1,6 @@ """ Gate.io exchange subclass """ import logging -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Tuple from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import OperationalException @@ -59,16 +59,3 @@ class Gateio(Exchange): if any(v == 'market' for k, v in order_types.items()): raise OperationalException( f'Exchange {self.name} does not support market orders.') - - def get_funding_rate_history( - self, - pair: str, - start: int, - end: Optional[int] = None - ) -> Dict: - ''' - :param start: timestamp in ms of the beginning time - :param end: timestamp in ms of the end time - ''' - # TODO-lev: Has a max limit into the past of 333 days - return super().get_funding_rate_history(pair, start, end) From 5c52b2134635229d3a80c3677be14286351edc18 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 31 Oct 2021 04:40:23 -0600 Subject: [PATCH 17/53] Added tests for funding_fee_dry_run --- freqtrade/exchange/exchange.py | 13 ++- tests/exchange/test_exchange.py | 162 +++++++++++++++++++++++++++++--- 2 files changed, 157 insertions(+), 18 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5b9ebcbcd..479a788a8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1739,7 +1739,7 @@ class Exchange: def _get_mark_price_history( self, pair: str, - start: int + since: int ) -> Dict: """ Get's the mark price history for a pair @@ -1749,7 +1749,7 @@ class Exchange: candles = self._api.fetch_ohlcv( pair, timeframe="1h", - since=start, + since=since, params={ 'price': self._ft_has["mark_ohlcv_price"] } @@ -1813,12 +1813,11 @@ class Exchange: def get_funding_rate_history( self, pair: str, - start: int, - end: Optional[int] = None + since: int, ) -> Dict: ''' :param pair: quote/base currency pair - :param start: timestamp in ms of the beginning time + :param since: timestamp in ms of the beginning time :param end: timestamp in ms of the end time ''' if not self.exchange_has("fetchFundingRateHistory"): @@ -1827,13 +1826,13 @@ class Exchange: f"therefore, dry-run/backtesting for {self.name} is currently unavailable" ) + # TODO-lev: Gateio has a max limit into the past of 333 days try: funding_history: Dict = {} response = self._api.fetch_funding_rate_history( pair, limit=1000, - start=start, - end=end + since=since ) for fund in response: funding_history[fund['timestamp']] = fund['fundingRate'] diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 1c863e4da..d1daf7a1c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3290,21 +3290,161 @@ def test_get_max_leverage(default_conf, mocker, pair, nominal_value, max_lev): assert exchange.get_max_leverage(pair, nominal_value) == max_lev -def test_get_mark_price(): +@pytest.mark.parametrize('contract_size,funding_rate,mark_price,funding_fee', [ + (10, 0.0001, 2.0, 0.002), + (10, 0.0002, 2.0, 0.004), + (10, 0.0002, 2.5, 0.005) +]) +def test__get_funding_fee( + default_conf, + mocker, + contract_size, + funding_rate, + mark_price, + funding_fee +): + exchange = get_patched_exchange(mocker, default_conf) + assert exchange._get_funding_fee(contract_size, funding_rate, mark_price) == funding_fee + + +@pytest.mark.parametrize('exchange,d1,d2', [ + ('binance', "2021-09-01 00:00:00", "2021-09-01 08:00:00"), + ('binance', "2021-09-01 00:00:15", "2021-09-01 08:00:00"), + ('binance', "2021-09-01 00:00:16", "2021-09-01 08:00:00"), + ('binance', "2021-09-01 00:00:00", "2021-09-01 07:59:45"), + ('binance', "2021-09-01 00:00:00", "2021-09-01 07:59:44"), + ('binance', "2021-09-01 00:00:00", "2021-09-01 12:00:00"), + ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00"), + ('kraken', "2021-09-01 00:00:15", "2021-09-01 08:00:00"), + ('kraken', "2021-09-01 00:00:16", "2021-09-01 08:00:00"), + ('kraken', "2021-09-01 00:00:00", "2021-09-01 07:59:45"), + ('kraken', "2021-09-01 00:00:00", "2021-09-01 07:59:44"), + ('kraken', "2021-09-01 00:00:00", "2021-09-01 12:00:00"), + ('ftx', "2021-09-01 00:00:00", "2021-09-01 08:00:00"), + ('ftx', "2021-09-01 00:00:15", "2021-09-01 08:00:00"), + ('ftx', "2021-09-01 00:00:16", "2021-09-01 08:00:00"), + ('ftx', "2021-09-01 00:00:00", "2021-09-01 07:59:45"), + ('ftx', "2021-09-01 00:00:00", "2021-09-01 07:59:44"), + ('ftx', "2021-09-01 00:00:00", "2021-09-01 12:00:00"), + ('gateio', "2021-09-01 00:00:00", "2021-09-01 08:00:00"), + ('gateio', "2021-09-01 00:00:15", "2021-09-01 08:00:00"), + ('gateio', "2021-09-01 00:00:16", "2021-09-01 08:00:00"), + ('gateio', "2021-09-01 00:00:00", "2021-09-01 07:59:45"), + ('gateio', "2021-09-01 00:00:00", "2021-09-01 07:59:44"), + ('gateio', "2021-09-01 00:00:00", "2021-09-01 12:00:00"), +]) +def test__get_funding_fee_dates(exchange, d1, d2): return -def test_get_funding_fee_dates(): - return +def test__get_mark_price_history(mocker, default_conf): + api_mock = MagicMock() + api_mock.fetch_ohlcv = MagicMock(return_value=[ + [ + 1635674520000, + 1.954, + 1.95435369, + 1.9524, + 1.95255532, + 0 + ], + [ + 1635674580000, + 1.95255532, + 1.95356934, + 1.9507, + 1.9507, + 0 + ], + [ + 1635674640000, + 1.9505, + 1.95240962, + 1.9502, + 1.9506914, + 0 + ], + [ + 1635674700000, + 1.95067489, + 1.95124984, + 1.94852208, + 1.9486, + 0 + ] + ]) + 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", 1635674520000) + assert mark_prices == { + 1635674520000: 1.954, + 1635674580000: 1.95255532, + 1635674640000: 1.9505, + 1635674700000: 1.95067489, + } + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "binance", + "_get_mark_price_history", + "fetch_ohlcv", + pair="ADA/USDT", + since=1635674520000 + ) + + +def test_get_funding_rate_history(mocker, default_conf): + api_mock = MagicMock() + api_mock.fetch_funding_rate_history = MagicMock(return_value=[ + { + "symbol": "ADA/USDT", + "fundingRate": 0.00042396, + "timestamp": 1635580800001 + }, + { + "symbol": "ADA/USDT", + "fundingRate": 0.00036859, + "timestamp": 1635609600013 + }, + { + "symbol": "ADA/USDT", + "fundingRate": 0.0005205, + "timestamp": 1635638400008 + }, + { + "symbol": "ADA/USDT", + "fundingRate": 0.00068396, + "timestamp": 1635667200010 + } + ]) + 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 == { + 1635580800001: 0.00042396, + 1635609600013: 0.00036859, + 1635638400008: 0.0005205, + 1635667200010: 0.00068396, + } + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "binance", + "get_funding_rate_history", + "fetch_funding_rate_history", + pair="ADA/USDT", + since=1635580800001 + ) def test_calculate_funding_fees(): return - - -def test__get_funding_rate(default_conf, mocker): - return - - -def test__get_funding_fee(): - return From 77d247e1794133cf4730d52075d367529a751e8e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 1 Nov 2021 01:04:42 -0600 Subject: [PATCH 18/53] Created fixtures mark_ohlcv and funding_rate_history --- tests/conftest.py | 64 +++++++++++++++++++++++++++++++++ tests/exchange/test_exchange.py | 62 +++----------------------------- 2 files changed, 68 insertions(+), 58 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6d424c246..344ce5a80 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2365,3 +2365,67 @@ 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 [ + [ + 1635674520000, + 1.954, + 1.95435369, + 1.9524, + 1.95255532, + 0 + ], + [ + 1635674580000, + 1.95255532, + 1.95356934, + 1.9507, + 1.9507, + 0 + ], + [ + 1635674640000, + 1.9505, + 1.95240962, + 1.9502, + 1.9506914, + 0 + ], + [ + 1635674700000, + 1.95067489, + 1.95124984, + 1.94852208, + 1.9486, + 0 + ] + ] + + +@pytest.fixture(scope='function') +def funding_rate_history(): + return [ + { + "symbol": "ADA/USDT", + "fundingRate": 0.00042396, + "timestamp": 1635580800001 + }, + { + "symbol": "ADA/USDT", + "fundingRate": 0.00036859, + "timestamp": 1635609600013 + }, + { + "symbol": "ADA/USDT", + "fundingRate": 0.0005205, + "timestamp": 1635638400008 + }, + { + "symbol": "ADA/USDT", + "fundingRate": 0.00068396, + "timestamp": 1635667200010 + } + ] diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d1daf7a1c..eaf6960c4 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3337,42 +3337,9 @@ def test__get_funding_fee_dates(exchange, d1, d2): return -def test__get_mark_price_history(mocker, default_conf): +def test__get_mark_price_history(mocker, default_conf, mark_ohlcv): api_mock = MagicMock() - api_mock.fetch_ohlcv = MagicMock(return_value=[ - [ - 1635674520000, - 1.954, - 1.95435369, - 1.9524, - 1.95255532, - 0 - ], - [ - 1635674580000, - 1.95255532, - 1.95356934, - 1.9507, - 1.9507, - 0 - ], - [ - 1635674640000, - 1.9505, - 1.95240962, - 1.9502, - 1.9506914, - 0 - ], - [ - 1635674700000, - 1.95067489, - 1.95124984, - 1.94852208, - 1.9486, - 0 - ] - ]) + 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) @@ -3397,30 +3364,9 @@ def test__get_mark_price_history(mocker, default_conf): ) -def test_get_funding_rate_history(mocker, default_conf): +def test_get_funding_rate_history(mocker, default_conf, funding_rate_history): api_mock = MagicMock() - api_mock.fetch_funding_rate_history = MagicMock(return_value=[ - { - "symbol": "ADA/USDT", - "fundingRate": 0.00042396, - "timestamp": 1635580800001 - }, - { - "symbol": "ADA/USDT", - "fundingRate": 0.00036859, - "timestamp": 1635609600013 - }, - { - "symbol": "ADA/USDT", - "fundingRate": 0.0005205, - "timestamp": 1635638400008 - }, - { - "symbol": "ADA/USDT", - "fundingRate": 0.00068396, - "timestamp": 1635667200010 - } - ]) + api_mock.fetch_funding_rate_history = MagicMock(return_value=funding_rate_history) type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True}) # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) From edfc3377c54ff9f732fdaa13b59fc37cac5b611d Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 1 Nov 2021 01:09:11 -0600 Subject: [PATCH 19/53] Updated exchange._get_funding_fee_dates to use new method funding_fee_cutoff --- freqtrade/exchange/binance.py | 9 +++++++++ freqtrade/exchange/exchange.py | 15 +++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index d23f84e7b..cc317b759 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -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 @@ -227,3 +228,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, d: datetime): + ''' + # TODO-lev: Double check that gateio, ftx, and kraken don't also have this + :param d: The open date for a trade + :return: The cutoff open time for when a funding fee is charged + ''' + return d.minute > 0 or (d.minute == 0 and d.second > 15) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 479a788a8..3e82dd626 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1702,17 +1702,24 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def _get_funding_fee_dates(self, d1, d2): - d1_hours = d1.hour + 1 if d1.minute > 0 or (d1.minute == 0 and d1.second > 15) else d1.hour + def funding_fee_cutoff(self, d: datetime): + ''' + :param d: The open date for a trade + :return: The cutoff open time for when a funding fee is charged + ''' + return d.minute > 0 or d.second > 0 + + def _get_funding_fee_dates(self, d1: datetime, d2: datetime): + d1_hours = d1.hour + 1 if self.funding_fee_cutoff(d1) else d1.hour d1 = datetime(d1.year, d1.month, d1.day, d1_hours) d2 = datetime(d2.year, d2.month, d2.day, d2.hour) results = [] d3 = d1 - while d3 < d2: - d3 += timedelta(hours=1) + while d3 <= d2: if d3.hour in self.funding_fee_times: results.append(d3) + d3 += timedelta(hours=1) return results From 8b9dfafdf4195591c0571bb5c756c779dd28e188 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 1 Nov 2021 01:09:57 -0600 Subject: [PATCH 20/53] Tests for _get_funding_fee_dates --- tests/exchange/test_exchange.py | 135 +++++++++++++++++++++++++------- 1 file changed, 108 insertions(+), 27 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index eaf6960c4..b95064f5c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3307,34 +3307,115 @@ def test__get_funding_fee( assert exchange._get_funding_fee(contract_size, funding_rate, mark_price) == funding_fee -@pytest.mark.parametrize('exchange,d1,d2', [ - ('binance', "2021-09-01 00:00:00", "2021-09-01 08:00:00"), - ('binance', "2021-09-01 00:00:15", "2021-09-01 08:00:00"), - ('binance', "2021-09-01 00:00:16", "2021-09-01 08:00:00"), - ('binance', "2021-09-01 00:00:00", "2021-09-01 07:59:45"), - ('binance', "2021-09-01 00:00:00", "2021-09-01 07:59:44"), - ('binance', "2021-09-01 00:00:00", "2021-09-01 12:00:00"), - ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00"), - ('kraken', "2021-09-01 00:00:15", "2021-09-01 08:00:00"), - ('kraken', "2021-09-01 00:00:16", "2021-09-01 08:00:00"), - ('kraken', "2021-09-01 00:00:00", "2021-09-01 07:59:45"), - ('kraken', "2021-09-01 00:00:00", "2021-09-01 07:59:44"), - ('kraken', "2021-09-01 00:00:00", "2021-09-01 12:00:00"), - ('ftx', "2021-09-01 00:00:00", "2021-09-01 08:00:00"), - ('ftx', "2021-09-01 00:00:15", "2021-09-01 08:00:00"), - ('ftx', "2021-09-01 00:00:16", "2021-09-01 08:00:00"), - ('ftx', "2021-09-01 00:00:00", "2021-09-01 07:59:45"), - ('ftx', "2021-09-01 00:00:00", "2021-09-01 07:59:44"), - ('ftx', "2021-09-01 00:00:00", "2021-09-01 12:00:00"), - ('gateio', "2021-09-01 00:00:00", "2021-09-01 08:00:00"), - ('gateio', "2021-09-01 00:00:15", "2021-09-01 08:00:00"), - ('gateio', "2021-09-01 00:00:16", "2021-09-01 08:00:00"), - ('gateio', "2021-09-01 00:00:00", "2021-09-01 07:59:45"), - ('gateio', "2021-09-01 00:00:00", "2021-09-01 07:59:44"), - ('gateio', "2021-09-01 00:00:00", "2021-09-01 12:00:00"), +@pytest.mark.parametrize('exchange,d1,d2,funding_times', [ + ( + 'binance', + "2021-09-01 00:00:00", + "2021-09-01 08:00:00", + ["2021-09-01 00", "2021-09-01 08"] + ), + ('binance', "2021-09-01 00:00:15", "2021-09-01 08:00:00", ["2021-09-01 00", "2021-09-01 08"]), + ('binance', "2021-09-01 00:00:16", "2021-09-01 08:00:00", ["2021-09-01 08"]), + ('binance', "2021-09-01 00:00:00", "2021-09-01 07:59:59", ["2021-09-01 00"]), + ('binance', "2021-09-01 00:00:00", "2021-09-01 12:00:00", ["2021-09-01 00", "2021-09-01 08"]), + ( + 'binance', + "2021-09-01 00:00:01", + "2021-09-01 08:00:00", + ["2021-09-01 00", "2021-09-01 08"] + ), + ( + 'kraken', + "2021-09-01 00:00:00", + "2021-09-01 08:00:00", + ["2021-09-01 00", "2021-09-01 04", "2021-09-01 08"] + ), + ( + 'kraken', + "2021-09-01 00:00:15", + "2021-09-01 08:00:00", + ["2021-09-01 04", "2021-09-01 08"] + ), + ( + 'kraken', + "2021-09-01 00:00:00", + "2021-09-01 07:59:59", + ["2021-09-01 00", "2021-09-01 04"] + ), + ( + 'kraken', + "2021-09-01 00:00:00", + "2021-09-01 12:00:00", + ["2021-09-01 00", "2021-09-01 04", "2021-09-01 08", "2021-09-01 12"] + ), + ( + 'kraken', + "2021-09-01 00:00:01", + "2021-09-01 08:00:00", + ["2021-09-01 04", "2021-09-01 08"] + ), + ( + 'ftx', + "2021-09-01 00:00:00", + "2021-09-01 08:00:00", + [ + "2021-09-01 00", + "2021-09-01 01", + "2021-09-01 02", + "2021-09-01 03", + "2021-09-01 04", + "2021-09-01 05", + "2021-09-01 06", + "2021-09-01 07", + "2021-09-01 08" + ] + ), + ( + 'ftx', + "2021-09-01 00:00:00", + "2021-09-01 12:00:00", + [ + "2021-09-01 00", + "2021-09-01 01", + "2021-09-01 02", + "2021-09-01 03", + "2021-09-01 04", + "2021-09-01 05", + "2021-09-01 06", + "2021-09-01 07", + "2021-09-01 08", + "2021-09-01 09", + "2021-09-01 10", + "2021-09-01 11", + "2021-09-01 12" + ] + ), + ( + 'ftx', + "2021-09-01 00:00:01", + "2021-09-01 08:00:00", + [ + "2021-09-01 01", + "2021-09-01 02", + "2021-09-01 03", + "2021-09-01 04", + "2021-09-01 05", + "2021-09-01 06", + "2021-09-01 07", + "2021-09-01 08" + ] + ), + ('gateio', "2021-09-01 00:00:00", "2021-09-01 08:00:00", ["2021-09-01 00", "2021-09-01 08"]), + ('gateio', "2021-09-01 00:00:00", "2021-09-01 12:00:00", ["2021-09-01 00", "2021-09-01 08"]), + ('gateio', "2021-09-01 00:00:01", "2021-09-01 08:00:00", ["2021-09-01 08"]), ]) -def test__get_funding_fee_dates(exchange, d1, d2): - return +def test__get_funding_fee_dates(mocker, default_conf, exchange, d1, d2, funding_times): + expected_result = [datetime.strptime(d, '%Y-%m-%d %H') for d in funding_times] + d1 = datetime.strptime(d1, '%Y-%m-%d %H:%M:%S') + d2 = datetime.strptime(d2, '%Y-%m-%d %H:%M:%S') + exchange = get_patched_exchange(mocker, default_conf, id=exchange) + result = exchange._get_funding_fee_dates(d1, d2) + assert result == expected_result def test__get_mark_price_history(mocker, default_conf, mark_ohlcv): From 33b0778c0a480998992bd73e7f4922ccf2631ae3 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 1 Nov 2021 01:13:37 -0600 Subject: [PATCH 21/53] updated exchange.calculate_funding_fees to have default close_date --- freqtrade/exchange/exchange.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3e82dd626..24ab26eb3 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1795,12 +1795,11 @@ class Exchange: """ fees: float = 0 - if close_date: - close_date_timestamp: Optional[int] = int(close_date.timestamp()) + if not close_date: + close_date = datetime.now(timezone.utc) funding_rate_history = self.get_funding_rate_history( pair, - int(open_date.timestamp()), - close_date_timestamp + int(open_date.timestamp()) ) mark_price_history = self._get_mark_price_history( pair, From 765ee5af5028606aca245b42650dbee8de3053f0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 1 Nov 2021 02:51:59 -0600 Subject: [PATCH 22/53] Updated conftest funding_rate and mark_price --- tests/conftest.py | 202 +++++++++++++++++++++++++++----- tests/exchange/test_exchange.py | 42 +++++-- 2 files changed, 203 insertions(+), 41 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 344ce5a80..071b6132f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2371,37 +2371,115 @@ def limit_order_open(limit_buy_order_usdt_open, limit_sell_order_usdt_open): def mark_ohlcv(): return [ [ - 1635674520000, - 1.954, - 1.95435369, - 1.9524, - 1.95255532, + 1630454400000, + 2.770211435326142, + 2.7760202570103396, + 2.7347342529855143, + 2.7357522788430635, 0 ], [ - 1635674580000, - 1.95255532, - 1.95356934, - 1.9507, - 1.9507, + 1630458000000, + 2.735269545167237, + 2.7651119207106896, + 2.7248808874275636, + 2.7492972616764053, 0 ], [ - 1635674640000, - 1.9505, - 1.95240962, - 1.9502, - 1.9506914, + 1630461600000, + 2.7491481048915243, + 2.7671609375432853, + 2.745229551784277, + 2.760245773504276, 0 ], [ - 1635674700000, - 1.95067489, - 1.95124984, - 1.94852208, - 1.9486, + 1630465200000, + 2.760401812866193, + 2.761749613398891, + 2.742224897842422, + 2.761749613398891, 0 - ] + ], + [ + 1630468800000, + 2.7620775456230717, + 2.775325047797592, + 2.755971115233453, + 2.77160966718816, + 0 + ], + [ + 1630472400000, + 2.7728718875620535, + 2.7955600146848196, + 2.7592691116925816, + 2.787961168625268 + ], + [ + 1630476000000, + 2.788924005374514, + 2.80182349539391, + 2.774329229105576, + 2.7775662803443466, + 0 + ], + [ + 1630479600000, + 2.7813766192350453, + 2.798346488192056, + 2.77645121073195, + 2.7799615628667596, + 0 + ], + [ + 1630483200000, + 2.779641041095253, + 2.7925407904097304, + 2.7759817614742652, + 2.780262741297638, + 0 + ], + [ + 1630486800000, + 2.77978981220767, + 2.8464871136756833, + 2.7757262968052983, + 2.846220775920381, + 0 + ], + [ + 1630490400000, + 2.846414592861413, + 2.8518148465268256, + 2.8155014025617695, + 2.817651577376391 + ], + [ + 1630494000000, + 2.8180253150511034, + 2.8343230172207017, + 2.8101780247041037, + 2.817772761324752, + 0 + ], + [ + 1630497600000, + 2.8179208712533828, + 2.849455604187112, + 2.8133565804933927, + 2.8276620505921377, + 0 + ], + [ + 1630501200000, + 2.829210740051151, + 2.833768886983365, + 2.811042782941919, + 2.81926481267932, + 0 + ], ] @@ -2410,22 +2488,86 @@ def funding_rate_history(): return [ { "symbol": "ADA/USDT", - "fundingRate": 0.00042396, - "timestamp": 1635580800001 + "fundingRate": -0.000008, + "timestamp": 1630454400000, + "datetime": "2021-09-01T00:00:00.000Z" }, { "symbol": "ADA/USDT", - "fundingRate": 0.00036859, - "timestamp": 1635609600013 + "fundingRate": -0.000004, + "timestamp": 1630458000000, + "datetime": "2021-09-01T01:00:00.000Z" }, { "symbol": "ADA/USDT", - "fundingRate": 0.0005205, - "timestamp": 1635638400008 + "fundingRate": 0.000012, + "timestamp": 1630461600000, + "datetime": "2021-09-01T02:00:00.000Z" }, { "symbol": "ADA/USDT", - "fundingRate": 0.00068396, - "timestamp": 1635667200010 - } + "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, + "timestamp": 1630483200000, + "datetime": "2021-09-01T08:00:00.000Z" + }, + { + "symbol": "ADA/USDT", + "fundingRate": -0.000003, + "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" + }, ] diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index b95064f5c..2944205d7 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3425,12 +3425,22 @@ def test__get_mark_price_history(mocker, default_conf, mark_ohlcv): # 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", 1635674520000) + mark_prices = exchange._get_mark_price_history("ADA/USDT", 1630454400000) assert mark_prices == { - 1635674520000: 1.954, - 1635674580000: 1.95255532, - 1635674640000: 1.9505, - 1635674700000: 1.95067489, + 1630454400000: 2.770211435326142, + 1630458000000: 2.735269545167237, + 1630461600000: 2.7491481048915243, + 1630465200000: 2.760401812866193, + 1630468800000: 2.7620775456230717, + 1630472400000: 2.7728718875620535, + 1630476000000: 2.788924005374514, + 1630479600000: 2.7813766192350453, + 1630483200000: 2.779641041095253, + 1630486800000: 2.77978981220767, + 1630490400000: 2.846414592861413, + 1630494000000: 2.8180253150511034, + 1630497600000: 2.8179208712533828, + 1630501200000: 2.829210740051151, } ccxt_exceptionhandlers( @@ -3441,7 +3451,7 @@ def test__get_mark_price_history(mocker, default_conf, mark_ohlcv): "_get_mark_price_history", "fetch_ohlcv", pair="ADA/USDT", - since=1635674520000 + since=1635580800001 ) @@ -3455,10 +3465,20 @@ def test_get_funding_rate_history(mocker, default_conf, funding_rate_history): funding_rates = exchange.get_funding_rate_history('ADA/USDT', 1635580800001) assert funding_rates == { - 1635580800001: 0.00042396, - 1635609600013: 0.00036859, - 1635638400008: 0.0005205, - 1635667200010: 0.00068396, + 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, + 1630486800000: -0.000003, + 1630490400000: 0.000013, + 1630494000000: 0.000077, + 1630497600000: 0.000072, + 1630501200000: 0.000097, } ccxt_exceptionhandlers( @@ -3469,7 +3489,7 @@ def test_get_funding_rate_history(mocker, default_conf, funding_rate_history): "get_funding_rate_history", "fetch_funding_rate_history", pair="ADA/USDT", - since=1635580800001 + since=1630454400000 ) From ba95172d0758f5b6245cede50c3e6b02e59220af Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 1 Nov 2021 06:28:03 -0600 Subject: [PATCH 23/53] Finished test_calculate_funding_fees --- freqtrade/exchange/exchange.py | 12 +-- tests/conftest.py | 128 ++++--------------------------- tests/exchange/test_exchange.py | 129 ++++++++++++++++++++++++++++---- 3 files changed, 135 insertions(+), 134 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 24ab26eb3..c046a83d8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1711,8 +1711,8 @@ class Exchange: def _get_funding_fee_dates(self, d1: datetime, d2: datetime): d1_hours = d1.hour + 1 if self.funding_fee_cutoff(d1) else d1.hour - d1 = datetime(d1.year, d1.month, d1.day, d1_hours) - d2 = datetime(d2.year, d2.month, d2.day, d2.hour) + d1 = datetime(d1.year, d1.month, d1.day, d1_hours, tzinfo=timezone.utc) + d2 = datetime(d2.year, d2.month, d2.day, d2.hour, tzinfo=timezone.utc) results = [] d3 = d1 @@ -1799,15 +1799,15 @@ class Exchange: close_date = datetime.now(timezone.utc) funding_rate_history = self.get_funding_rate_history( pair, - int(open_date.timestamp()) + int(open_date.timestamp() * 1000) ) mark_price_history = self._get_mark_price_history( pair, - int(open_date.timestamp()) + int(open_date.timestamp() * 1000) ) for date in self._get_funding_fee_dates(open_date, close_date): - funding_rate = funding_rate_history[date.timestamp] - mark_price = mark_price_history[date.timestamp] + funding_rate = funding_rate_history[int(date.timestamp()) * 1000] + mark_price = mark_price_history[int(date.timestamp()) * 1000] fees += self._get_funding_fee( contract_size=amount, mark_price=mark_price, diff --git a/tests/conftest.py b/tests/conftest.py index 071b6132f..fbbeee9bf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2370,116 +2370,20 @@ def limit_order_open(limit_buy_order_usdt_open, limit_sell_order_usdt_open): @pytest.fixture(scope='function') def mark_ohlcv(): return [ - [ - 1630454400000, - 2.770211435326142, - 2.7760202570103396, - 2.7347342529855143, - 2.7357522788430635, - 0 - ], - [ - 1630458000000, - 2.735269545167237, - 2.7651119207106896, - 2.7248808874275636, - 2.7492972616764053, - 0 - ], - [ - 1630461600000, - 2.7491481048915243, - 2.7671609375432853, - 2.745229551784277, - 2.760245773504276, - 0 - ], - [ - 1630465200000, - 2.760401812866193, - 2.761749613398891, - 2.742224897842422, - 2.761749613398891, - 0 - ], - [ - 1630468800000, - 2.7620775456230717, - 2.775325047797592, - 2.755971115233453, - 2.77160966718816, - 0 - ], - [ - 1630472400000, - 2.7728718875620535, - 2.7955600146848196, - 2.7592691116925816, - 2.787961168625268 - ], - [ - 1630476000000, - 2.788924005374514, - 2.80182349539391, - 2.774329229105576, - 2.7775662803443466, - 0 - ], - [ - 1630479600000, - 2.7813766192350453, - 2.798346488192056, - 2.77645121073195, - 2.7799615628667596, - 0 - ], - [ - 1630483200000, - 2.779641041095253, - 2.7925407904097304, - 2.7759817614742652, - 2.780262741297638, - 0 - ], - [ - 1630486800000, - 2.77978981220767, - 2.8464871136756833, - 2.7757262968052983, - 2.846220775920381, - 0 - ], - [ - 1630490400000, - 2.846414592861413, - 2.8518148465268256, - 2.8155014025617695, - 2.817651577376391 - ], - [ - 1630494000000, - 2.8180253150511034, - 2.8343230172207017, - 2.8101780247041037, - 2.817772761324752, - 0 - ], - [ - 1630497600000, - 2.8179208712533828, - 2.849455604187112, - 2.8133565804933927, - 2.8276620505921377, - 0 - ], - [ - 1630501200000, - 2.829210740051151, - 2.833768886983365, - 2.811042782941919, - 2.81926481267932, - 0 - ], + [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], ] @@ -2536,13 +2440,13 @@ def funding_rate_history(): }, { "symbol": "ADA/USDT", - "fundingRate": 0, + "fundingRate": -0.000003, "timestamp": 1630483200000, "datetime": "2021-09-01T08:00:00.000Z" }, { "symbol": "ADA/USDT", - "fundingRate": -0.000003, + "fundingRate": 0, "timestamp": 1630486800000, "datetime": "2021-09-01T09:00:00.000Z" }, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 2944205d7..5defbc6d7 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3427,20 +3427,20 @@ def test__get_mark_price_history(mocker, default_conf, mark_ohlcv): exchange = get_patched_exchange(mocker, default_conf, api_mock) mark_prices = exchange._get_mark_price_history("ADA/USDT", 1630454400000) assert mark_prices == { - 1630454400000: 2.770211435326142, - 1630458000000: 2.735269545167237, - 1630461600000: 2.7491481048915243, - 1630465200000: 2.760401812866193, - 1630468800000: 2.7620775456230717, - 1630472400000: 2.7728718875620535, - 1630476000000: 2.788924005374514, - 1630479600000: 2.7813766192350453, - 1630483200000: 2.779641041095253, - 1630486800000: 2.77978981220767, - 1630490400000: 2.846414592861413, - 1630494000000: 2.8180253150511034, - 1630497600000: 2.8179208712533828, - 1630501200000: 2.829210740051151, + 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( @@ -3493,5 +3493,102 @@ def test_get_funding_rate_history(mocker, default_conf, funding_rate_history): ) -def test_calculate_funding_fees(): - return +@pytest.mark.parametrize('exchange,d1,d2,amount,expected_fees', [ + ('binance', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), + ('binance', "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), + ('binance', "2021-09-01 00:00:16", "2021-09-01 08:00:00", 30.0, -0.0002493), + ('binance', "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0006647999999999999), + ('binance', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999), + ('binance', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), + ('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 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', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, 0.0010008000000000003), + ('ftx', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0146691), + ('ftx', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, 0.0016656000000000002), + ('gateio', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), + ('gateio', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999), + ('gateio', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493), + ('binance', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0015235000000000001), + ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0024895), + ('ftx', "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, + mark_ohlcv, + exchange, + d1, + d2, + amount, + expected_fees +): + ''' + nominal_value = mark_price * contract_size + funding_fee = nominal_value * funding_rate + contract_size: 30 + time: 0, mark_price: 2.77, nominal_value: 83.1, fundingRate: -0.000008, fundingFee: -0.0006647999999999999 + time: 1, mark_price: 2.73, nominal_value: 81.9, fundingRate: -0.000004, fundingFee: -0.0003276 + time: 2, mark_price: 2.74, nominal_value: 82.2, fundingRate: 0.000012, fundingFee: 0.0009864000000000001 + time: 3, mark_price: 2.76, nominal_value: 82.8, fundingRate: -0.000003, fundingFee: -0.0002484 + time: 4, mark_price: 2.76, nominal_value: 82.8, fundingRate: -0.000007, fundingFee: -0.0005796 + time: 5, mark_price: 2.77, nominal_value: 83.1, fundingRate: 0.000003, fundingFee: 0.0002493 + time: 6, mark_price: 2.78, nominal_value: 83.39999999999999, fundingRate: 0.000019, fundingFee: 0.0015846 + time: 7, mark_price: 2.78, nominal_value: 83.39999999999999, fundingRate: 0.000003, fundingFee: 0.00025019999999999996 + time: 8, mark_price: 2.77, nominal_value: 83.1, fundingRate: -0.000003, fundingFee: -0.0002493 + time: 9, mark_price: 2.77, nominal_value: 83.1, fundingRate: 0, fundingFee: 0.0 + time: 10, mark_price: 2.84, nominal_value: 85.19999999999999, fundingRate: 0.000013, fundingFee: 0.0011075999999999998 + time: 11, mark_price: 2.81, nominal_value: 84.3, fundingRate: 0.000077, fundingFee: 0.0064911 + time: 12, mark_price: 2.81, nominal_value: 84.3, fundingRate: 0.000072, fundingFee: 0.0060696 + time: 13, mark_price: 2.82, nominal_value: 84.6, fundingRate: 0.000097, fundingFee: 0.008206199999999999 + + contract_size: 50 + time: 0, mark_price: 2.77, nominal_value: 138.5, fundingRate: -0.000008, fundingFee: -0.001108 + time: 1, mark_price: 2.73, nominal_value: 136.5, fundingRate: -0.000004, fundingFee: -0.0005459999999999999 + time: 2, mark_price: 2.74, nominal_value: 137.0, fundingRate: 0.000012, fundingFee: 0.001644 + time: 3, mark_price: 2.76, nominal_value: 138.0, fundingRate: -0.000003, fundingFee: -0.00041400000000000003 + time: 4, mark_price: 2.76, nominal_value: 138.0, fundingRate: -0.000007, fundingFee: -0.000966 + time: 5, mark_price: 2.77, nominal_value: 138.5, fundingRate: 0.000003, fundingFee: 0.0004155 + time: 6, mark_price: 2.78, nominal_value: 139.0, fundingRate: 0.000019, fundingFee: 0.002641 + time: 7, mark_price: 2.78, nominal_value: 139.0, fundingRate: 0.000003, fundingFee: 0.000417 + time: 8, mark_price: 2.77, nominal_value: 138.5, fundingRate: -0.000003, fundingFee: -0.0004155 + time: 9, mark_price: 2.77, nominal_value: 138.5, fundingRate: 0, fundingFee: 0.0 + time: 10, mark_price: 2.84, nominal_value: 142.0, fundingRate: 0.000013, fundingFee: 0.001846 + time: 11, mark_price: 2.81, nominal_value: 140.5, fundingRate: 0.000077, fundingFee: 0.0108185 + time: 12, mark_price: 2.81, nominal_value: 140.5, fundingRate: 0.000072, fundingFee: 0.010116 + time: 13, mark_price: 2.82, nominal_value: 141.0, fundingRate: 0.000097, fundingFee: 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') + 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 + + +def test_calculate_funding_fees_datetime_called( + mocker, + default_conf, + funding_rate_history, + mark_ohlcv +): + api_mock = MagicMock() + api_mock.fetch_ohlcv = MagicMock(return_value=mark_ohlcv) + api_mock.fetch_funding_rate_history = MagicMock(return_value=funding_rate_history) + datetime = MagicMock() + datetime.now = MagicMock() + type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) + type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True}) + + # TODO-lev: Add datetime MagicMock + exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange.calculate_funding_fees('ADA/USDT', 30.0, datetime("2021-09-01 00:00:00")) + assert datetime.now.call_count == 1 From 74b6335acf57d12422fd2c12e10ac0ff636ed608 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 1 Nov 2021 06:34:22 -0600 Subject: [PATCH 24/53] Adding timezone utc to test__get_funding_fee_dates --- tests/exchange/test_exchange.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5defbc6d7..5e2f533ce 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3410,9 +3410,9 @@ def test__get_funding_fee( ('gateio', "2021-09-01 00:00:01", "2021-09-01 08:00:00", ["2021-09-01 08"]), ]) def test__get_funding_fee_dates(mocker, default_conf, exchange, d1, d2, funding_times): - expected_result = [datetime.strptime(d, '%Y-%m-%d %H') for d in funding_times] - d1 = datetime.strptime(d1, '%Y-%m-%d %H:%M:%S') - d2 = datetime.strptime(d2, '%Y-%m-%d %H:%M:%S') + expected_result = [datetime.strptime(f"{d} +0000", '%Y-%m-%d %H %z') for d in funding_times] + 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') exchange = get_patched_exchange(mocker, default_conf, id=exchange) result = exchange._get_funding_fee_dates(d1, d2) assert result == expected_result @@ -3473,8 +3473,8 @@ def test_get_funding_rate_history(mocker, default_conf, funding_rate_history): 1630472400000: 0.000003, 1630476000000: 0.000019, 1630479600000: 0.000003, - 1630483200000: 0, - 1630486800000: -0.000003, + 1630483200000: -0.000003, + 1630486800000: 0, 1630490400000: 0.000013, 1630494000000: 0.000077, 1630497600000: 0.000072, From 863e0bf83730c964291ac506ce5c0dd831f4401e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 1 Nov 2021 06:40:20 -0600 Subject: [PATCH 25/53] Adding 1am tests to funding_fee_dates --- tests/exchange/test_exchange.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5e2f533ce..8352ed173 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3316,6 +3316,7 @@ def test__get_funding_fee( ), ('binance', "2021-09-01 00:00:15", "2021-09-01 08:00:00", ["2021-09-01 00", "2021-09-01 08"]), ('binance', "2021-09-01 00:00:16", "2021-09-01 08:00:00", ["2021-09-01 08"]), + ('binance', "2021-09-01 01:00:14", "2021-09-01 08:00:00", ["2021-09-01 08"]), ('binance', "2021-09-01 00:00:00", "2021-09-01 07:59:59", ["2021-09-01 00"]), ('binance', "2021-09-01 00:00:00", "2021-09-01 12:00:00", ["2021-09-01 00", "2021-09-01 08"]), ( @@ -3336,6 +3337,12 @@ def test__get_funding_fee( "2021-09-01 08:00:00", ["2021-09-01 04", "2021-09-01 08"] ), + ( + 'kraken', + "2021-09-01 01:00:14", + "2021-09-01 08:00:00", + ["2021-09-01 04", "2021-09-01 08"] + ), ( 'kraken', "2021-09-01 00:00:00", @@ -3497,11 +3504,13 @@ def test_get_funding_rate_history(mocker, default_conf, funding_rate_history): ('binance', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), ('binance', "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), ('binance', "2021-09-01 00:00:16", "2021-09-01 08:00:00", 30.0, -0.0002493), + ('binance', "2021-09-01 01:00:14", "2021-09-01 08:00:00", 30.0, -0.0002493), ('binance', "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0006647999999999999), ('binance', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999), ('binance', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), ('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), From 3de42da29a52f618730218bda1cb7159410bef54 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 1 Nov 2021 07:52:40 -0600 Subject: [PATCH 26/53] All funding fee test_exchange tests pass --- freqtrade/exchange/exchange.py | 4 +++- tests/exchange/test_exchange.py | 31 ++++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c046a83d8..7eda75450 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1743,6 +1743,7 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + @retrier def _get_mark_price_history( self, pair: str, @@ -1784,7 +1785,7 @@ class Exchange: pair: str, amount: float, open_date: datetime, - close_date: Optional[datetime] + close_date: Optional[datetime] = None ) -> float: """ calculates the sum of all funding fees that occurred for a pair during a futures trade @@ -1816,6 +1817,7 @@ class Exchange: return fees + @retrier def get_funding_rate_history( self, pair: str, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8352ed173..4fa429839 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3583,21 +3583,38 @@ def test_calculate_funding_fees( assert funding_fees == expected_fees +@pytest.mark.parametrize('name,expected_fees_8,expected_fees_10,expected_fees_12', [ + ('binance', -0.0009140999999999999, -0.0009140999999999999, -0.0009140999999999999), + ('kraken', -0.0014937, -0.0014937, 0.0045759), + ('ftx', 0.0010008000000000003, 0.0021084, 0.0146691), + ('gateio', -0.0009140999999999999, -0.0009140999999999999, -0.0009140999999999999), +]) def test_calculate_funding_fees_datetime_called( mocker, default_conf, funding_rate_history, - mark_ohlcv + mark_ohlcv, + name, + time_machine, + expected_fees_8, + expected_fees_10, + expected_fees_12 ): api_mock = MagicMock() api_mock.fetch_ohlcv = MagicMock(return_value=mark_ohlcv) api_mock.fetch_funding_rate_history = MagicMock(return_value=funding_rate_history) - datetime = MagicMock() - datetime.now = MagicMock() type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True}) - # TODO-lev: Add datetime MagicMock - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.calculate_funding_fees('ADA/USDT', 30.0, datetime("2021-09-01 00:00:00")) - assert datetime.now.call_count == 1 + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=name) + 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_8 + time_machine.move_to("2021-09-01 10:00:00 +00:00") + funding_fees = exchange.calculate_funding_fees('ADA/USDT', 30.0, d1) + assert funding_fees == expected_fees_10 + time_machine.move_to("2021-09-01 12:00:00 +00:00") + funding_fees = exchange.calculate_funding_fees('ADA/USDT', 30.0, d1) + assert funding_fees == expected_fees_12 From 8a4236198f3541460b37922da6ffcb9e317efaa0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 3 Nov 2021 22:52:37 -0600 Subject: [PATCH 27/53] Added test_update_funding_fees in freqtradebot, test currently fails --- freqtrade/exchange/exchange.py | 4 +- freqtrade/freqtradebot.py | 3 +- tests/exchange/test_exchange.py | 64 ++++++++++---------- tests/test_freqtradebot.py | 102 ++++++++++++++++++++++++++++++-- 4 files changed, 133 insertions(+), 40 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7eda75450..49468ce29 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1807,8 +1807,8 @@ class Exchange: int(open_date.timestamp() * 1000) ) for date in self._get_funding_fee_dates(open_date, close_date): - funding_rate = funding_rate_history[int(date.timestamp()) * 1000] - mark_price = mark_price_history[int(date.timestamp()) * 1000] + funding_rate = funding_rate_history[int(date.timestamp() * 1000)] + mark_price = mark_price_history[int(date.timestamp() * 1000)] fees += self._get_funding_fee( contract_size=amount, mark_price=mark_price, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index cfac786c0..a046f85b9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -268,7 +268,8 @@ class FreqtradeBot(LoggingMixin): def update_funding_fees(self): if self.trading_mode == TradingMode.FUTURES: - for trade in Trade.get_open_trades(): + trades = Trade.get_open_trades() + for trade in trades: if self.config['dry_run']: funding_fees = self.exchange.calculate_funding_fees( trade.pair, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4fa429839..44e99e551 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3536,39 +3536,39 @@ def test_calculate_funding_fees( expected_fees ): ''' - nominal_value = mark_price * contract_size - funding_fee = nominal_value * funding_rate - contract_size: 30 - time: 0, mark_price: 2.77, nominal_value: 83.1, fundingRate: -0.000008, fundingFee: -0.0006647999999999999 - time: 1, mark_price: 2.73, nominal_value: 81.9, fundingRate: -0.000004, fundingFee: -0.0003276 - time: 2, mark_price: 2.74, nominal_value: 82.2, fundingRate: 0.000012, fundingFee: 0.0009864000000000001 - time: 3, mark_price: 2.76, nominal_value: 82.8, fundingRate: -0.000003, fundingFee: -0.0002484 - time: 4, mark_price: 2.76, nominal_value: 82.8, fundingRate: -0.000007, fundingFee: -0.0005796 - time: 5, mark_price: 2.77, nominal_value: 83.1, fundingRate: 0.000003, fundingFee: 0.0002493 - time: 6, mark_price: 2.78, nominal_value: 83.39999999999999, fundingRate: 0.000019, fundingFee: 0.0015846 - time: 7, mark_price: 2.78, nominal_value: 83.39999999999999, fundingRate: 0.000003, fundingFee: 0.00025019999999999996 - time: 8, mark_price: 2.77, nominal_value: 83.1, fundingRate: -0.000003, fundingFee: -0.0002493 - time: 9, mark_price: 2.77, nominal_value: 83.1, fundingRate: 0, fundingFee: 0.0 - time: 10, mark_price: 2.84, nominal_value: 85.19999999999999, fundingRate: 0.000013, fundingFee: 0.0011075999999999998 - time: 11, mark_price: 2.81, nominal_value: 84.3, fundingRate: 0.000077, fundingFee: 0.0064911 - time: 12, mark_price: 2.81, nominal_value: 84.3, fundingRate: 0.000072, fundingFee: 0.0060696 - time: 13, mark_price: 2.82, nominal_value: 84.6, fundingRate: 0.000097, fundingFee: 0.008206199999999999 + nominal_value = mark_price * contract_size + funding_fee = nominal_value * funding_rate + contract_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 - contract_size: 50 - time: 0, mark_price: 2.77, nominal_value: 138.5, fundingRate: -0.000008, fundingFee: -0.001108 - time: 1, mark_price: 2.73, nominal_value: 136.5, fundingRate: -0.000004, fundingFee: -0.0005459999999999999 - time: 2, mark_price: 2.74, nominal_value: 137.0, fundingRate: 0.000012, fundingFee: 0.001644 - time: 3, mark_price: 2.76, nominal_value: 138.0, fundingRate: -0.000003, fundingFee: -0.00041400000000000003 - time: 4, mark_price: 2.76, nominal_value: 138.0, fundingRate: -0.000007, fundingFee: -0.000966 - time: 5, mark_price: 2.77, nominal_value: 138.5, fundingRate: 0.000003, fundingFee: 0.0004155 - time: 6, mark_price: 2.78, nominal_value: 139.0, fundingRate: 0.000019, fundingFee: 0.002641 - time: 7, mark_price: 2.78, nominal_value: 139.0, fundingRate: 0.000003, fundingFee: 0.000417 - time: 8, mark_price: 2.77, nominal_value: 138.5, fundingRate: -0.000003, fundingFee: -0.0004155 - time: 9, mark_price: 2.77, nominal_value: 138.5, fundingRate: 0, fundingFee: 0.0 - time: 10, mark_price: 2.84, nominal_value: 142.0, fundingRate: 0.000013, fundingFee: 0.001846 - time: 11, mark_price: 2.81, nominal_value: 140.5, fundingRate: 0.000077, fundingFee: 0.0108185 - time: 12, mark_price: 2.81, nominal_value: 140.5, fundingRate: 0.000072, fundingFee: 0.010116 - time: 13, mark_price: 2.82, nominal_value: 141.0, fundingRate: 0.000097, fundingFee: 0.013677 + contract_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') diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3d91d738b..9f05a6518 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -20,9 +20,9 @@ from freqtrade.persistence import Order, PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.strategy.interface import SellCheckTuple from freqtrade.worker import Worker -from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, - log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, - patch_wallet, patch_whitelist) +from tests.conftest import (create_mock_trades, create_mock_trades_usdt, get_patched_freqtradebot, + get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, + patch_get_signal, patch_wallet, patch_whitelist) from tests.conftest_trades import (MOCK_TRADE_COUNT, enter_side, exit_side, mock_order_1, mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell, mock_order_4, mock_order_5_stoploss, mock_order_6_sell) @@ -4682,8 +4682,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) @@ -4698,3 +4698,95 @@ 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 + + +def test_update_funding_fees(mocker, default_conf, time_machine, fee): + ''' + nominal_value = mark_price * contract_size + funding_fee = nominal_value * funding_rate + contract_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 + ''' + time_machine.move_to("2021-09-01 00:00:00") + + funding_rates = { + "LTC/BTC": { + 1630454400000: 0.00032583, + 1630483200000: 0.00024472, + }, + "ETH/BTC": { + 1630454400000: 0.0001, + 1630483200000: 0.0001, + }, + "ETC/BTC": { + 1630454400000: 0.00031077, + 1630483200000: 0.00022655, + }, + "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, + }, + "ETC/BTC": { + 1630454400000: 4.3, + 1630483200000: 4.1, + }, + "XRP/BTC": { + 1630454400000: 1.2, + 1630483200000: 1.2, + } + } + + mocker.patch( + 'freqtrade.exchange.Exchange._get_mark_price_history', + side_effect=[ + mark_prices["LTC/BTC"], + mark_prices["ETH/BTC"], + mark_prices["ETC/BTC"], + mark_prices["XRP/BTC"], + ] + ) + mocker.patch( + 'freqtrade.exchange.Exchange.get_funding_rate_history', + side_effect=[ + funding_rates["LTC/BTC"], + funding_rates["ETH/BTC"], + funding_rates["ETC/BTC"], + funding_rates["XRP/BTC"], + ] + ) + patch_RPCManager(mocker) + patch_exchange(mocker) + default_conf['trading_mode'] = 'futures' + default_conf['collateral'] = 'isolated' + default_conf['dry_run'] = True + freqtrade = get_patched_freqtradebot(mocker, default_conf) + create_mock_trades(fee, False) + time_machine.move_to("2021-09-01 08:00:00 +00:00") + freqtrade._schedule.run_pending() + + trades = Trade.get_open_trades() + for trade in trades: + assert trade.funding_fees == 123 * mark_prices[trade.pair] * funding_rates[trade.pair] + return From 98b475a00b4a0fcc78af21ce221a75be58f14c89 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 6 Nov 2021 10:23:46 +0200 Subject: [PATCH 28/53] Use lambdas instead of a static number of side-effects. --- tests/test_freqtradebot.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 9f05a6518..ab2626ee7 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4760,21 +4760,12 @@ def test_update_funding_fees(mocker, default_conf, time_machine, fee): mocker.patch( 'freqtrade.exchange.Exchange._get_mark_price_history', - side_effect=[ - mark_prices["LTC/BTC"], - mark_prices["ETH/BTC"], - mark_prices["ETC/BTC"], - mark_prices["XRP/BTC"], - ] + side_effect=lambda pair, since: mark_prices[pair] ) + mocker.patch( 'freqtrade.exchange.Exchange.get_funding_rate_history', - side_effect=[ - funding_rates["LTC/BTC"], - funding_rates["ETH/BTC"], - funding_rates["ETC/BTC"], - funding_rates["XRP/BTC"], - ] + side_effect=lambda pair, since: funding_rates[pair] ) patch_RPCManager(mocker) patch_exchange(mocker) From fd63fa7dda07ae2afa418524da88a7f57e3dfd3e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 6 Nov 2021 05:42:41 -0600 Subject: [PATCH 29/53] Updated test_update_funding_fees to compile fine but the assertion is incorrect --- tests/test_freqtradebot.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ab2626ee7..c021da231 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4779,5 +4779,9 @@ def test_update_funding_fees(mocker, default_conf, time_machine, fee): trades = Trade.get_open_trades() for trade in trades: - assert trade.funding_fees == 123 * mark_prices[trade.pair] * funding_rates[trade.pair] + assert trade.funding_fees == sum([ + 123 * + mark_prices[trade.pair][time] * + funding_rates[trade.pair][time] for time in mark_prices[trade.pair].keys() + ]) return From cb97c6f388524a58d8e73063e8a6677f368eb809 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 6 Nov 2021 05:56:58 -0600 Subject: [PATCH 30/53] Updated time to utc in test_update_funding_fees, some funding rate key errors because a timestamp is likely not in utc --- freqtrade/exchange/exchange.py | 2 ++ freqtrade/freqtradebot.py | 2 +- tests/test_freqtradebot.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 49468ce29..f50c024e9 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1711,6 +1711,8 @@ class Exchange: def _get_funding_fee_dates(self, d1: datetime, d2: datetime): d1_hours = d1.hour + 1 if self.funding_fee_cutoff(d1) else d1.hour + if d1_hours == 24: + d1_hours = 0 d1 = datetime(d1.year, d1.month, d1.day, d1_hours, tzinfo=timezone.utc) d2 = datetime(d2.year, d2.month, d2.day, d2.hour, tzinfo=timezone.utc) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a046f85b9..0cb99c7bf 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -275,7 +275,7 @@ class FreqtradeBot(LoggingMixin): trade.pair, trade.amount, trade.open_date - ) + ) + (trade.funding_fees or 0.0) else: funding_fees = self.exchange.get_funding_fees_from_exchange( trade.pair, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c021da231..b223c9097 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4718,7 +4718,7 @@ def test_update_funding_fees(mocker, default_conf, time_machine, fee): 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 ''' - time_machine.move_to("2021-09-01 00:00:00") + time_machine.move_to("2021-09-01 00:00:00 +00:00") funding_rates = { "LTC/BTC": { From 6e912c1053c5bb4146bbe2a1d3bd44cd024b0bd2 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 6 Nov 2021 17:12:48 -0600 Subject: [PATCH 31/53] Updated _get_funding_fee method names, added kraken._get_funding_fee --- freqtrade/exchange/exchange.py | 10 ++++++---- freqtrade/exchange/kraken.py | 22 ++++++++++++++++++++++ tests/exchange/test_exchange.py | 33 ++++++++++++++++++++++----------- tests/test_freqtradebot.py | 4 ++-- 4 files changed, 52 insertions(+), 17 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index f50c024e9..91218b560 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1662,19 +1662,21 @@ class Exchange: def _get_funding_fee( self, - contract_size: float, + size: float, funding_rate: float, mark_price: float, + time_in_ratio: Optional[float] = None ) -> float: """ Calculates a single funding fee - :param contract_size: The amount/quanity + :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 * contract_size + nominal_value = mark_price * size return nominal_value * funding_rate @retrier @@ -1812,7 +1814,7 @@ class Exchange: funding_rate = funding_rate_history[int(date.timestamp() * 1000)] mark_price = mark_price_history[int(date.timestamp() * 1000)] fees += self._get_funding_fee( - contract_size=amount, + size=amount, mark_price=mark_price, funding_rate=funding_rate ) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index d2cbcd347..22a2d5038 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -156,3 +156,25 @@ 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: + """ + 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 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 44e99e551..c2c7da291 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3290,21 +3290,32 @@ def test_get_max_leverage(default_conf, mocker, pair, nominal_value, max_lev): assert exchange.get_max_leverage(pair, nominal_value) == max_lev -@pytest.mark.parametrize('contract_size,funding_rate,mark_price,funding_fee', [ - (10, 0.0001, 2.0, 0.002), - (10, 0.0002, 2.0, 0.004), - (10, 0.0002, 2.5, 0.005) -]) +@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, - contract_size, + size, funding_rate, mark_price, - funding_fee + funding_fee, + kraken_fee, + time_in_ratio ): exchange = get_patched_exchange(mocker, default_conf) - assert exchange._get_funding_fee(contract_size, funding_rate, mark_price) == funding_fee + 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 @pytest.mark.parametrize('exchange,d1,d2,funding_times', [ @@ -3536,9 +3547,9 @@ def test_calculate_funding_fees( expected_fees ): ''' - nominal_value = mark_price * contract_size + nominal_value = mark_price * size funding_fee = nominal_value * funding_rate - contract_size: 30 + 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 @@ -3554,7 +3565,7 @@ def test_calculate_funding_fees( 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 - contract_size: 50 + 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 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index b223c9097..d36afef8b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4702,9 +4702,9 @@ def test_update_funding_fees_schedule(mocker, default_conf, trading_mode, calls, def test_update_funding_fees(mocker, default_conf, time_machine, fee): ''' - nominal_value = mark_price * contract_size + nominal_value = mark_price * size funding_fee = nominal_value * funding_rate - contract_size = 123 + 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 From f795288d90437de1f975fd8bdaebe3a0ce3812de Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 6 Nov 2021 20:48:03 -0600 Subject: [PATCH 32/53] Fixed timestamp/datetime issues for mark price, funding rate and _get_funding_fee_dates --- freqtrade/exchange/exchange.py | 23 +++++++++++++---------- tests/test_freqtradebot.py | 32 ++++++++++++++++---------------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 91218b560..ce1549000 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1712,10 +1712,9 @@ class Exchange: return d.minute > 0 or d.second > 0 def _get_funding_fee_dates(self, d1: datetime, d2: datetime): - d1_hours = d1.hour + 1 if self.funding_fee_cutoff(d1) else d1.hour - if d1_hours == 24: - d1_hours = 0 - d1 = datetime(d1.year, d1.month, d1.day, d1_hours, tzinfo=timezone.utc) + d1 = datetime(d1.year, d1.month, d1.day, d1.hour, tzinfo=timezone.utc) + if self.funding_fee_cutoff(d1): + d1 += timedelta(hours=1) d2 = datetime(d2.year, d2.month, d2.day, d2.hour, tzinfo=timezone.utc) results = [] @@ -1768,7 +1767,10 @@ class Exchange: ) history = {} for candle in candles: - history[candle[0]] = candle[1] + # TODO-lev: Round down to the nearest funding fee time, incase a timestamp ever has a delay of > 1s + seconds = int(candle[0] / 1000) # The millisecond timestamps can be delayed ~20ms + opening_mark_price = candle[1] + history[seconds] = opening_mark_price return history except ccxt.NotSupported as e: raise OperationalException( @@ -1804,15 +1806,16 @@ class Exchange: close_date = datetime.now(timezone.utc) funding_rate_history = self.get_funding_rate_history( pair, - int(open_date.timestamp() * 1000) + int(open_date.timestamp()) ) mark_price_history = self._get_mark_price_history( pair, - int(open_date.timestamp() * 1000) + int(open_date.timestamp()) ) - for date in self._get_funding_fee_dates(open_date, close_date): - funding_rate = funding_rate_history[int(date.timestamp() * 1000)] - mark_price = mark_price_history[int(date.timestamp() * 1000)] + funding_fee_dates = self._get_funding_fee_dates(open_date, close_date) + for date in funding_fee_dates: + funding_rate = funding_rate_history[int(date.timestamp())] + mark_price = mark_price_history[int(date.timestamp())] fees += self._get_funding_fee( size=amount, mark_price=mark_price, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d36afef8b..a7153598f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4722,39 +4722,39 @@ def test_update_funding_fees(mocker, default_conf, time_machine, fee): funding_rates = { "LTC/BTC": { - 1630454400000: 0.00032583, - 1630483200000: 0.00024472, + 1630454400: 0.00032583, + 1630483200: 0.00024472, }, "ETH/BTC": { - 1630454400000: 0.0001, - 1630483200000: 0.0001, + 1630454400: 0.0001, + 1630483200: 0.0001, }, "ETC/BTC": { - 1630454400000: 0.00031077, - 1630483200000: 0.00022655, + 1630454400: 0.00031077, + 1630483200: 0.00022655, }, "XRP/BTC": { - 1630454400000: 0.00049426, - 1630483200000: 0.00032715, + 1630454400: 0.00049426, + 1630483200: 0.00032715, } } mark_prices = { "LTC/BTC": { - 1630454400000: 3.3, - 1630483200000: 3.2, + 1630454400: 3.3, + 1630483200: 3.2, }, "ETH/BTC": { - 1630454400000: 2.4, - 1630483200000: 2.5, + 1630454400: 2.4, + 1630483200: 2.5, }, "ETC/BTC": { - 1630454400000: 4.3, - 1630483200000: 4.1, + 1630454400: 4.3, + 1630483200: 4.1, }, "XRP/BTC": { - 1630454400000: 1.2, - 1630483200000: 1.2, + 1630454400: 1.2, + 1630483200: 1.2, } } From 48b34c8fd06b2f5cc634e813ddaa9860a6210733 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 6 Nov 2021 21:03:18 -0600 Subject: [PATCH 33/53] Fixed issues with funding-fee being miscalculated on trade objects in freqtradebot --- freqtrade/freqtradebot.py | 2 +- tests/test_freqtradebot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0cb99c7bf..a046f85b9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -275,7 +275,7 @@ class FreqtradeBot(LoggingMixin): trade.pair, trade.amount, trade.open_date - ) + (trade.funding_fees or 0.0) + ) else: funding_fees = self.exchange.get_funding_fees_from_exchange( trade.pair, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a7153598f..970f26ccf 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4780,7 +4780,7 @@ def test_update_funding_fees(mocker, default_conf, time_machine, fee): trades = Trade.get_open_trades() for trade in trades: assert trade.funding_fees == sum([ - 123 * + trade.amount * mark_prices[trade.pair][time] * funding_rates[trade.pair][time] for time in mark_prices[trade.pair].keys() ]) From b88482b2e9f86dd1b3ff610b511ac210a165d654 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 6 Nov 2021 21:45:35 -0600 Subject: [PATCH 34/53] Fixed millisecond timestamp issue errors with funding fees --- freqtrade/exchange/exchange.py | 17 +++++++++-------- tests/exchange/test_exchange.py | 16 ++++++++-------- tests/test_freqtradebot.py | 32 ++++++++++++++++---------------- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ce1549000..04c3104ce 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1768,9 +1768,10 @@ class Exchange: history = {} for candle in candles: # TODO-lev: Round down to the nearest funding fee time, incase a timestamp ever has a delay of > 1s - seconds = int(candle[0] / 1000) # The millisecond timestamps can be delayed ~20ms + # The millisecond timestamps can be delayed ~20ms + milliseconds = int(candle[0] / 1000) * 1000 opening_mark_price = candle[1] - history[seconds] = opening_mark_price + history[milliseconds] = opening_mark_price return history except ccxt.NotSupported as e: raise OperationalException( @@ -1806,16 +1807,16 @@ class Exchange: close_date = datetime.now(timezone.utc) funding_rate_history = self.get_funding_rate_history( pair, - int(open_date.timestamp()) + int(open_date.timestamp()) * 1000 ) mark_price_history = self._get_mark_price_history( pair, - int(open_date.timestamp()) + int(open_date.timestamp()) * 1000 ) funding_fee_dates = self._get_funding_fee_dates(open_date, close_date) for date in funding_fee_dates: - funding_rate = funding_rate_history[int(date.timestamp())] - mark_price = mark_price_history[int(date.timestamp())] + funding_rate = funding_rate_history[int(date.timestamp()) * 1000] + mark_price = mark_price_history[int(date.timestamp()) * 1000] fees += self._get_funding_fee( size=amount, mark_price=mark_price, @@ -1841,7 +1842,7 @@ class Exchange: f"therefore, dry-run/backtesting for {self.name} is currently unavailable" ) - # TODO-lev: Gateio has a max limit into the past of 333 days + # 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( @@ -1850,7 +1851,7 @@ class Exchange: since=since ) for fund in response: - funding_history[fund['timestamp']] = fund['fundingRate'] + funding_history[int(fund['timestamp'] / 1000) * 1000] = fund['fundingRate'] return funding_history except ccxt.DDoSProtection as e: raise DDosProtection(e) from e diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index c2c7da291..26b9448cb 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3519,12 +3519,12 @@ def test_get_funding_rate_history(mocker, default_conf, funding_rate_history): ('binance', "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0006647999999999999), ('binance', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999), ('binance', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), - ('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), + # ('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', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, 0.0010008000000000003), ('ftx', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0146691), ('ftx', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, 0.0016656000000000002), @@ -3532,7 +3532,7 @@ def test_get_funding_rate_history(mocker, default_conf, funding_rate_history): ('gateio', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999), ('gateio', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493), ('binance', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0015235000000000001), - ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0024895), + # ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0024895), ('ftx', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, 0.0016680000000000002), ]) def test_calculate_funding_fees( @@ -3596,7 +3596,7 @@ def test_calculate_funding_fees( @pytest.mark.parametrize('name,expected_fees_8,expected_fees_10,expected_fees_12', [ ('binance', -0.0009140999999999999, -0.0009140999999999999, -0.0009140999999999999), - ('kraken', -0.0014937, -0.0014937, 0.0045759), + # ('kraken', -0.0014937, -0.0014937, 0.0045759), ('ftx', 0.0010008000000000003, 0.0021084, 0.0146691), ('gateio', -0.0009140999999999999, -0.0009140999999999999, -0.0009140999999999999), ]) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 970f26ccf..9a878dd6f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4722,39 +4722,39 @@ def test_update_funding_fees(mocker, default_conf, time_machine, fee): funding_rates = { "LTC/BTC": { - 1630454400: 0.00032583, - 1630483200: 0.00024472, + 1630454400000: 0.00032583, + 1630483200000: 0.00024472, }, "ETH/BTC": { - 1630454400: 0.0001, - 1630483200: 0.0001, + 1630454400000: 0.0001, + 1630483200000: 0.0001, }, "ETC/BTC": { - 1630454400: 0.00031077, - 1630483200: 0.00022655, + 1630454400000: 0.00031077, + 1630483200000: 0.00022655, }, "XRP/BTC": { - 1630454400: 0.00049426, - 1630483200: 0.00032715, + 1630454400000: 0.00049426, + 1630483200000: 0.00032715, } } mark_prices = { "LTC/BTC": { - 1630454400: 3.3, - 1630483200: 3.2, + 1630454400000: 3.3, + 1630483200000: 3.2, }, "ETH/BTC": { - 1630454400: 2.4, - 1630483200: 2.5, + 1630454400000: 2.4, + 1630483200000: 2.5, }, "ETC/BTC": { - 1630454400: 4.3, - 1630483200: 4.1, + 1630454400000: 4.3, + 1630483200000: 4.1, }, "XRP/BTC": { - 1630454400: 1.2, - 1630483200: 1.2, + 1630454400000: 1.2, + 1630483200000: 1.2, } } From 8bfcf4ee0987d317f0e88a58c769064ac6573bdf Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 6 Nov 2021 22:05:38 -0600 Subject: [PATCH 35/53] Fixed breaking exchange tests from _get_funding_fee_dates, and commented out kraken get_funding_fees tests --- freqtrade/exchange/exchange.py | 18 +++++++++++------- tests/exchange/test_exchange.py | 3 +++ tests/test_freqtradebot.py | 6 +++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 04c3104ce..5853ec761 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1712,9 +1712,9 @@ class Exchange: return d.minute > 0 or d.second > 0 def _get_funding_fee_dates(self, d1: datetime, d2: datetime): - d1 = datetime(d1.year, d1.month, d1.day, d1.hour, tzinfo=timezone.utc) if self.funding_fee_cutoff(d1): d1 += timedelta(hours=1) + d1 = datetime(d1.year, d1.month, d1.day, d1.hour, tzinfo=timezone.utc) d2 = datetime(d2.year, d2.month, d2.day, d2.hour, tzinfo=timezone.utc) results = [] @@ -1767,9 +1767,10 @@ class Exchange: ) history = {} for candle in candles: - # TODO-lev: Round down to the nearest funding fee time, incase a timestamp ever has a delay of > 1s - # The millisecond timestamps can be delayed ~20ms + # TODO: Round down to the nearest funding fee time, + # incase a timestamp ever has a delay of > 1s milliseconds = int(candle[0] / 1000) * 1000 + # The millisecond timestamps can be delayed ~20ms opening_mark_price = candle[1] history[milliseconds] = opening_mark_price return history @@ -1805,18 +1806,21 @@ class Exchange: 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, - int(open_date.timestamp()) * 1000 + open_timestamp ) mark_price_history = self._get_mark_price_history( pair, - int(open_date.timestamp()) * 1000 + open_timestamp ) funding_fee_dates = self._get_funding_fee_dates(open_date, close_date) for date in funding_fee_dates: - funding_rate = funding_rate_history[int(date.timestamp()) * 1000] - mark_price = mark_price_history[int(date.timestamp()) * 1000] + timestamp = int(date.timestamp()) * 1000 + funding_rate = funding_rate_history[timestamp] + mark_price = mark_price_history[timestamp] fees += self._get_funding_fee( size=amount, mark_price=mark_price, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 26b9448cb..00b2897e9 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3519,6 +3519,7 @@ def test_get_funding_rate_history(mocker, default_conf, funding_rate_history): ('binance', "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0006647999999999999), ('binance', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999), ('binance', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), + # TODO: Uncoment once calculate_funding_fees can pass 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), @@ -3532,6 +3533,7 @@ def test_get_funding_rate_history(mocker, default_conf, funding_rate_history): ('gateio', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999), ('gateio', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493), ('binance', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0015235000000000001), + # TODO: Uncoment once calculate_funding_fees can pass 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', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, 0.0016680000000000002), ]) @@ -3596,6 +3598,7 @@ def test_calculate_funding_fees( @pytest.mark.parametrize('name,expected_fees_8,expected_fees_10,expected_fees_12', [ ('binance', -0.0009140999999999999, -0.0009140999999999999, -0.0009140999999999999), + # TODO: Uncoment once calculate_funding_fees can pass time_in_ratio to exchange._get_funding_fee # ('kraken', -0.0014937, -0.0014937, 0.0045759), ('ftx', 0.0010008000000000003, 0.0021084, 0.0146691), ('gateio', -0.0009140999999999999, -0.0009140999999999999, -0.0009140999999999999), diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 9a878dd6f..e4ced7ec9 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -20,9 +20,9 @@ from freqtrade.persistence import Order, PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.strategy.interface import SellCheckTuple from freqtrade.worker import Worker -from tests.conftest import (create_mock_trades, create_mock_trades_usdt, get_patched_freqtradebot, - get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, - patch_get_signal, patch_wallet, patch_whitelist) +from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, + log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, + patch_wallet, patch_whitelist) from tests.conftest_trades import (MOCK_TRADE_COUNT, enter_side, exit_side, mock_order_1, mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell, mock_order_4, mock_order_5_stoploss, mock_order_6_sell) From 0c2501e11b91a0a467ab4763333064e5c02590e9 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 6 Nov 2021 22:31:38 -0600 Subject: [PATCH 36/53] Safer keys for funding_rate and mark_price dictionaries, based on rounding down the hour --- freqtrade/exchange/exchange.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5853ec761..ba712ee4b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1767,12 +1767,18 @@ class Exchange: ) history = {} for candle in candles: - # TODO: Round down to the nearest funding fee time, - # incase a timestamp ever has a delay of > 1s - milliseconds = int(candle[0] / 1000) * 1000 + 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[milliseconds] = opening_mark_price + history[time] = opening_mark_price return history except ccxt.NotSupported as e: raise OperationalException( @@ -1855,7 +1861,17 @@ class Exchange: since=since ) for fund in response: - funding_history[int(fund['timestamp'] / 1000) * 1000] = fund['fundingRate'] + 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 = datetime( + d.year, + d.month, + d.day, + d.hour, + tzinfo=timezone.utc + ).timestamp() * 1000 + funding_history[time] = fund['fundingRate'] return funding_history except ccxt.DDoSProtection as e: raise DDosProtection(e) from e From bea37e5ea37936514bce20632b9fc5b99b3b0de5 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 8 Nov 2021 01:50:50 -0600 Subject: [PATCH 37/53] moved dry run check for funding fees to exchange --- freqtrade/exchange/exchange.py | 45 ++++++++++++++++++++++++++------- freqtrade/freqtradebot.py | 18 +++++-------- tests/exchange/test_exchange.py | 26 +++++++++---------- 3 files changed, 55 insertions(+), 34 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 928429629..6f45b6f4c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1606,7 +1606,7 @@ 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 :param pair: (e.g. ADA/USDT) @@ -1794,7 +1794,7 @@ class Exchange: 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, pair: str, amount: float, @@ -1825,16 +1825,43 @@ class Exchange: funding_fee_dates = self._get_funding_fee_dates(open_date, close_date) for date in funding_fee_dates: timestamp = int(date.timestamp()) * 1000 - funding_rate = funding_rate_history[timestamp] - mark_price = mark_price_history[timestamp] - fees += self._get_funding_fee( - size=amount, - mark_price=mark_price, - funding_rate=funding_rate - ) + if timestamp in funding_rate_history: + funding_rate = funding_rate_history[timestamp] + else: + logger.warning( + f"Funding rate for {pair} at {date} not found in funding_rate_history" + f"Funding fee calculation may be incorrect" + ) + if timestamp in mark_price_history: + mark_price = mark_price_history[timestamp] + else: + logger.warning( + f"Mark price for {pair} at {date} not found in funding_rate_history" + f"Funding fee calculation may be incorrect" + ) + if funding_rate and mark_price: + fees += self._get_funding_fee( + size=amount, + mark_price=mark_price, + funding_rate=funding_rate + ) return fees + def get_funding_fees(self, pair: str, amount: float, open_date: datetime): + 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 + @retrier def get_funding_rate_history( self, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index cfd3ae3fd..defc02a6c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -270,17 +270,11 @@ class FreqtradeBot(LoggingMixin): if self.trading_mode == TradingMode.FUTURES: trades = Trade.get_open_trades() for trade in trades: - if self.config['dry_run']: - funding_fees = self.exchange.calculate_funding_fees( - trade.pair, - trade.amount, - trade.open_date - ) - else: - funding_fees = self.exchange.get_funding_fees_from_exchange( - trade.pair, - trade.open_date - ) + funding_fees = self.exchange.get_funding_fees( + trade.pair, + trade.amount, + trade.open_date + ) trade.funding_fees = funding_fees def startup_update_open_orders(self): @@ -712,7 +706,7 @@ class FreqtradeBot(LoggingMixin): 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) + funding_fees = self.exchange.get_funding_fees(pair, amount, open_date) else: funding_fees = 0.0 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 00b2897e9..611d09254 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -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 @@ -3519,7 +3519,7 @@ def test_get_funding_rate_history(mocker, default_conf, funding_rate_history): ('binance', "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0006647999999999999), ('binance', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999), ('binance', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), - # TODO: Uncoment once calculate_funding_fees can pass time_in_ratio to exchange._get_funding_fee + # 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), @@ -3533,11 +3533,11 @@ def test_get_funding_rate_history(mocker, default_conf, funding_rate_history): ('gateio', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999), ('gateio', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493), ('binance', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0015235000000000001), - # TODO: Uncoment once calculate_funding_fees can pass time_in_ratio to exchange._get_funding_fee + # 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', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, 0.0016680000000000002), ]) -def test_calculate_funding_fees( +def test__calculate_funding_fees( mocker, default_conf, funding_rate_history, @@ -3592,18 +3592,18 @@ 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._calculate_funding_fees('ADA/USDT', amount, d1, d2) assert funding_fees == expected_fees @pytest.mark.parametrize('name,expected_fees_8,expected_fees_10,expected_fees_12', [ ('binance', -0.0009140999999999999, -0.0009140999999999999, -0.0009140999999999999), - # TODO: Uncoment once calculate_funding_fees can pass time_in_ratio to exchange._get_funding_fee + # TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._get_funding_fee # ('kraken', -0.0014937, -0.0014937, 0.0045759), ('ftx', 0.0010008000000000003, 0.0021084, 0.0146691), ('gateio', -0.0009140999999999999, -0.0009140999999999999, -0.0009140999999999999), ]) -def test_calculate_funding_fees_datetime_called( +def test__calculate_funding_fees_datetime_called( mocker, default_conf, funding_rate_history, @@ -3624,11 +3624,11 @@ 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) + funding_fees = exchange._calculate_funding_fees('ADA/USDT', 30.0, d1) assert funding_fees == expected_fees_8 time_machine.move_to("2021-09-01 10:00:00 +00:00") - funding_fees = exchange.calculate_funding_fees('ADA/USDT', 30.0, d1) + funding_fees = exchange._calculate_funding_fees('ADA/USDT', 30.0, d1) assert funding_fees == expected_fees_10 time_machine.move_to("2021-09-01 12:00:00 +00:00") - funding_fees = exchange.calculate_funding_fees('ADA/USDT', 30.0, d1) + funding_fees = exchange._calculate_funding_fees('ADA/USDT', 30.0, d1) assert funding_fees == expected_fees_12 From 7122cb2fe9ebd41db27079caaacee309e371c600 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 8 Nov 2021 01:51:17 -0600 Subject: [PATCH 38/53] updated test_get_funding_fees to test for funding fees at beginning of trade also --- tests/test_freqtradebot.py | 60 ++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 8814f9122..d565879cd 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4715,7 +4715,8 @@ def test_update_funding_fees_schedule(mocker, default_conf, trading_mode, calls, assert freqtrade.update_funding_fees.call_count == calls -def test_update_funding_fees(mocker, default_conf, time_machine, fee): +@pytest.mark.parametrize('is_short', [True, False]) +def test_update_funding_fees(mocker, default_conf, time_machine, fee, is_short, limit_order_open): ''' nominal_value = mark_price * size funding_fee = nominal_value * funding_rate @@ -4733,8 +4734,19 @@ def test_update_funding_fees(mocker, default_conf, time_machine, fee): 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)] + 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 = { "LTC/BTC": { 1630454400000: 0.00032583, @@ -4744,10 +4756,6 @@ def test_update_funding_fees(mocker, default_conf, time_machine, fee): 1630454400000: 0.0001, 1630483200000: 0.0001, }, - "ETC/BTC": { - 1630454400000: 0.00031077, - 1630483200000: 0.00022655, - }, "XRP/BTC": { 1630454400000: 0.00049426, 1630483200000: 0.00032715, @@ -4763,10 +4771,6 @@ def test_update_funding_fees(mocker, default_conf, time_machine, fee): 1630454400000: 2.4, 1630483200000: 2.5, }, - "ETC/BTC": { - 1630454400000: 4.3, - 1630483200000: 4.1, - }, "XRP/BTC": { 1630454400000: 1.2, 1630483200000: 1.2, @@ -4782,17 +4786,41 @@ def test_update_funding_fees(mocker, default_conf, time_machine, fee): 'freqtrade.exchange.Exchange.get_funding_rate_history', side_effect=lambda pair, since: funding_rates[pair] ) - patch_RPCManager(mocker) - patch_exchange(mocker) - default_conf['trading_mode'] = 'futures' - default_conf['collateral'] = 'isolated' - default_conf['dry_run'] = True + + 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) - create_mock_trades(fee, False) + + # 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[trade.pair][1630454400000] + ) + + # create_mock_trades(fee, False) time_machine.move_to("2021-09-01 08:00:00 +00:00") freqtrade._schedule.run_pending() - trades = Trade.get_open_trades() + # Funding fees for 00:00 and 08:00 for trade in trades: assert trade.funding_fees == sum([ trade.amount * From 01229ad63128a99998dca62da350a8187d40291b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 8 Nov 2021 01:51:52 -0600 Subject: [PATCH 39/53] updated exchange.get_funding_fee_dates with better names --- freqtrade/exchange/exchange.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 6f45b6f4c..249cc38e8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1711,18 +1711,18 @@ class Exchange: ''' return d.minute > 0 or d.second > 0 - def _get_funding_fee_dates(self, d1: datetime, d2: datetime): - if self.funding_fee_cutoff(d1): - d1 += timedelta(hours=1) - d1 = datetime(d1.year, d1.month, d1.day, d1.hour, tzinfo=timezone.utc) - d2 = datetime(d2.year, d2.month, d2.day, d2.hour, tzinfo=timezone.utc) + def _get_funding_fee_dates(self, start: datetime, end: datetime): + if self.funding_fee_cutoff(start): + start += timedelta(hours=1) + start = datetime(start.year, start.month, start.day, start.hour, tzinfo=timezone.utc) + end = datetime(end.year, end.month, end.day, end.hour, tzinfo=timezone.utc) results = [] - d3 = d1 - while d3 <= d2: - if d3.hour in self.funding_fee_times: - results.append(d3) - d3 += timedelta(hours=1) + iterator = start + while iterator <= end: + if iterator.hour in self.funding_fee_times: + results.append(iterator) + iterator += timedelta(hours=1) return results From 090b3d29b7c15092cf742b1c6c15ae06ea3812d0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 8 Nov 2021 01:52:28 -0600 Subject: [PATCH 40/53] Updated kraken._get_funding_fee docstring with notification that it won't work in the bot yet --- freqtrade/exchange/kraken.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 22a2d5038..8ab29ee90 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -165,6 +165,10 @@ class Kraken(Exchange): 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 From 6c8501dadc2cad357ff0395e8ba4fc8a588d8981 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 9 Nov 2021 01:00:57 -0600 Subject: [PATCH 41/53] Removed docstring indents --- freqtrade/exchange/binance.py | 6 ++-- freqtrade/exchange/exchange.py | 64 +++++++++++++++++----------------- freqtrade/exchange/kraken.py | 22 ++++++------ tests/test_freqtradebot.py | 30 ++++++++-------- 4 files changed, 61 insertions(+), 61 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index cc317b759..e3662235e 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -231,8 +231,8 @@ class Binance(Exchange): def funding_fee_cutoff(self, d: datetime): ''' - # TODO-lev: Double check that gateio, ftx, and kraken don't also have this - :param d: The open date for a trade - :return: The cutoff open time for when a funding fee is charged + # TODO-lev: Double check that gateio, ftx, and kraken don't also have this + :param d: The open date for a trade + :return: The cutoff open time for when a funding fee is charged ''' return d.minute > 0 or (d.minute == 0 and d.second > 15) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 249cc38e8..cacbe4f7e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1608,10 +1608,10 @@ class Exchange: @retrier 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 - :param pair: (e.g. ADA/USDT) - :param since: The earliest time of consideration for calculating funding fees, - in unix time or as a datetime + Returns the sum of all funding fees that were exchanged for a pair within a timeframe + :param pair: (e.g. ADA/USDT) + :param since: The earliest time of consideration for calculating funding fees, + in unix time or as a datetime """ # TODO-lev: Add dry-run handling for this. @@ -1638,17 +1638,17 @@ class Exchange: def fill_leverage_brackets(self): """ - Assigns property _leverage_brackets to a dictionary of information about the leverage - allowed on each pair - Not used if the exchange has a static max leverage value for the account or each pair + Assigns property _leverage_brackets to a dictionary of information about the leverage + allowed on each pair + Not used if the exchange has a static max leverage value for the account or each pair """ return def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: """ - Returns the maximum leverage that a pair can be traded at - :param pair: The base/quote currency pair being traded - :nominal_value: The total value of the trade in quote currency (collateral + debt) + Returns the maximum leverage that a pair can be traded at + :param pair: The base/quote currency pair being traded + :nominal_value: The total value of the trade in quote currency (collateral + debt) """ market = self.markets[pair] if ( @@ -1668,13 +1668,13 @@ class Exchange: 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 + 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 @@ -1687,8 +1687,8 @@ class Exchange: trading_mode: Optional[TradingMode] = None ): """ - Set's the leverage before making a trade, in order to not - have the same leverage on every trade + Set's the leverage before making a trade, in order to not + have the same leverage on every trade """ if self._config['dry_run'] or not self.exchange_has("setLeverage"): # Some exchanges only support one collateral type @@ -1706,8 +1706,8 @@ class Exchange: def funding_fee_cutoff(self, d: datetime): ''' - :param d: The open date for a trade - :return: The cutoff open time for when a funding fee is charged + :param d: The open date for a trade + :return: The cutoff open time for when a funding fee is charged ''' return d.minute > 0 or d.second > 0 @@ -1729,8 +1729,8 @@ class Exchange: @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 pair: base/quote currency pair (e.g. "ADA/USDT") + Set's the margin mode on the exchange to cross or isolated for a specific pair + :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 @@ -1753,7 +1753,7 @@ class Exchange: since: int ) -> Dict: """ - Get's the mark price history for a pair + Get's the mark price history for a pair """ try: @@ -1802,11 +1802,11 @@ class Exchange: close_date: Optional[datetime] = None ) -> float: """ - calculates the sum of all funding fees that occurred for a pair during a futures trade - :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 + calculates the sum of all funding fees that occurred for a pair during a futures trade + :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 """ fees: float = 0 @@ -1869,9 +1869,9 @@ class Exchange: 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 + :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( diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 8ab29ee90..0e9642ec7 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -165,17 +165,17 @@ class Kraken(Exchange): 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 + # ! 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( diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d565879cd..cbf4becbe 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4718,21 +4718,21 @@ def test_update_funding_fees_schedule(mocker, default_conf, trading_mode, calls, @pytest.mark.parametrize('is_short', [True, False]) def test_update_funding_fees(mocker, default_conf, time_machine, fee, is_short, limit_order_open): ''' - 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 + 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") From fbe9e73c5d34c61635ec2ba80c41ad1f60c4dd87 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 9 Nov 2021 01:17:29 -0600 Subject: [PATCH 42/53] better param for funding_fee_cutoff --- freqtrade/exchange/binance.py | 6 +++--- freqtrade/exchange/exchange.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index e3662235e..0bb051272 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -229,10 +229,10 @@ class Binance(Exchange): 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, d: datetime): + def funding_fee_cutoff(self, open_date: datetime): ''' # TODO-lev: Double check that gateio, ftx, and kraken don't also have this - :param d: The open date for a trade + :param open_date: The open date for a trade :return: The cutoff open time for when a funding fee is charged ''' - return d.minute > 0 or (d.minute == 0 and d.second > 15) + return open_date.minute > 0 or (open_date.minute == 0 and open_date.second > 15) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index cacbe4f7e..582e0d003 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1704,12 +1704,12 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def funding_fee_cutoff(self, d: datetime): + def funding_fee_cutoff(self, open_date: datetime): ''' - :param d: The open date for a trade + :param open_date: The open date for a trade :return: The cutoff open time for when a funding fee is charged ''' - return d.minute > 0 or d.second > 0 + return open_date.minute > 0 or open_date.second > 0 def _get_funding_fee_dates(self, start: datetime, end: datetime): if self.funding_fee_cutoff(start): From 4a67b33cb38dec7b50b5070ede4cb167d94cd924 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Nov 2021 19:40:42 +0100 Subject: [PATCH 43/53] Fix some formatting --- freqtrade/exchange/binance.py | 4 ++-- freqtrade/exchange/exchange.py | 32 ++++++++++++++------------------ 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 59ca2a54f..9ebe84517 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -230,9 +230,9 @@ class Binance(Exchange): 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) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index f98f47433..60f678cd7 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1705,10 +1705,10 @@ class Exchange: 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 def _get_funding_fee_dates(self, start: datetime, end: datetime): @@ -1849,30 +1849,26 @@ class Exchange: return fees def get_funding_fees(self, pair: str, amount: float, open_date: datetime): + """ + 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._config['dry_run']: - funding_fees = self._calculate_funding_fees( - pair, - amount, - open_date - ) + funding_fees = self._calculate_funding_fees(pair, amount, open_date) else: - funding_fees = self._get_funding_fees_from_exchange( - pair, - open_date - ) + funding_fees = self._get_funding_fees_from_exchange(pair, open_date) return funding_fees @retrier - def get_funding_rate_history( - self, - pair: str, - since: int, - ) -> Dict: - ''' + 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"CCXT has not implemented fetchFundingRateHistory for {self.name}; " From 43174760ef45e68c18d42931121a424fc8cbbdd3 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 10 Nov 2021 01:19:51 -0600 Subject: [PATCH 44/53] Added exit trade funding_fees check but test fails because of sql integrity error test_update_funding_fees --- tests/test_freqtradebot.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index cbf4becbe..d406f9642 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4715,8 +4715,18 @@ def test_update_funding_fees_schedule(mocker, default_conf, trading_mode, calls, 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, is_short, limit_order_open): +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 @@ -4818,7 +4828,21 @@ def test_update_funding_fees(mocker, default_conf, time_machine, fee, is_short, # create_mock_trades(fee, False) time_machine.move_to("2021-09-01 08:00:00 +00:00") - freqtrade._schedule.run_pending() + if schedule_off: + for trade in trades: + assert trade.funding_fees == ( + trade.amount * + mark_prices[trade.pair][1630454400000] * + funding_rates[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: @@ -4827,4 +4851,3 @@ def test_update_funding_fees(mocker, default_conf, time_machine, fee, is_short, mark_prices[trade.pair][time] * funding_rates[trade.pair][time] for time in mark_prices[trade.pair].keys() ]) - return From 68083b7fdd2cb12dbe65ac420481d21e1929c748 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Nov 2021 20:07:56 +0100 Subject: [PATCH 45/53] Fix sqlinsert failure in test --- tests/test_freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d406f9642..82319627e 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4748,6 +4748,7 @@ def test_update_funding_fees( 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) @@ -4825,7 +4826,7 @@ def test_update_funding_fees( mark_prices[trade.pair][1630454400000] * funding_rates[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") if schedule_off: From 76ced8acf6b43c90340d3879899b62dd64b9f321 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Nov 2021 20:34:45 +0100 Subject: [PATCH 46/53] Add some documentation to class --- freqtrade/exchange/exchange.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 306339651..d3dff75a8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1747,13 +1747,11 @@ class Exchange: raise OperationalException(e) from e @retrier - def _get_mark_price_history( - self, - pair: str, - since: int - ) -> Dict: + 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: @@ -1803,6 +1801,7 @@ class Exchange: ) -> 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 From 592b7e0ce38b4ba365414f96fb792856531359af Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 11 Nov 2021 17:49:32 -0600 Subject: [PATCH 47/53] All test_update_funding_fees tests pass --- freqtrade/freqtradebot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index defc02a6c..335ae6052 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -276,6 +276,8 @@ class FreqtradeBot(LoggingMixin): trade.open_date ) trade.funding_fees = funding_fees + else: + return 0.0 def startup_update_open_orders(self): """ @@ -705,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(pair, amount, open_date) - else: - funding_fees = 0.0 + funding_fees = self.exchange.get_funding_fees(pair, amount, open_date) trade = Trade( pair=pair, @@ -1263,6 +1262,7 @@ 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' From 9a65f486ed955bba1dbffb65ce0178f1e96405cf Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 11 Nov 2021 18:32:39 -0600 Subject: [PATCH 48/53] updated exchangeError messages regarding fetch_funding_rate_history --- freqtrade/exchange/exchange.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d3dff75a8..b9b071021 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1617,7 +1617,8 @@ class Exchange: 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 @@ -1870,8 +1871,7 @@ class Exchange: """ if not self.exchange_has("fetchFundingRateHistory"): raise ExchangeError( - f"CCXT has not implemented fetchFundingRateHistory for {self.name}; " - f"therefore, dry-run/backtesting for {self.name} is currently unavailable" + 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 From c8c2d89893b29f7e0e93b5a2b5821cbacfa53c4d Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 11 Nov 2021 19:02:07 -0600 Subject: [PATCH 49/53] exchange.get_funding_fees returns 0 by default --- freqtrade/exchange/exchange.py | 13 ++++++++----- freqtrade/freqtradebot.py | 6 +++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b9b071021..9be3169c2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1848,7 +1848,7 @@ class Exchange: return fees - def get_funding_fees(self, pair: str, amount: float, open_date: datetime): + 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 @@ -1856,11 +1856,14 @@ class Exchange: :param amount: Trade amount :param open_date: Open date of the trade """ - if self._config['dry_run']: - funding_fees = self._calculate_funding_fees(pair, amount, open_date) + 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: - funding_fees = self._get_funding_fees_from_exchange(pair, open_date) - return funding_fees + return 0.0 @retrier def get_funding_rate_history(self, pair: str, since: int) -> Dict: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 335ae6052..18127288b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1262,7 +1262,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) + 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' From 8d4163d0036c397c2015760e5db88fc9e955f873 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Nov 2021 07:26:59 +0100 Subject: [PATCH 50/53] Add compat tests --- freqtrade/exchange/exchange.py | 12 +++------ tests/exchange/test_ccxt_compat.py | 40 +++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9be3169c2..cf1e16b8d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1609,12 +1609,11 @@ class Exchange: 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 """ - # TODO-lev: Add dry-run handling for this. - if not self.exchange_has("fetchFundingHistory"): raise OperationalException( f"fetch_funding_history() is not available using {self.name}" @@ -1889,13 +1888,8 @@ class Exchange: 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 = datetime( - d.year, - d.month, - d.day, - d.hour, - tzinfo=timezone.utc - ).timestamp() * 1000 + time = int(timeframe_to_prev_date('1h', d).timestamp() * 1000) + funding_history[time] = fund['fundingRate'] return funding_history except ccxt.DDoSProtection as e: diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 2f629528c..c3aee7e92 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -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,6 +48,7 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'hasQuoteVolume': True, 'timeframe': '5m', + 'futures': True, }, 'okex': { 'pair': 'BTC/USDT', @@ -57,7 +60,7 @@ EXCHANGES = { @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 +76,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 +165,24 @@ 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) + this_hour = timeframe_to_prev_date('1h') + prev_hour = this_hour - timedelta(hours=1) + assert rate[int(this_hour.timestamp() * 1000)] != 0.0 + assert rate[int(prev_hour.timestamp() * 1000)] != 0.0 + # TODO: tests fetch_trades (?) def test_ccxt_get_fee(self, exchange): From 3c509a1f9b59949b4418db7dee6ee1d4bb464d9a Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 13 Nov 2021 04:45:23 -0600 Subject: [PATCH 51/53] New method for combining all funding fees within a time period --- freqtrade/exchange/bibox.py | 2 - freqtrade/exchange/binance.py | 2 - freqtrade/exchange/bybit.py | 2 - freqtrade/exchange/exchange.py | 52 +++------ freqtrade/exchange/ftx.py | 1 - freqtrade/exchange/gateio.py | 2 - freqtrade/exchange/hitbtc.py | 2 - freqtrade/exchange/kraken.py | 1 - freqtrade/exchange/kucoin.py | 2 - freqtrade/exchange/okex.py | 1 - tests/conftest.py | 20 +++- tests/exchange/test_exchange.py | 193 ++++++-------------------------- tests/test_freqtradebot.py | 26 ++++- 13 files changed, 93 insertions(+), 213 deletions(-) diff --git a/freqtrade/exchange/bibox.py b/freqtrade/exchange/bibox.py index e0741e34a..988a1843e 100644 --- a/freqtrade/exchange/bibox.py +++ b/freqtrade/exchange/bibox.py @@ -24,5 +24,3 @@ class Bibox(Exchange): def _ccxt_config(self) -> Dict: # Parameters to add directly to ccxt sync/async initialization. return {"has": {"fetchCurrencies": False}} - - funding_fee_times: List[int] = [0, 8, 16] # hours of the day diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 9ebe84517..fda248289 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -29,8 +29,6 @@ class Binance(Exchange): "trades_pagination_arg": "fromId", "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } - 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 diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index df19a671b..e94f48878 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -23,8 +23,6 @@ class Bybit(Exchange): "ohlcv_candle_limit": 200, } - 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 # (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index cf1e16b8d..88887abbb 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -73,10 +73,6 @@ class Exchange: } _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 ] @@ -1711,21 +1707,6 @@ class Exchange: """ return open_date.minute > 0 or open_date.second > 0 - def _get_funding_fee_dates(self, start: datetime, end: datetime): - if self.funding_fee_cutoff(start): - start += timedelta(hours=1) - start = datetime(start.year, start.month, start.day, start.hour, tzinfo=timezone.utc) - end = datetime(end.year, end.month, end.day, end.hour, tzinfo=timezone.utc) - - results = [] - iterator = start - while iterator <= end: - if iterator.hour in self.funding_fee_times: - results.append(iterator) - iterator += timedelta(hours=1) - - return results - @retrier def set_margin_mode(self, pair: str, collateral: Collateral, params: dict = {}): """ @@ -1808,6 +1789,16 @@ class Exchange: :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) @@ -1821,29 +1812,20 @@ class Exchange: pair, open_timestamp ) - funding_fee_dates = self._get_funding_fee_dates(open_date, close_date) - for date in funding_fee_dates: - timestamp = int(date.timestamp()) * 1000 - if timestamp in funding_rate_history: - funding_rate = funding_rate_history[timestamp] - else: - logger.warning( - f"Funding rate for {pair} at {date} not found in funding_rate_history" - f"Funding fee calculation may be incorrect" - ) + 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] - else: - logger.warning( - f"Mark price for {pair} at {date} not found in funding_rate_history" - f"Funding fee calculation may be incorrect" - ) - if funding_rate and mark_price: 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 diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index d84b3a5d4..962e604ec 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -22,7 +22,6 @@ class Ftx(Exchange): "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 diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 83abd1266..f8f0047e2 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -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 # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported diff --git a/freqtrade/exchange/hitbtc.py b/freqtrade/exchange/hitbtc.py index 8e0a009f0..97e2e4594 100644 --- a/freqtrade/exchange/hitbtc.py +++ b/freqtrade/exchange/hitbtc.py @@ -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 diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 6e4249393..eb4bfaa29 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -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 diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index 51de75ea4..e516ad10e 100644 --- a/freqtrade/exchange/kucoin.py +++ b/freqtrade/exchange/kucoin.py @@ -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 diff --git a/freqtrade/exchange/okex.py b/freqtrade/exchange/okex.py index 100bf3adf..178932fa2 100644 --- a/freqtrade/exchange/okex.py +++ b/freqtrade/exchange/okex.py @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index 5bc4f5fc6..9d04e994b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2389,7 +2389,7 @@ def mark_ohlcv(): @pytest.fixture(scope='function') -def funding_rate_history(): +def funding_rate_history_hourly(): return [ { "symbol": "ADA/USDT", @@ -2476,3 +2476,21 @@ def funding_rate_history(): "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" + } + ] diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 611d09254..567ab6c2f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3318,124 +3318,6 @@ def test__get_funding_fee( assert kraken._get_funding_fee(size, funding_rate, mark_price, time_in_ratio) == kraken_fee -@pytest.mark.parametrize('exchange,d1,d2,funding_times', [ - ( - 'binance', - "2021-09-01 00:00:00", - "2021-09-01 08:00:00", - ["2021-09-01 00", "2021-09-01 08"] - ), - ('binance', "2021-09-01 00:00:15", "2021-09-01 08:00:00", ["2021-09-01 00", "2021-09-01 08"]), - ('binance', "2021-09-01 00:00:16", "2021-09-01 08:00:00", ["2021-09-01 08"]), - ('binance', "2021-09-01 01:00:14", "2021-09-01 08:00:00", ["2021-09-01 08"]), - ('binance', "2021-09-01 00:00:00", "2021-09-01 07:59:59", ["2021-09-01 00"]), - ('binance', "2021-09-01 00:00:00", "2021-09-01 12:00:00", ["2021-09-01 00", "2021-09-01 08"]), - ( - 'binance', - "2021-09-01 00:00:01", - "2021-09-01 08:00:00", - ["2021-09-01 00", "2021-09-01 08"] - ), - ( - 'kraken', - "2021-09-01 00:00:00", - "2021-09-01 08:00:00", - ["2021-09-01 00", "2021-09-01 04", "2021-09-01 08"] - ), - ( - 'kraken', - "2021-09-01 00:00:15", - "2021-09-01 08:00:00", - ["2021-09-01 04", "2021-09-01 08"] - ), - ( - 'kraken', - "2021-09-01 01:00:14", - "2021-09-01 08:00:00", - ["2021-09-01 04", "2021-09-01 08"] - ), - ( - 'kraken', - "2021-09-01 00:00:00", - "2021-09-01 07:59:59", - ["2021-09-01 00", "2021-09-01 04"] - ), - ( - 'kraken', - "2021-09-01 00:00:00", - "2021-09-01 12:00:00", - ["2021-09-01 00", "2021-09-01 04", "2021-09-01 08", "2021-09-01 12"] - ), - ( - 'kraken', - "2021-09-01 00:00:01", - "2021-09-01 08:00:00", - ["2021-09-01 04", "2021-09-01 08"] - ), - ( - 'ftx', - "2021-09-01 00:00:00", - "2021-09-01 08:00:00", - [ - "2021-09-01 00", - "2021-09-01 01", - "2021-09-01 02", - "2021-09-01 03", - "2021-09-01 04", - "2021-09-01 05", - "2021-09-01 06", - "2021-09-01 07", - "2021-09-01 08" - ] - ), - ( - 'ftx', - "2021-09-01 00:00:00", - "2021-09-01 12:00:00", - [ - "2021-09-01 00", - "2021-09-01 01", - "2021-09-01 02", - "2021-09-01 03", - "2021-09-01 04", - "2021-09-01 05", - "2021-09-01 06", - "2021-09-01 07", - "2021-09-01 08", - "2021-09-01 09", - "2021-09-01 10", - "2021-09-01 11", - "2021-09-01 12" - ] - ), - ( - 'ftx', - "2021-09-01 00:00:01", - "2021-09-01 08:00:00", - [ - "2021-09-01 01", - "2021-09-01 02", - "2021-09-01 03", - "2021-09-01 04", - "2021-09-01 05", - "2021-09-01 06", - "2021-09-01 07", - "2021-09-01 08" - ] - ), - ('gateio', "2021-09-01 00:00:00", "2021-09-01 08:00:00", ["2021-09-01 00", "2021-09-01 08"]), - ('gateio', "2021-09-01 00:00:00", "2021-09-01 12:00:00", ["2021-09-01 00", "2021-09-01 08"]), - ('gateio', "2021-09-01 00:00:01", "2021-09-01 08:00:00", ["2021-09-01 08"]), -]) -def test__get_funding_fee_dates(mocker, default_conf, exchange, d1, d2, funding_times): - expected_result = [datetime.strptime(f"{d} +0000", '%Y-%m-%d %H %z') for d in funding_times] - 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') - exchange = get_patched_exchange(mocker, default_conf, id=exchange) - result = exchange._get_funding_fee_dates(d1, d2) - assert result == expected_result - - def test__get_mark_price_history(mocker, default_conf, mark_ohlcv): api_mock = MagicMock() api_mock.fetch_ohlcv = MagicMock(return_value=mark_ohlcv) @@ -3473,9 +3355,9 @@ def test__get_mark_price_history(mocker, default_conf, mark_ohlcv): ) -def test_get_funding_rate_history(mocker, default_conf, funding_rate_history): +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) + 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) @@ -3511,14 +3393,14 @@ def test_get_funding_rate_history(mocker, default_conf, funding_rate_history): ) -@pytest.mark.parametrize('exchange,d1,d2,amount,expected_fees', [ - ('binance', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), - ('binance', "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), - ('binance', "2021-09-01 00:00:16", "2021-09-01 08:00:00", 30.0, -0.0002493), - ('binance', "2021-09-01 01:00:14", "2021-09-01 08:00:00", 30.0, -0.0002493), - ('binance', "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0006647999999999999), - ('binance', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999), - ('binance', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), +@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), @@ -3526,21 +3408,24 @@ def test_get_funding_rate_history(mocker, default_conf, funding_rate_history): # ('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', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, 0.0010008000000000003), - ('ftx', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0146691), - ('ftx', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, 0.0016656000000000002), - ('gateio', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), - ('gateio', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999), - ('gateio', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493), - ('binance', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0015235000000000001), + ('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', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, 0.0016680000000000002), + ('ftx', 0, 9, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, 0.0016680000000000002), ]) def test__calculate_funding_fees( mocker, default_conf, - funding_rate_history, + funding_rate_history_hourly, + funding_rate_history_octohourly, + rate_start, + rate_end, mark_ohlcv, exchange, d1, @@ -3585,6 +3470,11 @@ def test__calculate_funding_fees( ''' 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) @@ -3596,39 +3486,28 @@ def test__calculate_funding_fees( assert funding_fees == expected_fees -@pytest.mark.parametrize('name,expected_fees_8,expected_fees_10,expected_fees_12', [ - ('binance', -0.0009140999999999999, -0.0009140999999999999, -0.0009140999999999999), - # TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._get_funding_fee - # ('kraken', -0.0014937, -0.0014937, 0.0045759), - ('ftx', 0.0010008000000000003, 0.0021084, 0.0146691), - ('gateio', -0.0009140999999999999, -0.0009140999999999999, -0.0009140999999999999), +@ 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, + funding_rate_history_octohourly, mark_ohlcv, - name, + exchange, time_machine, - expected_fees_8, - expected_fees_10, - expected_fees_12 + 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) + 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=name) + 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_8 - time_machine.move_to("2021-09-01 10:00:00 +00:00") - funding_fees = exchange._calculate_funding_fees('ADA/USDT', 30.0, d1) - assert funding_fees == expected_fees_10 - time_machine.move_to("2021-09-01 12:00:00 +00:00") - funding_fees = exchange._calculate_funding_fees('ADA/USDT', 30.0, d1) - assert funding_fees == expected_fees_12 + assert funding_fees == expected_fees diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5f6b90de7..bd4e1ebc9 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4761,7 +4761,19 @@ def test_update_funding_fees( default_conf['collateral'] = 'isolated' default_conf['dry_run'] = True - funding_rates = { + 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, @@ -4798,7 +4810,7 @@ def test_update_funding_fees( mocker.patch( 'freqtrade.exchange.Exchange.get_funding_rate_history', - side_effect=lambda pair, since: funding_rates[pair] + side_effect=lambda pair, since: funding_rates_midnight[pair] ) mocker.patch.multiple( @@ -4827,17 +4839,21 @@ def test_update_funding_fees( assert trade.funding_fees == ( trade.amount * mark_prices[trade.pair][1630454400000] * - funding_rates[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[trade.pair][1630454400000] + funding_rates_eight[trade.pair][1630454400000] ) freqtrade.execute_trade_exit( trade=trade, @@ -4853,5 +4869,5 @@ def test_update_funding_fees( assert trade.funding_fees == sum([ trade.amount * mark_prices[trade.pair][time] * - funding_rates[trade.pair][time] for time in mark_prices[trade.pair].keys() + funding_rates_eight[trade.pair][time] for time in mark_prices[trade.pair].keys() ]) From 867ac3f82aa194d0270e1efa4389dd508b01f150 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 13 Nov 2021 06:21:17 -0600 Subject: [PATCH 52/53] Removed typing.List from bibox, hitbtc and kucoin --- freqtrade/exchange/bibox.py | 2 +- freqtrade/exchange/hitbtc.py | 2 +- freqtrade/exchange/kucoin.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/bibox.py b/freqtrade/exchange/bibox.py index 988a1843e..074dd2b10 100644 --- a/freqtrade/exchange/bibox.py +++ b/freqtrade/exchange/bibox.py @@ -1,6 +1,6 @@ """ Bibox exchange subclass """ import logging -from typing import Dict, List +from typing import Dict from freqtrade.exchange import Exchange diff --git a/freqtrade/exchange/hitbtc.py b/freqtrade/exchange/hitbtc.py index 97e2e4594..a48c9a198 100644 --- a/freqtrade/exchange/hitbtc.py +++ b/freqtrade/exchange/hitbtc.py @@ -1,5 +1,5 @@ import logging -from typing import Dict, List +from typing import Dict from freqtrade.exchange import Exchange diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index e516ad10e..5d818f6a2 100644 --- a/freqtrade/exchange/kucoin.py +++ b/freqtrade/exchange/kucoin.py @@ -1,6 +1,6 @@ """ Kucoin exchange subclass """ import logging -from typing import Dict, List +from typing import Dict from freqtrade.exchange import Exchange From b3afca2a9d47c69bb79d8ea5e573e412bb442e1c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Nov 2021 13:37:09 +0100 Subject: [PATCH 53/53] Improve ccxt_compat test for funding rate --- tests/exchange/test_ccxt_compat.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index c3aee7e92..b14df070c 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -49,11 +49,16 @@ EXCHANGES = { '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, }, } @@ -178,10 +183,11 @@ class TestCCXTExchange(): rate = exchange.get_funding_rate_history(pair, since) assert isinstance(rate, dict) - this_hour = timeframe_to_prev_date('1h') - prev_hour = this_hour - timedelta(hours=1) + 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_hour.timestamp() * 1000)] != 0.0 + assert rate[int(prev_tick.timestamp() * 1000)] != 0.0 # TODO: tests fetch_trades (?)