Added validating checks for trading_mode and collateral on each exchange

This commit is contained in:
Sam Germain 2021-09-04 21:55:55 -06:00
parent 7eab855476
commit 23ba49fec2
5 changed files with 133 additions and 23 deletions

View File

@ -1,9 +1,10 @@
""" Binance exchange subclass """ """ Binance exchange subclass """
import logging import logging
from typing import Dict, Optional from typing import Dict, List, Optional, Tuple
import ccxt import ccxt
from freqtrade.enums import Collateral, TradingMode
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -24,6 +25,13 @@ class Binance(Exchange):
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], "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: def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
""" """
Verify stop_loss against stoploss-order value (limit or price) Verify stop_loss against stoploss-order value (limit or price)

View File

@ -22,7 +22,7 @@ from pandas import DataFrame
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES,
ListPairsWithTimeframes) ListPairsWithTimeframes)
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list 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, from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
InvalidOrderException, OperationalException, PricingError, InvalidOrderException, OperationalException, PricingError,
RetryableOrderError, TemporaryError) RetryableOrderError, TemporaryError)
@ -73,6 +73,10 @@ class Exchange:
_leverage_brackets: Dict = {} _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: def __init__(self, config: Dict[str, Any], validate: bool = True) -> None:
""" """
Initializes this module with the given config, Initializes this module with the given config,
@ -138,6 +142,26 @@ class Exchange:
self._api_async = self._init_ccxt( self._api_async = self._init_ccxt(
exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) 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) logger.info('Using Exchange "%s"', self.name)
if validate: if validate:
@ -155,21 +179,11 @@ class Exchange:
self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_order_time_in_force(config.get('order_time_in_force', {}))
self.validate_required_startup_candles(config.get('startup_candle_count', 0), self.validate_required_startup_candles(config.get('startup_candle_count', 0),
config.get('timeframe', '')) config.get('timeframe', ''))
self.validate_trading_mode_and_collateral(trading_mode, collateral)
# Converts the interval provided in minutes in config to seconds # Converts the interval provided in minutes in config to seconds
self.markets_refresh_interval: int = exchange_config.get( self.markets_refresh_interval: int = exchange_config.get(
"markets_refresh_interval", 60) * 60 "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): def __del__(self):
""" """
Destructor - clean up async stuff Destructor - clean up async stuff
@ -376,7 +390,7 @@ class Exchange:
raise OperationalException( raise OperationalException(
'Could not load markets, therefore cannot start. ' 'Could not load markets, therefore cannot start. '
'Please investigate the above error for more details.' 'Please investigate the above error for more details.'
) )
quote_currencies = self.get_quote_currencies() quote_currencies = self.get_quote_currencies()
if stake_currency not in quote_currencies: if stake_currency not in quote_currencies:
raise OperationalException( raise OperationalException(
@ -488,6 +502,25 @@ class Exchange:
f"This strategy requires {startup_candles} candles to start. " f"This strategy requires {startup_candles} candles to start. "
f"{self.name} only provides {candle_limit} for {timeframe}.") 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: def exchange_has(self, endpoint: str) -> bool:
""" """
Checks if exchange implements a specific API endpoint. Checks if exchange implements a specific API endpoint.

View File

@ -1,9 +1,10 @@
""" FTX exchange subclass """ """ FTX exchange subclass """
import logging import logging
from typing import Any, Dict, Optional from typing import Any, Dict, List, Optional, Tuple
import ccxt import ccxt
from freqtrade.enums import Collateral, TradingMode
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -21,6 +22,12 @@ class Ftx(Exchange):
"ohlcv_candle_limit": 1500, "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: def market_is_tradable(self, market: Dict[str, Any]) -> bool:
""" """
Check if the market symbol is tradable by Freqtrade. Check if the market symbol is tradable by Freqtrade.

View File

@ -1,9 +1,10 @@
""" Kraken exchange subclass """ """ Kraken exchange subclass """
import logging import logging
from typing import Any, Dict, Optional from typing import Any, Dict, List, Optional, Tuple
import ccxt import ccxt
from freqtrade.enums import Collateral, TradingMode
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -23,6 +24,12 @@ class Kraken(Exchange):
"trades_pagination_arg": "since", "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: def market_is_tradable(self, market: Dict[str, Any]) -> bool:
""" """
Check if the market symbol is tradable by Freqtrade. Check if the market symbol is tradable by Freqtrade.
@ -33,7 +40,7 @@ class Kraken(Exchange):
return (parent_check and return (parent_check and
market.get('darkpool', False) is False) market.get('darkpool', False) is False)
@retrier @ retrier
def get_balances(self) -> dict: def get_balances(self) -> dict:
if self._config['dry_run']: if self._config['dry_run']:
return {} return {}
@ -48,8 +55,8 @@ class Kraken(Exchange):
orders = self._api.fetch_open_orders() orders = self._api.fetch_open_orders()
order_list = [(x["symbol"].split("/")[0 if x["side"] == "sell" else 1], order_list = [(x["symbol"].split("/")[0 if x["side"] == "sell" else 1],
x["remaining"] if x["side"] == "sell" else x["remaining"] * x["price"], x["remaining"] if x["side"] == "sell" else x["remaining"] * x["price"],
# Don't remove the below comment, this can be important for debugging # Don't remove the below comment, this can be important for debugging
# x["side"], x["amount"], # x["side"], x["amount"],
) for x in orders] ) for x in orders]
for bal in balances: for bal in balances:
@ -77,7 +84,7 @@ class Kraken(Exchange):
(side == "buy" and stop_loss < float(order['price'])) (side == "buy" and stop_loss < float(order['price']))
)) ))
@retrier(retries=0) @ retrier(retries=0)
def stoploss(self, pair: str, amount: float, def stoploss(self, pair: str, amount: float,
stop_price: float, order_types: Dict, side: str) -> Dict: stop_price: float, order_types: Dict, side: str) -> Dict:
""" """

View File

@ -10,7 +10,7 @@ import ccxt
import pytest import pytest
from pandas import DataFrame from pandas import DataFrame
from freqtrade.enums import Collateral from freqtrade.enums import Collateral, TradingMode
from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException,
OperationalException, PricingError, TemporaryError) OperationalException, PricingError, TemporaryError)
from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken 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("exchange_name", [("binance"), ("ftx"), ("kraken")])
@pytest.mark.parametrize("maker_or_taker", [("maker"), ("taker")]) @pytest.mark.parametrize("maker_or_taker", [("maker"), ("taker")])
@pytest.mark.parametrize("is_short", [(True), (False)]) @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() # 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() # api_mock.get_interest_rate = MagicMock()
# type(api_mock).has = PropertyMock(return_value={'getInterestRate': True}) # 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", pair="XRP/USDT",
collateral=collateral 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)