From d54117990b1f1ddcd3043e42c5a7c1159194696e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 9 Sep 2021 01:19:24 -0600 Subject: [PATCH] Added funding_fee method headers to exchange, and implemented some of the methods --- freqtrade/exchange/binance.py | 6 ++-- freqtrade/exchange/exchange.py | 58 +++++++++++++++++++++++++++++++-- freqtrade/exchange/ftx.py | 13 +++----- freqtrade/exchange/kraken.py | 6 ++-- freqtrade/freqtradebot.py | 9 +++-- freqtrade/leverage/__init__.py | 1 - tests/exchange/test_exchange.py | 6 ++-- tests/rpc/test_rpc.py | 4 +-- 8 files changed, 78 insertions(+), 25 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 8c2713c72..aa18634cf 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -3,12 +3,12 @@ import logging from typing import Dict, List, Optional import ccxt -from datetime import time + from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier -from freqtrade.utils import hours_to_time + logger = logging.getLogger(__name__) @@ -23,7 +23,7 @@ class Binance(Exchange): "trades_pagination_arg": "fromId", "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } - funding_fee_times: List[time] = hours_to_time([0, 8, 16]) + funding_fee_times: List[int] = [0, 8, 16] # hours of the day def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: """ diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index cd41f2b13..c9a932bff 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, time, timezone +from datetime import datetime, timedelta, timezone from math import ceil from typing import Any, Dict, List, Optional, Tuple, Union @@ -69,7 +69,7 @@ class Exchange: "l2_limit_range_required": True, # Allow Empty L2 limit (kucoin) } _ft_has: Dict = {} - funding_fee_times: List[time] = [] + funding_fee_times: List[int] = [] # hours of the day def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: """ @@ -1555,6 +1555,21 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + def get_mark_price(self, pair: str, when: datetime): + """ + Get's the value of the underlying asset for a futures contract + at a specific date and time in the past + """ + # TODO-lev: implement + raise OperationalException(f"get_mark_price has not been implemented for {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, contract_size: float, @@ -1572,6 +1587,45 @@ class Exchange: """ raise OperationalException(f"Funding fee has not been implemented for {self.name}") + def get_funding_fee_dates(self, open_date: datetime, close_date: datetime): + """ + Get's the date and time of every funding fee that happened between two datetimes + """ + open_date = datetime(open_date.year, open_date.month, open_date.day, open_date.hour) + close_date = datetime(close_date.year, close_date.month, close_date.day, close_date.hour) + + results = [] + date_iterator = open_date + while date_iterator < close_date: + date_iterator += timedelta(hours=1) + if date_iterator.hour in self.funding_fee_times: + results.append(date_iterator) + + return results + + 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): + funding_rate = self.get_funding_rate(pair, date) + mark_price = self.get_mark_price(pair, date) + fees += self._get_funding_fee(amount, mark_price, funding_rate) + + 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 c442924fa..42d7ce050 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -3,13 +3,13 @@ import logging from typing import Any, Dict, List, Optional import ccxt -from datetime import time + from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier from freqtrade.misc import safe_value_fallback2 -from freqtrade.utils import hours_to_time + logger = logging.getLogger(__name__) @@ -20,7 +20,7 @@ class Ftx(Exchange): "stoploss_on_exchange": True, "ohlcv_candle_limit": 1500, } - funding_fee_times: List[time] = hours_to_time(list(range(0, 23))) + funding_fee_times: List[int] = list(range(0, 23)) def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ @@ -159,9 +159,7 @@ class Ftx(Exchange): contract_size: float, mark_price: float, funding_rate: 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 @@ -169,5 +167,4 @@ class Ftx(Exchange): :param mark_price: The price of the asset that the contract is based off of :param funding_rate: Must be None on ftx """ - (contract_size * mark_price) / 24 - return + return (contract_size * mark_price) / 24 diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 6aaf00214..a83b9f9cb 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -3,12 +3,12 @@ import logging from typing import Any, Dict, List import ccxt -from datetime import time + from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier -from freqtrade.utils import hours_to_time + logger = logging.getLogger(__name__) @@ -22,7 +22,7 @@ class Kraken(Exchange): "trades_pagination": "id", "trades_pagination_arg": "since", } - funding_fee_times: List[time] = hours_to_time([0, 4, 8, 12, 16, 20]) + funding_fee_times: List[int] = [0, 4, 8, 12, 16, 20] # hours of the day def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 02f8b27cb..574ade803 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,13 +4,13 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback -import schedule from datetime import datetime, timezone from math import isclose from threading import Lock from typing import Any, Dict, List, Optional import arrow +import schedule from freqtrade import __version__, constants from freqtrade.configuration import validate_config_consistency @@ -251,7 +251,10 @@ 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(trade.pair, trade.open_date) + funding_fees = self.exchange.get_funding_fees_from_exchange( + trade.pair, + trade.open_date + ) trade.funding_fees = funding_fees def update_open_orders(self): @@ -583,7 +586,7 @@ class FreqtradeBot(LoggingMixin): fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') open_date = datetime.utcnow() if self.trading_mode == TradingMode.FUTURES: - funding_fees = self.exchange.get_funding_fees(pair, open_date) + funding_fees = self.exchange.get_funding_fees_from_exchange(pair, open_date) else: funding_fees = 0.0 diff --git a/freqtrade/leverage/__init__.py b/freqtrade/leverage/__init__.py index 54cd37481..ae78f4722 100644 --- a/freqtrade/leverage/__init__.py +++ b/freqtrade/leverage/__init__.py @@ -1,3 +1,2 @@ # flake8: noqa: F401 -from freqtrade.leverage.funding_fees import funding_fee from freqtrade.leverage.interest import interest diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e2a6639a3..1d23482fc 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2972,11 +2972,11 @@ def test_get_funding_fees(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.strftime('%s')) expected_fees = -0.001 # 0.14542341 + -0.14642341 - fees_from_datetime = exchange.get_funding_fees( + fees_from_datetime = exchange.get_funding_fees_from_exchange( pair='XRP/USDT', since=date_time ) - fees_from_unix_time = exchange.get_funding_fees( + fees_from_unix_time = exchange.get_funding_fees_from_exchange( pair='XRP/USDT', since=unix_time ) @@ -2989,7 +2989,7 @@ def test_get_funding_fees(default_conf, mocker, exchange_name): default_conf, api_mock, exchange_name, - "get_funding_fees", + "get_funding_fees_from_exchange", "fetch_funding_history", pair="XRP/USDT", since=unix_time diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index d78f40a96..586fadff8 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -112,7 +112,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'interest_rate': 0.0, 'isolated_liq': None, 'is_short': False, - 'funding_fees': None, + 'funding_fees': 0.0, 'trading_mode': TradingMode.SPOT } @@ -185,7 +185,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'interest_rate': 0.0, 'isolated_liq': None, 'is_short': False, - 'funding_fees': None, + 'funding_fees': 0.0, 'trading_mode': TradingMode.SPOT }