Added validating checks for trading_mode and collateral on each exchange
This commit is contained in:
parent
7eab855476
commit
23ba49fec2
@ -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)
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user