From 23ba49fec2de9fa00728bc70bb8245fc98a542a4 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 4 Sep 2021 21:55:55 -0600 Subject: [PATCH] Added validating checks for trading_mode and collateral on each exchange --- freqtrade/exchange/binance.py | 10 +++++- freqtrade/exchange/exchange.py | 59 ++++++++++++++++++++++++------- freqtrade/exchange/ftx.py | 9 ++++- freqtrade/exchange/kraken.py | 17 ++++++--- tests/exchange/test_exchange.py | 61 +++++++++++++++++++++++++++++++-- 5 files changed, 133 insertions(+), 23 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 3721136ea..d1506cb6f 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,9 +1,10 @@ """ Binance exchange subclass """ import logging -from typing import Dict, Optional +from typing import Dict, List, Optional, Tuple import ccxt +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -24,6 +25,13 @@ class Binance(Exchange): "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } + _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 + ] + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 06538314e..9abdc9b0b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -22,7 +22,7 @@ from pandas import DataFrame from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, ListPairsWithTimeframes) from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list -from freqtrade.enums import Collateral +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, RetryableOrderError, TemporaryError) @@ -73,6 +73,10 @@ class Exchange: _leverage_brackets: Dict = {} + _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ + # TradingMode.SPOT always supported and not required in this list + ] + def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: """ Initializes this module with the given config, @@ -138,6 +142,26 @@ class Exchange: self._api_async = self._init_ccxt( exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) + trading_mode: TradingMode = ( + TradingMode(config.get('trading_mode')) + if config.get('trading_mode') + else TradingMode.SPOT + ) + collateral: Optional[Collateral] = ( + Collateral(config.get('collateral')) + if config.get('collateral') + else None + ) + + if trading_mode != TradingMode.SPOT: + try: + # TODO-lev: This shouldn't need to happen, but for some reason I get that the + # TODO-lev: method isn't implemented + self.fill_leverage_brackets() + except Exception as error: + logger.debug(error) + logger.debug("Could not load leverage_brackets") + logger.info('Using Exchange "%s"', self.name) if validate: @@ -155,21 +179,11 @@ class Exchange: self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_required_startup_candles(config.get('startup_candle_count', 0), config.get('timeframe', '')) - + self.validate_trading_mode_and_collateral(trading_mode, collateral) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( "markets_refresh_interval", 60) * 60 - leverage = config.get('leverage_mode') - if leverage is not False: - try: - # TODO-lev: This shouldn't need to happen, but for some reason I get that the - # TODO-lev: method isn't implemented - self.fill_leverage_brackets() - except Exception as error: - logger.debug(error) - logger.debug("Could not load leverage_brackets") - def __del__(self): """ Destructor - clean up async stuff @@ -376,7 +390,7 @@ class Exchange: raise OperationalException( 'Could not load markets, therefore cannot start. ' 'Please investigate the above error for more details.' - ) + ) quote_currencies = self.get_quote_currencies() if stake_currency not in quote_currencies: raise OperationalException( @@ -488,6 +502,25 @@ class Exchange: f"This strategy requires {startup_candles} candles to start. " f"{self.name} only provides {candle_limit} for {timeframe}.") + def validate_trading_mode_and_collateral( + self, + trading_mode: TradingMode, + 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 + """ + if trading_mode != TradingMode.SPOT and ( + (trading_mode, collateral) not in self._supported_trading_mode_collateral_pairs + ): + collateral_value = collateral and collateral.value + raise OperationalException( + f"Freqtrade does not support {collateral_value} {trading_mode.value} on {self.name}" + ) + def exchange_has(self, endpoint: str) -> bool: """ Checks if exchange implements a specific API endpoint. diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 1dc30002e..3e6ff01a3 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,9 +1,10 @@ """ FTX exchange subclass """ import logging -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional, Tuple import ccxt +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -21,6 +22,12 @@ class Ftx(Exchange): "ohlcv_candle_limit": 1500, } + _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 + ] + def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ Check if the market symbol is tradable by Freqtrade. diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 567bd6735..7c36c421b 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -1,9 +1,10 @@ """ Kraken exchange subclass """ import logging -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional, Tuple import ccxt +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -23,6 +24,12 @@ class Kraken(Exchange): "trades_pagination_arg": "since", } + _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 + ] + def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ Check if the market symbol is tradable by Freqtrade. @@ -33,7 +40,7 @@ class Kraken(Exchange): return (parent_check and market.get('darkpool', False) is False) - @retrier + @ retrier def get_balances(self) -> dict: if self._config['dry_run']: return {} @@ -48,8 +55,8 @@ class Kraken(Exchange): orders = self._api.fetch_open_orders() order_list = [(x["symbol"].split("/")[0 if x["side"] == "sell" else 1], - x["remaining"] if x["side"] == "sell" else x["remaining"] * x["price"], - # Don't remove the below comment, this can be important for debugging + x["remaining"] if x["side"] == "sell" else x["remaining"] * x["price"], + # Don't remove the below comment, this can be important for debugging # x["side"], x["amount"], ) for x in orders] for bal in balances: @@ -77,7 +84,7 @@ class Kraken(Exchange): (side == "buy" and stop_loss < float(order['price'])) )) - @retrier(retries=0) + @ retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, side: str) -> Dict: """ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 729cdf373..a1d417b0a 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -10,7 +10,7 @@ import ccxt import pytest from pandas import DataFrame -from freqtrade.enums import Collateral +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken @@ -3027,10 +3027,16 @@ def test_get_interest_rate( @pytest.mark.parametrize("exchange_name", [("binance"), ("ftx"), ("kraken")]) @pytest.mark.parametrize("maker_or_taker", [("maker"), ("taker")]) @pytest.mark.parametrize("is_short", [(True), (False)]) -def test_get_interest_rate_exceptions(mocker, default_conf, exchange_name, maker_or_taker, is_short): +def test_get_interest_rate_exceptions( + mocker, + default_conf, + exchange_name, + maker_or_taker, + is_short +): # api_mock = MagicMock() - # # TODO-lev: get_interest_rate currently not implemented on CCXT, so this may need to be renamed + # # TODO-lev: get_interest_rate currently not implemented on CCXT, so this may be renamed # api_mock.get_interest_rate = MagicMock() # type(api_mock).has = PropertyMock(return_value={'getInterestRate': True}) @@ -3092,3 +3098,52 @@ def test_set_margin_mode(mocker, default_conf, exchange_name, collateral): pair="XRP/USDT", collateral=collateral ) + + +@pytest.mark.parametrize("exchange_name, trading_mode, collateral, exception_thrown", [ + ("binance", TradingMode.SPOT, None, False), + ("binance", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("kraken", TradingMode.SPOT, None, False), + ("kraken", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("kraken", TradingMode.FUTURES, Collateral.ISOLATED, True), + ("ftx", TradingMode.SPOT, None, False), + ("ftx", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("ftx", TradingMode.FUTURES, Collateral.ISOLATED, True), + ("bittrex", TradingMode.SPOT, None, False), + ("bittrex", TradingMode.MARGIN, Collateral.CROSS, True), + ("bittrex", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("bittrex", TradingMode.FUTURES, Collateral.CROSS, True), + ("bittrex", TradingMode.FUTURES, Collateral.ISOLATED, True), + + # TODO-lev: Remove once implemented + ("binance", TradingMode.MARGIN, Collateral.CROSS, True), + ("binance", TradingMode.FUTURES, Collateral.CROSS, True), + ("binance", TradingMode.FUTURES, Collateral.ISOLATED, True), + ("kraken", TradingMode.MARGIN, Collateral.CROSS, True), + ("kraken", TradingMode.FUTURES, Collateral.CROSS, True), + ("ftx", TradingMode.MARGIN, Collateral.CROSS, True), + ("ftx", TradingMode.FUTURES, Collateral.CROSS, True), + + # TODO-lev: Uncomment once implemented + # ("binance", TradingMode.MARGIN, Collateral.CROSS, False), + # ("binance", TradingMode.FUTURES, Collateral.CROSS, False), + # ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), + # ("kraken", TradingMode.MARGIN, Collateral.CROSS, False), + # ("kraken", TradingMode.FUTURES, Collateral.CROSS, False), + # ("ftx", TradingMode.MARGIN, Collateral.ISOLATED, False), + # ("ftx", TradingMode.FUTURES, Collateral.ISOLATED, False) +]) +def test_validate_trading_mode_and_collateral( + default_conf, + mocker, + exchange_name, + trading_mode, + collateral, + exception_thrown +): + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + if (exception_thrown): + with pytest.raises(OperationalException): + exchange.validate_trading_mode_and_collateral(trading_mode, collateral) + else: + exchange.validate_trading_mode_and_collateral(trading_mode, collateral)