Add Binance exchange support
This commit is contained in:
@@ -11,6 +11,7 @@ from cachetools import cached, TTLCache
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.exchange.bittrex import Bittrex
|
||||
from freqtrade.exchange.binance import Binance
|
||||
from freqtrade.exchange.interface import Exchange
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -28,6 +29,7 @@ class Exchanges(enum.Enum):
|
||||
Maps supported exchange names to correspondent classes.
|
||||
"""
|
||||
BITTREX = Bittrex
|
||||
BINANCE = Binance
|
||||
|
||||
|
||||
def init(config: dict) -> None:
|
||||
@@ -143,14 +145,14 @@ def get_ticker_history(pair: str, tick_interval) -> List[Dict]:
|
||||
return _API.get_ticker_history(pair, tick_interval)
|
||||
|
||||
|
||||
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)
|
||||
return _API.cancel_order(order_id, pair)
|
||||
|
||||
|
||||
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({
|
||||
@@ -158,7 +160,7 @@ def get_order(order_id: str) -> Dict:
|
||||
})
|
||||
return order
|
||||
|
||||
return _API.get_order(order_id)
|
||||
return _API.get_order(order_id, pair)
|
||||
|
||||
|
||||
def get_pair_detail_url(pair: str) -> str:
|
||||
@@ -183,3 +185,7 @@ def get_fee() -> float:
|
||||
|
||||
def get_wallet_health() -> List[Dict]:
|
||||
return _API.get_wallet_health()
|
||||
|
||||
|
||||
def get_trade_qty(pair: str) -> tuple:
|
||||
return _API.get_trade_qty(pair)
|
||||
|
374
freqtrade/exchange/binance.py
Normal file
374
freqtrade/exchange/binance.py
Normal file
@@ -0,0 +1,374 @@
|
||||
import logging
|
||||
import datetime
|
||||
import json
|
||||
import http
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
from binance.client import Client as _Binance
|
||||
from binance.exceptions import BinanceAPIException
|
||||
from binance.enums import *
|
||||
from decimal import Decimal
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.exchange.interface import Exchange
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_API: _Binance = None
|
||||
_EXCHANGE_CONF: dict = {}
|
||||
_CONF: dict = {}
|
||||
|
||||
|
||||
class Binance(Exchange):
|
||||
"""
|
||||
Binance API wrapper.
|
||||
"""
|
||||
# Base URL and API endpoints
|
||||
BASE_URL: str = 'https://www.binance.com'
|
||||
|
||||
def __init__(self, config: dict) -> None:
|
||||
global _API, _EXCHANGE_CONF, _CONF
|
||||
|
||||
_EXCHANGE_CONF.update(config)
|
||||
|
||||
_API = _Binance(_EXCHANGE_CONF['key'], _EXCHANGE_CONF['secret'])
|
||||
|
||||
def _pair_to_symbol(self, pair, seperator='') -> str:
|
||||
"""
|
||||
Turns freqtrade pair into Binance symbol
|
||||
- Freqtrade pair = <stake_currency>_<currency>
|
||||
i.e.: BTC_XALT
|
||||
- Binance symbol = <currency><stake_currency>
|
||||
i.e.: XALTBTC
|
||||
"""
|
||||
|
||||
pair_currencies = pair.split('_')
|
||||
|
||||
return '{0}{1}{2}'.format(pair_currencies[1], seperator, pair_currencies[0])
|
||||
|
||||
def _symbol_to_pair(self, symbol) -> str:
|
||||
"""
|
||||
Turns Binance symbol into freqtrade pair
|
||||
- Freqtrade pair = <stake_currency>_<currency>
|
||||
i.e.: BTC_XALT
|
||||
- Binance symbol = <currency><stake_currency>
|
||||
i.e.: XALTBTC
|
||||
"""
|
||||
stake = _EXCHANGE_CONF['stake_currency']
|
||||
|
||||
symbol_stake_currency = symbol[-len(stake):]
|
||||
symbol_currency = symbol[:-len(stake)]
|
||||
|
||||
return '{0}_{1}'.format(symbol_stake_currency, symbol_currency)
|
||||
|
||||
@staticmethod
|
||||
def _handle_exception(excepter) -> None:
|
||||
"""
|
||||
Validates the given Binance response/exception
|
||||
and raises a ContentDecodingError if a non-fatal issue happened.
|
||||
"""
|
||||
# Could to alternate exception handling for specific exceptions/errors
|
||||
# See: http://python-binance.readthedocs.io/en/latest/binance.html#module-binance.exceptions
|
||||
if type(excepter) == http.client.RemoteDisconnected:
|
||||
logger.info(
|
||||
'Got HTTP error from Binance: %s' % excepter
|
||||
)
|
||||
return True
|
||||
|
||||
if type(excepter) == json.decoder.JSONDecodeError:
|
||||
logger.info(
|
||||
'Got JSON error from Binance: %s' % excepter
|
||||
)
|
||||
return True
|
||||
|
||||
if type(excepter) == BinanceAPIException:
|
||||
|
||||
logger.info(
|
||||
'Got API error from Binance: %s' % excepter
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
raise type(excepter)(excepter.args)
|
||||
|
||||
@property
|
||||
def fee(self) -> float:
|
||||
# 0.1 %: See https://support.binance.com/hc/en-us
|
||||
# /articles/115000429332-Fee-Structure-on-Binance
|
||||
return 0.001
|
||||
|
||||
def buy(self, pair: str, rate: float, amount: float) -> str:
|
||||
|
||||
symbol = self._pair_to_symbol(pair)
|
||||
|
||||
try:
|
||||
data = _API.order_limit_buy(
|
||||
symbol=symbol,
|
||||
quantity="{0:.8f}".format(amount),
|
||||
price="{0:.8f}".format(rate))
|
||||
except Exception as e:
|
||||
Binance._handle_exception(e)
|
||||
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
|
||||
message=str(e),
|
||||
pair=pair,
|
||||
rate=Decimal(rate),
|
||||
amount=Decimal(amount)))
|
||||
|
||||
return data['orderId']
|
||||
|
||||
def sell(self, pair: str, rate: float, amount: float) -> str:
|
||||
|
||||
symbol = self._pair_to_symbol(pair)
|
||||
|
||||
try:
|
||||
data = _API.order_limit_sell(
|
||||
symbol=symbol,
|
||||
quantity="{0:.8f}".format(amount),
|
||||
price="{0:.8f}".format(rate))
|
||||
except Exception as e:
|
||||
Binance._handle_exception(e)
|
||||
raise OperationalException(
|
||||
'{message} params=({pair}, {rate}, {amount})'.format(
|
||||
message=str(e),
|
||||
pair=pair,
|
||||
rate=rate,
|
||||
amount=amount))
|
||||
|
||||
return data['orderId']
|
||||
|
||||
def get_balance(self, currency: str) -> float:
|
||||
|
||||
try:
|
||||
data = _API.get_asset_balance(asset=currency)
|
||||
except Exception as e:
|
||||
Binance._handle_exception(e)
|
||||
raise OperationalException('{message} params=({currency})'.format(
|
||||
message=str(e),
|
||||
currency=currency))
|
||||
|
||||
return float(data['free'] or 0.0)
|
||||
|
||||
def get_balances(self) -> List[Dict]:
|
||||
|
||||
try:
|
||||
data = _API.get_account()
|
||||
except Exception as e:
|
||||
Binance._handle_exception(e)
|
||||
raise OperationalException('{message}'.format(message=str(e)))
|
||||
|
||||
balances = data['balances']
|
||||
|
||||
currency_balances = []
|
||||
for currency in balances:
|
||||
balance = {}
|
||||
|
||||
if float(currency['free']) == 0 and float(currency['locked']) == 0:
|
||||
continue
|
||||
balance['Currency'] = currency.pop('asset')
|
||||
balance['Available'] = currency.pop('free')
|
||||
balance['Pending'] = currency.pop('locked')
|
||||
balance['Balance'] = float(balance['Available']) + float(balance['Pending'])
|
||||
|
||||
currency_balances.append(balance)
|
||||
|
||||
return currency_balances
|
||||
|
||||
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
|
||||
|
||||
symbol = self._pair_to_symbol(pair)
|
||||
|
||||
try:
|
||||
data = _API.get_ticker(symbol=symbol)
|
||||
except Exception as e:
|
||||
Binance._handle_exception(e)
|
||||
raise OperationalException('{message} params=({pair})'.format(
|
||||
message=str(e),
|
||||
pair=pair))
|
||||
|
||||
return {
|
||||
'bid': float(data['bidPrice']),
|
||||
'ask': float(data['askPrice']),
|
||||
'last': float(data['lastPrice']),
|
||||
}
|
||||
|
||||
def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
|
||||
|
||||
INTERVAL_ENUM = eval('KLINE_INTERVAL_' + str(tick_interval) + 'MINUTE')
|
||||
|
||||
if INTERVAL_ENUM in ['', None]:
|
||||
raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))
|
||||
|
||||
symbol = self._pair_to_symbol(pair)
|
||||
|
||||
try:
|
||||
data = _API.get_klines(symbol=symbol, interval=INTERVAL_ENUM)
|
||||
except Exception as e:
|
||||
Binance._handle_exception(e)
|
||||
raise OperationalException('{message} params=({pair})'.format(
|
||||
message=str(e),
|
||||
pair=pair))
|
||||
|
||||
tick_data = []
|
||||
|
||||
for tick in data:
|
||||
t = {}
|
||||
t['O'] = float(tick[1])
|
||||
t['H'] = float(tick[2])
|
||||
t['L'] = float(tick[3])
|
||||
t['C'] = float(tick[4])
|
||||
t['V'] = float(tick[5])
|
||||
t['T'] = datetime.datetime.fromtimestamp(int(tick[6])/1000).isoformat()
|
||||
t['BV'] = float(tick[7])
|
||||
|
||||
tick_data.append(t)
|
||||
|
||||
return tick_data
|
||||
|
||||
def get_order(self, order_id: str, pair: str) -> Dict:
|
||||
|
||||
symbol = self._pair_to_symbol(pair)
|
||||
|
||||
try:
|
||||
data = _API.get_all_orders(symbol=symbol, orderId=order_id)
|
||||
except Exception as e:
|
||||
Binance._handle_exception(e)
|
||||
raise OperationalException(
|
||||
'{message} params=({symbol},{order_id})'.format(
|
||||
message=str(e),
|
||||
symbol=symbol,
|
||||
order_id=order_id))
|
||||
|
||||
order = {}
|
||||
|
||||
for o in data:
|
||||
|
||||
if o['orderId'] == int(order_id):
|
||||
|
||||
order['id'] = o['orderId']
|
||||
order['type'] = "{}_{}".format(o['type'], o['side'])
|
||||
order['pair'] = self._symbol_to_pair(o['symbol'])
|
||||
order['opened'] = datetime.datetime.fromtimestamp(
|
||||
int(o['time'])/1000).isoformat()
|
||||
order['closed'] = datetime.datetime.fromtimestamp(
|
||||
int(o['time'])/1000).isoformat()\
|
||||
if o['status'] == 'FILLED' else None
|
||||
order['rate'] = float(o['price'])
|
||||
order['amount'] = float(o['origQty'])
|
||||
order['remaining'] = int(float(o['origQty']) - float(o['executedQty']))
|
||||
|
||||
return order
|
||||
|
||||
def cancel_order(self, order_id: str, pair: str) -> None:
|
||||
|
||||
symbol = self._pair_to_symbol(pair)
|
||||
|
||||
try:
|
||||
data = _API.cancel_order(symbol=symbol, orderId=order_id)
|
||||
except Exception as e:
|
||||
Binance._handle_exception(e)
|
||||
raise OperationalException('{message} params=({order_id})'.format(
|
||||
message=str(e),
|
||||
order_id=order_id))
|
||||
|
||||
return data
|
||||
|
||||
def get_pair_detail_url(self, pair: str) -> str:
|
||||
symbol = self._pair_to_symbol(pair, '_')
|
||||
return 'https://www.binance.com/indexSpa.html#/trade/index?symbol={}'.format(symbol)
|
||||
|
||||
def get_markets(self) -> List[str]:
|
||||
try:
|
||||
data = _API.get_all_tickers()
|
||||
except Exception as e:
|
||||
Binance._handle_exception(e)
|
||||
raise OperationalException('{message}'.format(message=str(e)))
|
||||
|
||||
markets = []
|
||||
|
||||
stake = _EXCHANGE_CONF['stake_currency']
|
||||
|
||||
for t in data:
|
||||
symbol = t['symbol']
|
||||
symbol_stake_currency = symbol[-len(stake):]
|
||||
|
||||
if symbol_stake_currency == stake:
|
||||
pair = self._symbol_to_pair(symbol)
|
||||
markets.append(pair)
|
||||
|
||||
return markets
|
||||
|
||||
def get_market_summaries(self) -> List[Dict]:
|
||||
|
||||
try:
|
||||
data = _API.get_ticker()
|
||||
except Exception as e:
|
||||
Binance._handle_exception(e)
|
||||
raise OperationalException('{message}'.format(message=str(e)))
|
||||
|
||||
market_summaries = []
|
||||
|
||||
for t in data:
|
||||
market = {}
|
||||
|
||||
# Looks like this one is only one actually used
|
||||
market['MarketName'] = self._symbol_to_pair(t['symbol'])
|
||||
|
||||
market['High'] = t['highPrice']
|
||||
market['Low'] = t['lowPrice']
|
||||
market['Volume'] = t['volume']
|
||||
market['Last'] = t['lastPrice']
|
||||
market['TimeStamp'] = t['closeTime']
|
||||
market['BaseVolume'] = t['volume']
|
||||
market['Bid'] = t['bidPrice']
|
||||
market['Ask'] = t['askPrice']
|
||||
market['OpenBuyOrders'] = None # TODO: Implement me (or dont care)
|
||||
market['OpenSellOrders'] = None # TODO: Implement me (or dont care)
|
||||
market['PrevDay'] = t['prevClosePrice']
|
||||
market['Created'] = None # TODO: Implement me (or don't care)
|
||||
|
||||
market_summaries.append(market)
|
||||
|
||||
return market_summaries
|
||||
|
||||
def get_trade_qty(self, pair: str) -> tuple:
|
||||
|
||||
try:
|
||||
data = _API.get_exchange_info()
|
||||
except Exception as e:
|
||||
Binance._handle_exception(e)
|
||||
raise OperationalException('{message}'.format(message=str(e)))
|
||||
|
||||
symbol = self._pair_to_symbol(pair)
|
||||
|
||||
for s in data['symbols']:
|
||||
|
||||
if symbol == s['symbol']:
|
||||
|
||||
for f in s['filters']:
|
||||
|
||||
if f['filterType'] == 'LOT_SIZE':
|
||||
|
||||
return (float(f['minQty']), float(f['maxQty']), float(f['stepSize']))
|
||||
|
||||
return (None, None, None)
|
||||
|
||||
def get_wallet_health(self) -> List[Dict]:
|
||||
|
||||
try:
|
||||
data = _API.get_exchange_info()
|
||||
except Exception as e:
|
||||
Binance._handle_exception(e)
|
||||
raise OperationalException('{message}'.format(message=str(e)))
|
||||
|
||||
wallet_health = []
|
||||
|
||||
for s in data['symbols']:
|
||||
wallet = {}
|
||||
wallet['Currency'] = s['baseAsset']
|
||||
wallet['IsActive'] = True if s['status'] == 'TRADING' else False
|
||||
wallet['LastChecked'] = None # TODO
|
||||
wallet['Notice'] = s['status'] if s['status'] != 'TRADING' else ''
|
||||
|
||||
wallet_health.append(wallet)
|
||||
|
||||
return wallet_health
|
@@ -157,7 +157,8 @@ class Bittrex(Exchange):
|
||||
|
||||
return data['result']
|
||||
|
||||
def get_order(self, order_id: str) -> Dict:
|
||||
def get_order(self, order_id: str, pair: str) -> Dict:
|
||||
|
||||
data = _API.get_order(order_id)
|
||||
if not data['success']:
|
||||
Bittrex._validate_response(data)
|
||||
@@ -176,7 +177,8 @@ class Bittrex(Exchange):
|
||||
'closed': data['Closed'],
|
||||
}
|
||||
|
||||
def cancel_order(self, order_id: str) -> None:
|
||||
def cancel_order(self, order_id: str, pair: str) -> None:
|
||||
|
||||
data = _API.cancel(order_id)
|
||||
if not data['success']:
|
||||
Bittrex._validate_response(data)
|
||||
@@ -212,3 +214,6 @@ class Bittrex(Exchange):
|
||||
'LastChecked': entry['Health']['LastChecked'],
|
||||
'Notice': entry['Currency'].get('Notice'),
|
||||
} for entry in data['result']]
|
||||
|
||||
def get_trade_qty(self, pair: str) -> tuple:
|
||||
return (None, None, None)
|
||||
|
@@ -94,7 +94,7 @@ class Exchange(ABC):
|
||||
]
|
||||
"""
|
||||
|
||||
def get_order(self, order_id: str) -> Dict:
|
||||
def get_order(self, order_id: str, pair: str) -> Dict:
|
||||
"""
|
||||
Get order details for the given order_id.
|
||||
:param order_id: ID as str
|
||||
@@ -111,7 +111,7 @@ class Exchange(ABC):
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def cancel_order(self, order_id: str) -> None:
|
||||
def cancel_order(self, order_id: str, pair: str) -> None:
|
||||
"""
|
||||
Cancels order for given order_id.
|
||||
:param order_id: ID as str
|
||||
@@ -170,3 +170,11 @@ class Exchange(ABC):
|
||||
},
|
||||
...
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_trade_qty(self, pair: str) -> tuple:
|
||||
"""
|
||||
Returns a tuple of trade quantity limits
|
||||
:return: tuple, format: ( min_qty: str, max_qty: str, step_qty: str )
|
||||
...
|
||||
"""
|
||||
|
Reference in New Issue
Block a user