Adapt exchange functions to ccxt API

Remove get_market_summaries and get_wallet_health, add exception handling
This commit is contained in:
enenn 2018-03-23 22:41:09 +01:00
parent 19928fb6f0
commit d387b68b85

View File

@ -5,23 +5,25 @@ import logging
from random import randint from random import randint
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
import ccxt
import arrow import arrow
import requests
from cachetools import cached, TTLCache from cachetools import cached, TTLCache
from freqtrade import OperationalException from freqtrade import OperationalException, DependencyException, NetworkException
from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.interface import Exchange from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Current selected exchange # Current selected exchange
_API: Exchange = None _API: ccxt.Exchange = None
_CONF: dict = {} _CONF: dict = {}
# Holds all open sell orders for dry_run # Holds all open sell orders for dry_run
_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {} _DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {}
_TICKER_CACHE: dict = {}
class Exchanges(enum.Enum): class Exchanges(enum.Enum):
""" """
@ -49,12 +51,19 @@ def init(config: dict) -> None:
# Find matching class for the given exchange name # Find matching class for the given exchange name
name = exchange_config['name'] name = exchange_config['name']
try:
exchange_class = Exchanges[name.upper()].value if name not in ccxt.exchanges:
except KeyError:
raise OperationalException('Exchange {} is not supported'.format(name)) 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 # Check if all pairs are available
validate_pairs(config['exchange']['pair_whitelist']) validate_pairs(config['exchange']['pair_whitelist'])
@ -68,109 +77,230 @@ def validate_pairs(pairs: List[str]) -> None:
:return: None :return: None
""" """
try: try:
markets = _API.get_markets() markets = _API.load_markets()
except requests.exceptions.RequestException as e: except ccxt.BaseError as e:
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e) logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
return return
stake_cur = _CONF['stake_currency'] stake_cur = _CONF['stake_currency']
for pair in pairs: 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( raise OperationalException(
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur) 'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
) )
if pair not in markets: if pair not in markets:
raise OperationalException( 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']: if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_buy_{}'.format(randint(0, 10**6)) order_id = 'dry_run_buy_{}'.format(randint(0, 10**6))
_DRY_RUN_OPEN_ORDERS[order_id] = { _DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair, 'pair': pair,
'rate': rate, 'price': rate,
'amount': amount, 'amount': amount,
'type': 'LIMIT_BUY', 'type': 'limit',
'side': 'buy',
'remaining': 0.0, 'remaining': 0.0,
'opened': arrow.utcnow().datetime, 'datetime': arrow.utcnow().isoformat(),
'closed': arrow.utcnow().datetime, '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']: if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_sell_{}'.format(randint(0, 10**6)) order_id = 'dry_run_sell_{}'.format(randint(0, 10**6))
_DRY_RUN_OPEN_ORDERS[order_id] = { _DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair, 'pair': pair,
'rate': rate, 'price': rate,
'amount': amount, 'amount': amount,
'type': 'LIMIT_SELL', 'type': 'limit',
'side': 'sell',
'remaining': 0.0, 'remaining': 0.0,
'opened': arrow.utcnow().datetime, 'datetime': arrow.utcnow().isoformat(),
'closed': arrow.utcnow().datetime, '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: def get_balance(currency: str) -> float:
if _CONF['dry_run']: if _CONF['dry_run']:
return 999.9 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']: 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: 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)) @cached(TTLCache(maxsize=100, ttl=30))
def get_ticker_history(pair: str, tick_interval) -> List[Dict]: def get_ticker_history(pair: str, tick_interval: str) -> List[Dict]:
return _API.get_ticker_history(pair, tick_interval) 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']: if _CONF['dry_run']:
return 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']: if _CONF['dry_run']:
order = _DRY_RUN_OPEN_ORDERS[order_id] order = _DRY_RUN_OPEN_ORDERS[order_id]
order.update({ order.update({
'id': order_id 'id': order_id
}) })
return order return order
try:
return _API.get_order(order_id) 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: def get_pair_detail_url(pair: str) -> str:
return _API.get_pair_detail_url(pair) return ""
def get_markets() -> List[str]: def get_markets() -> List[dict]:
return _API.get_markets() try:
return _API.fetch_markets()
except ccxt.NetworkError as e:
def get_market_summaries() -> List[Dict]: raise NetworkException(
return _API.get_market_summaries() 'Could not load markets due to networking error. Message: {}'.format(e)
)
except ccxt.BaseError as e:
raise OperationalException(e)
def get_name() -> str: def get_name() -> str:
@ -178,8 +308,8 @@ def get_name() -> str:
def get_fee() -> float: 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()
return _API.calculate_fee('ETH/BTC', '', '', 1, 1)['rate']
def get_wallet_health() -> List[Dict]:
return _API.get_wallet_health()