From 5459e07e943a3d0086e11d503953586325e052f2 Mon Sep 17 00:00:00 2001 From: msincenselee Date: Thu, 4 Oct 2018 10:29:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vnpy/api/binance/client.py | 945 ++++++++++++++++++++++----------- vnpy/api/binance/helpers.py | 45 ++ vnpy/api/binance/vnbinance.py | 20 +- vnpy/api/binance/websockets.py | 203 +++---- 4 files changed, 787 insertions(+), 426 deletions(-) create mode 100644 vnpy/api/binance/helpers.py diff --git a/vnpy/api/binance/client.py b/vnpy/api/binance/client.py index 7a139201..54814daa 100644 --- a/vnpy/api/binance/client.py +++ b/vnpy/api/binance/client.py @@ -7,6 +7,8 @@ import requests import six import time import sys +from operator import itemgetter +from .helpers import date_to_milliseconds, interval_to_milliseconds from .exceptions import BinanceAPIException, BinanceRequestException, BinanceWithdrawException if six.PY2: @@ -14,7 +16,6 @@ if six.PY2: elif six.PY3: from urllib.parse import urlencode - class Client(object): API_URL = 'https://api.binance.com/api' @@ -69,14 +70,25 @@ class Client(object): ORDER_RESP_TYPE_RESULT = 'RESULT' ORDER_RESP_TYPE_FULL = 'FULL' - def __init__(self, api_key, api_secret, api=None): + # For accessing the data returned by Client.aggregate_trades(). + AGG_ID = 'a' + AGG_PRICE = 'p' + AGG_QUANTITY = 'q' + AGG_FIRST_TRADE_ID = 'f' + AGG_LAST_TRADE_ID = 'l' + AGG_TIME = 'T' + AGG_BUYER_MAKES = 'm' + AGG_BEST_MATCH = 'M' + + def __init__(self, api_key, api_secret, api=None,requests_params=None): """Binance API Client constructor :param api_key: Api Key :type api_key: str. :param api_secret: Api Secret :type api_secret: str. - + :param requests_params: optional - Dictionary of requests params to use for all calls + :type requests_params: dict. """ self.API_KEY = api_key @@ -84,6 +96,7 @@ class Client(object): self.api = api self.DEBUG = False self.session = self._init_session() + self._requests_params = requests_params # init DNS and SSL cert self.ping() @@ -130,9 +143,9 @@ class Client(object): return self.WEBSITE_URL + '/' + path def _generate_signature(self, data): - query_string = urlencode(data) + ordered_data = self._order_params(data) + query_string = '&'.join(["{}={}".format(d[0], d[1]) for d in ordered_data]) m = hmac.new(self.API_SECRET.encode('utf-8'), query_string.encode('utf-8'), hashlib.sha256) - return m.hexdigest() def _order_params(self, data): @@ -149,15 +162,22 @@ class Client(object): has_signature = True else: params.append((key, value)) + # sort parameters by key + params.sort(key=itemgetter(0)) if has_signature: params.append(('signature', data['signature'])) return params def _request(self, method, uri, signed, force_params=False, **kwargs): - data = kwargs.get('data', None) - if data is None: - kwargs['data'] = data + # set default requests timeout + kwargs['timeout'] = 10 + + # add our global requests params + if self._requests_params: + kwargs.update(self._requests_params) + + data = kwargs.get('data', None) if data and isinstance(data, dict): kwargs['data'] = data if signed: @@ -166,8 +186,20 @@ class Client(object): kwargs['data']['timestamp'] = int(time.time() * 1000) kwargs['data']['signature'] = self._generate_signature(kwargs['data']) + # sort get and post params to match signature order + if data: + # find any requests params passed and apply them + if 'requests_params' in kwargs['data']: + # merge requests params into kwargs + kwargs.update(kwargs['data']['requests_params']) + del(kwargs['data']['requests_params']) + + # sort post params + kwargs['data'] = self._order_params(kwargs['data']) + + # if get request assign data array to params value for requests lib if data and (method == 'get' or force_params): - kwargs['params'] = self._order_params(kwargs['data']) + kwargs['params'] = kwargs['data'] del(kwargs['data']) # kwargs["verify"] = False # I don't know whay this is error @@ -223,13 +255,9 @@ class Client(object): def get_products(self): """Return list of products currently listed on Binance - Use get_exchange_info() call instead - :returns: list - List of product dictionaries - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ products = self._request_website('get', 'exchange/public/product') @@ -237,11 +265,8 @@ class Client(object): def get_exchange_info(self): """Return rate limits and list of symbols - :returns: list - List of product dictionaries - .. code-block:: python - { "timezone": "UTC", "serverTime": 1508631584636, @@ -292,46 +317,75 @@ class Client(object): } ] } - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('exchangeInfo') + def get_symbol_info(self, symbol): + """Return information about a symbol + :param symbol: required e.g BNBBTC + :type symbol: str + :returns: Dict if found, None if not + .. code-block:: python + { + "symbol": "ETHBTC", + "status": "TRADING", + "baseAsset": "ETH", + "baseAssetPrecision": 8, + "quoteAsset": "BTC", + "quotePrecision": 8, + "orderTypes": ["LIMIT", "MARKET"], + "icebergAllowed": false, + "filters": [ + { + "filterType": "PRICE_FILTER", + "minPrice": "0.00000100", + "maxPrice": "100000.00000000", + "tickSize": "0.00000100" + }, { + "filterType": "LOT_SIZE", + "minQty": "0.00100000", + "maxQty": "100000.00000000", + "stepSize": "0.00100000" + }, { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.00100000" + } + ] + } + :raises: BinanceRequestException, BinanceAPIException + """ + + res = self._get('exchangeInfo') + + for item in res['symbols']: + if item['symbol'] == symbol.upper(): + return item + + return None + # General Endpoints def ping(self): """Test connectivity to the Rest API. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#test-connectivity - :returns: Empty array - .. code-block:: python - {} - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('ping') def get_server_time(self): """Test connectivity to the Rest API and get the current server time. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#check-server-time - :returns: Current server time - .. code-block:: python - { "serverTime": 1499827319559 } - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('time') @@ -339,13 +393,9 @@ class Client(object): def get_all_tickers(self): """Latest price for all symbols. - https://www.binance.com/restapipub.html#symbols-price-ticker - :returns: List of market tickers - .. code-block:: python - [ { "symbol": "LTCBTC", @@ -356,21 +406,15 @@ class Client(object): "price": "0.07946600" } ] - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('ticker/allPrices') def get_orderbook_tickers(self): """Best price/qty on the order book for all symbols. - https://www.binance.com/restapipub.html#symbols-order-book-ticker - :returns: List of order book market entries - .. code-block:: python - [ { "symbol": "LTCBTC", @@ -387,26 +431,19 @@ class Client(object): "askQty": "1000.00000000" } ] - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('ticker/allBookTickers') def get_order_book(self, **params): """Get the Order Book for the market - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#order-book - :param symbol: required :type symbol: str - :param limit: Default 100; max 100 + :param limit: Default 100; max 1000 :type limit: int - :returns: API response - .. code-block:: python - { "lastUpdateId": 1027024, "bids": [ @@ -424,27 +461,19 @@ class Client(object): ] ] } - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ - return self._get('depth', data=params) def get_recent_trades(self, **params): """Get recent trades (up to last 500). - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#recent-trades-list - :param symbol: required :type symbol: str :param limit: Default 500; max 500. :type limit: int - :returns: API response - .. code-block:: python - [ { "id": 28457, @@ -455,28 +484,21 @@ class Client(object): "isBestMatch": true } ] - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('trades', data=params) def get_historical_trades(self, **params): """Get older trades. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#recent-trades-list - :param symbol: required :type symbol: str :param limit: Default 500; max 500. :type limit: int :param fromId: TradeId to fetch from. Default gets most recent trades. :type fromId: str - :returns: API response - .. code-block:: python - [ { "id": 28457, @@ -487,18 +509,14 @@ class Client(object): "isBestMatch": true } ] - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('historicalTrades', data=params) def get_aggregate_trades(self, **params): """Get compressed, aggregate trades. Trades that fill at the time, from the same order, with the same price will have the quantity aggregated. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#compressedaggregate-trades-list - :param symbol: required :type symbol: str :param fromId: ID to get aggregate trades from INCLUSIVE. @@ -509,11 +527,8 @@ class Client(object): :type endTime: int :param limit: Default 500; max 500. :type limit: int - :returns: API response - .. code-block:: python - [ { "a": 26129, # Aggregate tradeId @@ -526,32 +541,91 @@ class Client(object): "M": true # Was the trade the best price match? } ] - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('aggTrades', data=params) + def aggregate_trade_iter(self, symbol, start_str=None, last_id=None): + """Iterate over aggregate trade data from (start_time or last_id) to + the end of the history so far. + If start_time is specified, start with the first trade after + start_time. Meant to initialise a local cache of trade data. + If last_id is specified, start with the trade after it. This is meant + for updating a pre-existing local trade data cache. + Only allows start_str or last_id—not both. Not guaranteed to work + right if you're running more than one of these simultaneously. You + will probably hit your rate limit. + See dateparser docs for valid start and end string formats http://dateparser.readthedocs.io/en/latest/ + If using offset strings for dates add "UTC" to date string e.g. "now UTC", "11 hours ago UTC" + :param symbol: Symbol string e.g. ETHBTC + :type symbol: str + :param start_str: Start date string in UTC format or timestamp in milliseconds. The iterator will + return the first trade occurring later than this time. + :type start_str: str|int + :param last_id: aggregate trade ID of the last known aggregate trade. + Not a regular trade ID. See https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#compressedaggregate-trades-list. + :returns: an iterator of JSON objects, one per trade. The format of + each object is identical to Client.aggregate_trades(). + :type last_id: int + """ + if start_str is not None and last_id is not None: + raise ValueError( + 'start_time and last_id may not be simultaneously specified.') + + # If there's no last_id, get one. + if last_id is None: + # Without a last_id, we actually need the first trade. Normally, + # we'd get rid of it. See the next loop. + if start_str is None: + trades = self.get_aggregate_trades(symbol=symbol, fromId=0) + else: + # The difference between startTime and endTime should be less + # or equal than an hour and the result set should contain at + # least one trade. + if type(start_str) == int: + start_ts = start_str + else: + start_ts = date_to_milliseconds(start_str) + trades = self.get_aggregate_trades( + symbol=symbol, + startTime=start_ts, + endTime=start_ts + (60 * 60 * 1000)) + for t in trades: + yield t + last_id = trades[-1][self.AGG_ID] + + while True: + # There is no need to wait between queries, to avoid hitting the + # rate limit. We're using blocking IO, and as long as we're the + # only thread running calls like this, Binance will automatically + # add the right delay time on their end, forcing us to wait for + # data. That really simplifies this function's job. Binance is + # fucking awesome. + trades = self.get_aggregate_trades(symbol=symbol, fromId=last_id) + # fromId=n returns a set starting with id n, but we already have + # that one. So get rid of the first item in the result set. + trades = trades[1:] + if len(trades) == 0: + return + for t in trades: + yield t + last_id = trades[-1][self.AGG_ID] + def get_klines(self, **params): """Kline/candlestick bars for a symbol. Klines are uniquely identified by their open time. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#klinecandlestick-data - :param symbol: required :type symbol: str :param interval: - - :type interval: enum + :type interval: str :param limit: - Default 500; max 500. :type limit: int :param startTime: :type startTime: int :param endTime: :type endTime: int - :returns: API response - .. code-block:: python - [ [ 1499040000000, # Open time @@ -568,24 +642,185 @@ class Client(object): "17928899.62484339" # Can be ignored ] ] - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('klines', data=params) + def _get_earliest_valid_timestamp(self, symbol, interval): + """Get earliest valid open timestamp from Binance + :param symbol: Name of symbol pair e.g BNBBTC + :type symbol: str + :param interval: Binance Kline interval + :type interval: str + :return: first valid timestamp + """ + kline = self.get_klines( + symbol=symbol, + interval=interval, + limit=1, + startTime=0, + endTime=None + ) + return kline[0][0] + + def get_historical_klines(self, symbol, interval, start_str, end_str=None): + """Get Historical Klines from Binance + See dateparser docs for valid start and end string formats http://dateparser.readthedocs.io/en/latest/ + If using offset strings for dates add "UTC" to date string e.g. "now UTC", "11 hours ago UTC" + :param symbol: Name of symbol pair e.g BNBBTC + :type symbol: str + :param interval: Binance Kline interval + :type interval: str + :param start_str: Start date string in UTC format or timestamp in milliseconds + :type start_str: str|int + :param end_str: optional - end date string in UTC format or timestamp in milliseconds (default will fetch everything up to now) + :type end_str: str|int + :return: list of OHLCV values + """ + # init our list + output_data = [] + + # setup the max limit + limit = 500 + + # convert interval to useful value in seconds + timeframe = interval_to_milliseconds(interval) + + # convert our date strings to milliseconds + if type(start_str) == int: + start_ts = start_str + else: + start_ts = date_to_milliseconds(start_str) + + # establish first available start timestamp + first_valid_ts = self._get_earliest_valid_timestamp(symbol, interval) + start_ts = max(start_ts, first_valid_ts) + + # if an end time was passed convert it + end_ts = None + if end_str: + if type(end_str) == int: + end_ts = end_str + else: + end_ts = date_to_milliseconds(end_str) + + idx = 0 + while True: + # fetch the klines from start_ts up to max 500 entries or the end_ts if set + temp_data = self.get_klines( + symbol=symbol, + interval=interval, + limit=limit, + startTime=start_ts, + endTime=end_ts + ) + + # handle the case where exactly the limit amount of data was returned last loop + if not len(temp_data): + break + + # append this loops data to our output data + output_data += temp_data + + # set our start timestamp using the last value in the array + start_ts = temp_data[-1][0] + + idx += 1 + # check if we received less than the required limit and exit the loop + if len(temp_data) < limit: + # exit the while loop + break + + # increment next call by our timeframe + start_ts += timeframe + + # sleep after every 3rd call to be kind to the API + if idx % 3 == 0: + time.sleep(1) + + return output_data + + def get_historical_klines_generator(self, symbol, interval, start_str, end_str=None): + """Get Historical Klines from Binance + See dateparser docs for valid start and end string formats http://dateparser.readthedocs.io/en/latest/ + If using offset strings for dates add "UTC" to date string e.g. "now UTC", "11 hours ago UTC" + :param symbol: Name of symbol pair e.g BNBBTC + :type symbol: str + :param interval: Binance Kline interval + :type interval: str + :param start_str: Start date string in UTC format or timestamp in milliseconds + :type start_str: str|int + :param end_str: optional - end date string in UTC format or timestamp in milliseconds (default will fetch everything up to now) + :type end_str: str|int + :return: generator of OHLCV values + """ + + # setup the max limit + limit = 500 + + # convert interval to useful value in seconds + timeframe = interval_to_milliseconds(interval) + + # convert our date strings to milliseconds + if type(start_str) == int: + start_ts = start_str + else: + start_ts = date_to_milliseconds(start_str) + + # establish first available start timestamp + first_valid_ts = self._get_earliest_valid_timestamp(symbol, interval) + start_ts = max(start_ts, first_valid_ts) + + # if an end time was passed convert it + end_ts = None + if end_str: + if type(end_str) == int: + end_ts = end_str + else: + end_ts = date_to_milliseconds(end_str) + + idx = 0 + while True: + # fetch the klines from start_ts up to max 500 entries or the end_ts if set + output_data = self.get_klines( + symbol=symbol, + interval=interval, + limit=limit, + startTime=start_ts, + endTime=end_ts + ) + + # handle the case where exactly the limit amount of data was returned last loop + if not len(output_data): + break + + # yield data + for o in output_data: + yield o + + # set our start timestamp using the last value in the array + start_ts = output_data[-1][0] + + idx += 1 + # check if we received less than the required limit and exit the loop + if len(output_data) < limit: + # exit the while loop + break + + # increment next call by our timeframe + start_ts += timeframe + + # sleep after every 3rd call to be kind to the API + if idx % 3 == 0: + time.sleep(1) + def get_ticker(self, **params): """24 hour price change statistics. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#24hr-ticker-price-change-statistics - :param symbol: :type symbol: str - :returns: API response - .. code-block:: python - { "priceChange": "-94.99999800", "priceChangePercent": "-95.960", @@ -604,11 +839,8 @@ class Client(object): "lastId": 28460, # Last tradeId "count": 76 # Trade count } - OR - .. code-block:: python - [ { "priceChange": "-94.99999800", @@ -629,33 +861,23 @@ class Client(object): "count": 76 # Trade count } ] - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('ticker/24hr', data=params) def get_symbol_ticker(self, **params): """Latest price for a symbol or symbols. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#24hr-ticker-price-change-statistics - :param symbol: :type symbol: str - :returns: API response - .. code-block:: python - { "symbol": "LTCBTC", "price": "4.00000200" } - OR - .. code-block:: python - [ { "symbol": "LTCBTC", @@ -666,24 +888,17 @@ class Client(object): "price": "0.07946600" } ] - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('ticker/price', data=params, version=self.PRIVATE_API_VERSION) def get_orderbook_ticker(self, **params): """Latest price for a symbol or symbols. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#symbol-order-book-ticker - :param symbol: :type symbol: str - :returns: API response - .. code-block:: python - { "symbol": "LTCBTC", "bidPrice": "4.00000000", @@ -691,11 +906,8 @@ class Client(object): "askPrice": "4.00000200", "askQty": "9.00000000" } - OR - .. code-block:: python - [ { "symbol": "LTCBTC", @@ -712,9 +924,7 @@ class Client(object): "askQty": "1000.00000000" } ] - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('ticker/bookTicker', data=params, version=self.PRIVATE_API_VERSION) @@ -722,47 +932,39 @@ class Client(object): def create_order(self, **params): """Send in a new order - Any order with an icebergQty MUST have timeInForce set to GTC. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#new-order--trade - :param symbol: required :type symbol: str :param side: required - :type side: enum + :type side: str :param type: required - :type type: enum + :type type: str :param timeInForce: required if limit order - :type timeInForce: enum + :type timeInForce: str :param quantity: required :type quantity: decimal :param price: required - :type price: decimal + :type price: str :param newClientOrderId: A unique id for the order. Automatically generated if not sent. :type newClientOrderId: str :param icebergQty: Used with LIMIT, STOP_LOSS_LIMIT, and TAKE_PROFIT_LIMIT to create an iceberg order. :type icebergQty: decimal :param newOrderRespType: Set the response JSON. ACK, RESULT, or FULL; default: RESULT. - :type newOrderRespType: enum - + :type newOrderRespType: str + :param recvWindow: the number of milliseconds the request is valid for + :type recvWindow: int :returns: API response - Response ACK: - .. code-block:: python - { "symbol":"LTCBTC", "orderId": 1, "clientOrderId": "myOrder1" # Will be newClientOrderId "transactTime": 1499827319559 } - Response RESULT: - .. code-block:: python - { "symbol": "BTCUSDT", "orderId": 28, @@ -776,11 +978,8 @@ class Client(object): "type": "MARKET", "side": "SELL" } - Response FULL: - .. code-block:: python - { "symbol": "BTCUSDT", "orderId": 28, @@ -826,40 +1025,34 @@ class Client(object): } ] } - - :raises: BinanceResponseException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException - + :raises: BinanceRequestException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException """ return self._post('order', True, data=params) def order_limit(self, timeInForce=TIME_IN_FORCE_GTC, **params): """Send in a new limit order - Any order with an icebergQty MUST have timeInForce set to GTC. - :param symbol: required :type symbol: str :param side: required - :type side: enum + :type side: str :param quantity: required :type quantity: decimal :param price: required - :type price: decimal + :type price: str :param timeInForce: default Good till cancelled - :type timeInForce: enum + :type timeInForce: str :param newClientOrderId: A unique id for the order. Automatically generated if not sent. :type newClientOrderId: str :param icebergQty: Used with LIMIT, STOP_LOSS_LIMIT, and TAKE_PROFIT_LIMIT to create an iceberg order. :type icebergQty: decimal :param newOrderRespType: Set the response JSON. ACK, RESULT, or FULL; default: RESULT. - :type newOrderRespType: enum - + :type newOrderRespType: str + :param recvWindow: the number of milliseconds the request is valid for + :type recvWindow: int :returns: API response - See order endpoint for full response options - - :raises: BinanceResponseException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException - + :raises: BinanceRequestException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException """ params.update({ 'type': self.ORDER_TYPE_LIMIT, @@ -869,17 +1062,15 @@ class Client(object): def order_limit_buy(self, timeInForce=TIME_IN_FORCE_GTC, **params): """Send in a new limit buy order - Any order with an icebergQty MUST have timeInForce set to GTC. - :param symbol: required :type symbol: str :param quantity: required :type quantity: decimal :param price: required - :type price: decimal + :type price: str :param timeInForce: default Good till cancelled - :type timeInForce: enum + :type timeInForce: str :param newClientOrderId: A unique id for the order. Automatically generated if not sent. :type newClientOrderId: str :param stopPrice: Used with stop orders @@ -887,14 +1078,12 @@ class Client(object): :param icebergQty: Used with iceberg orders :type icebergQty: decimal :param newOrderRespType: Set the response JSON. ACK, RESULT, or FULL; default: RESULT. - :type newOrderRespType: enum - + :type newOrderRespType: str + :param recvWindow: the number of milliseconds the request is valid for + :type recvWindow: int :returns: API response - See order endpoint for full response options - - :raises: BinanceResponseException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException - + :raises: BinanceRequestException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException """ params.update({ 'side': self.SIDE_BUY, @@ -903,28 +1092,27 @@ class Client(object): def order_limit_sell(self, timeInForce=TIME_IN_FORCE_GTC, **params): """Send in a new limit sell order - :param symbol: required :type symbol: str :param quantity: required :type quantity: decimal :param price: required - :type price: decimal + :type price: str :param timeInForce: default Good till cancelled - :type timeInForce: enum + :type timeInForce: str :param newClientOrderId: A unique id for the order. Automatically generated if not sent. :type newClientOrderId: str :param stopPrice: Used with stop orders :type stopPrice: decimal :param icebergQty: Used with iceberg orders :type icebergQty: decimal - + :param newOrderRespType: Set the response JSON. ACK, RESULT, or FULL; default: RESULT. + :type newOrderRespType: str + :param recvWindow: the number of milliseconds the request is valid for + :type recvWindow: int :returns: API response - See order endpoint for full response options - - :raises: BinanceResponseException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException - + :raises: BinanceRequestException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException """ params.update({ 'side': self.SIDE_SELL @@ -933,24 +1121,21 @@ class Client(object): def order_market(self, **params): """Send in a new market order - :param symbol: required :type symbol: str :param side: required - :type side: enum + :type side: str :param quantity: required :type quantity: decimal :param newClientOrderId: A unique id for the order. Automatically generated if not sent. :type newClientOrderId: str :param newOrderRespType: Set the response JSON. ACK, RESULT, or FULL; default: RESULT. - :type newOrderRespType: enum - + :type newOrderRespType: str + :param recvWindow: the number of milliseconds the request is valid for + :type recvWindow: int :returns: API response - See order endpoint for full response options - - :raises: BinanceResponseException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException - + :raises: BinanceRequestException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException """ params.update({ 'type': self.ORDER_TYPE_MARKET @@ -959,7 +1144,6 @@ class Client(object): def order_market_buy(self, **params): """Send in a new market buy order - :param symbol: required :type symbol: str :param quantity: required @@ -967,14 +1151,12 @@ class Client(object): :param newClientOrderId: A unique id for the order. Automatically generated if not sent. :type newClientOrderId: str :param newOrderRespType: Set the response JSON. ACK, RESULT, or FULL; default: RESULT. - :type newOrderRespType: enum - + :type newOrderRespType: str + :param recvWindow: the number of milliseconds the request is valid for + :type recvWindow: int :returns: API response - See order endpoint for full response options - - :raises: BinanceResponseException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException - + :raises: BinanceRequestException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException """ params.update({ 'side': self.SIDE_BUY @@ -983,7 +1165,6 @@ class Client(object): def order_market_sell(self, **params): """Send in a new market sell order - :param symbol: required :type symbol: str :param quantity: required @@ -991,14 +1172,12 @@ class Client(object): :param newClientOrderId: A unique id for the order. Automatically generated if not sent. :type newClientOrderId: str :param newOrderRespType: Set the response JSON. ACK, RESULT, or FULL; default: RESULT. - :type newOrderRespType: enum - + :type newOrderRespType: str + :param recvWindow: the number of milliseconds the request is valid for + :type recvWindow: int :returns: API response - See order endpoint for full response options - - :raises: BinanceResponseException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException - + :raises: BinanceRequestException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException """ params.update({ 'side': self.SIDE_SELL @@ -1007,47 +1186,37 @@ class Client(object): def create_test_order(self, **params): """Test new order creation and signature/recvWindow long. Creates and validates a new order but does not send it into the matching engine. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#test-new-order-trade - :param symbol: required :type symbol: str :param side: required - :type side: enum + :type side: str :param type: required - :type type: enum + :type type: str :param timeInForce: required if limit order - :type timeInForce: enum + :type timeInForce: str :param quantity: required :type quantity: decimal :param price: required - :type price: decimal + :type price: str :param newClientOrderId: A unique id for the order. Automatically generated if not sent. :type newClientOrderId: str :param icebergQty: Used with iceberg orders :type icebergQty: decimal :param newOrderRespType: Set the response JSON. ACK, RESULT, or FULL; default: RESULT. - :type newOrderRespType: enum + :type newOrderRespType: str :param recvWindow: The number of milliseconds the request is valid for :type recvWindow: int - :returns: API response - .. code-block:: python - {} - - :raises: BinanceResponseException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException - - + :raises: BinanceRequestException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException """ return self._post('order/test', True, data=params) def get_order(self, **params): """Check an order's status. Either orderId or origClientOrderId must be sent. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#query-order-user_data - :param symbol: required :type symbol: str :param orderId: The unique order id @@ -1056,11 +1225,8 @@ class Client(object): :type origClientOrderId: str :param recvWindow: the number of milliseconds the request is valid for :type recvWindow: int - :returns: API response - .. code-block:: python - { "symbol": "LTCBTC", "orderId": 1, @@ -1076,17 +1242,13 @@ class Client(object): "icebergQty": "0.0", "time": 1499827319559 } - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('order', True, data=params) def get_all_orders(self, **params): """Get all account orders; active, canceled, or filled. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#all-orders-user_data - :param symbol: required :type symbol: str :param orderId: The unique order id @@ -1095,11 +1257,8 @@ class Client(object): :type limit: int :param recvWindow: the number of milliseconds the request is valid for :type recvWindow: int - :returns: API response - .. code-block:: python - [ { "symbol": "LTCBTC", @@ -1117,17 +1276,13 @@ class Client(object): "time": 1499827319559 } ] - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('allOrders', True, data=params) def cancel_order(self, **params): """Cancel an active order. Either orderId or origClientOrderId must be sent. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#cancel-order-trade - :param symbol: required :type symbol: str :param orderId: The unique order id @@ -1138,37 +1293,27 @@ class Client(object): :type newClientOrderId: str :param recvWindow: the number of milliseconds the request is valid for :type recvWindow: int - :returns: API response - .. code-block:: python - { "symbol": "LTCBTC", "origClientOrderId": "myOrder1", "orderId": 1, "clientOrderId": "cancelMyOrder1" } - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._delete('order', True, data=params) def get_open_orders(self, **params): """Get all open orders on a symbol. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#current-open-orders-user_data - - :param symbol: required + :param symbol: optional :type symbol: str :param recvWindow: the number of milliseconds the request is valid for :type recvWindow: int - :returns: API response - .. code-block:: python - [ { "symbol": "LTCBTC", @@ -1186,25 +1331,18 @@ class Client(object): "time": 1499827319559 } ] - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('openOrders', True, data=params) # User Stream Endpoints def get_account(self, **params): """Get current account information. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#account-information-user_data - :param recvWindow: the number of milliseconds the request is valid for :type recvWindow: int - :returns: API response - .. code-block:: python - { "makerCommission": 15, "takerCommission": 15, @@ -1226,17 +1364,37 @@ class Client(object): } ] } - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('account', True, data=params) + def get_asset_balance(self, asset, **params): + """Get current asset balance. + https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#account-information-user_data + :param asset: required + :type asset: str + :param recvWindow: the number of milliseconds the request is valid for + :type recvWindow: int + :returns: dictionary or None if not found + .. code-block:: python + { + "asset": "BTC", + "free": "4723846.89208129", + "locked": "0.00000000" + } + :raises: BinanceRequestException, BinanceAPIException + """ + res = self.get_account(**params) + # find asset balance in list of balances + if "balances" in res: + for bal in res['balances']: + if bal['asset'].lower() == asset.lower(): + return bal + return None + def get_my_trades(self, **params): """Get trades for a specific symbol. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#account-trade-list-user_data - :param symbol: required :type symbol: str :param limit: Default 500; max 500. @@ -1245,11 +1403,8 @@ class Client(object): :type fromId: int :param recvWindow: the number of milliseconds the request is valid for :type recvWindow: int - :returns: API response - .. code-block:: python - [ { "id": 28457, @@ -1263,47 +1418,212 @@ class Client(object): "isBestMatch": true } ] - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._get('myTrades', True, data=params) + def get_system_status(self): + """Get system status detail. + https://github.com/binance-exchange/binance-official-api-docs/blob/master/wapi-api.md#system-status-system + :returns: API response + .. code-block:: python + { + "status": 0, # 0: normal,1:system maintenance + "msg": "normal" # normal or System maintenance. + } + :raises: BinanceAPIException + """ + return self._request_withdraw_api('get', 'systemStatus.html') + + def get_account_status(self, **params): + """Get account status detail. + https://github.com/binance-exchange/binance-official-api-docs/blob/master/wapi-api.md#account-status-user_data + :param recvWindow: the number of milliseconds the request is valid for + :type recvWindow: int + :returns: API response + .. code-block:: python + { + "msg": "Order failed:Low Order fill rate! Will be reactivated after 5 minutes.", + "success": true, + "objs": [ + "5" + ] + } + :raises: BinanceWithdrawException + """ + res = self._request_withdraw_api('get', 'accountStatus.html', True, data=params) + if not res['success']: + raise BinanceWithdrawException(res['msg']) + return res + + def get_dust_log(self, **params): + """Get log of small amounts exchanged for BNB. + https://github.com/binance-exchange/binance-official-api-docs/blob/master/wapi-api.md#dustlog-user_data + :param recvWindow: the number of milliseconds the request is valid for + :type recvWindow: int + :returns: API response + .. code-block:: python + { + "success": true, + "results": { + "total": 2, //Total counts of exchange + "rows": [ + { + "transfered_total": "0.00132256", # Total transfered BNB amount for this exchange. + "service_charge_total": "0.00002699", # Total service charge amount for this exchange. + "tran_id": 4359321, + "logs": [ # Details of this exchange. + { + "tranId": 4359321, + "serviceChargeAmount": "0.000009", + "uid": "10000015", + "amount": "0.0009", + "operateTime": "2018-05-03 17:07:04", + "transferedAmount": "0.000441", + "fromAsset": "USDT" + }, + { + "tranId": 4359321, + "serviceChargeAmount": "0.00001799", + "uid": "10000015", + "amount": "0.0009", + "operateTime": "2018-05-03 17:07:04", + "transferedAmount": "0.00088156", + "fromAsset": "ETH" + } + ], + "operate_time": "2018-05-03 17:07:04" //The time of this exchange. + }, + { + "transfered_total": "0.00058795", + "service_charge_total": "0.000012", + "tran_id": 4357015, + "logs": [ // Details of this exchange. + { + "tranId": 4357015, + "serviceChargeAmount": "0.00001", + "uid": "10000015", + "amount": "0.001", + "operateTime": "2018-05-02 13:52:24", + "transferedAmount": "0.00049", + "fromAsset": "USDT" + }, + { + "tranId": 4357015, + "serviceChargeAmount": "0.000002", + "uid": "10000015", + "amount": "0.0001", + "operateTime": "2018-05-02 13:51:11", + "transferedAmount": "0.00009795", + "fromAsset": "ETH" + } + ], + "operate_time": "2018-05-02 13:51:11" + } + ] + } + } + :raises: BinanceWithdrawException + """ + res = self._request_withdraw_api('get', 'userAssetDribbletLog.html', True, data=params) + if not res['success']: + raise BinanceWithdrawException(res['msg']) + return res + + def get_trade_fee(self, **params): + """Get trade fee. + https://github.com/binance-exchange/binance-official-api-docs/blob/master/wapi-api.md#trade-fee-user_data + :param symbol: optional + :type symbol: str + :param recvWindow: the number of milliseconds the request is valid for + :type recvWindow: int + :returns: API response + .. code-block:: python + { + "tradeFee": [ + { + "symbol": "ADABNB", + "maker": 0.9000, + "taker": 1.0000 + }, { + "symbol": "BNBBTC", + "maker": 0.3000, + "taker": 0.3000 + } + ], + "success": true + } + :raises: BinanceWithdrawException + """ + res = self._request_withdraw_api('get', 'tradeFee.html', True, data=params) + if not res['success']: + raise BinanceWithdrawException(res['msg']) + return res + + def get_asset_details(self, **params): + """Fetch details on assets. + https://github.com/binance-exchange/binance-official-api-docs/blob/master/wapi-api.md#asset-detail-user_data + :param recvWindow: the number of milliseconds the request is valid for + :type recvWindow: int + :returns: API response + .. code-block:: python + { + "success": true, + "assetDetail": { + "CTR": { + "minWithdrawAmount": "70.00000000", //min withdraw amount + "depositStatus": false,//deposit status + "withdrawFee": 35, // withdraw fee + "withdrawStatus": true, //withdraw status + "depositTip": "Delisted, Deposit Suspended" //reason + }, + "SKY": { + "minWithdrawAmount": "0.02000000", + "depositStatus": true, + "withdrawFee": 0.01, + "withdrawStatus": true + } + } + } + :raises: BinanceWithdrawException + """ + res = self._request_withdraw_api('get', 'assetDetail.html', True, data=params) + if not res['success']: + raise BinanceWithdrawException(res['msg']) + return res + # Withdraw Endpoints def withdraw(self, **params): """Submit a withdraw request. - https://www.binance.com/restapipub.html - Assumptions: - - You must have Withdraw permissions enabled on your API key - You must have withdrawn to the address specified through the website and approved the transaction via email - :param asset: required :type asset: str :type address: required :type address: str + :type addressTag: optional - Secondary address identifier for coins like XRP,XMR etc. + :type address: str :param amount: required :type amount: decimal - :param name: Description of the address - optional + :param name: optional - Description of the address, default asset value passed will be used :type name: str :param recvWindow: the number of milliseconds the request is valid for :type recvWindow: int - :returns: API response - .. code-block:: python - { "msg": "success", - "success": true + "success": true, + "id":"7213fea8e94b4a5593d507237e5a555b" } - - :raises: BinanceResponseException, BinanceAPIException, BinanceWithdrawException - + :raises: BinanceRequestException, BinanceAPIException, BinanceWithdrawException """ + # force a name for the withdrawal if one not set + if 'asset' in params and 'name' not in params: + params['name'] = params['asset'] res = self._request_withdraw_api('post', 'withdraw.html', True, data=params) if not res['success']: raise BinanceWithdrawException(res['msg']) @@ -1311,9 +1631,7 @@ class Client(object): def get_deposit_history(self, **params): """Fetch deposit history. - https://www.binance.com/restapipub.html - :param asset: optional :type asset: str :type status: 0(0:pending,1:success) optional @@ -1324,11 +1642,8 @@ class Client(object): :type endTime: long :param recvWindow: the number of milliseconds the request is valid for :type recvWindow: int - :returns: API response - .. code-block:: python - { "depositList": [ { @@ -1340,17 +1655,13 @@ class Client(object): ], "success": true } - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._request_withdraw_api('get', 'depositHistory.html', True, data=params) def get_withdraw_history(self, **params): """Fetch withdraw history. - https://www.binance.com/restapipub.html - :param asset: optional :type asset: str :type status: 0(0:Email Sent,1:Cancelled 2:Awaiting Approval 3:Rejected 4:Processing 5:Failure 6Completed) optional @@ -1361,11 +1672,8 @@ class Client(object): :type endTime: long :param recvWindow: the number of milliseconds the request is valid for :type recvWindow: int - :returns: API response - .. code-block:: python - { "withdrawList": [ { @@ -1386,94 +1694,89 @@ class Client(object): ], "success": true } - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._request_withdraw_api('get', 'withdrawHistory.html', True, data=params) def get_deposit_address(self, **params): """Fetch a deposit address for a symbol - https://www.binance.com/restapipub.html - :param asset: required :type asset: str :param recvWindow: the number of milliseconds the request is valid for :type recvWindow: int - :returns: API response - .. code-block:: python - { "address": "0x6915f16f8791d0a1cc2bf47c13a6b2a92000504b", "success": true, "addressTag": "1231212", "asset": "BNB" } - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ return self._request_withdraw_api('get', 'depositAddress.html', True, data=params) + def get_withdraw_fee(self, **params): + """Fetch the withdrawal fee for an asset + :param asset: required + :type asset: str + :param recvWindow: the number of milliseconds the request is valid for + :type recvWindow: int + :returns: API response + .. code-block:: python + { + "withdrawFee": "0.0005", + "success": true + } + :raises: BinanceRequestException, BinanceAPIException + """ + return self._request_withdraw_api('get', 'withdrawFee.html', True, data=params) + # User Stream Endpoints def stream_get_listen_key(self): """Start a new user data stream and return the listen key If a stream already exists it should return the same key. If the stream becomes invalid a new key is returned. - Can be used to keep the user stream alive. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#start-user-data-stream-user_stream - :returns: API response - .. code-block:: python - { "listenKey": "pqia91ma19a5s61cv6a81va65sdf19v8a65a1a5s61cv6a81va65sdf19v8a65a1" } - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ res = self._post('userDataStream', False, data={}) return res['listenKey'] - def stream_keepalive(self, **params): + def stream_keepalive(self, listenKey): """PING a user data stream to prevent a time out. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#keepalive-user-data-stream-user_stream - :param listenKey: required :type listenKey: str - :returns: API response - .. code-block:: python - {} - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ + params = { + 'listenKey': listenKey + } return self._put('userDataStream', False, data=params) - def stream_close(self, **params): + def stream_close(self, listenKey): """Close out a user data stream. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#close-user-data-stream-user_stream - + :param listenKey: required + :type listenKey: str :returns: API response - .. code-block:: python - {} - - :raises: BinanceResponseException, BinanceAPIException - + :raises: BinanceRequestException, BinanceAPIException """ - return self._delete('userDataStream', False, data=params) + params = { + 'listenKey': listenKey + } + return self._delete('userDataStream', False, data=params) \ No newline at end of file diff --git a/vnpy/api/binance/helpers.py b/vnpy/api/binance/helpers.py new file mode 100644 index 00000000..f28934a9 --- /dev/null +++ b/vnpy/api/binance/helpers.py @@ -0,0 +1,45 @@ +# coding=utf-8 + +import dateparser +import pytz + +from datetime import datetime + + +def date_to_milliseconds(date_str): + """Convert UTC date to milliseconds + If using offset strings add "UTC" to date string e.g. "now UTC", "11 hours ago UTC" + See dateparse docs for formats http://dateparser.readthedocs.io/en/latest/ + :param date_str: date in readable format, i.e. "January 01, 2018", "11 hours ago UTC", "now UTC" + :type date_str: str + """ + # get epoch value in UTC + epoch = datetime.utcfromtimestamp(0).replace(tzinfo=pytz.utc) + # parse our date string + d = dateparser.parse(date_str) + # if the date is not timezone aware apply UTC timezone + if d.tzinfo is None or d.tzinfo.utcoffset(d) is None: + d = d.replace(tzinfo=pytz.utc) + + # return the difference in time + return int((d - epoch).total_seconds() * 1000.0) + +def interval_to_milliseconds(interval): + """Convert a Binance interval string to milliseconds + :param interval: Binance interval string, e.g.: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w + :type interval: str + :return: + int value of interval in milliseconds + None if interval prefix is not a decimal integer + None if interval suffix is not one of m, h, d, w + """ + seconds_per_unit = { + "m": 60, + "h": 60 * 60, + "d": 24 * 60 * 60, + "w": 7 * 24 * 60 * 60, + } + try: + return int(interval[:-1]) * seconds_per_unit[interval[-1]] * 1000 + except (ValueError, KeyError): + return None \ No newline at end of file diff --git a/vnpy/api/binance/vnbinance.py b/vnpy/api/binance/vnbinance.py index df0ec5d3..688298a5 100644 --- a/vnpy/api/binance/vnbinance.py +++ b/vnpy/api/binance/vnbinance.py @@ -144,7 +144,7 @@ class BinanceSpotApi(object): :param req: :return: """ - + reqID = None try: method = req['method'] reqID = req["reqID"] @@ -275,6 +275,7 @@ class BinanceSpotApi(object): req['kwargs'] = kwargs req['reqID'] = self.reqID + # 如果方法是查询委托订单和查询账号信息,则需要检查是否重复 if method in [FUNCTIONCODE_GET_OPEN_ORDERS , FUNCTIONCODE_GET_ACCOUNT_BINANCE ]: flag = False for use_method ,r in self.reqQueue: @@ -292,8 +293,17 @@ class BinanceSpotApi(object): #---------------------------------------------------------------------- def spotTrade(self, symbol_pair, type_, price, amount): """现货委托""" - symbol_pair = self.legalSymbolUpper(symbol_pair) - symbol_pair = symbolFromOtherExchangesToBinance(symbol_pair) + print('binance:spotTrade:symbol_pair:{}'.format(symbol_pair)) + upper_symbol_pair = self.legalSymbolUpper(symbol_pair) + if upper_symbol_pair != symbol_pair: + print('upper symbol_pair:{}=>{}'.format(symbol_pair,upper_symbol_pair)) + symbol_pair = upper_symbol_pair + + other_symbol_pair = symbolFromOtherExchangesToBinance(symbol_pair) + if other_symbol_pair != symbol_pair: + print('symbol_pair:{}=>{}'.format(symbol_pair,other_symbol_pair)) + symbol_pair = other_symbol_pair + if type_ == "buy": return self.sendTradingRequest( method = FUNCTIONCODE_BUY_ORDER_BINANCE , callback = self.onTradeOrder , kwargs = {"symbol":symbol_pair, "price":price,"quantity":amount} , optional=None) elif type_ == "sell": @@ -344,8 +354,6 @@ class BinanceSpotApi(object): #---------------------------------------------------------------------- def subscribeSpotTicker(self , symbol): - if '.' in symbol: - symbol = symbol.split('.')[0] if self.bm != None: # print "self.bm != None:" symbol = self.legalSymbolLower(symbol) @@ -354,8 +362,6 @@ class BinanceSpotApi(object): #---------------------------------------------------------------------- def subscribeSpotDepth(self, symbol): - if '.' in symbol: - symbol = symbol.split('.')[0] if self.bm != None: symbol = self.legalSymbolLower(symbol) symbol = symbolFromOtherExchangesToBinance(symbol) diff --git a/vnpy/api/binance/websockets.py b/vnpy/api/binance/websockets.py index 0c497e2d..5adbc63f 100644 --- a/vnpy/api/binance/websockets.py +++ b/vnpy/api/binance/websockets.py @@ -18,6 +18,9 @@ from datetime import datetime class BinanceClientProtocol(WebSocketClientProtocol): + def __init__(self): + super().__init__() + def onConnect(self, response): # reset the delay after reconnecting self.factory.resetDelay() @@ -45,33 +48,38 @@ class BinanceReconnectingClientFactory(ReconnectingClientFactory): class BinanceClientFactory(WebSocketClientFactory, BinanceReconnectingClientFactory): protocol = BinanceClientProtocol + _reconnect_error_payload = { + 'e': 'error', + 'm': 'Max reconnect retries reached' + } def clientConnectionFailed(self, connector, reason): self.retry(connector) + if self.retries > self.maxRetries: + self.callback(self._reconnect_error_payload) def clientConnectionLost(self, connector, reason): - # check if closed cleanly - if reason.getErrorMessage() != 'Connection was closed cleanly.': - self.retry(connector) + self.retry(connector) + if self.retries > self.maxRetries: + self.callback(self._reconnect_error_payload) class BinanceSocketManager(threading.Thread): STREAM_URL = 'wss://stream.binance.com:9443/' - WEBSOCKET_DEPTH_1 = '1' WEBSOCKET_DEPTH_5 = '5' WEBSOCKET_DEPTH_10 = '10' WEBSOCKET_DEPTH_20 = '20' - _user_timeout = 30 * 60 # 30 minutes + DEFAULT_USER_TIMEOUT = 30 * 60 # 30 minutes - def __init__(self, client): + def __init__(self, client, user_timeout=DEFAULT_USER_TIMEOUT): """Initialise the BinanceSocketManager - :param client: Binance API client :type client: binance.Client - + :param user_timeout: Custom websocket timeout + :type user_timeout: int """ threading.Thread.__init__(self) self._conns = {} @@ -79,6 +87,7 @@ class BinanceSocketManager(threading.Thread): self._user_listen_key = None self._user_callback = None self._client = client + self._user_timeout = user_timeout def _start_socket(self, path, callback, prefix='ws/'): if path in self._conns: @@ -88,83 +97,82 @@ class BinanceSocketManager(threading.Thread): factory = BinanceClientFactory(factory_url) factory.protocol = BinanceClientProtocol factory.callback = callback + factory.reconnect = True context_factory = ssl.ClientContextFactory() self._conns[path] = connectWS(factory, context_factory) return path - def start_depth_socket(self, symbol, callback, depth=WEBSOCKET_DEPTH_1): - """Start a websocket for symbol market depth - + def start_depth_socket(self, symbol, callback, depth=None): + """Start a websocket for symbol market depth returning either a diff or a partial book https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#partial-book-depth-streams - :param symbol: required :type symbol: str :param callback: callback function to handle messages :type callback: function - :param depth: Number of depth entries to return, default WEBSOCKET_DEPTH_1 - :type depth: enum - + :param depth: optional Number of depth entries to return, default None. If passed returns a partial book instead of a diff + :type depth: str :returns: connection key string if successful, False otherwise - - Message Format - + Partial Message Format .. code-block:: python - { - "e": "depthUpdate", # event type - "E": 1499404630606, # event time - "s": "ETHBTC", # symbol - "u": 7913455, # updateId to sync up with updateid in /api/v1/depth - "b": [ # bid depth delta + "lastUpdateId": 160, # Last update ID + "bids": [ # Bids to be updated [ - "0.10376590", # price (need to update the quantity on this price) - "59.15767010", # quantity - [] # can be ignored - ], + "0.0024", # price level to be updated + "10", # quantity + [] # ignore + ] ], - "a": [ # ask depth delta + "asks": [ # Asks to be updated [ - "0.10376586", # price (need to update the quantity on this price) - "159.15767010", # quantity - [] # can be ignored - ], + "0.0026", # price level to be updated + "100", # quantity + [] # ignore + ] + ] + } + Diff Message Format + .. code-block:: python + { + "e": "depthUpdate", # Event type + "E": 123456789, # Event time + "s": "BNBBTC", # Symbol + "U": 157, # First update ID in event + "u": 160, # Final update ID in event + "b": [ # Bids to be updated [ - "0.10383109", - "345.86845230", - [] - ], + "0.0024", # price level to be updated + "10", # quantity + [] # ignore + ] + ], + "a": [ # Asks to be updated [ - "0.10490700", - "0.00000000", # quantity=0 means remove this level - [] + "0.0026", # price level to be updated + "100", # quantity + [] # ignore ] ] } """ socket_name = symbol.lower() + '@depth' - if depth != self.WEBSOCKET_DEPTH_1: + if depth and depth != '1': socket_name = '{}{}'.format(socket_name, depth) return self._start_socket(socket_name, callback) def start_kline_socket(self, symbol, callback, interval=Client.KLINE_INTERVAL_1MINUTE): """Start a websocket for symbol kline data - https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#klinecandlestick-streams - :param symbol: required :type symbol: str :param callback: callback function to handle messages :type callback: function :param interval: Kline interval, default KLINE_INTERVAL_1MINUTE - :type interval: enum - + :type interval: str :returns: connection key string if successful, False otherwise - Message Format - .. code-block:: python - { "e": "kline", # event type "E": 1499404907056, # event time @@ -193,22 +201,44 @@ class BinanceSocketManager(threading.Thread): socket_name = '{}@kline_{}'.format(symbol.lower(), interval) return self._start_socket(socket_name, callback) + def start_miniticker_socket(self, callback, update_time=1000): + """Start a miniticker websocket for all trades + This is not in the official Binance api docs, but this is what + feeds the right column on a ticker page on Binance. + :param callback: callback function to handle messages + :type callback: function + :param update_time: time between callbacks in milliseconds, must be 1000 or greater + :type update_time: int + :returns: connection key string if successful, False otherwise + Message Format + .. code-block:: python + [ + { + 'e': '24hrMiniTicker', # Event type + 'E': 1515906156273, # Event time + 's': 'QTUMETH', # Symbol + 'c': '0.03836900', # close + 'o': '0.03953500', # open + 'h': '0.04400000', # high + 'l': '0.03756000', # low + 'v': '147435.80000000', # volume + 'q': '5903.84338533' # quote volume + } + ] + """ + + return self._start_socket('!miniTicker@arr@{}ms'.format(update_time), callback) + def start_trade_socket(self, symbol, callback): """Start a websocket for symbol trade data - https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#trade-streams - :param symbol: required :type symbol: str :param callback: callback function to handle messages :type callback: function - :returns: connection key string if successful, False otherwise - Message Format - .. code-block:: python - { "e": "trade", # Event type "E": 123456789, # Event time @@ -222,26 +252,19 @@ class BinanceSocketManager(threading.Thread): "m": true, # Is the buyer the market maker? "M": true # Ignore. } - """ return self._start_socket(symbol.lower() + '@trade', callback) def start_aggtrade_socket(self, symbol, callback): """Start a websocket for symbol trade data - https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#aggregate-trade-streams - :param symbol: required :type symbol: str :param callback: callback function to handle messages :type callback: function - :returns: connection key string if successful, False otherwise - Message Format - .. code-block:: python - { "e": "aggTrade", # event type "E": 1499405254326, # event time @@ -255,26 +278,19 @@ class BinanceSocketManager(threading.Thread): "m": false, # whether buyer is a maker "M": true # can be ignored } - """ return self._start_socket(symbol.lower() + '@aggTrade', callback) def start_symbol_ticker_socket(self, symbol, callback): """Start a websocket for a symbol's ticker data - https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#individual-symbol-ticker-streams - :param symbol: required :type symbol: str :param callback: callback function to handle messages :type callback: function - :returns: connection key string if successful, False otherwise - Message Format - .. code-block:: python - { "e": "24hrTicker", # Event type "E": 123456789, # Event time @@ -300,26 +316,18 @@ class BinanceSocketManager(threading.Thread): "L": 18150, # Last trade Id "n": 18151 # Total number of trades } - """ return self._start_socket(symbol.lower() + '@ticker', callback) def start_ticker_socket(self, callback): """Start a websocket for all ticker data - By default all markets are included in an array. - https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#all-market-tickers-stream - :param callback: callback function to handle messages :type callback: function - :returns: connection key string if successful, False otherwise - Message Format - .. code-block:: python - [ { 'F': 278610, @@ -351,45 +359,42 @@ class BinanceSocketManager(threading.Thread): def start_multiplex_socket(self, streams, callback): """Start a multiplexed socket using a list of socket names. User stream sockets can not be included. - Symbols in socket name must be lowercase i.e bnbbtc@aggTrade, neobtc@ticker - Combined stream events are wrapped as follows: {"stream":"","data":} - https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md - :param streams: list of stream names in lower case :type streams: list :param callback: callback function to handle messages :type callback: function - :returns: connection key string if successful, False otherwise - Message Format - see Binance API docs for all types - """ stream_path = 'streams={}'.format('/'.join(streams)) return self._start_socket(stream_path, callback, 'stream?') def start_user_socket(self, callback): """Start a websocket for user data - https://www.binance.com/restapipub.html#user-wss-endpoint - :param callback: callback function to handle messages :type callback: function - :returns: connection key string if successful, False otherwise - Message Format - see Binance API docs for all types """ + # Get the user listen key + user_listen_key = self._client.stream_get_listen_key() + # and start the socket with this specific key + conn_key = self._start_user_socket(user_listen_key, callback) + return conn_key + + def _start_user_socket(self, user_listen_key, callback): + # With this function we can start a user socket with a specific key if self._user_listen_key: # cleanup any sockets with this key for conn_key in self._conns: if len(conn_key) >= 60 and conn_key[:60] == self._user_listen_key: self.stop_socket(conn_key) break - self._user_listen_key = self._client.stream_get_listen_key() + self._user_listen_key = user_listen_key self._user_callback = callback conn_key = self._start_socket(self._user_listen_key, callback) if conn_key: @@ -405,26 +410,31 @@ class BinanceSocketManager(threading.Thread): def _keepalive_user_socket(self): try: - listen_key = self._client.stream_get_listen_key() + user_listen_key = self._client.stream_get_listen_key() # check if they key changed and - if listen_key != self._user_listen_key: - self.start_user_socket(self._user_callback) - self._start_user_timer() + if user_listen_key != self._user_listen_key: + # Start a new socket with the key received + # `_start_user_socket` automatically cleanup open sockets + # and starts timer to keep socket alive + self._start_user_socket(user_listen_key, self._user_callback) + else: + # Restart timer only if the user listen key is not changed + self._start_user_timer() except Exception as ex: print(datetime.now().strftime("%Y-%m-%d %H:%M:%S") + " in _keepalive_user_socket") print(str(ex),file=sys.stderr) def stop_socket(self, conn_key): """Stop a websocket given the connection key - :param conn_key: Socket connection key :type conn_key: string - :returns: connection key string if successful, False otherwise """ if conn_key not in self._conns: return + # disable reconnecting if we are closing + self._conns[conn_key].factory = WebSocketClientFactory(self.STREAM_URL + 'tmp_path') self._conns[conn_key].disconnect() del(self._conns[conn_key]) @@ -438,8 +448,6 @@ class BinanceSocketManager(threading.Thread): # stop the timer self._user_timer.cancel() self._user_timer = None - # close the stream - self._client.stream_close(listenKey=self._user_listen_key) self._user_listen_key = None def run(self): @@ -451,10 +459,9 @@ class BinanceSocketManager(threading.Thread): def close(self): """Close all connections - """ keys = set(self._conns.keys()) for key in keys: self.stop_socket(key) - self._conns = {} + self._conns = {} \ No newline at end of file