From f5248be043afa27f6264ec24848ed882a0ea9bca Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 6 Sep 2021 02:24:15 -0600 Subject: [PATCH] Changed funding fee tracking method, need to get funding_rate and open prices at multiple candles --- freqtrade/exchange/binance.py | 2 +- freqtrade/exchange/exchange.py | 32 ++------ freqtrade/exchange/ftx.py | 2 +- freqtrade/freqtradebot.py | 5 -- freqtrade/leverage/__init__.py | 1 + freqtrade/leverage/funding_fees.py | 74 +++++++++++++++++ freqtrade/persistence/models.py | 13 ++- tests/exchange/test_exchange.py | 126 ++++++++++++++--------------- 8 files changed, 157 insertions(+), 98 deletions(-) create mode 100644 freqtrade/leverage/funding_fees.py diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 0c470cb24..ba4f510d3 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,6 +1,6 @@ """ Binance exchange subclass """ import logging -from typing import Dict +from typing import Dict, Optional import ccxt diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 67eb0ad15..d82c20599 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -9,7 +9,7 @@ import logging from copy import deepcopy from datetime import datetime, timezone from math import ceil -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple import arrow import ccxt @@ -361,7 +361,7 @@ class Exchange: raise OperationalException( 'Could not load markets, therefore cannot start. ' 'Please investigate the above error for more details.' - ) + ) quote_currencies = self.get_quote_currencies() if stake_currency not in quote_currencies: raise OperationalException( @@ -1516,35 +1516,13 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - @retrier - def get_funding_fees(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 - """ - + # https://www.binance.com/en/support/faq/360033525031 + def fetch_funding_rate(self): if not self.exchange_has("fetchFundingHistory"): raise OperationalException( f"fetch_funding_history() has not been implemented on ccxt.{self.name}") - if type(since) is datetime: - since = int(since.strftime('%s')) - - try: - funding_history = self._api.fetch_funding_history( - pair=pair, - since=since - ) - return sum(fee['amount'] for fee in funding_history) - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not get funding fees due to {e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e + return self._api.fetch_funding_rates() def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 6cd549d60..f1d633ca9 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 +from typing import Any, Dict, Optional import ccxt diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 69b669f63..a6793a79a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -242,11 +242,6 @@ class FreqtradeBot(LoggingMixin): open_trades = len(Trade.get_open_trades()) return max(0, self.config['max_open_trades'] - open_trades) - def add_funding_fees(self, trade: Trade): - if self.trading_mode == TradingMode.FUTURES: - funding_fees = self.exchange.get_funding_fees(trade.pair, trade.open_date) - trade.funding_fees = funding_fees - def update_open_orders(self): """ Updates open orders based on order list kept in the database. diff --git a/freqtrade/leverage/__init__.py b/freqtrade/leverage/__init__.py index ae78f4722..54cd37481 100644 --- a/freqtrade/leverage/__init__.py +++ b/freqtrade/leverage/__init__.py @@ -1,2 +1,3 @@ # flake8: noqa: F401 +from freqtrade.leverage.funding_fees import funding_fee from freqtrade.leverage.interest import interest diff --git a/freqtrade/leverage/funding_fees.py b/freqtrade/leverage/funding_fees.py new file mode 100644 index 000000000..754d3ec96 --- /dev/null +++ b/freqtrade/leverage/funding_fees.py @@ -0,0 +1,74 @@ +from datetime import datetime +from typing import Optional + +from freqtrade.exceptions import OperationalException + + +def funding_fees( + exchange_name: str, + pair: str, + contract_size: float, + open_date: datetime, + close_date: datetime + # index_price: float, + # interest_rate: float +): + """ + Equation to calculate funding_fees on futures trades + + :param exchange_name: The exchanged being trading on + :param borrowed: The amount of currency being borrowed + :param rate: The rate of interest + :param hours: The time in hours that the currency has been borrowed for + + Raises: + OperationalException: Raised if freqtrade does + not support margin trading for this exchange + + Returns: The amount of interest owed (currency matches borrowed) + """ + exchange_name = exchange_name.lower() + # fees = 0 + if exchange_name == "binance": + for timeslot in ["23:59:45", "07:59:45", "15:59:45"]: + # for each day in close_date - open_date + # mark_price = mark_price at this time + # rate = rate at this time + # fees = fees + funding_fee(exchange_name, contract_size, mark_price, rate) + # return fees + return + elif exchange_name == "kraken": + raise OperationalException("Funding_fees has not been implemented for Kraken") + elif exchange_name == "ftx": + # for timeslot in every hour since open_date: + # mark_price = mark_price at this time + # fees = fees + funding_fee(exchange_name, contract_size, mark_price, rate) + return + else: + raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") + + +def funding_fee( + exchange_name: str, + contract_size: float, + mark_price: float, + rate: Optional[float], + # index_price: float, + # interest_rate: float +): + """ + Calculates a single funding fee + """ + if exchange_name == "binance": + assert isinstance(rate, float) + nominal_value = mark_price * contract_size + adjustment = nominal_value * rate + return adjustment + elif exchange_name == "kraken": + raise OperationalException("Funding fee has not been implemented for kraken") + elif exchange_name == "ftx": + """ + Always paid in USD on FTX # TODO: How do we account for this + """ + (contract_size * mark_price) / 24 + return diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index eabc36509..1bbc0d296 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -16,7 +16,7 @@ from sqlalchemy.sql.schema import UniqueConstraint from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES from freqtrade.enums import SellType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.leverage import interest +from freqtrade.leverage import funding_fees, interest from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate @@ -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 return float(self._calc_base_close(amount, rate, fee)) + funding_fees else: @@ -785,6 +786,16 @@ class LocalTrade(): else: return None + def add_funding_fees(self): + if self.trading_mode == TradingMode.FUTURES: + self.funding_fees = funding_fees( + self.exchange, + self.pair, + self.amount, + self.open_date_utc, + self.close_date_utc + ) + @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_exchange.py b/tests/exchange/test_exchange.py index e2a6639a3..8e4a099c5 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2928,69 +2928,69 @@ def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected -@pytest.mark.parametrize("exchange_name", ['binance', 'ftx']) -def test_get_funding_fees(default_conf, mocker, exchange_name): - api_mock = MagicMock() - api_mock.fetch_funding_history = MagicMock(return_value=[ - { - 'amount': 0.14542341, - 'code': 'USDT', - 'datetime': '2021-09-01T08:00:01.000Z', - 'id': '485478', - 'info': {'asset': 'USDT', - 'income': '0.14542341', - 'incomeType': 'FUNDING_FEE', - 'info': 'FUNDING_FEE', - 'symbol': 'XRPUSDT', - 'time': '1630512001000', - 'tradeId': '', - 'tranId': '4854789484855218760'}, - 'symbol': 'XRP/USDT', - 'timestamp': 1630512001000 - }, - { - 'amount': -0.14642341, - 'code': 'USDT', - 'datetime': '2021-09-01T16:00:01.000Z', - 'id': '485479', - 'info': {'asset': 'USDT', - 'income': '-0.14642341', - 'incomeType': 'FUNDING_FEE', - 'info': 'FUNDING_FEE', - 'symbol': 'XRPUSDT', - 'time': '1630512001000', - 'tradeId': '', - 'tranId': '4854789484855218760'}, - 'symbol': 'XRP/USDT', - 'timestamp': 1630512001000 - } - ]) - type(api_mock).has = PropertyMock(return_value={'fetchFundingHistory': True}) +# @pytest.mark.parametrize("exchange_name", ['binance', 'ftx']) +# def test_get_funding_fees(default_conf, mocker, exchange_name): +# api_mock = MagicMock() +# api_mock.fetch_funding_history = MagicMock(return_value=[ +# { +# 'amount': 0.14542341, +# 'code': 'USDT', +# 'datetime': '2021-09-01T08:00:01.000Z', +# 'id': '485478', +# 'info': {'asset': 'USDT', +# 'income': '0.14542341', +# 'incomeType': 'FUNDING_FEE', +# 'info': 'FUNDING_FEE', +# 'symbol': 'XRPUSDT', +# 'time': '1630512001000', +# 'tradeId': '', +# 'tranId': '4854789484855218760'}, +# 'symbol': 'XRP/USDT', +# 'timestamp': 1630512001000 +# }, +# { +# 'amount': -0.14642341, +# 'code': 'USDT', +# 'datetime': '2021-09-01T16:00:01.000Z', +# 'id': '485479', +# 'info': {'asset': 'USDT', +# 'income': '-0.14642341', +# 'incomeType': 'FUNDING_FEE', +# 'info': 'FUNDING_FEE', +# 'symbol': 'XRPUSDT', +# 'time': '1630512001000', +# 'tradeId': '', +# 'tranId': '4854789484855218760'}, +# 'symbol': 'XRP/USDT', +# 'timestamp': 1630512001000 +# } +# ]) +# type(api_mock).has = PropertyMock(return_value={'fetchFundingHistory': True}) - # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ') - unix_time = int(date_time.strftime('%s')) - expected_fees = -0.001 # 0.14542341 + -0.14642341 - fees_from_datetime = exchange.get_funding_fees( - pair='XRP/USDT', - since=date_time - ) - fees_from_unix_time = exchange.get_funding_fees( - pair='XRP/USDT', - since=unix_time - ) +# # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) +# exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) +# date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ') +# unix_time = int(date_time.strftime('%s')) +# expected_fees = -0.001 # 0.14542341 + -0.14642341 +# fees_from_datetime = exchange.get_funding_fees( +# pair='XRP/USDT', +# since=date_time +# ) +# fees_from_unix_time = exchange.get_funding_fees( +# pair='XRP/USDT', +# since=unix_time +# ) - assert(isclose(expected_fees, fees_from_datetime)) - assert(isclose(expected_fees, fees_from_unix_time)) +# assert(isclose(expected_fees, fees_from_datetime)) +# assert(isclose(expected_fees, fees_from_unix_time)) - ccxt_exceptionhandlers( - mocker, - default_conf, - api_mock, - exchange_name, - "get_funding_fees", - "fetch_funding_history", - pair="XRP/USDT", - since=unix_time - ) +# ccxt_exceptionhandlers( +# mocker, +# default_conf, +# api_mock, +# exchange_name, +# "get_funding_fees", +# "fetch_funding_history", +# pair="XRP/USDT", +# since=unix_time +# )