diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index dc85bfedb..03d69a7ed 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -5,23 +5,25 @@ import logging from random import randint from typing import List, Dict, Any, Optional +import ccxt import arrow -import requests from cachetools import cached, TTLCache -from freqtrade import OperationalException +from freqtrade import OperationalException, DependencyException, NetworkException from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.interface import Exchange logger = logging.getLogger(__name__) # Current selected exchange -_API: Exchange = None +_API: ccxt.Exchange = None _CONF: dict = {} # Holds all open sell orders for dry_run _DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {} +_TICKER_CACHE: dict = {} + class Exchanges(enum.Enum): """ @@ -49,12 +51,19 @@ def init(config: dict) -> None: # Find matching class for the given exchange name name = exchange_config['name'] - try: - exchange_class = Exchanges[name.upper()].value - except KeyError: + + if name not in ccxt.exchanges: raise OperationalException('Exchange {} is not supported'.format(name)) - _API = exchange_class(exchange_config) + try: + _API = getattr(ccxt, name.lower())({ + 'apiKey': exchange_config.get('key'), + 'secret': exchange_config.get('secret'), + 'password': exchange_config.get('password'), + 'uid': exchange_config.get('uid'), + }) + except KeyError: + raise OperationalException('Exchange {} is not supported'.format(name)) # Check if all pairs are available validate_pairs(config['exchange']['pair_whitelist']) @@ -68,109 +77,230 @@ def validate_pairs(pairs: List[str]) -> None: :return: None """ try: - markets = _API.get_markets() - except requests.exceptions.RequestException as e: + markets = _API.load_markets() + except ccxt.BaseError as e: logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e) return stake_cur = _CONF['stake_currency'] for pair in pairs: - if not pair.startswith(stake_cur): + # 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( 'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur) ) if pair not in markets: raise OperationalException( - 'Pair {} is not available at {}'.format(pair, _API.name.lower())) + 'Pair {} is not available at {}'.format(pair, _API.id.lower())) -def buy(pair: str, rate: float, amount: float) -> str: +def buy(pair: str, rate: float, amount: float) -> Dict: if _CONF['dry_run']: global _DRY_RUN_OPEN_ORDERS order_id = 'dry_run_buy_{}'.format(randint(0, 10**6)) _DRY_RUN_OPEN_ORDERS[order_id] = { 'pair': pair, - 'rate': rate, + 'price': rate, 'amount': amount, - 'type': 'LIMIT_BUY', + 'type': 'limit', + 'side': 'buy', 'remaining': 0.0, - 'opened': arrow.utcnow().datetime, - 'closed': arrow.utcnow().datetime, + 'datetime': arrow.utcnow().isoformat(), + 'status': 'closed' } - return order_id + return {'id': order_id} - return _API.buy(pair, rate, amount) + try: + return _API.create_limit_buy_order(pair, amount, rate) + except ccxt.InsufficientFunds as e: + raise DependencyException( + 'Insufficient funds to create limit buy order on market {}.' + 'Tried to buy amount {} at rate {} (total {}).' + 'Message: {}'.format(pair, amount, rate, rate*amount, e) + ) + except ccxt.InvalidOrder as e: + raise DependencyException( + 'Could not create limit buy order on market {}.' + 'Tried to buy amount {} at rate {} (total {}).' + 'Message: {}'.format(pair, amount, rate, rate*amount, e) + ) + except ccxt.NetworkError as e: + raise NetworkException( + 'Could not place buy order due to networking error. Message: {}'.format(e) + ) + except ccxt.BaseError as e: + raise OperationalException(e) -def sell(pair: str, rate: float, amount: float) -> str: +def sell(pair: str, rate: float, amount: float) -> Dict: if _CONF['dry_run']: global _DRY_RUN_OPEN_ORDERS order_id = 'dry_run_sell_{}'.format(randint(0, 10**6)) _DRY_RUN_OPEN_ORDERS[order_id] = { 'pair': pair, - 'rate': rate, + 'price': rate, 'amount': amount, - 'type': 'LIMIT_SELL', + 'type': 'limit', + 'side': 'sell', 'remaining': 0.0, - 'opened': arrow.utcnow().datetime, - 'closed': arrow.utcnow().datetime, + 'datetime': arrow.utcnow().isoformat(), + 'status': 'closed' } - return order_id + return {'id': order_id} - return _API.sell(pair, rate, amount) + try: + return _API.create_limit_sell_order(pair, amount, rate) + except ccxt.InsufficientFunds as e: + raise DependencyException( + 'Insufficient funds to create limit sell order on market {}.' + 'Tried to sell amount {} at rate {} (total {}).' + 'Message: {}'.format(pair, amount, rate, rate*amount, e) + ) + except ccxt.InvalidOrder as e: + raise DependencyException( + 'Could not create limit sell order on market {}.' + 'Tried to sell amount {} at rate {} (total {}).' + 'Message: {}'.format(pair, amount, rate, rate*amount, e) + ) + except ccxt.NetworkError as e: + raise NetworkException( + 'Could not place sell order due to networking error. Message: {}'.format(e) + ) + except ccxt.BaseError as e: + raise OperationalException(e) def get_balance(currency: str) -> float: if _CONF['dry_run']: return 999.9 - return _API.get_balance(currency) + # ccxt exception is already handled by get_balances + balances = get_balances() + return balances[currency]['free'] -def get_balances(): +def get_balances() -> dict: if _CONF['dry_run']: - return [] + return {} - return _API.get_balances() + 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 as e: + raise NetworkException( + 'Could not get balance due to networking error. Message: {}'.format(e) + ) + except ccxt.BaseError as e: + raise OperationalException(e) def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict: - return _API.get_ticker(pair, refresh) + global _TICKER_CACHE + try: + if not refresh: + if _TICKER_CACHE and pair in _TICKER_CACHE: + return _TICKER_CACHE[pair] + _TICKER_CACHE[pair] = _API.fetch_ticker(pair) + return _TICKER_CACHE[pair] + except ccxt.NetworkError as e: + raise NetworkException( + 'Could not load tickers due to networking error. Message: {}'.format(e) + ) + except ccxt.BaseError as e: + raise OperationalException(e) @cached(TTLCache(maxsize=100, ttl=30)) -def get_ticker_history(pair: str, tick_interval) -> List[Dict]: - return _API.get_ticker_history(pair, tick_interval) +def get_ticker_history(pair: str, tick_interval: str) -> List[Dict]: + if 'fetchOHLCV' not in _API.has or not _API.has['fetchOHLCV']: + raise OperationalException( + 'Exhange {} does not support fetching historical candlestick data.'.format(_API.name) + ) + + try: + history = _API.fetch_ohlcv(pair, timeframe=tick_interval) + history_json = [] + for candlestick in history: + history_json.append({ + 'T': arrow.get(candlestick[0]/1000.0).strftime('%Y-%m-%dT%H:%M:%S.%f'), + 'O': candlestick[1], + 'H': candlestick[2], + 'L': candlestick[3], + 'C': candlestick[4], + 'V': candlestick[5], + }) + return history_json + except IndexError as e: + logger.warning('Empty ticker history. Msg %s', str(e)) + return [] + except ccxt.NetworkError as e: + raise NetworkException( + 'Could not load ticker history due to networking error. Message: {}'.format(e) + ) + except ccxt.BaseError as e: + raise OperationalException('Could not fetch ticker data. Msg: {}'.format(e)) -def cancel_order(order_id: str) -> None: +def cancel_order(order_id: str, pair: str) -> None: if _CONF['dry_run']: return - return _API.cancel_order(order_id) + try: + return _API.cancel_order(order_id, pair) + except ccxt.NetworkError as e: + raise NetworkException( + 'Could not get order due to networking error. Message: {}'.format(e) + ) + except ccxt.InvalidOrder as e: + raise DependencyException( + 'Could not cancel order. Message: {}'.format(e) + ) + except ccxt.BaseError as e: + raise OperationalException(e) -def get_order(order_id: str) -> Dict: +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 - - return _API.get_order(order_id) + try: + return _API.fetch_order(order_id, pair) + except ccxt.NetworkError as e: + raise NetworkException( + 'Could not get order due to networking error. Message: {}'.format(e) + ) + except ccxt.InvalidOrder as e: + raise DependencyException( + 'Could not get order. Message: {}'.format(e) + ) + except ccxt.BaseError as e: + raise OperationalException(e) +# TODO: reimplement, not part of ccxt def get_pair_detail_url(pair: str) -> str: - return _API.get_pair_detail_url(pair) + return "" -def get_markets() -> List[str]: - return _API.get_markets() - - -def get_market_summaries() -> List[Dict]: - return _API.get_market_summaries() +def get_markets() -> List[dict]: + try: + return _API.fetch_markets() + except ccxt.NetworkError as e: + raise NetworkException( + 'Could not load markets due to networking error. Message: {}'.format(e) + ) + except ccxt.BaseError as e: + raise OperationalException(e) def get_name() -> str: @@ -178,8 +308,8 @@ def get_name() -> str: def get_fee() -> float: - return _API.fee + # validate that markets are loaded before trying to get fee + if _API.markets is None or len(_API.markets) == 0: + _API.load_markets() - -def get_wallet_health() -> List[Dict]: - return _API.get_wallet_health() + return _API.calculate_fee('ETH/BTC', '', '', 1, 1)['rate']