Introduce BaseExchange class

This commit is contained in:
hroff-1902 2019-10-12 11:48:56 +03:00
parent 31389b38f1
commit b556891bf2
4 changed files with 132 additions and 110 deletions

View File

@ -1,3 +1,4 @@
from freqtrade.exchange.exchange import BaseExchange # noqa: F401
from freqtrade.exchange.exchange import Exchange # noqa: F401 from freqtrade.exchange.exchange import Exchange # noqa: F401
from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401 from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401
is_exchange_bad, is_exchange_bad,

View File

@ -139,9 +139,121 @@ def retrier(f):
return wrapper return wrapper
class Exchange: class BaseExchange:
_config: Dict = {} _config: Dict = {}
def __init__(self, config: dict) -> None:
"""
Initializes this module with the given config,
it does basic validation whether the specified exchange and pairs are valid.
:return: None
"""
self._api: ccxt.Exchange = None
self._api_async: ccxt_async.Exchange = None
self._config.update(config)
exchange_config = config['exchange']
# Initialize ccxt objects
self._api = self._init_ccxt(
exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config'))
self._api_async = self._init_ccxt(
exchange_config, ccxt_async, ccxt_kwargs=exchange_config.get('ccxt_async_config'))
logger.info('Using Exchange "%s"', self.name)
# Check if timeframes is available
self.validate_timeframes()
def _init_ccxt(self, exchange_config: dict, ccxt_module=ccxt,
ccxt_kwargs: dict = None) -> ccxt.Exchange:
"""
Initialize ccxt with given config and return valid
ccxt instance.
"""
# Find matching class for the given exchange name
name = exchange_config['name']
if not is_exchange_known_ccxt(name, ccxt_module):
raise OperationalException(f'Exchange {name} is not supported by ccxt')
ex_config = {
'apiKey': exchange_config.get('key'),
'secret': exchange_config.get('secret'),
'password': exchange_config.get('password'),
'uid': exchange_config.get('uid', ''),
}
if ccxt_kwargs:
logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
ex_config.update(ccxt_kwargs)
try:
api = getattr(ccxt_module, name.lower())(ex_config)
except (KeyError, AttributeError) as e:
raise OperationalException(f'Exchange {name} is not supported') from e
except ccxt.BaseError as e:
raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e
self.set_sandbox(api, exchange_config, name)
return api
def __del__(self):
"""
Destructor - clean up async stuff
"""
logger.debug("Exchange object destroyed, closing async loop")
if self._api_async and inspect.iscoroutinefunction(self._api_async.close):
asyncio.get_event_loop().run_until_complete(self._api_async.close())
@property
def name(self) -> str:
"""exchange Name (from ccxt)"""
return self._api.name
@property
def id(self) -> str:
"""exchange ccxt id"""
return self._api.id
def set_sandbox(self, api, exchange_config: dict, name: str):
if exchange_config.get('sandbox'):
if api.urls.get('test'):
api.urls['api'] = api.urls['test']
logger.info("Enabled Sandbox API on %s", name)
else:
logger.warning(name, "No Sandbox URL in CCXT, exiting. "
"Please check your config.json")
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
def exchange_has(self, endpoint: str) -> bool:
"""
Checks if exchange implements a specific API endpoint.
Wrapper around ccxt 'has' attribute
:param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers')
:return: bool
"""
return endpoint in self._api.has and self._api.has[endpoint]
@property
def timeframes(self) -> List[str]:
return list((self._api.timeframes or {}).keys())
def validate_timeframes(self) -> None:
if not hasattr(self._api, "timeframes") or self._api.timeframes is None:
# If timeframes attribute is missing (or is None), the exchange probably
# has no fetchOHLCV method.
# Therefore we also show that.
raise OperationalException(
f"The ccxt library does not provide the list of timeframes "
f"for the exchange \"{self.name}\" and this exchange "
f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}")
class Exchange(BaseExchange):
_params: Dict = {} _params: Dict = {}
# Dict to specify which options each exchange implements # Dict to specify which options each exchange implements
@ -161,10 +273,8 @@ class Exchange:
it does basic validation whether the specified exchange and pairs are valid. it does basic validation whether the specified exchange and pairs are valid.
:return: None :return: None
""" """
self._api: ccxt.Exchange = None
self._api_async: ccxt_async.Exchange = None
self._config.update(config) BaseExchange.__init__(self, config)
self._cached_ticker: Dict[str, Any] = {} self._cached_ticker: Dict[str, Any] = {}
@ -195,16 +305,8 @@ class Exchange:
self._ohlcv_candle_limit = self._ft_has['ohlcv_candle_limit'] self._ohlcv_candle_limit = self._ft_has['ohlcv_candle_limit']
self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle'] self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle']
# Initialize ccxt objects
self._api = self._init_ccxt(
exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config'))
self._api_async = self._init_ccxt(
exchange_config, ccxt_async, ccxt_kwargs=exchange_config.get('ccxt_async_config'))
logger.info('Using Exchange "%s"', self.name)
# Check if timeframe is available # Check if timeframe is available
self.validate_timeframes(config.get('ticker_interval')) self.validate_timeframe(config.get('ticker_interval'))
# 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(
@ -217,61 +319,6 @@ class Exchange:
self.validate_ordertypes(config.get('order_types', {})) self.validate_ordertypes(config.get('order_types', {}))
self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_order_time_in_force(config.get('order_time_in_force', {}))
def __del__(self):
"""
Destructor - clean up async stuff
"""
logger.debug("Exchange object destroyed, closing async loop")
if self._api_async and inspect.iscoroutinefunction(self._api_async.close):
asyncio.get_event_loop().run_until_complete(self._api_async.close())
def _init_ccxt(self, exchange_config: dict, ccxt_module=ccxt,
ccxt_kwargs: dict = None) -> ccxt.Exchange:
"""
Initialize ccxt with given config and return valid
ccxt instance.
"""
# Find matching class for the given exchange name
name = exchange_config['name']
if not is_exchange_known_ccxt(name, ccxt_module):
raise OperationalException(f'Exchange {name} is not supported by ccxt')
ex_config = {
'apiKey': exchange_config.get('key'),
'secret': exchange_config.get('secret'),
'password': exchange_config.get('password'),
'uid': exchange_config.get('uid', ''),
}
if ccxt_kwargs:
logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
ex_config.update(ccxt_kwargs)
try:
api = getattr(ccxt_module, name.lower())(ex_config)
except (KeyError, AttributeError) as e:
raise OperationalException(f'Exchange {name} is not supported') from e
except ccxt.BaseError as e:
raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e
self.set_sandbox(api, exchange_config, name)
return api
@property
def name(self) -> str:
"""exchange Name (from ccxt)"""
return self._api.name
@property
def id(self) -> str:
"""exchange ccxt id"""
return self._api.id
@property
def timeframes(self) -> List[str]:
return list((self._api.timeframes or {}).keys())
@property @property
def markets(self) -> Dict: def markets(self) -> Dict:
"""exchange ccxt markets""" """exchange ccxt markets"""
@ -286,16 +333,6 @@ class Exchange:
else: else:
return DataFrame() return DataFrame()
def set_sandbox(self, api, exchange_config: dict, name: str):
if exchange_config.get('sandbox'):
if api.urls.get('test'):
api.urls['api'] = api.urls['test']
logger.info("Enabled Sandbox API on %s", name)
else:
logger.warning(name, "No Sandbox URL in CCXT, exiting. "
"Please check your config.json")
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
def _load_async_markets(self, reload=False) -> None: def _load_async_markets(self, reload=False) -> None:
try: try:
if self._api_async: if self._api_async:
@ -364,19 +401,11 @@ class Exchange:
return pair return pair
raise DependencyException(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") raise DependencyException(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
def validate_timeframes(self, timeframe: Optional[str]) -> None: def validate_timeframe(self, timeframe: Optional[str]) -> None:
""" """
Checks if ticker interval from config is a supported timeframe on the exchange Checks if ticker interval from config is a supported timeframe on the exchange
""" """
if not hasattr(self._api, "timeframes") or self._api.timeframes is None: # self.validate_timeframes()
# If timeframes attribute is missing (or is None), the exchange probably
# has no fetchOHLCV method.
# Therefore we also show that.
raise OperationalException(
f"The ccxt library does not provide the list of timeframes "
f"for the exchange \"{self.name}\" and this exchange "
f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}")
if timeframe and (timeframe not in self.timeframes): if timeframe and (timeframe not in self.timeframes):
raise OperationalException( raise OperationalException(
f"Invalid ticker interval '{timeframe}'. This exchange supports: {self.timeframes}") f"Invalid ticker interval '{timeframe}'. This exchange supports: {self.timeframes}")
@ -405,15 +434,6 @@ class Exchange:
raise OperationalException( raise OperationalException(
f'Time in force policies are not supported for {self.name} yet.') f'Time in force policies are not supported for {self.name} yet.')
def exchange_has(self, endpoint: str) -> bool:
"""
Checks if exchange implements a specific API endpoint.
Wrapper around ccxt 'has' attribute
:param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers')
:return: bool
"""
return endpoint in self._api.has and self._api.has[endpoint]
def symbol_amount_prec(self, pair, amount: float): def symbol_amount_prec(self, pair, amount: float):
''' '''
Returns the amount to buy or sell to a precision the Exchange accepts Returns the amount to buy or sell to a precision the Exchange accepts

View File

@ -3,7 +3,7 @@ This module loads custom exchanges
""" """
import logging import logging
from freqtrade.exchange import Exchange from freqtrade.exchange import BaseExchange, Exchange
import freqtrade.exchange as exchanges import freqtrade.exchange as exchanges
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
@ -17,11 +17,14 @@ class ExchangeResolver(IResolver):
__slots__ = ['exchange'] __slots__ = ['exchange']
def __init__(self, exchange_name: str, config: dict) -> None: def __init__(self, exchange_name: str, config: dict, base: bool = False) -> None:
""" """
Load the custom class from config parameter Load the custom class from config parameter
:param config: configuration dictionary :param config: configuration dictionary
""" """
if base:
self.exchange = BaseExchange(config)
else:
exchange_name = exchange_name.title() exchange_name = exchange_name.title()
try: try:
self.exchange = self._load_exchange(exchange_name, kwargs={'config': config}) self.exchange = self._load_exchange(exchange_name, kwargs={'config': config})

View File

@ -106,11 +106,9 @@ def start_list_timeframes(args: Dict[str, Any]) -> None:
Print ticker intervals (timeframes) available on Exchange Print ticker intervals (timeframes) available on Exchange
""" """
config = setup_utils_configuration(args, RunMode.OTHER) config = setup_utils_configuration(args, RunMode.OTHER)
# Do not use ticker_interval set in the config
config['ticker_interval'] = None
# Init exchange # Init exchange
exchange = ExchangeResolver(config['exchange']['name'], config).exchange exchange = ExchangeResolver(config['exchange']['name'], config, base=True).exchange
if args['print_one_column']: if args['print_one_column']:
print('\n'.join(exchange.timeframes)) print('\n'.join(exchange.timeframes))