From b556891bf24802e06112b5ee2f1f8507a459051e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 12 Oct 2019 11:48:56 +0300 Subject: [PATCH] Introduce BaseExchange class --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/exchange.py | 214 +++++++++++++---------- freqtrade/resolvers/exchange_resolver.py | 23 +-- freqtrade/utils.py | 4 +- 4 files changed, 132 insertions(+), 110 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 29971c897..49c9430d4 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -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 (get_exchange_bad_reason, # noqa: F401 is_exchange_bad, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index df7e5e2b4..e65abb180 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -139,9 +139,121 @@ def retrier(f): return wrapper -class Exchange: +class BaseExchange: _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 = {} # 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. :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] = {} @@ -195,16 +305,8 @@ class Exchange: self._ohlcv_candle_limit = self._ft_has['ohlcv_candle_limit'] 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 - self.validate_timeframes(config.get('ticker_interval')) + self.validate_timeframe(config.get('ticker_interval')) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( @@ -217,61 +319,6 @@ class Exchange: self.validate_ordertypes(config.get('order_types', {})) 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 def markets(self) -> Dict: """exchange ccxt markets""" @@ -286,16 +333,6 @@ class Exchange: else: 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: try: if self._api_async: @@ -364,19 +401,11 @@ class Exchange: return 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 """ - 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')}") - +# self.validate_timeframes() if timeframe and (timeframe not in self.timeframes): raise OperationalException( f"Invalid ticker interval '{timeframe}'. This exchange supports: {self.timeframes}") @@ -405,15 +434,6 @@ class Exchange: raise OperationalException( 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): ''' Returns the amount to buy or sell to a precision the Exchange accepts diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 6fb12a65f..a8da5d8b0 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -3,7 +3,7 @@ This module loads custom exchanges """ import logging -from freqtrade.exchange import Exchange +from freqtrade.exchange import BaseExchange, Exchange import freqtrade.exchange as exchanges from freqtrade.resolvers import IResolver @@ -17,19 +17,22 @@ class ExchangeResolver(IResolver): __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 :param config: configuration dictionary """ - exchange_name = exchange_name.title() - try: - self.exchange = self._load_exchange(exchange_name, kwargs={'config': config}) - except ImportError: - logger.info( - f"No {exchange_name} specific subclass found. Using the generic class instead.") - if not hasattr(self, "exchange"): - self.exchange = Exchange(config) + if base: + self.exchange = BaseExchange(config) + else: + exchange_name = exchange_name.title() + try: + self.exchange = self._load_exchange(exchange_name, kwargs={'config': config}) + except ImportError: + logger.info( + f"No {exchange_name} specific subclass found. Using the generic class instead.") + if not hasattr(self, "exchange"): + self.exchange = Exchange(config) def _load_exchange( self, exchange_name: str, kwargs: dict) -> Exchange: diff --git a/freqtrade/utils.py b/freqtrade/utils.py index b3ff43aca..46a7ca3bf 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -106,11 +106,9 @@ def start_list_timeframes(args: Dict[str, Any]) -> None: Print ticker intervals (timeframes) available on Exchange """ config = setup_utils_configuration(args, RunMode.OTHER) - # Do not use ticker_interval set in the config - config['ticker_interval'] = None # Init exchange - exchange = ExchangeResolver(config['exchange']['name'], config).exchange + exchange = ExchangeResolver(config['exchange']['name'], config, base=True).exchange if args['print_one_column']: print('\n'.join(exchange.timeframes))