diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index f18ae291c..aa64f04e5 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -10,7 +10,7 @@ import arrow from pandas import DataFrame, to_datetime from freqtrade import constants -from freqtrade.exchange import get_ticker_history +from freqtrade.exchange import Exchange from freqtrade.persistence import Trade from freqtrade.strategy.resolver import StrategyResolver, IStrategy @@ -110,14 +110,14 @@ class Analyze(object): dataframe = self.populate_sell_trend(dataframe) return dataframe - def get_signal(self, pair: str, interval: str) -> Tuple[bool, bool]: + def get_signal(self, exchange: Exchange, pair: str, interval: str) -> Tuple[bool, bool]: """ Calculates current signal based several technical analysis indicators :param pair: pair in format ANT/BTC :param interval: Interval to use (in min) :return: (Buy, Sell) A bool-tuple indicating buy/sell signal """ - ticker_hist = get_ticker_history(pair, interval) + ticker_hist = exchange.get_ticker_history(pair, interval) if not ticker_hist: logger.warning('Empty ticker history for pair %s', pair) return False, False diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 54d564f04..5ac5f8a0b 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -12,16 +12,8 @@ from freqtrade import constants, OperationalException, DependencyException, Temp logger = logging.getLogger(__name__) -# Current selected exchange -_API: ccxt.Exchange = None - -_CONF: Dict = {} API_RETRY_COUNT = 4 -_CACHED_TICKER: Dict[str, Any] = {} - -# Holds all open sell orders for dry_run -_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {} # Urls to exchange markets, insert quote and base with .format() _EXCHANGE_URLS = { @@ -74,364 +66,354 @@ def init_ccxt(exchange_config: dict) -> ccxt.Exchange: return api -def init(config: dict) -> None: - """ - Initializes this module with the given config, - it does basic validation whether the specified - exchange and pairs are valid. - :param config: config to use - :return: None - """ - global _CONF, _API +class Exchange(object): - _CONF.update(config) + # Current selected exchange + _API: ccxt.Exchange = None + _CONF: Dict = {} + _CACHED_TICKER: Dict[str, Any] = {} - if config['dry_run']: - logger.info('Instance is running with dry_run enabled') + # Holds all open sell orders for dry_run + _DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {} - exchange_config = config['exchange'] - _API = init_ccxt(exchange_config) + 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. + :param config: config to use + :return: None + """ + self._API - logger.info('Using Exchange "%s"', get_name()) + self._CONF.update(config) - # Check if all pairs are available - validate_pairs(config['exchange']['pair_whitelist']) + if config['dry_run']: + logger.info('Instance is running with dry_run enabled') + exchange_config = config['exchange'] + self._API = init_ccxt(exchange_config) -def validate_pairs(pairs: List[str]) -> None: - """ - Checks if all given pairs are tradable on the current exchange. - Raises OperationalException if one pair is not available. - :param pairs: list of pairs - :return: None - """ + logger.info('Using Exchange "%s"', self.get_name()) - try: - markets = _API.load_markets() - except ccxt.BaseError as e: - logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e) - return + # Check if all pairs are available + self.validate_pairs(config['exchange']['pair_whitelist']) - stake_cur = _CONF['stake_currency'] - for pair in pairs: - # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs - # TODO: add a support for having coins in BTC/USDT format - if not pair.endswith(stake_cur): - raise OperationalException( - f'Pair {pair} not compatible with stake_currency: {stake_cur}') - if pair not in markets: - raise OperationalException( - f'Pair {pair} is not available at {get_name()}') + def get_name(self) -> str: + return self._API.name + def get_id(self) -> str: + return self._API.id -def exchange_has(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 _API.has and _API.has[endpoint] + def validate_pairs(self, pairs: List[str]) -> None: + """ + Checks if all given pairs are tradable on the current exchange. + Raises OperationalException if one pair is not available. + :param pairs: list of pairs + :return: None + """ - -def buy(pair: str, rate: float, amount: float) -> Dict: - if _CONF['dry_run']: - global _DRY_RUN_OPEN_ORDERS - order_id = f'dry_run_buy_{randint(0, 10**6)}' - _DRY_RUN_OPEN_ORDERS[order_id] = { - 'pair': pair, - 'price': rate, - 'amount': amount, - 'type': 'limit', - 'side': 'buy', - 'remaining': 0.0, - 'datetime': arrow.utcnow().isoformat(), - 'status': 'closed', - 'fee': None - } - return {'id': order_id} - - try: - return _API.create_limit_buy_order(pair, amount, rate) - except ccxt.InsufficientFunds as e: - raise DependencyException( - f'Insufficient funds to create limit buy order on market {pair}.' - f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).' - f'Message: {e}') - except ccxt.InvalidOrder as e: - raise DependencyException( - f'Could not create limit buy order on market {pair}.' - f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).' - f'Message: {e}') - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not place buy order due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) - - -def sell(pair: str, rate: float, amount: float) -> Dict: - if _CONF['dry_run']: - global _DRY_RUN_OPEN_ORDERS - order_id = f'dry_run_sell_{randint(0, 10**6)}' - _DRY_RUN_OPEN_ORDERS[order_id] = { - 'pair': pair, - 'price': rate, - 'amount': amount, - 'type': 'limit', - 'side': 'sell', - 'remaining': 0.0, - 'datetime': arrow.utcnow().isoformat(), - 'status': 'closed' - } - return {'id': order_id} - - try: - return _API.create_limit_sell_order(pair, amount, rate) - except ccxt.InsufficientFunds as e: - raise DependencyException( - f'Insufficient funds to create limit sell order on market {pair}.' - f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).' - f'Message: {e}') - except ccxt.InvalidOrder as e: - raise DependencyException( - f'Could not create limit sell order on market {pair}.' - f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).' - f'Message: {e}') - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) - - -@retrier -def get_balance(currency: str) -> float: - if _CONF['dry_run']: - return 999.9 - - # ccxt exception is already handled by get_balances - balances = get_balances() - balance = balances.get(currency) - if balance is None: - raise TemporaryError( - f'Could not get {currency} balance due to malformed exchange response: {balances}') - return balance['free'] - - -@retrier -def get_balances() -> dict: - if _CONF['dry_run']: - return {} - - try: - balances = _API.fetch_balance() - # Remove additional info from ccxt results - balances.pop("info", None) - balances.pop("free", None) - balances.pop("total", None) - balances.pop("used", None) - - return balances - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not get balance due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) - - -@retrier -def get_tickers() -> Dict: - try: - return _API.fetch_tickers() - except ccxt.NotSupported as e: - raise OperationalException( - f'Exchange {_API.name} does not support fetching tickers in batch.' - f'Message: {e}') - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) - - -@retrier -def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict: - global _CACHED_TICKER - if refresh or pair not in _CACHED_TICKER.keys(): try: - data = _API.fetch_ticker(pair) + markets = self._API.load_markets() + except ccxt.BaseError as e: + logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e) + return + + stake_cur = self._CONF['stake_currency'] + for pair in pairs: + # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs + # TODO: add a support for having coins in BTC/USDT format + if not pair.endswith(stake_cur): + raise OperationalException( + f'Pair {pair} not compatible with stake_currency: {stake_cur}') + if pair not in markets: + raise OperationalException( + f'Pair {pair} is not available at {self.get_name()}') + + 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 buy(self, pair: str, rate: float, amount: float) -> Dict: + if self._CONF['dry_run']: + order_id = f'dry_run_buy_{randint(0, 10**6)}' + self._DRY_RUN_OPEN_ORDERS[order_id] = { + 'pair': pair, + 'price': rate, + 'amount': amount, + 'type': 'limit', + 'side': 'buy', + 'remaining': 0.0, + 'datetime': arrow.utcnow().isoformat(), + 'status': 'closed', + 'fee': None + } + return {'id': order_id} + + try: + return self._API.create_limit_buy_order(pair, amount, rate) + except ccxt.InsufficientFunds as e: + raise DependencyException( + f'Insufficient funds to create limit buy order on market {pair}.' + f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).' + f'Message: {e}') + except ccxt.InvalidOrder as e: + raise DependencyException( + f'Could not create limit buy order on market {pair}.' + f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).' + f'Message: {e}') + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not place buy order due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) + + def sell(self, pair: str, rate: float, amount: float) -> Dict: + if self._CONF['dry_run']: + order_id = f'dry_run_sell_{randint(0, 10**6)}' + self._DRY_RUN_OPEN_ORDERS[order_id] = { + 'pair': pair, + 'price': rate, + 'amount': amount, + 'type': 'limit', + 'side': 'sell', + 'remaining': 0.0, + 'datetime': arrow.utcnow().isoformat(), + 'status': 'closed' + } + return {'id': order_id} + + try: + return self._API.create_limit_sell_order(pair, amount, rate) + except ccxt.InsufficientFunds as e: + raise DependencyException( + f'Insufficient funds to create limit sell order on market {pair}.' + f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).' + f'Message: {e}') + except ccxt.InvalidOrder as e: + raise DependencyException( + f'Could not create limit sell order on market {pair}.' + f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).' + f'Message: {e}') + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) + + @retrier + def get_balance(self, currency: str) -> float: + if self._CONF['dry_run']: + return 999.9 + + # ccxt exception is already handled by get_balances + balances = self.get_balances() + balance = balances.get(currency) + if balance is None: + raise TemporaryError( + f'Could not get {currency} balance due to malformed exchange response: {balances}') + return balance['free'] + + @retrier + def get_balances(self) -> dict: + if self._CONF['dry_run']: + return {} + + try: + balances = self._API.fetch_balance() + # Remove additional info from ccxt results + balances.pop("info", None) + balances.pop("free", None) + balances.pop("total", None) + balances.pop("used", None) + + return balances + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get balance due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) + + @retrier + def get_tickers(self) -> Dict: + try: + return self._API.fetch_tickers() + except ccxt.NotSupported as e: + raise OperationalException( + f'Exchange {self._API.name} does not support fetching tickers in batch.' + f'Message: {e}') + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) + + @retrier + def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: + if refresh or pair not in self._CACHED_TICKER.keys(): try: - _CACHED_TICKER[pair] = { - 'bid': float(data['bid']), - 'ask': float(data['ask']), - } - except KeyError: - logger.debug("Could not cache ticker data for %s", pair) + data = self._API.fetch_ticker(pair) + try: + self._CACHED_TICKER[pair] = { + 'bid': float(data['bid']), + 'ask': float(data['ask']), + } + except KeyError: + logger.debug("Could not cache ticker data for %s", pair) + return data + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) + else: + logger.info("returning cached ticker-data for %s", pair) + return self._CACHED_TICKER[pair] + + @retrier + def get_ticker_history(self, pair: str, tick_interval: str, + since_ms: Optional[int] = None) -> List[Dict]: + try: + # last item should be in the time interval [now - tick_interval, now] + till_time_ms = arrow.utcnow().shift( + minutes=-constants.TICKER_INTERVAL_MINUTES[tick_interval] + ).timestamp * 1000 + # it looks as if some exchanges return cached data + # and they update it one in several minute, so 10 mins interval + # is necessary to skeep downloading of an empty array when all + # chached data was already downloaded + till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000) + + data: List[Dict[Any, Any]] = [] + while not since_ms or since_ms < till_time_ms: + data_part = self._API.fetch_ohlcv(pair, timeframe=tick_interval, since=since_ms) + + # Because some exchange sort Tickers ASC and other DESC. + # Ex: Bittrex returns a list of tickers ASC (oldest first, newest last) + # when GDAX returns a list of tickers DESC (newest first, oldest last) + data_part = sorted(data_part, key=lambda x: x[0]) + + if not data_part: + break + + logger.debug('Downloaded data for %s time range [%s, %s]', + pair, + arrow.get(data_part[0][0] / 1000).format(), + arrow.get(data_part[-1][0] / 1000).format()) + + data.extend(data_part) + since_ms = data[-1][0] + 1 + return data + except ccxt.NotSupported as e: + raise OperationalException( + f'Exchange {self._API.name} does not support fetching historical candlestick data.' + f'Message: {e}') except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(f'Could not fetch ticker data. Msg: {e}') + + @retrier + def cancel_order(self, order_id: str, pair: str) -> None: + if self._CONF['dry_run']: + return + + try: + return self._API.cancel_order(order_id, pair) + except ccxt.InvalidOrder as e: + raise DependencyException( + f'Could not cancel order. Message: {e}') + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') except ccxt.BaseError as e: raise OperationalException(e) - else: - logger.info("returning cached ticker-data for %s", pair) - return _CACHED_TICKER[pair] + @retrier + def get_order(self, order_id: str, pair: str) -> Dict: + if self._CONF['dry_run']: + order = self._DRY_RUN_OPEN_ORDERS[order_id] + order.update({ + 'id': order_id + }) + return order + try: + return self._API.fetch_order(order_id, pair) + except ccxt.InvalidOrder as e: + raise DependencyException( + f'Could not get order. Message: {e}') + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get order due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) -@retrier -def get_ticker_history(pair: str, tick_interval: str, since_ms: Optional[int] = None) -> List[Dict]: - try: - # last item should be in the time interval [now - tick_interval, now] - till_time_ms = arrow.utcnow().shift( - minutes=-constants.TICKER_INTERVAL_MINUTES[tick_interval] - ).timestamp * 1000 - # it looks as if some exchanges return cached data - # and they update it one in several minute, so 10 mins interval - # is necessary to skeep downloading of an empty array when all - # chached data was already downloaded - till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000) + @retrier + def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List: + if self._CONF['dry_run']: + return [] + if not self.exchange_has('fetchMyTrades'): + return [] + try: + my_trades = self._API.fetch_my_trades(pair, since.timestamp()) + matched_trades = [trade for trade in my_trades if trade['order'] == order_id] - data: List[Dict[Any, Any]] = [] - while not since_ms or since_ms < till_time_ms: - data_part = _API.fetch_ohlcv(pair, timeframe=tick_interval, since=since_ms) + return matched_trades - # Because some exchange sort Tickers ASC and other DESC. - # Ex: Bittrex returns a list of tickers ASC (oldest first, newest last) - # when GDAX returns a list of tickers DESC (newest first, oldest last) - data_part = sorted(data_part, key=lambda x: x[0]) + except ccxt.NetworkError as e: + raise TemporaryError( + f'Could not get trades due to networking error. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) - if not data_part: - break + def get_pair_detail_url(self, pair: str) -> str: + try: + url_base = self._API.urls.get('www') + base, quote = pair.split('/') - logger.debug('Downloaded data for %s time range [%s, %s]', - pair, - arrow.get(data_part[0][0] / 1000).format(), - arrow.get(data_part[-1][0] / 1000).format()) + return url_base + _EXCHANGE_URLS[self._API.id].format(base=base, quote=quote) + except KeyError: + logger.warning('Could not get exchange url for %s', self.get_name()) + return "" - data.extend(data_part) - since_ms = data[-1][0] + 1 + @retrier + def get_markets(self) -> List[dict]: + try: + return self._API.fetch_markets() + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not load markets due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) - return data - except ccxt.NotSupported as e: - raise OperationalException( - f'Exchange {_API.name} does not support fetching historical candlestick data.' - f'Message: {e}') - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(f'Could not fetch ticker data. Msg: {e}') + @retrier + def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1, + price=1, taker_or_maker='maker') -> float: + try: + # validate that markets are loaded before trying to get fee + if self._API.markets is None or len(self._API.markets) == 0: + self._API.load_markets() + return self._API.calculate_fee(symbol=symbol, type=type, side=side, amount=amount, + price=price, takerOrMaker=taker_or_maker)['rate'] + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') + except ccxt.BaseError as e: + raise OperationalException(e) -@retrier -def cancel_order(order_id: str, pair: str) -> None: - if _CONF['dry_run']: - return - - try: - return _API.cancel_order(order_id, pair) - except ccxt.InvalidOrder as e: - raise DependencyException( - f'Could not cancel order. Message: {e}') - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) - - -@retrier -def get_order(order_id: str, pair: str) -> Dict: - if _CONF['dry_run']: - order = _DRY_RUN_OPEN_ORDERS[order_id] - order.update({ - 'id': order_id - }) - return order - try: - return _API.fetch_order(order_id, pair) - except ccxt.InvalidOrder as e: - raise DependencyException( - f'Could not get order. Message: {e}') - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not get order due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) - - -@retrier -def get_trades_for_order(order_id: str, pair: str, since: datetime) -> List: - if _CONF['dry_run']: - return [] - if not exchange_has('fetchMyTrades'): - return [] - try: - my_trades = _API.fetch_my_trades(pair, since.timestamp()) - matched_trades = [trade for trade in my_trades if trade['order'] == order_id] - - return matched_trades - - except ccxt.NetworkError as e: - raise TemporaryError( - f'Could not get trades due to networking error. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) - - -def get_pair_detail_url(pair: str) -> str: - try: - url_base = _API.urls.get('www') - base, quote = pair.split('/') - - return url_base + _EXCHANGE_URLS[_API.id].format(base=base, quote=quote) - except KeyError: - logger.warning('Could not get exchange url for %s', get_name()) - return "" - - -@retrier -def get_markets() -> List[dict]: - try: - return _API.fetch_markets() - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not load markets due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) - - -def get_name() -> str: - return _API.name - - -def get_id() -> str: - return _API.id - - -@retrier -def get_fee(symbol='ETH/BTC', type='', side='', amount=1, - price=1, taker_or_maker='maker') -> float: - try: + def get_amount_lots(self, pair: str, amount: float) -> float: + """ + get buyable amount rounding, .. + """ # validate that markets are loaded before trying to get fee - if _API.markets is None or len(_API.markets) == 0: - _API.load_markets() - - return _API.calculate_fee(symbol=symbol, type=type, side=side, amount=amount, - price=price, takerOrMaker=taker_or_maker)['rate'] - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') - except ccxt.BaseError as e: - raise OperationalException(e) - - -def get_amount_lots(pair: str, amount: float) -> float: - """ - get buyable amount rounding, .. - """ - # validate that markets are loaded before trying to get fee - if not _API.markets: - _API.load_markets() - return _API.amount_to_lots(pair, amount) + if not self._API.markets: + self._API.load_markets() + return self._API.amount_to_lots(pair, amount) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 157de862f..5f69b8115 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -14,11 +14,11 @@ import requests from cachetools import TTLCache, cached from freqtrade import ( - DependencyException, OperationalException, TemporaryError, - exchange, persistence, __version__, + DependencyException, OperationalException, TemporaryError, persistence, __version__, ) from freqtrade import constants from freqtrade.analyze import Analyze +from freqtrade.exchange import Exchange from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.persistence import Trade from freqtrade.rpc.rpc_manager import RPCManager @@ -66,7 +66,7 @@ class FreqtradeBot(object): # Initialize all modules persistence.init(self.config) - exchange.init(self.config) + self.exchange = Exchange(self.config) # Set initial application state initial_state = self.config.get('initial_state') @@ -186,13 +186,13 @@ class FreqtradeBot(object): :return: List of pairs """ - if not exchange.exchange_has('fetchTickers'): + if not self.exchange.exchange_has('fetchTickers'): raise OperationalException( 'Exchange does not support dynamic whitelist.' 'Please edit your config and restart the bot' ) - tickers = exchange.get_tickers() + tickers = self.exchange.get_tickers() # check length so that we make sure that '/' is actually in the string tickers = [v for k, v in tickers.items() if len(k.split('/')) == 2 and k.split('/')[1] == base_currency] @@ -210,7 +210,7 @@ class FreqtradeBot(object): black_listed """ sanitized_whitelist = whitelist - markets = exchange.get_markets() + markets = self.exchange.get_markets() markets = [m for m in markets if m['quote'] == self.config['stake_currency']] known_pairs = set() @@ -255,7 +255,7 @@ class FreqtradeBot(object): interval = self.analyze.get_ticker_interval() stake_currency = self.config['stake_currency'] fiat_currency = self.config['fiat_display_currency'] - exc_name = exchange.get_name() + exc_name = self.exchange.get_name() logger.info( 'Checking buy signals to create a new trade with stake_amount: %f ...', @@ -263,7 +263,7 @@ class FreqtradeBot(object): ) whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist']) # Check if stake_amount is fulfilled - if exchange.get_balance(stake_currency) < stake_amount: + if self.exchange.get_balance(stake_currency) < stake_amount: raise DependencyException( f'stake amount is not fulfilled (currency={stake_currency})') @@ -278,19 +278,19 @@ class FreqtradeBot(object): # Pick pair based on buy signals for _pair in whitelist: - (buy, sell) = self.analyze.get_signal(_pair, interval) + (buy, sell) = self.analyze.get_signal(self.exchange, _pair, interval) if buy and not sell: pair = _pair break else: return False pair_s = pair.replace('_', '/') - pair_url = exchange.get_pair_detail_url(pair) + pair_url = self.exchange.get_pair_detail_url(pair) # Calculate amount - buy_limit = self.get_target_bid(exchange.get_ticker(pair)) + buy_limit = self.get_target_bid(self.exchange.get_ticker(pair)) amount = stake_amount / buy_limit - order_id = exchange.buy(pair, buy_limit, amount)['id'] + order_id = self.exchange.buy(pair, buy_limit, amount)['id'] stake_amount_fiat = self.fiat_converter.convert_amount( stake_amount, @@ -305,7 +305,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ {stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`""" ) # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL - fee = exchange.get_fee(symbol=pair, taker_or_maker='maker') + fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') trade = Trade( pair=pair, stake_amount=stake_amount, @@ -315,7 +315,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ open_rate=buy_limit, open_rate_requested=buy_limit, open_date=datetime.utcnow(), - exchange=exchange.get_id(), + exchange=self.exchange.get_id(), open_order_id=order_id ) Trade.session.add(trade) @@ -348,7 +348,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ if trade.open_order_id: # Update trade with order values logger.info('Found open order for %s', trade) - order = exchange.get_order(trade.open_order_id, trade.pair) + order = self.exchange.get_order(trade.open_order_id, trade.pair) # Try update amount (binance-fix) try: new_amount = self.get_real_amount(trade, order) @@ -372,7 +372,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ def get_real_amount(self, trade: Trade, order: Dict) -> float: """ Get real amount for the trade - Necessary for exchanges which charge fees in base currency (e.g. binance) + Necessary for self.exchanges which charge fees in base currency (e.g. binance) """ order_amount = order['amount'] # Only run for closed orders @@ -388,7 +388,8 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ return new_amount # Fallback to Trades - trades = exchange.get_trades_for_order(trade.open_order_id, trade.pair, trade.open_date) + trades = self.exchange.get_trades_for_order(trade.open_order_id, trade.pair, + trade.open_date) if len(trades) == 0: logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade) @@ -420,7 +421,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ raise ValueError(f'attempt to handle closed trade: {trade}') logger.debug('Handling %s ...', trade) - current_rate = exchange.get_ticker(trade.pair)['bid'] + current_rate = self.exchange.get_ticker(trade.pair)['bid'] (buy, sell) = (False, False) @@ -449,7 +450,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ # updated via /forcesell in a different thread. if not trade.open_order_id: continue - order = exchange.get_order(trade.open_order_id, trade.pair) + order = self.exchange.get_order(trade.open_order_id, trade.pair) except requests.exceptions.RequestException: logger.info( 'Cannot query order for %s due to %s', @@ -475,7 +476,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ :return: True if order was fully cancelled """ pair_s = trade.pair.replace('_', '/') - exchange.cancel_order(trade.open_order_id, trade.pair) + self.exchange.cancel_order(trade.open_order_id, trade.pair) if order['remaining'] == order['amount']: # if trade is not partially completed, just delete the trade Trade.session.delete(trade) @@ -502,7 +503,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ pair_s = trade.pair.replace('_', '/') if order['remaining'] == order['amount']: # if trade is not partially completed, just cancel the trade - exchange.cancel_order(trade.open_order_id, trade.pair) + self.exchange.cancel_order(trade.open_order_id, trade.pair) trade.close_rate = None trade.close_profit = None trade.close_date = None @@ -525,15 +526,15 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ exc = trade.exchange pair = trade.pair # Execute sell and update trade record - order_id = exchange.sell(str(trade.pair), limit, trade.amount)['id'] + order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id'] trade.open_order_id = order_id trade.close_rate_requested = limit fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2) profit_trade = trade.calc_profit(rate=limit) - current_rate = exchange.get_ticker(trade.pair)['bid'] + current_rate = self.exchange.get_ticker(trade.pair)['bid'] profit = trade.calc_profit_percent(limit) - pair_url = exchange.get_pair_detail_url(trade.pair) + pair_url = self.exchange.get_pair_detail_url(trade.pair) gain = "profit" if fmt_exp_profit > 0 else "loss" message = f"*{exc}:* Selling\n" \ diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 867e8c7dc..3e2fb4554 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -8,7 +8,7 @@ from typing import Optional, List, Dict, Tuple, Any import arrow from freqtrade import misc, constants -from freqtrade.exchange import get_ticker_history +from freqtrade.exchange import Exchange from freqtrade.arguments import TimeRange logger = logging.getLogger(__name__) @@ -183,6 +183,7 @@ def load_cached_data_for_updating(filename: str, def download_backtesting_testdata(datadir: str, + exchange: Exchange, pair: str, tick_interval: str = '5m', timerange: Optional[TimeRange] = None) -> None: @@ -216,7 +217,8 @@ def download_backtesting_testdata(datadir: str, logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None') logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') - new_data = get_ticker_history(pair=pair, tick_interval=tick_interval, since_ms=since_ms) + new_data = exchange.get_ticker_history(pair=pair, tick_interval=tick_interval, + since_ms=since_ms) data.extend(new_data) logger.debug("New Start: %s", misc.format_ms_time(data[0][0])) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 23bd7741a..58b552237 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -14,7 +14,7 @@ from pandas import DataFrame from tabulate import tabulate import freqtrade.optimize as optimize -from freqtrade import exchange +from freqtrade.exchange import Exchange from freqtrade.analyze import Analyze from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration @@ -61,7 +61,7 @@ class Backtesting(object): self.config['exchange']['password'] = '' self.config['exchange']['uid'] = '' self.config['dry_run'] = True - exchange.init(self.config) + self.exchange = Exchange(self.config) @staticmethod def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: @@ -130,7 +130,7 @@ class Backtesting(object): stake_amount = args['stake_amount'] max_open_trades = args.get('max_open_trades', 0) - fee = exchange.get_fee() + fee = self.exchange.get_fee() trade = Trade( open_rate=buy_row.close, open_date=buy_row.date, @@ -256,7 +256,7 @@ class Backtesting(object): if self.config.get('live'): logger.info('Downloading data for all pairs in whitelist ...') for pair in pairs: - data[pair] = exchange.get_ticker_history(pair, self.ticker_interval) + data[pair] = self.exchange.get_ticker_history(pair, self.ticker_interval) else: logger.info('Using local backtesting data (using whitelist in given config) ...')