So long bittrex, all hail new bittrex from ccxt

This commit is contained in:
Samuel Husso 2018-01-31 16:47:08 +02:00
parent 529e4d0131
commit f0945c354e
4 changed files with 14 additions and 220 deletions

View File

@ -2,6 +2,7 @@
""" Cryptocurrency Exchanges support """
import enum
import logging
import ccxt
from random import randint
from typing import List, Dict, Any, Optional
@ -10,7 +11,6 @@ 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__)
@ -49,12 +49,20 @@ 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'),
})
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'])

View File

@ -1,214 +0,0 @@
import logging
from typing import Dict, List, Optional
from bittrex.bittrex import Bittrex as _Bittrex
from bittrex.bittrex import API_V1_1, API_V2_0
from requests.exceptions import ContentDecodingError
from freqtrade import OperationalException
from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__)
_API: _Bittrex = None
_API_V2: _Bittrex = None
_EXCHANGE_CONF: dict = {}
class Bittrex(Exchange):
"""
Bittrex API wrapper.
"""
# Base URL and API endpoints
BASE_URL: str = 'https://www.bittrex.com'
PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index'
def __init__(self, config: dict) -> None:
global _API, _API_V2, _EXCHANGE_CONF
_EXCHANGE_CONF.update(config)
_API = _Bittrex(
api_key=_EXCHANGE_CONF['key'],
api_secret=_EXCHANGE_CONF['secret'],
calls_per_second=1,
api_version=API_V1_1,
)
_API_V2 = _Bittrex(
api_key=_EXCHANGE_CONF['key'],
api_secret=_EXCHANGE_CONF['secret'],
calls_per_second=1,
api_version=API_V2_0,
)
self.cached_ticker = {}
@staticmethod
def _validate_response(response) -> None:
"""
Validates the given bittrex response
and raises a ContentDecodingError if a non-fatal issue happened.
"""
temp_error_messages = [
'NO_API_RESPONSE',
'MIN_TRADE_REQUIREMENT_NOT_MET',
]
if response['message'] in temp_error_messages:
raise ContentDecodingError('Got {}'.format(response['message']))
@property
def fee(self) -> float:
# 0.25 %: See https://bittrex.com/fees
return 0.0025
def buy(self, pair: str, rate: float, amount: float) -> str:
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'],
pair=pair,
rate=rate,
amount=amount))
return data['result']['uuid']
def sell(self, pair: str, rate: float, amount: float) -> str:
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'],
pair=pair,
rate=rate,
amount=amount))
return data['result']['uuid']
def get_balance(self, currency: str) -> float:
data = _API.get_balance(currency)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({currency})'.format(
message=data['message'],
currency=currency))
return float(data['result']['Balance'] or 0.0)
def get_balances(self):
data = _API.get_balances()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message}'.format(message=data['message']))
return data['result']
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
if refresh or pair not in self.cached_ticker.keys():
data = _API.get_ticker(pair.replace('_', '-'))
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair})'.format(
message=data['message'],
pair=pair))
keys = ['Bid', 'Ask', 'Last']
if not data.get('result') or\
not all(key in data.get('result', {}) for key in keys) or\
not all(data.get('result', {})[key] is not None for key in keys):
raise ContentDecodingError('{message} params=({pair})'.format(
message='Got invalid response from bittrex',
pair=pair))
# Update the pair
self.cached_ticker[pair] = {
'bid': float(data['result']['Bid']),
'ask': float(data['result']['Ask']),
'last': float(data['result']['Last']),
}
return self.cached_ticker[pair]
def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
if tick_interval == 1:
interval = 'oneMin'
elif tick_interval == 5:
interval = 'fiveMin'
elif tick_interval == 30:
interval = 'thirtyMin'
elif tick_interval == 60:
interval = 'hour'
elif tick_interval == 1440:
interval = 'Day'
else:
raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))
data = _API_V2.get_candles(pair.replace('_', '-'), interval)
# These sanity check are necessary because bittrex cannot keep their API stable.
if not data.get('result'):
raise ContentDecodingError('{message} params=({pair})'.format(
message='Got invalid response from bittrex',
pair=pair))
for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
for tick in data['result']:
if prop not in tick.keys():
raise ContentDecodingError('{message} params=({pair})'.format(
message='Required property {} not present in response'.format(prop),
pair=pair))
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair})'.format(
message=data['message'],
pair=pair))
return data['result']
def get_order(self, order_id: str) -> Dict:
data = _API.get_order(order_id)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({order_id})'.format(
message=data['message'],
order_id=order_id))
data = data['result']
return {
'id': data['OrderUuid'],
'type': data['Type'],
'pair': data['Exchange'].replace('-', '_'),
'opened': data['Opened'],
'rate': data['PricePerUnit'],
'amount': data['Quantity'],
'remaining': data['QuantityRemaining'],
'closed': data['Closed'],
}
def cancel_order(self, order_id: str) -> None:
data = _API.cancel(order_id)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({order_id})'.format(
message=data['message'],
order_id=order_id))
def get_pair_detail_url(self, pair: str) -> str:
return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-'))
def get_markets(self) -> List[str]:
data = _API.get_markets()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message}'.format(message=data['message']))
return [m['MarketName'].replace('-', '_') for m in data['result']]
def get_market_summaries(self) -> List[Dict]:
data = _API.get_market_summaries()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message}'.format(message=data['message']))
return data['result']
def get_wallet_health(self) -> List[Dict]:
data = _API_V2.get_wallet_health()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message}'.format(message=data['message']))
return [{
'Currency': entry['Health']['Currency'],
'IsActive': entry['Health']['IsActive'],
'LastChecked': entry['Health']['LastChecked'],
'Notice': entry['Currency'].get('Notice'),
} for entry in data['result']]

View File

@ -1,4 +1,4 @@
python-bittrex==0.3.0
ccxt==1.10.941
SQLAlchemy==1.2.2
python-telegram-bot==9.0.0
arrow==0.12.1

View File

@ -21,7 +21,7 @@ setup(name='freqtrade',
setup_requires=['pytest-runner'],
tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
install_requires=[
'python-bittrex',
'ccxt',
'SQLAlchemy',
'python-telegram-bot',
'arrow',