2017-10-06 10:22:04 +00:00
|
|
|
import logging
|
2018-01-11 05:24:05 +00:00
|
|
|
import requests
|
2018-01-10 07:51:36 +00:00
|
|
|
from typing import Dict, List, Optional
|
2017-10-06 10:22:04 +00:00
|
|
|
|
2018-01-10 07:51:36 +00:00
|
|
|
from bittrex.bittrex import Bittrex as _Bittrex
|
|
|
|
from bittrex.bittrex import API_V1_1, API_V2_0
|
2017-11-16 15:39:06 +00:00
|
|
|
from requests.exceptions import ContentDecodingError
|
2017-10-06 10:22:04 +00:00
|
|
|
|
2017-11-20 21:15:19 +00:00
|
|
|
from freqtrade import OperationalException
|
2017-11-20 21:26:32 +00:00
|
|
|
from freqtrade.exchange.interface import Exchange
|
2017-10-06 10:22:04 +00:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
_API: _Bittrex = None
|
2017-11-11 16:14:55 +00:00
|
|
|
_API_V2: _Bittrex = None
|
2017-10-06 10:22:04 +00:00
|
|
|
_EXCHANGE_CONF: dict = {}
|
|
|
|
|
2018-01-11 05:24:05 +00:00
|
|
|
# API socket timeout
|
|
|
|
API_TIMEOUT = 60
|
|
|
|
|
|
|
|
|
|
|
|
def custom_requests(request_url, apisign):
|
|
|
|
"""
|
|
|
|
Set timeout for requests
|
|
|
|
"""
|
|
|
|
return requests.get(
|
|
|
|
request_url,
|
|
|
|
headers={"apisign": apisign},
|
|
|
|
timeout=API_TIMEOUT
|
|
|
|
).json()
|
|
|
|
|
2017-10-06 10:22:04 +00:00
|
|
|
|
|
|
|
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:
|
2017-11-11 16:14:55 +00:00
|
|
|
global _API, _API_V2, _EXCHANGE_CONF
|
2017-10-06 10:22:04 +00:00
|
|
|
|
|
|
|
_EXCHANGE_CONF.update(config)
|
2017-11-08 23:31:53 +00:00
|
|
|
_API = _Bittrex(
|
|
|
|
api_key=_EXCHANGE_CONF['key'],
|
|
|
|
api_secret=_EXCHANGE_CONF['secret'],
|
2017-11-15 22:16:39 +00:00
|
|
|
calls_per_second=1,
|
2017-11-11 16:14:55 +00:00
|
|
|
api_version=API_V1_1,
|
2018-01-11 05:24:05 +00:00
|
|
|
dispatch=custom_requests
|
2017-11-11 16:14:55 +00:00
|
|
|
)
|
|
|
|
_API_V2 = _Bittrex(
|
|
|
|
api_key=_EXCHANGE_CONF['key'],
|
|
|
|
api_secret=_EXCHANGE_CONF['secret'],
|
2017-11-15 22:16:39 +00:00
|
|
|
calls_per_second=1,
|
2017-11-11 16:14:55 +00:00
|
|
|
api_version=API_V2_0,
|
2018-01-11 05:24:05 +00:00
|
|
|
dispatch=custom_requests
|
2017-11-08 23:31:53 +00:00
|
|
|
)
|
2018-01-02 09:56:42 +00:00
|
|
|
self.cached_ticker = {}
|
2017-10-06 10:22:04 +00:00
|
|
|
|
2017-12-14 19:27:04 +00:00
|
|
|
@staticmethod
|
|
|
|
def _validate_response(response) -> None:
|
|
|
|
"""
|
|
|
|
Validates the given bittrex response
|
|
|
|
and raises a ContentDecodingError if a non-fatal issue happened.
|
|
|
|
"""
|
2017-12-26 08:39:29 +00:00
|
|
|
temp_error_messages = [
|
|
|
|
'NO_API_RESPONSE',
|
|
|
|
'MIN_TRADE_REQUIREMENT_NOT_MET',
|
|
|
|
]
|
|
|
|
if response['message'] in temp_error_messages:
|
|
|
|
raise ContentDecodingError('Got {}'.format(response['message']))
|
2017-12-14 19:27:04 +00:00
|
|
|
|
2017-10-31 23:13:23 +00:00
|
|
|
@property
|
|
|
|
def fee(self) -> float:
|
2017-12-19 05:58:02 +00:00
|
|
|
# 0.25 %: See https://bittrex.com/fees
|
2017-10-31 23:13:23 +00:00
|
|
|
return 0.0025
|
|
|
|
|
2017-10-06 10:22:04 +00:00
|
|
|
def buy(self, pair: str, rate: float, amount: float) -> str:
|
|
|
|
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
|
|
|
|
if not data['success']:
|
2017-12-14 19:27:04 +00:00
|
|
|
Bittrex._validate_response(data)
|
2017-11-20 21:15:19 +00:00
|
|
|
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
|
2017-11-05 21:47:55 +00:00
|
|
|
message=data['message'],
|
|
|
|
pair=pair,
|
|
|
|
rate=rate,
|
|
|
|
amount=amount))
|
2017-10-06 10:22:04 +00:00
|
|
|
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']:
|
2017-12-14 19:27:04 +00:00
|
|
|
Bittrex._validate_response(data)
|
2017-11-20 21:15:19 +00:00
|
|
|
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
|
2017-11-05 21:47:55 +00:00
|
|
|
message=data['message'],
|
|
|
|
pair=pair,
|
|
|
|
rate=rate,
|
|
|
|
amount=amount))
|
2017-10-06 10:22:04 +00:00
|
|
|
return data['result']['uuid']
|
|
|
|
|
|
|
|
def get_balance(self, currency: str) -> float:
|
|
|
|
data = _API.get_balance(currency)
|
|
|
|
if not data['success']:
|
2017-12-14 19:27:04 +00:00
|
|
|
Bittrex._validate_response(data)
|
2017-11-20 21:15:19 +00:00
|
|
|
raise OperationalException('{message} params=({currency})'.format(
|
2017-11-05 21:47:55 +00:00
|
|
|
message=data['message'],
|
|
|
|
currency=currency))
|
2017-10-06 10:22:04 +00:00
|
|
|
return float(data['result']['Balance'] or 0.0)
|
|
|
|
|
2017-10-28 05:44:49 +00:00
|
|
|
def get_balances(self):
|
|
|
|
data = _API.get_balances()
|
|
|
|
if not data['success']:
|
2017-12-14 19:27:04 +00:00
|
|
|
Bittrex._validate_response(data)
|
2017-11-20 21:15:19 +00:00
|
|
|
raise OperationalException('{message}'.format(message=data['message']))
|
2017-10-28 05:44:49 +00:00
|
|
|
return data['result']
|
2017-11-04 17:55:41 +00:00
|
|
|
|
2018-01-02 09:56:42 +00:00
|
|
|
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
|
2018-01-02 10:00:22 +00:00
|
|
|
if refresh or pair not in self.cached_ticker.keys():
|
2018-01-02 13:09:56 +00:00
|
|
|
data = _API.get_ticker(pair.replace('_', '-'))
|
2018-01-02 09:56:42 +00:00
|
|
|
if not data['success']:
|
|
|
|
Bittrex._validate_response(data)
|
|
|
|
raise OperationalException('{message} params=({pair})'.format(
|
|
|
|
message=data['message'],
|
|
|
|
pair=pair))
|
2018-01-20 13:44:13 +00:00
|
|
|
keys = ['Bid', 'Ask', 'Last']
|
2018-01-13 08:50:02 +00:00
|
|
|
if not data.get('result') or\
|
2018-01-20 13:44:13 +00:00
|
|
|
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):
|
2018-01-02 09:56:42 +00:00
|
|
|
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]
|
2017-10-06 10:22:04 +00:00
|
|
|
|
2017-11-13 18:54:09 +00:00
|
|
|
def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
|
2017-11-07 17:41:48 +00:00
|
|
|
if tick_interval == 1:
|
|
|
|
interval = 'oneMin'
|
|
|
|
elif tick_interval == 5:
|
|
|
|
interval = 'fiveMin'
|
2018-01-12 16:02:35 +00:00
|
|
|
elif tick_interval == 30:
|
|
|
|
interval = 'thirtyMin'
|
|
|
|
elif tick_interval == 60:
|
|
|
|
interval = 'hour'
|
|
|
|
elif tick_interval == 1440:
|
2018-01-15 21:36:38 +00:00
|
|
|
interval = 'Day'
|
2017-11-07 17:41:48 +00:00
|
|
|
else:
|
|
|
|
raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))
|
|
|
|
|
2017-11-11 16:14:55 +00:00
|
|
|
data = _API_V2.get_candles(pair.replace('_', '-'), interval)
|
2017-11-15 22:16:54 +00:00
|
|
|
|
|
|
|
# These sanity check are necessary because bittrex cannot keep their API stable.
|
|
|
|
if not data.get('result'):
|
2017-11-16 15:39:06 +00:00
|
|
|
raise ContentDecodingError('{message} params=({pair})'.format(
|
|
|
|
message='Got invalid response from bittrex',
|
|
|
|
pair=pair))
|
2017-11-15 22:16:54 +00:00
|
|
|
|
2017-11-13 18:54:09 +00:00
|
|
|
for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
|
|
|
|
for tick in data['result']:
|
|
|
|
if prop not in tick.keys():
|
2017-11-16 15:39:06 +00:00
|
|
|
raise ContentDecodingError('{message} params=({pair})'.format(
|
|
|
|
message='Required property {} not present in response'.format(prop),
|
|
|
|
pair=pair))
|
2017-11-13 18:54:09 +00:00
|
|
|
|
2017-10-06 10:22:04 +00:00
|
|
|
if not data['success']:
|
2017-12-14 19:27:04 +00:00
|
|
|
Bittrex._validate_response(data)
|
2017-11-20 21:15:19 +00:00
|
|
|
raise OperationalException('{message} params=({pair})'.format(
|
2017-11-05 21:47:55 +00:00
|
|
|
message=data['message'],
|
2017-11-05 23:06:59 +00:00
|
|
|
pair=pair))
|
2017-11-07 17:49:16 +00:00
|
|
|
|
2017-11-05 22:47:59 +00:00
|
|
|
return data['result']
|
2017-10-06 10:22:04 +00:00
|
|
|
|
2017-10-31 23:13:23 +00:00
|
|
|
def get_order(self, order_id: str) -> Dict:
|
|
|
|
data = _API.get_order(order_id)
|
2017-10-06 10:22:04 +00:00
|
|
|
if not data['success']:
|
2017-12-14 19:27:04 +00:00
|
|
|
Bittrex._validate_response(data)
|
2017-11-20 21:15:19 +00:00
|
|
|
raise OperationalException('{message} params=({order_id})'.format(
|
2017-11-05 21:47:55 +00:00
|
|
|
message=data['message'],
|
|
|
|
order_id=order_id))
|
2017-10-31 23:13:23 +00:00
|
|
|
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'],
|
|
|
|
}
|
2017-10-06 10:22:04 +00:00
|
|
|
|
2017-10-31 23:13:23 +00:00
|
|
|
def cancel_order(self, order_id: str) -> None:
|
|
|
|
data = _API.cancel(order_id)
|
2017-10-06 10:22:04 +00:00
|
|
|
if not data['success']:
|
2017-12-14 19:27:04 +00:00
|
|
|
Bittrex._validate_response(data)
|
2017-11-20 21:15:19 +00:00
|
|
|
raise OperationalException('{message} params=({order_id})'.format(
|
2017-11-05 21:47:55 +00:00
|
|
|
message=data['message'],
|
|
|
|
order_id=order_id))
|
2017-10-06 10:22:04 +00:00
|
|
|
|
|
|
|
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']:
|
2017-12-14 19:27:04 +00:00
|
|
|
Bittrex._validate_response(data)
|
2017-11-20 21:15:19 +00:00
|
|
|
raise OperationalException('{message}'.format(message=data['message']))
|
2017-10-06 10:22:04 +00:00
|
|
|
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
2017-11-11 18:20:16 +00:00
|
|
|
|
|
|
|
def get_market_summaries(self) -> List[Dict]:
|
|
|
|
data = _API.get_market_summaries()
|
|
|
|
if not data['success']:
|
2017-12-14 19:27:04 +00:00
|
|
|
Bittrex._validate_response(data)
|
2017-11-20 21:15:19 +00:00
|
|
|
raise OperationalException('{message}'.format(message=data['message']))
|
2017-11-11 18:20:16 +00:00
|
|
|
return data['result']
|
2017-11-13 20:34:47 +00:00
|
|
|
|
|
|
|
def get_wallet_health(self) -> List[Dict]:
|
|
|
|
data = _API_V2.get_wallet_health()
|
|
|
|
if not data['success']:
|
2017-12-14 19:27:04 +00:00
|
|
|
Bittrex._validate_response(data)
|
2017-11-20 21:15:19 +00:00
|
|
|
raise OperationalException('{message}'.format(message=data['message']))
|
2017-11-13 20:34:47 +00:00
|
|
|
return [{
|
|
|
|
'Currency': entry['Health']['Currency'],
|
|
|
|
'IsActive': entry['Health']['IsActive'],
|
|
|
|
'LastChecked': entry['Health']['LastChecked'],
|
|
|
|
'Notice': entry['Currency'].get('Notice'),
|
|
|
|
} for entry in data['result']]
|