diff --git a/freqtrade/enums/collateral.py b/freqtrade/enums/collateral.py index 0a5988698..979496f7b 100644 --- a/freqtrade/enums/collateral.py +++ b/freqtrade/enums/collateral.py @@ -3,9 +3,9 @@ from enum import Enum class Collateral(Enum): """ - Enum to distinguish between - cross margin/futures collateral and - isolated margin/futures collateral + Enum to distinguish between + cross margin/futures collateral and + isolated margin/futures collateral """ CROSS = "cross" ISOLATED = "isolated" diff --git a/freqtrade/enums/tradingmode.py b/freqtrade/enums/tradingmode.py index a8de60c19..4a5756e4b 100644 --- a/freqtrade/enums/tradingmode.py +++ b/freqtrade/enums/tradingmode.py @@ -3,8 +3,8 @@ from enum import Enum class TradingMode(Enum): """ - Enum to distinguish between - spot, margin, futures or any other trading method + Enum to distinguish between + spot, margin, futures or any other trading method """ SPOT = "spot" MARGIN = "margin" diff --git a/freqtrade/exchange/bibox.py b/freqtrade/exchange/bibox.py index e0741e34a..da1effbfe 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 @@ -23,6 +23,6 @@ class Bibox(Exchange): @property 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 + config = {"has": {"fetchCurrencies": False}} + config.update(super()._ccxt_config) + return config diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index b71b58151..1cbbffc51 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 @@ -27,35 +28,17 @@ class Binance(Exchange): "trades_pagination": "id", "trades_pagination_arg": "fromId", "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], + "ccxt_futures_name": "future" } - 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 - # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported - # (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported - # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported + # TODO-lev: Uncomment once supported + # (TradingMode.MARGIN, Collateral.CROSS), + # (TradingMode.FUTURES, Collateral.CROSS), + # (TradingMode.FUTURES, Collateral.ISOLATED) ] - @property - def _ccxt_config(self) -> Dict: - # Parameters to add directly to ccxt sync/async initialization. - if self.trading_mode == TradingMode.MARGIN: - return { - "options": { - "defaultType": "margin" - } - } - elif self.trading_mode == TradingMode.FUTURES: - return { - "options": { - "defaultType": "future" - } - } - else: - return {} - def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) @@ -139,8 +122,8 @@ class Binance(Exchange): @retrier def fill_leverage_brackets(self): """ - Assigns property _leverage_brackets to a dictionary of information about the leverage - allowed on each pair + Assigns property _leverage_brackets to a dictionary of information about the leverage + allowed on each pair """ if self.trading_mode == TradingMode.FUTURES: try: @@ -174,9 +157,9 @@ class Binance(Exchange): 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) """ if pair not in self._leverage_brackets: return 1.0 @@ -195,8 +178,8 @@ class Binance(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 """ trading_mode = trading_mode or self.trading_mode @@ -229,3 +212,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, 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/bybit.py b/freqtrade/exchange/bybit.py index df19a671b..a1cd40ac6 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -21,12 +21,12 @@ class Bybit(Exchange): _ft_has: Dict = { "ohlcv_candle_limit": 200, + "ccxt_futures_name": "linear" } - 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 - # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported + # TODO-lev: Uncomment once supported + # (TradingMode.FUTURES, Collateral.CROSS), + # (TradingMode.FUTURES, Collateral.ISOLATED) ] diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c55c7ffbb..0b47c98b8 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 @@ -69,13 +69,11 @@ class Exchange: "trades_pagination_arg": "since", "l2_limit_range": None, "l2_limit_range_required": True, # Allow Empty L2 limit (kucoin) + "mark_ohlcv_price": "mark", + "ccxt_futures_name": "swap" } _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 ] @@ -89,6 +87,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) @@ -179,7 +178,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() @@ -234,7 +232,20 @@ class Exchange: @property def _ccxt_config(self) -> Dict: # Parameters to add directly to ccxt sync/async initialization. - return {} + if self.trading_mode == TradingMode.MARGIN: + return { + "options": { + "defaultType": "margin" + } + } + elif self.trading_mode == TradingMode.FUTURES: + return { + "options": { + "defaultType": self._ft_has["ccxt_futures_name"] + } + } + else: + return {} @property def name(self) -> str: @@ -532,10 +543,10 @@ class Exchange: collateral: Optional[Collateral] # Only None when trading_mode = TradingMode.SPOT ): """ - Checks if freqtrade can perform trades using the configured - trading mode(Margin, Futures) and Collateral(Cross, Isolated) - Throws OperationalException: - If the trading_mode/collateral type are not supported by freqtrade on this exchange + Checks if freqtrade can perform trades using the configured + trading mode(Margin, Futures) and Collateral(Cross, Isolated) + Throws OperationalException: + If the trading_mode/collateral type are not supported by freqtrade on this exchange """ if trading_mode != TradingMode.SPOT and ( (trading_mode, collateral) not in self._supported_trading_mode_collateral_pairs @@ -1622,18 +1633,18 @@ 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) - :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 + 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() 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 @@ -1654,17 +1665,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 + :param nominal_value: The total value of the trade in quote currency (collateral + debt) """ market = self.markets[pair] if ( @@ -1676,6 +1687,25 @@ class Exchange: else: return 1.0 + 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: Not used by most exchange classes + """ + nominal_value = mark_price * size + return nominal_value * funding_rate + @retrier def _set_leverage( self, @@ -1684,8 +1714,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 @@ -1701,12 +1731,19 @@ class Exchange: except ccxt.BaseError as e: 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 + @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 symbol: 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 return @@ -1721,6 +1758,150 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + @retrier + def _get_mark_price_history(self, pair: str, since: int) -> Dict: + """ + Get's the mark price history for a pair + :param pair: The quote/base pair of the trade + :param since: The earliest time to start downloading candles, in ms. + """ + + try: + candles = self._api.fetch_ohlcv( + pair, + timeframe="1h", + since=since, + params={ + 'price': self._ft_has["mark_ohlcv_price"] + } + ) + history = {} + for candle in candles: + d = datetime.fromtimestamp(int(candle[0] / 1000), timezone.utc) + # Round down to the nearest hour, in case of a delayed timestamp + # The millisecond timestamps can be delayed ~20ms + time = timeframe_to_prev_date('1h', d).timestamp() * 1000 + opening_mark_price = candle[1] + history[time] = opening_mark_price + return history + except ccxt.NotSupported as e: + raise OperationalException( + f'Exchange {self._api.name} does not support fetching historical ' + f'mark price candle (OHLCV) data. Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError(f'Could not fetch historical mark price candle (OHLCV) data ' + f'for pair {pair} due to {e.__class__.__name__}. ' + f'Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(f'Could not fetch historical mark price candle (OHLCV) data ' + f'for pair {pair}. Message: {e}') from e + + def _calculate_funding_fees( + self, + pair: str, + amount: float, + open_date: datetime, + close_date: Optional[datetime] = None + ) -> 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 + :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 = timeframe_to_prev_date('1h', open_date) + + 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, + open_timestamp + ) + mark_price_history = self._get_mark_price_history( + pair, + open_timestamp + ) + 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] + 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 + + 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 + :param pair: The quote/base pair of the trade + :param amount: Trade amount + :param open_date: Open date of the trade + """ + 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: + return 0.0 + + @retrier + def get_funding_rate_history(self, pair: str, since: int) -> Dict: + """ + :param pair: quote/base currency pair + :param since: timestamp in ms of the beginning time + :param end: timestamp in ms of the end time + """ + if not self.exchange_has("fetchFundingRateHistory"): + raise ExchangeError( + f"fetch_funding_rate_history is not available using {self.name}" + ) + + # TODO-lev: Gateio has a max limit into the past of 333 days, okex has a limit of 3 months + try: + funding_history: Dict = {} + response = self._api.fetch_funding_rate_history( + pair, + limit=1000, + since=since + ) + for fund in response: + d = datetime.fromtimestamp(int(fund['timestamp'] / 1000), timezone.utc) + # Round down to the nearest hour, in case of a delayed timestamp + # The millisecond timestamps can be delayed ~20ms + time = int(timeframe_to_prev_date('1h', d).timestamp() * 1000) + + funding_history[time] = fund['fundingRate'] + return funding_history + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 066bd7704..8347993ee 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -20,13 +20,14 @@ 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)) _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 - # (TradingMode.FUTURES, Collateral.CROSS) # TODO-lev: Uncomment once supported + # TODO-lev: Uncomment once supported + # (TradingMode.MARGIN, Collateral.CROSS), + # (TradingMode.FUTURES, Collateral.CROSS) ] def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 83abd1266..a445fd70a 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -26,33 +26,14 @@ 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 - # (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported - # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported + # TODO-lev: Uncomment once supported + # (TradingMode.MARGIN, Collateral.CROSS), + # (TradingMode.FUTURES, Collateral.CROSS), + # (TradingMode.FUTURES, Collateral.ISOLATED) ] - @property - def _ccxt_config(self) -> Dict: - # Parameters to add directly to ccxt sync/async initialization. - if self.trading_mode == TradingMode.MARGIN: - return { - "options": { - "defaultType": "margin" - } - } - elif self.trading_mode == TradingMode.FUTURES: - return { - "options": { - "defaultType": "swap" - } - } - else: - return {} - def validate_ordertypes(self, order_types: Dict) -> None: super().validate_ordertypes(order_types) diff --git a/freqtrade/exchange/hitbtc.py b/freqtrade/exchange/hitbtc.py index 8e0a009f0..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 @@ -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 d2cbcd347..42d817222 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -23,12 +23,12 @@ 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 - # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported - # (TradingMode.FUTURES, Collateral.CROSS) # TODO-lev: No CCXT support + # TODO-lev: Uncomment once supported + # (TradingMode.MARGIN, Collateral.CROSS), + # (TradingMode.FUTURES, Collateral.CROSS) ] def market_is_tradable(self, market: Dict[str, Any]) -> bool: @@ -146,8 +146,8 @@ class Kraken(Exchange): trading_mode: Optional[TradingMode] = None ): """ - Kraken set's the leverage as an option in the order object, so we need to - add it to params + Kraken set's the leverage as an option in the order object, so we need to + add it to params """ return @@ -156,3 +156,29 @@ 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: + """ + # ! 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( + 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/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index 51de75ea4..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 @@ -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..98e493d9b 100644 --- a/freqtrade/exchange/okex.py +++ b/freqtrade/exchange/okex.py @@ -17,29 +17,11 @@ 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 - # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported - # (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported - # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported + # TODO-lev: Uncomment once supported + # (TradingMode.MARGIN, Collateral.CROSS), + # (TradingMode.FUTURES, Collateral.CROSS), + # (TradingMode.FUTURES, Collateral.ISOLATED) ] - - @property - def _ccxt_config(self) -> Dict: - # Parameters to add directly to ccxt sync/async initialization. - if self.trading_mode == TradingMode.MARGIN: - return { - "options": { - "defaultType": "margin" - } - } - elif self.trading_mode == TradingMode.FUTURES: - return { - "options": { - "defaultType": "swap" - } - } - else: - return {} diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index aec2a6891..4fd6d9b1b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -268,12 +268,16 @@ 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( + trades = Trade.get_open_trades() + for trade in trades: + funding_fees = self.exchange.get_funding_fees( trade.pair, + trade.amount, trade.open_date ) trade.funding_fees = funding_fees + else: + return 0.0 def startup_update_open_orders(self): """ @@ -617,8 +621,9 @@ class FreqtradeBot(LoggingMixin): default_retval=stake_amount)( pair=pair, current_time=datetime.now(timezone.utc), current_rate=enter_limit_requested, proposed_stake=stake_amount, - min_stake=min_stake_amount, max_stake=max_stake_amount, side='long') - # TODO-lev: Add non-hardcoded "side" parameter + min_stake=min_stake_amount, max_stake=max_stake_amount, + side='short' if is_short else 'long' + ) stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) @@ -638,7 +643,6 @@ class FreqtradeBot(LoggingMixin): order_type = self.strategy.order_types.get('forcebuy', order_type) # TODO-lev: Will this work for shorting? - # TODO-lev: Add non-hardcoded "side" parameter if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, time_in_force=time_in_force, current_time=datetime.now(timezone.utc), @@ -703,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_from_exchange(pair, open_date) - else: - funding_fees = 0.0 + funding_fees = self.exchange.get_funding_fees(pair, amount, open_date) trade = Trade( pair=pair, @@ -922,8 +923,7 @@ class FreqtradeBot(LoggingMixin): Check if trade is fulfilled in which case the stoploss on exchange should be added immediately if stoploss on exchange is enabled. - # TODO-lev: liquidation price will always be on exchange, even though - # TODO-lev: stoploss_on_exchange might not be enabled + # TODO-lev: liquidation price always on exchange, even without stoploss_on_exchange """ logger.debug('Handling stoploss on exchange %s ...', trade) @@ -1261,6 +1261,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 + ) exit_type = 'sell' # TODO-lev: Update to exit if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): exit_type = 'stoploss' @@ -1517,7 +1522,7 @@ class FreqtradeBot(LoggingMixin): self.wallets.update() if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount: # Eat into dust if we own more than base currency - # TODO-lev: won't be in base currency for shorts + # TODO-lev: settle currency for futures logger.info(f"Fee amount for {trade} was in base currency - " f"Eating Fee {fee_abs} into dust.") elif fee_abs != 0: diff --git a/freqtrade/leverage/interest.py b/freqtrade/leverage/interest.py index 2878ad784..ff375b05e 100644 --- a/freqtrade/leverage/interest.py +++ b/freqtrade/leverage/interest.py @@ -16,18 +16,18 @@ def interest( hours: Decimal ) -> Decimal: """ - Equation to calculate interest on margin trades + Equation to calculate interest on margin trades - :param exchange_name: The exchanged being trading on - :param borrowed: The amount of currency being borrowed - :param rate: The rate of interest (i.e daily interest rate) - :param hours: The time in hours that the currency has been borrowed for + :param exchange_name: The exchanged being trading on + :param borrowed: The amount of currency being borrowed + :param rate: The rate of interest (i.e daily interest rate) + :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 + Raises: + OperationalException: Raised if freqtrade does + not support margin trading for this exchange - Returns: The amount of interest owed (currency matches borrowed) + Returns: The amount of interest owed (currency matches borrowed) """ exchange_name = exchange_name.lower() if exchange_name == "binance": diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 404cfd6d2..9ba88057c 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -30,13 +30,13 @@ _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database def init_db(db_url: str, clean_open_orders: bool = False) -> None: """ - Initializes this module with the given config, - registers all known command handlers - and starts polling for message updates - :param db_url: Database to use - :param clean_open_orders: Remove open orders from the database. - Useful for dry-run or if all orders have been reset on the exchange. - :return: None + Initializes this module with the given config, + registers all known command handlers + and starts polling for message updates + :param db_url: Database to use + :param clean_open_orders: Remove open orders from the database. + Useful for dry-run or if all orders have been reset on the exchange. + :return: None """ kwargs = {} @@ -329,8 +329,8 @@ class LocalTrade(): def _set_stop_loss(self, stop_loss: float, percent: float): """ - Method you should use to set self.stop_loss. - Assures stop_loss is not passed the liquidation price + Method you should use to set self.stop_loss. + Assures stop_loss is not passed the liquidation price """ if self.isolated_liq is not None: if self.is_short: @@ -352,8 +352,8 @@ class LocalTrade(): def set_isolated_liq(self, isolated_liq: float): """ - Method you should use to set self.liquidation price. - Assures stop_loss is not passed the liquidation price + Method you should use to set self.liquidation price. + Assures stop_loss is not passed the liquidation price """ if self.stop_loss is not None: if self.is_short: @@ -916,8 +916,8 @@ class Trade(_DECL_BASE, LocalTrade): max_rate = Column(Float, nullable=True, default=0.0) # Lowest price reached min_rate = Column(Float, nullable=True) - sell_reason = Column(String(100), nullable=True) # TODO-lev: Change to close_reason - sell_order_status = Column(String(100), nullable=True) # TODO-lev: Change to close_order_status + sell_reason = Column(String(100), nullable=True) + sell_order_status = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True) buy_tag = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 888dc0316..79b311f2b 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -32,7 +32,7 @@ class StoplossGuard(IProtection): def _reason(self) -> str: """ LockReason to use - #TODO-lev: check if min is the right word for shorts + # TODO-lev: check if min is the right word for shorts """ return (f'{self._trade_limit} stoplosses in {self._lookback_period} min, ' f'locking for {self._stop_duration} min.') diff --git a/tests/conftest.py b/tests/conftest.py index 75654a83a..74dd6f360 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2404,3 +2404,131 @@ 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 [ + [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], + ] + + +@pytest.fixture(scope='function') +def funding_rate_history_hourly(): + return [ + { + "symbol": "ADA/USDT", + "fundingRate": -0.000008, + "timestamp": 1630454400000, + "datetime": "2021-09-01T00:00:00.000Z" + }, + { + "symbol": "ADA/USDT", + "fundingRate": -0.000004, + "timestamp": 1630458000000, + "datetime": "2021-09-01T01:00:00.000Z" + }, + { + "symbol": "ADA/USDT", + "fundingRate": 0.000012, + "timestamp": 1630461600000, + "datetime": "2021-09-01T02:00:00.000Z" + }, + { + "symbol": "ADA/USDT", + "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.000003, + "timestamp": 1630483200000, + "datetime": "2021-09-01T08:00:00.000Z" + }, + { + "symbol": "ADA/USDT", + "fundingRate": 0, + "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" + }, + ] + + +@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/conftest_trades.py b/tests/conftest_trades.py index c13b482dc..dcd5e70df 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -488,7 +488,7 @@ def leverage_trade(fee): open_order_id='dry_run_leverage_buy_12368', strategy='DefaultStrategy', timeframe=5, - sell_reason='sell_signal', # TODO-lev: Update to exit/close reason + sell_reason='sell_signal', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=300), close_date=datetime.now(tz=timezone.utc), interest_rate=0.0005 diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 2f629528c..b14df070c 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,18 +48,24 @@ EXCHANGES = { 'pair': 'BTC/USDT', '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, }, } @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 +81,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 +170,25 @@ 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) + 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_tick.timestamp() * 1000)] != 0.0 + # TODO: tests fetch_trades (?) def test_ccxt_get_fee(self, exchange): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 0d033008a..12c20a7ca 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2916,6 +2916,8 @@ def test_timeframe_to_prev_date(): # Does not round time = datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc) assert timeframe_to_prev_date('5m', time) == time + time = datetime(2019, 8, 12, 13, 0, 0, tzinfo=timezone.utc) + assert timeframe_to_prev_date('1h', time) == time def test_timeframe_to_next_date(): @@ -3097,7 +3099,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=[ { @@ -3140,11 +3142,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 ) @@ -3157,7 +3159,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 @@ -3292,16 +3294,16 @@ def test_validate_trading_mode_and_collateral( ("binance", "spot", {}), ("binance", "margin", {"options": {"defaultType": "margin"}}), ("binance", "futures", {"options": {"defaultType": "future"}}), - ("kraken", "spot", {}), - ("kraken", "margin", {}), - ("kraken", "futures", {}), - ("ftx", "spot", {}), - ("ftx", "margin", {}), - ("ftx", "futures", {}), - ("bittrex", "spot", {}), - ("gateio", "spot", {}), - ("gateio", "margin", {"options": {"defaultType": "margin"}}), + ("bibox", "spot", {"has": {"fetchCurrencies": False}}), + ("bibox", "margin", {"has": {"fetchCurrencies": False}, "options": {"defaultType": "margin"}}), + ("bibox", "futures", {"has": {"fetchCurrencies": False}, "options": {"defaultType": "swap"}}), + ("bybit", "futures", {"options": {"defaultType": "linear"}}), + ("ftx", "futures", {"options": {"defaultType": "swap"}}), ("gateio", "futures", {"options": {"defaultType": "swap"}}), + ("hitbtc", "futures", {"options": {"defaultType": "swap"}}), + ("kraken", "futures", {"options": {"defaultType": "swap"}}), + ("kucoin", "futures", {"options": {"defaultType": "swap"}}), + ("okex", "futures", {"options": {"defaultType": "swap"}}), ]) def test__ccxt_config( default_conf, @@ -3327,3 +3329,226 @@ 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 + + +@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, + size, + funding_rate, + mark_price, + funding_fee, + kraken_fee, + time_in_ratio +): + exchange = get_patched_exchange(mocker, default_conf) + 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 + + +def test__get_mark_price_history(mocker, default_conf, mark_ohlcv): + api_mock = MagicMock() + api_mock.fetch_ohlcv = MagicMock(return_value=mark_ohlcv) + type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) + + # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + mark_prices = exchange._get_mark_price_history("ADA/USDT", 1630454400000) + assert mark_prices == { + 1630454400000: 2.77, + 1630458000000: 2.73, + 1630461600000: 2.74, + 1630465200000: 2.76, + 1630468800000: 2.76, + 1630472400000: 2.77, + 1630476000000: 2.78, + 1630479600000: 2.78, + 1630483200000: 2.77, + 1630486800000: 2.77, + 1630490400000: 2.84, + 1630494000000: 2.81, + 1630497600000: 2.81, + 1630501200000: 2.82, + } + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "binance", + "_get_mark_price_history", + "fetch_ohlcv", + pair="ADA/USDT", + since=1635580800001 + ) + + +def test_get_funding_rate_history(mocker, default_conf, funding_rate_history_hourly): + api_mock = MagicMock() + api_mock.fetch_funding_rate_history = MagicMock(return_value=funding_rate_history_hourly) + type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True}) + + # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + funding_rates = exchange.get_funding_rate_history('ADA/USDT', 1635580800001) + + assert funding_rates == { + 1630454400000: -0.000008, + 1630458000000: -0.000004, + 1630461600000: 0.000012, + 1630465200000: -0.000003, + 1630468800000: -0.000007, + 1630472400000: 0.000003, + 1630476000000: 0.000019, + 1630479600000: 0.000003, + 1630483200000: -0.000003, + 1630486800000: 0, + 1630490400000: 0.000013, + 1630494000000: 0.000077, + 1630497600000: 0.000072, + 1630501200000: 0.000097, + } + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "binance", + "get_funding_rate_history", + "fetch_funding_rate_history", + pair="ADA/USDT", + since=1630454400000 + ) + + +@pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [ + ('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), + # ('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', 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', 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_hourly, + funding_rate_history_octohourly, + rate_start, + rate_end, + mark_ohlcv, + exchange, + d1, + d2, + amount, + expected_fees +): + ''' + nominal_value = mark_price * size + funding_fee = nominal_value * funding_rate + 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 + + 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') + 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) + 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 + + +@ 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_octohourly, + mark_ohlcv, + exchange, + time_machine, + 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_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=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 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a3788872f..e0e73f6f1 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -586,10 +586,10 @@ def test_api_trades(botclient, mocker, fee, markets, is_short): assert rc.json()['total_trades'] == 2 -# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) -def test_api_trade_single(botclient, mocker, fee, ticker, markets): +@pytest.mark.parametrize('is_short', [True, False]) +def test_api_trade_single(botclient, mocker, fee, ticker, markets, is_short): ftbot, client = botclient - patch_get_signal(ftbot) + patch_get_signal(ftbot, enter_long=not is_short, enter_short=is_short) mocker.patch.multiple( 'freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), @@ -599,7 +599,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets): assert_response(rc, 404) assert rc.json()['detail'] == 'Trade not found.' - create_mock_trades(fee, False) + create_mock_trades(fee, is_short=is_short) Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/trade/3") @@ -607,10 +607,10 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets): assert rc.json()['trade_id'] == 3 -# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) -def test_api_delete_trade(botclient, mocker, fee, markets): +@pytest.mark.parametrize('is_short', [True, False]) +def test_api_delete_trade(botclient, mocker, fee, markets, is_short): ftbot, client = botclient - patch_get_signal(ftbot) + patch_get_signal(ftbot, enter_long=not is_short, enter_short=is_short) stoploss_mock = MagicMock() cancel_mock = MagicMock() mocker.patch.multiple( @@ -749,10 +749,10 @@ def test_api_profit(botclient, mocker, ticker, fee, markets): } -# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) -def test_api_stats(botclient, mocker, ticker, fee, markets,): +@pytest.mark.parametrize('is_short', [True, False]) +def test_api_stats(botclient, mocker, ticker, fee, markets, is_short): ftbot, client = botclient - patch_get_signal(ftbot) + patch_get_signal(ftbot, enter_long=not is_short, enter_short=is_short) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), @@ -766,7 +766,7 @@ def test_api_stats(botclient, mocker, ticker, fee, markets,): assert 'durations' in rc.json() assert 'sell_reasons' in rc.json() - create_mock_trades(fee, False) + create_mock_trades(fee, is_short=is_short) rc = client_get(client, f"{BASE_URI}/stats") assert_response(rc, 200) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index d88c7b24e..323eddd4d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1294,8 +1294,8 @@ def test_telegram_trades(mocker, update, default_conf, fee): msg_mock.call_args_list[0][0][0])) -# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) -def test_telegram_delete_trade(mocker, update, default_conf, fee): +@pytest.mark.parametrize('is_short', [True, False]) +def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short): telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) context = MagicMock() @@ -1305,7 +1305,7 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee): assert "Trade-id not set." in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() - create_mock_trades(fee, False) + create_mock_trades(fee, is_short) context = MagicMock() context.args = [1] diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index a995491f2..ff2ce10a7 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -1,5 +1,6 @@ from datetime import datetime +import pytest from pandas import DataFrame from freqtrade.persistence.models import Trade @@ -7,7 +8,7 @@ from freqtrade.persistence.models import Trade from .strats.strategy_test_v3 import StrategyTestV3 -def test_strategy_test_v2_structure(): +def test_strategy_test_v3_structure(): assert hasattr(StrategyTestV3, 'minimal_roi') assert hasattr(StrategyTestV3, 'stoploss') assert hasattr(StrategyTestV3, 'timeframe') @@ -16,7 +17,11 @@ def test_strategy_test_v2_structure(): assert hasattr(StrategyTestV3, 'populate_sell_trend') -def test_strategy_test_v2(result, fee): +@pytest.mark.parametrize('is_short,side', [ + (True, 'short'), + (False, 'long'), +]) +def test_strategy_test_v3(result, fee, is_short, side): strategy = StrategyTestV3({}) metadata = {'pair': 'ETH/BTC'} @@ -32,16 +37,18 @@ def test_strategy_test_v2(result, fee): open_rate=19_000, amount=0.1, pair='ETH/BTC', - fee_open=fee.return_value + fee_open=fee.return_value, + is_short=is_short ) assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1, rate=20000, time_in_force='gtc', - current_time=datetime.utcnow(), side='long') is True + current_time=datetime.utcnow(), + side=side) is True assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, rate=20000, time_in_force='gtc', sell_reason='roi', - current_time=datetime.utcnow()) is True + current_time=datetime.utcnow(), + side=side) is True - # TODO-lev: Test for shorts? assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), current_rate=20_000, current_profit=0.05) == strategy.stoploss diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 385c360a0..ccade9b2e 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -906,7 +906,6 @@ def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_order pair = 'ETH/USDT' freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=ValueError) - # TODO-lev: KeyError happens on short, why? assert freqtrade.execute_entry(pair, stake_amount) limit_order[enter_side(is_short)]['id'] = '222' @@ -1228,7 +1227,6 @@ def test_create_stoploss_order_insufficient_funds( def test_handle_stoploss_on_exchange_trailing( mocker, default_conf_usdt, fee, is_short, bid, ask, limit_order, stop_price, amt, hang_price ) -> None: - # TODO-lev: test for short # When trailing stoploss is set enter_order = limit_order[enter_side(is_short)] exit_order = limit_order[exit_side(is_short)] @@ -1435,7 +1433,6 @@ def test_handle_stoploss_on_exchange_custom_stop( enter_order = limit_order[enter_side(is_short)] exit_order = limit_order[exit_side(is_short)] # When trailing stoploss is set - # TODO-lev: test for short stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) mocker.patch.multiple( @@ -2973,9 +2970,11 @@ def test_execute_trade_exit_custom_exit_price( # Set a custom exit price freqtrade.strategy.custom_exit_price = lambda **kwargs: 2.25 - # TODO-lev: side="buy" - freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_type=SellType.SELL_SIGNAL)) + freqtrade.execute_trade_exit( + trade=trade, + limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], + sell_reason=SellCheckTuple(sell_type=SellType.SELL_SIGNAL) + ) # Sell price must be different to default bid price @@ -3098,7 +3097,6 @@ def test_execute_trade_exit_sloe_cancel_exception( freqtrade.config['dry_run'] = False trade.stoploss_order_id = "abcd" - # TODO-lev: side="buy" freqtrade.execute_trade_exit(trade=trade, limit=1234, sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert create_order_mock.call_count == 2 @@ -3152,9 +3150,11 @@ def test_execute_trade_exit_with_stoploss_on_exchange( fetch_ticker=ticker_usdt_sell_up ) - # TODO-lev: side="buy" - freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) + freqtrade.execute_trade_exit( + trade=trade, + limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS) + ) trade = Trade.query.first() trade.is_short = is_short @@ -3288,7 +3288,6 @@ def test_execute_trade_exit_market_order( ) freqtrade.config['order_types']['sell'] = 'market' - # TODO-lev: side="buy" freqtrade.execute_trade_exit( trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], @@ -3354,9 +3353,11 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u ) sell_reason = SellCheckTuple(sell_type=SellType.ROI) - # TODO-lev: side="buy" - assert not freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'], - sell_reason=sell_reason) + assert not freqtrade.execute_trade_exit( + trade=trade, + limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], + sell_reason=sell_reason + ) assert mock_insuf.call_count == 1 @@ -3517,9 +3518,11 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee, fetch_ticker=ticker_usdt_sell_down ) - # TODO-lev: side="buy" - freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_down()['bid'], - sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) + freqtrade.execute_trade_exit( + trade=trade, + limit=ticker_usdt_sell_down()['ask' if is_short else 'bid'], + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS) + ) trade.close(ticker_usdt_sell_down()['bid']) assert freqtrade.strategy.is_pair_locked(trade.pair) @@ -4697,8 +4700,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) @@ -4713,3 +4716,159 @@ 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 + + +@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, + ticker_usdt_sell_up, + is_short, + limit_order_open, + schedule_off +): + ''' + 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") + + 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) + patch_RPCManager(mocker) + patch_exchange(mocker) + default_conf['trading_mode'] = 'futures' + default_conf['collateral'] = 'isolated' + default_conf['dry_run'] = True + timestamp_midnight = 1630454400000 + timestamp_eight = 1630483200000 + funding_rates_midnight = { + "LTC/BTC": { + timestamp_midnight: 0.00032583, + }, + "ETH/BTC": { + timestamp_midnight: 0.0001, + }, + "XRP/BTC": { + timestamp_midnight: 0.00049426, + } + } + + funding_rates_eight = { + "LTC/BTC": { + timestamp_midnight: 0.00032583, + timestamp_eight: 0.00024472, + }, + "ETH/BTC": { + timestamp_midnight: 0.0001, + timestamp_eight: 0.0001, + }, + "XRP/BTC": { + timestamp_midnight: 0.00049426, + timestamp_eight: 0.00032715, + } + } + + mark_prices = { + "LTC/BTC": { + timestamp_midnight: 3.3, + timestamp_eight: 3.2, + }, + "ETH/BTC": { + timestamp_midnight: 2.4, + timestamp_eight: 2.5, + }, + "XRP/BTC": { + timestamp_midnight: 1.2, + timestamp_eight: 1.2, + } + } + + mocker.patch( + 'freqtrade.exchange.Exchange._get_mark_price_history', + side_effect=lambda pair, since: mark_prices[pair] + ) + + mocker.patch( + 'freqtrade.exchange.Exchange.get_funding_rate_history', + side_effect=lambda pair, since: funding_rates_midnight[pair] + ) + + 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) + + # 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][timestamp_midnight] * + funding_rates_midnight[trade.pair][timestamp_midnight] + ) + 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][timestamp_midnight] * + funding_rates_eight[trade.pair][timestamp_midnight] + ) + 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: + assert trade.funding_fees == sum([ + trade.amount * + mark_prices[trade.pair][time] * + funding_rates_eight[trade.pair][time] for time in mark_prices[trade.pair].keys() + ]) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 9dd5b175b..2d4a7406f 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1907,12 +1907,12 @@ def test_get_total_closed_profit(fee, use_db): @pytest.mark.usefixtures("init_persistence") -# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) +@pytest.mark.parametrize('is_short', [True, False]) @pytest.mark.parametrize('use_db', [True, False]) -def test_get_trades_proxy(fee, use_db): +def test_get_trades_proxy(fee, use_db, is_short): Trade.use_db = use_db Trade.reset_trades() - create_mock_trades(fee, False, use_db) + create_mock_trades(fee, is_short, use_db) trades = Trade.get_trades_proxy() assert len(trades) == 6 @@ -2042,48 +2042,48 @@ def test_update_order_from_ccxt(caplog): @pytest.mark.usefixtures("init_persistence") -# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) -def test_select_order(fee): - create_mock_trades(fee, False) +@pytest.mark.parametrize('is_short', [True, False]) +def test_select_order(fee, is_short): + create_mock_trades(fee, is_short) trades = Trade.get_trades().all() # Open buy order, no sell order - order = trades[0].select_order('buy', True) + order = trades[0].select_order(trades[0].enter_side, True) assert order is None - order = trades[0].select_order('buy', False) + order = trades[0].select_order(trades[0].enter_side, False) assert order is not None - order = trades[0].select_order('sell', None) + order = trades[0].select_order(trades[0].exit_side, None) assert order is None # closed buy order, and open sell order - order = trades[1].select_order('buy', True) + order = trades[1].select_order(trades[1].enter_side, True) assert order is None - order = trades[1].select_order('buy', False) + order = trades[1].select_order(trades[1].enter_side, False) assert order is not None - order = trades[1].select_order('buy', None) + order = trades[1].select_order(trades[1].enter_side, None) assert order is not None - order = trades[1].select_order('sell', True) + order = trades[1].select_order(trades[1].exit_side, True) assert order is None - order = trades[1].select_order('sell', False) + order = trades[1].select_order(trades[1].exit_side, False) assert order is not None # Has open buy order - order = trades[3].select_order('buy', True) + order = trades[3].select_order(trades[3].enter_side, True) assert order is not None - order = trades[3].select_order('buy', False) + order = trades[3].select_order(trades[3].enter_side, False) assert order is None # Open sell order - order = trades[4].select_order('buy', True) + order = trades[4].select_order(trades[4].enter_side, True) assert order is None - order = trades[4].select_order('buy', False) + order = trades[4].select_order(trades[4].enter_side, False) assert order is not None - order = trades[4].select_order('sell', True) + order = trades[4].select_order(trades[4].exit_side, True) assert order is not None assert order.ft_order_side == 'stoploss' - order = trades[4].select_order('sell', False) + order = trades[4].select_order(trades[4].exit_side, False) assert order is None