diff --git a/freqtrade/constants.py b/freqtrade/constants.py index a3f91d774..af48ac4f9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -91,7 +91,7 @@ class Constants(object): 'type': 'array', 'items': { 'type': 'string', - 'pattern': '^[0-9A-Z]+_[0-9A-Z]+$' + 'pattern': '^[0-9A-Z]+/[0-9A-Z]+$' }, 'uniqueItems': True }, @@ -99,7 +99,7 @@ class Constants(object): 'type': 'array', 'items': { 'type': 'string', - 'pattern': '^[0-9A-Z]+_[0-9A-Z]+$' + 'pattern': '^[0-9A-Z]+/[0-9A-Z]+$' }, 'uniqueItems': True } diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index dc85bfedb..cdc393d35 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -2,32 +2,56 @@ """ Cryptocurrency Exchanges support """ import enum import logging +import ccxt from random import randint from typing import List, Dict, Any, Optional +from cachetools import cached, TTLCache import arrow import requests -from cachetools import cached, TTLCache from freqtrade import OperationalException -from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.interface import Exchange logger = logging.getLogger(__name__) # Current selected exchange -_API: Exchange = None +_API = None _CONF: dict = {} +API_RETRY_COUNT = 4 # Holds all open sell orders for dry_run _DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {} +def retrier(f): + def wrapper(*args, **kwargs): + count = kwargs.pop('count', API_RETRY_COUNT) + try: + return f(*args, **kwargs) + # TODO dont be a gotta-catch-them-all pokemon collector + except Exception as ex: + logger.warn('%s returned exception: "%s"', f, ex) + if count > 0: + count -= 1 + kwargs.update({'count': count}) + logger.warn('retrying %s still for %s times', f, count) + return wrapper(*args, **kwargs) + else: + raise OperationalException('Giving up retrying: %s', f) + return wrapper -class Exchanges(enum.Enum): - """ - Maps supported exchange names to correspondent classes. - """ - BITTREX = Bittrex + +def _get_market_url(exchange): + "get market url for exchange" + # TODO: PR to ccxt + base = exchange.urls.get('www') + market = "" + if 'bittrex' in get_name(): + market = base + '/Market/Index?MarketName={}' + if 'binance' in get_name(): + market = base + '/trade.html?symbol={}' + + return market def init(config: dict) -> None: @@ -49,12 +73,21 @@ def init(config: dict) -> None: # Find matching class for the given exchange name name = exchange_config['name'] + + # TODO add check for a list of supported exchanges + try: - exchange_class = Exchanges[name.upper()].value + # exchange_class = Exchanges[name.upper()].value + _API = getattr(ccxt, name.lower())({ + 'apiKey': exchange_config.get('key'), + 'secret': exchange_config.get('secret'), + }) + logger.info('Using Exchange %s', name.capitalize()) except KeyError: raise OperationalException('Exchange {} is not supported'.format(name)) - _API = exchange_class(exchange_config) + # we need load api markets + _API.load_markets() # Check if all pairs are available validate_pairs(config['exchange']['pair_whitelist']) @@ -67,15 +100,22 @@ def validate_pairs(pairs: List[str]) -> None: :param pairs: list of pairs :return: None """ + + if not _API.markets: + _API.load_markets() + try: - markets = _API.get_markets() + markets = _API.markets except requests.exceptions.RequestException 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 + pair = pair.replace('_', '/') + # 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) ) @@ -124,23 +164,31 @@ def get_balance(currency: str) -> float: if _CONF['dry_run']: return 999.9 - return _API.get_balance(currency) + return _API.fetch_balance()[currency] def get_balances(): if _CONF['dry_run']: return [] - return _API.get_balances() - + return _API.fetch_balance() +# @cached(TTLCache(maxsize=100, ttl=30)) +@retrier def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict: - return _API.get_ticker(pair, refresh) + return _API.fetch_ticker(pair) -@cached(TTLCache(maxsize=100, ttl=30)) +# @cached(TTLCache(maxsize=100, ttl=30)) +@retrier def get_ticker_history(pair: str, tick_interval) -> List[Dict]: - return _API.get_ticker_history(pair, tick_interval) + # TODO: tickers need to be in format 1m,5m + # fetch_ohlcv returns an [[datetime,o,h,l,c,v]] + if not _API.markets: + _API.load_markets() + ohlcv = _API.fetch_ohlcv(pair, str(tick_interval)+'m') + + return ohlcv def cancel_order(order_id: str) -> None: @@ -162,7 +210,9 @@ def get_order(order_id: str) -> Dict: def get_pair_detail_url(pair: str) -> str: - return _API.get_pair_detail_url(pair) + return _get_market_url(_API).format( + _API.markets[pair]['id'] + ) def get_markets() -> List[str]: @@ -170,16 +220,27 @@ def get_markets() -> List[str]: def get_market_summaries() -> List[Dict]: - return _API.get_market_summaries() + return _API.fetch_tickers() def get_name() -> str: - return _API.name + return _API.__class__.__name__ + + +def get_fee_maker() -> float: + return _API.fees['trading']['maker'] + + +def get_fee_taker() -> float: + return _API.fees['trading']['taker'] def get_fee() -> float: - return _API.fee + return _API.fees['trading'] def get_wallet_health() -> List[Dict]: - return _API.get_wallet_health() + if not _API.markets: + _API.load_markets() + + return _API.markets