From 2a26c6fbed747511834b1724ce2cad55ca1a6a9d Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 26 Sep 2021 04:11:35 -0600 Subject: [PATCH] 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