diff --git a/vnpy/api/binance/__init__.py b/vnpy/api/binance/__init__.py new file mode 100644 index 00000000..b95d1347 --- /dev/null +++ b/vnpy/api/binance/__init__.py @@ -0,0 +1,6 @@ +# encoding: UTF-8 + +from vnpy.api.binance.vnbinance import BinanceSpotApi +from vnpy.api.binance.client import Client +from vnpy.api.binance.websockets import BinanceSocketManager +from vnpy.api.binance.exceptions import BinanceAPIException, BinanceRequestException, BinanceWithdrawException \ No newline at end of file diff --git a/vnpy/api/binance/client.py b/vnpy/api/binance/client.py new file mode 100644 index 00000000..b30df1ae --- /dev/null +++ b/vnpy/api/binance/client.py @@ -0,0 +1,1443 @@ +#!/usr/bin/env python +# coding=utf-8 + +import hashlib +import hmac +import requests +import six +import time +from .exceptions import BinanceAPIException, BinanceRequestException, BinanceWithdrawException + +if six.PY2: + from urllib import urlencode +elif six.PY3: + from urllib.parse import urlencode + + +class Client(object): + + API_URL = 'https://api.binance.com/api' + WITHDRAW_API_URL = 'https://api.binance.com/wapi' + WEBSITE_URL = 'https://www.binance.com' + PUBLIC_API_VERSION = 'v1' + PRIVATE_API_VERSION = 'v3' + WITHDRAW_API_VERSION = 'v3' + + SYMBOL_TYPE_SPOT = 'SPOT' + + ORDER_STATUS_NEW = 'NEW' + ORDER_STATUS_PARTIALLY_FILLED = 'PARTIALLY_FILLED' + ORDER_STATUS_FILLED = 'FILLED' + ORDER_STATUS_CANCELED = 'CANCELED' + ORDER_STATUS_PENDING_CANCEL = 'PENDING_CANCEL' + ORDER_STATUS_REJECTED = 'REJECTED' + ORDER_STATUS_EXPIRED = 'EXPIRED' + + KLINE_INTERVAL_1MINUTE = '1m' + KLINE_INTERVAL_3MINUTE = '3m' + KLINE_INTERVAL_5MINUTE = '5m' + KLINE_INTERVAL_15MINUTE = '15m' + KLINE_INTERVAL_30MINUTE = '30m' + KLINE_INTERVAL_1HOUR = '1h' + KLINE_INTERVAL_2HOUR = '2h' + KLINE_INTERVAL_4HOUR = '4h' + KLINE_INTERVAL_6HOUR = '6h' + KLINE_INTERVAL_8HOUR = '8h' + KLINE_INTERVAL_12HOUR = '12h' + KLINE_INTERVAL_1DAY = '1d' + KLINE_INTERVAL_3DAY = '3d' + KLINE_INTERVAL_1WEEK = '1w' + KLINE_INTERVAL_1MONTH = '1M' + + SIDE_BUY = 'BUY' + SIDE_SELL = 'SELL' + + ORDER_TYPE_LIMIT = 'LIMIT' + ORDER_TYPE_MARKET = 'MARKET' + ORDER_TYPE_STOP_LOSS = 'STOP_LOSS' + ORDER_TYPE_STOP_LOSS_LIMIT = 'STOP_LOSS_LIMIT' + ORDER_TYPE_TAKE_PROFIT = 'TAKE_PROFIT' + ORDER_TYPE_TAKE_PROFIT_LIMIT = 'TAKE_PROFIT_LIMIT' + ORDER_TYPE_LIMIT_MAKER = 'LIMIT_MAKER' + + TIME_IN_FORCE_GTC = 'GTC' # Good till cancelled + TIME_IN_FORCE_IOC = 'IOC' # Immediate or cancel + TIME_IN_FORCE_FOK = 'FOK' # Fill or kill + + ORDER_RESP_TYPE_ACK = 'ACK' + ORDER_RESP_TYPE_RESULT = 'RESULT' + ORDER_RESP_TYPE_FULL = 'FULL' + + def __init__(self, api_key, api_secret): + """Binance API Client constructor + + :param api_key: Api Key + :type api_key: str. + :param api_secret: Api Secret + :type api_secret: str. + + """ + + self.API_KEY = api_key + self.API_SECRET = api_secret + self.session = self._init_session() + + # init DNS and SSL cert + self.ping() + + def _init_session(self): + session = requests.session() + session.headers.update({'Accept': 'application/json', + 'User-Agent': 'binance/python', + 'X-MBX-APIKEY': self.API_KEY}) + return session + + def _create_api_uri(self, path, signed=True, version=PUBLIC_API_VERSION): + v = self.PRIVATE_API_VERSION if signed else version + return self.API_URL + '/' + v + '/' + path + + def _create_withdraw_api_uri(self, path): + return self.WITHDRAW_API_URL + '/' + self.WITHDRAW_API_VERSION + '/' + path + + def _create_website_uri(self, path): + return self.WEBSITE_URL + '/' + path + + def _generate_signature(self, data): + query_string = urlencode(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): + """Convert params to list with signature as last element + + :param data: + :return: + + """ + has_signature = False + params = [] + for key, value in data.items(): + if key == 'signature': + has_signature = True + else: + params.append((key, value)) + 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 and isinstance(data, dict): + kwargs['data'] = data + if signed: + # generate signature + kwargs['data']['timestamp'] = int(time.time() * 1000) + kwargs['data']['recvWindow'] = 20000 + kwargs['data']['signature'] = self._generate_signature(kwargs['data']) + + + if data and (method == 'get' or force_params): + kwargs['params'] = self._order_params(kwargs['data']) + del(kwargs['data']) + + # kwargs["verify"] = False # I don't know whay this is error + + response = getattr(self.session, method)(uri, **kwargs ) + return self._handle_response(response , uri , **kwargs ) + + def _request_api(self, method, path, signed=False, version=PUBLIC_API_VERSION, **kwargs): + uri = self._create_api_uri(path, signed, version) + return self._request(method, uri, signed, **kwargs) + + def _request_withdraw_api(self, method, path, signed=False, **kwargs): + uri = self._create_withdraw_api_uri(path) + + return self._request(method, uri, signed, True, **kwargs) + + def _request_website(self, method, path, signed=False, **kwargs): + uri = self._create_website_uri(path) + + return self._request(method, uri, signed, **kwargs) + + def _handle_response(self, response , uri , **kwargs ): + """Internal helper for handling API responses from the Binance server. + Raises the appropriate exceptions when necessary; otherwise, returns the + response. + """ + if not str(response.status_code).startswith('2'): + raise BinanceAPIException(response , uri , **kwargs ) + try: + return response.json() + except ValueError: + raise BinanceRequestException('Invalid Response: %s' % response.text , uri , **kwargs ) + + def _get(self, path, signed=False, version=PUBLIC_API_VERSION, **kwargs): + return self._request_api('get', path, signed, version, **kwargs) + + def _post(self, path, signed=False, version=PUBLIC_API_VERSION, **kwargs): + return self._request_api('post', path, signed, version, **kwargs) + + def _put(self, path, signed=False, version=PUBLIC_API_VERSION, **kwargs): + return self._request_api('put', path, signed, version, **kwargs) + + def _delete(self, path, signed=False, version=PUBLIC_API_VERSION, **kwargs): + return self._request_api('delete', path, signed, version, **kwargs) + + # Exchange Endpoints + + 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 + + """ + + products = self._request_website('get', 'exchange/public/product') + return products + + 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, + "rateLimits": [ + { + "rateLimitType": "REQUESTS", + "interval": "MINUTE", + "limit": 1200 + }, + { + "rateLimitType": "ORDERS", + "interval": "SECOND", + "limit": 10 + }, + { + "rateLimitType": "ORDERS", + "interval": "DAY", + "limit": 100000 + } + ], + "exchangeFilters": [], + "symbols": [ + { + "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: BinanceResponseException, BinanceAPIException + + """ + + return self._get('exchangeInfo') + + # 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 + + """ + 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 + + """ + return self._get('time') + + # Market Data Endpoints + + 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", + "price": "4.00000200" + }, + { + "symbol": "ETHBTC", + "price": "0.07946600" + } + ] + + :raises: BinanceResponseException, 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", + "bidPrice": "4.00000000", + "bidQty": "431.00000000", + "askPrice": "4.00000200", + "askQty": "9.00000000" + }, + { + "symbol": "ETHBTC", + "bidPrice": "0.07946700", + "bidQty": "9.00000000", + "askPrice": "100000.00000000", + "askQty": "1000.00000000" + } + ] + + :raises: BinanceResponseException, 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 + :type limit: int + + :returns: API response + + .. code-block:: python + + { + "lastUpdateId": 1027024, + "bids": [ + [ + "4.00000000", # PRICE + "431.00000000", # QTY + [] # Can be ignored + ] + ], + "asks": [ + [ + "4.00000200", + "12.00000000", + [] + ] + ] + } + + :raises: BinanceResponseException, 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, + "price": "4.00000100", + "qty": "12.00000000", + "time": 1499865549590, + "isBuyerMaker": true, + "isBestMatch": true + } + ] + + :raises: BinanceResponseException, 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, + "price": "4.00000100", + "qty": "12.00000000", + "time": 1499865549590, + "isBuyerMaker": true, + "isBestMatch": true + } + ] + + :raises: BinanceResponseException, 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. + :type fromId: str + :param startTime: Timestamp in ms to get aggregate trades from INCLUSIVE. + :type startTime: int + :param endTime: Timestamp in ms to get aggregate trades until INCLUSIVE. + :type endTime: int + :param limit: Default 500; max 500. + :type limit: int + + :returns: API response + + .. code-block:: python + + [ + { + "a": 26129, # Aggregate tradeId + "p": "0.01633102", # Price + "q": "4.70443515", # Quantity + "f": 27781, # First tradeId + "l": 27781, # Last tradeId + "T": 1498793709153, # Timestamp + "m": true, # Was the buyer the maker? + "M": true # Was the trade the best price match? + } + ] + + :raises: BinanceResponseException, BinanceAPIException + + """ + return self._get('aggTrades', data=params) + + 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 + :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 + "0.01634790", # Open + "0.80000000", # High + "0.01575800", # Low + "0.01577100", # Close + "148976.11427815", # Volume + 1499644799999, # Close time + "2434.19055334", # Quote asset volume + 308, # Number of trades + "1756.87402397", # Taker buy base asset volume + "28.46694368", # Taker buy quote asset volume + "17928899.62484339" # Can be ignored + ] + ] + + :raises: BinanceResponseException, BinanceAPIException + + """ + return self._get('klines', data=params) + + 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", + "weightedAvgPrice": "0.29628482", + "prevClosePrice": "0.10002000", + "lastPrice": "4.00000200", + "bidPrice": "4.00000000", + "askPrice": "4.00000200", + "openPrice": "99.00000000", + "highPrice": "100.00000000", + "lowPrice": "0.10000000", + "volume": "8913.30000000", + "openTime": 1499783499040, + "closeTime": 1499869899040, + "fristId": 28385, # First tradeId + "lastId": 28460, # Last tradeId + "count": 76 # Trade count + } + + OR + + .. code-block:: python + + [ + { + "priceChange": "-94.99999800", + "priceChangePercent": "-95.960", + "weightedAvgPrice": "0.29628482", + "prevClosePrice": "0.10002000", + "lastPrice": "4.00000200", + "bidPrice": "4.00000000", + "askPrice": "4.00000200", + "openPrice": "99.00000000", + "highPrice": "100.00000000", + "lowPrice": "0.10000000", + "volume": "8913.30000000", + "openTime": 1499783499040, + "closeTime": 1499869899040, + "fristId": 28385, # First tradeId + "lastId": 28460, # Last tradeId + "count": 76 # Trade count + } + ] + + :raises: BinanceResponseException, 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", + "price": "4.00000200" + }, + { + "symbol": "ETHBTC", + "price": "0.07946600" + } + ] + + :raises: BinanceResponseException, 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", + "bidQty": "431.00000000", + "askPrice": "4.00000200", + "askQty": "9.00000000" + } + + OR + + .. code-block:: python + + [ + { + "symbol": "LTCBTC", + "bidPrice": "4.00000000", + "bidQty": "431.00000000", + "askPrice": "4.00000200", + "askQty": "9.00000000" + }, + { + "symbol": "ETHBTC", + "bidPrice": "0.07946700", + "bidQty": "9.00000000", + "askPrice": "100000.00000000", + "askQty": "1000.00000000" + } + ] + + :raises: BinanceResponseException, BinanceAPIException + + """ + return self._get('ticker/bookTicker', data=params, version=self.PRIVATE_API_VERSION) + + # Account Endpoints + + 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 + :param type: required + :type type: enum + :param timeInForce: required if limit order + :type timeInForce: enum + :param quantity: required + :type quantity: decimal + :param price: required + :type price: decimal + :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 + + :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, + "clientOrderId": "6gCrw2kRUAF9CvJDGP16IP", + "transactTime": 1507725176595, + "price": "0.00000000", + "origQty": "10.00000000", + "executedQty": "10.00000000", + "status": "FILLED", + "timeInForce": "GTC", + "type": "MARKET", + "side": "SELL" + } + + Response FULL: + + .. code-block:: python + + { + "symbol": "BTCUSDT", + "orderId": 28, + "clientOrderId": "6gCrw2kRUAF9CvJDGP16IP", + "transactTime": 1507725176595, + "price": "0.00000000", + "origQty": "10.00000000", + "executedQty": "10.00000000", + "status": "FILLED", + "timeInForce": "GTC", + "type": "MARKET", + "side": "SELL", + "fills": [ + { + "price": "4000.00000000", + "qty": "1.00000000", + "commission": "4.00000000", + "commissionAsset": "USDT" + }, + { + "price": "3999.00000000", + "qty": "5.00000000", + "commission": "19.99500000", + "commissionAsset": "USDT" + }, + { + "price": "3998.00000000", + "qty": "2.00000000", + "commission": "7.99600000", + "commissionAsset": "USDT" + }, + { + "price": "3997.00000000", + "qty": "1.00000000", + "commission": "3.99700000", + "commissionAsset": "USDT" + }, + { + "price": "3995.00000000", + "qty": "1.00000000", + "commission": "3.99500000", + "commissionAsset": "USDT" + } + ] + } + + :raises: BinanceResponseException, 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 + :param quantity: required + :type quantity: decimal + :param price: required + :type price: decimal + :param timeInForce: default Good till cancelled + :type timeInForce: enum + :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 + + :returns: API response + + See order endpoint for full response options + + :raises: BinanceResponseException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException + + """ + params.update({ + 'type': self.ORDER_TYPE_LIMIT, + 'timeInForce': timeInForce + }) + return self.create_order(**params) + + 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 + :param timeInForce: default Good till cancelled + :type timeInForce: enum + :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: enum + + :returns: API response + + See order endpoint for full response options + + :raises: BinanceResponseException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException + + """ + params.update({ + 'side': self.SIDE_BUY, + }) + return self.order_limit(timeInForce=timeInForce, **params) + + 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 + :param timeInForce: default Good till cancelled + :type timeInForce: enum + :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 + + :returns: API response + + See order endpoint for full response options + + :raises: BinanceResponseException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException + + """ + params.update({ + 'side': self.SIDE_SELL + }) + return self.order_limit(timeInForce=timeInForce, **params) + + def order_market(self, **params): + """Send in a new market order + + :param symbol: required + :type symbol: str + :param side: required + :type side: enum + :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 + + :returns: API response + + See order endpoint for full response options + + :raises: BinanceResponseException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException + + """ + params.update({ + 'type': self.ORDER_TYPE_MARKET + }) + return self.create_order(**params) + + def order_market_buy(self, **params): + """Send in a new market buy order + + :param symbol: required + :type symbol: 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 + + :returns: API response + + See order endpoint for full response options + + :raises: BinanceResponseException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException + + """ + params.update({ + 'side': self.SIDE_BUY + }) + return self.order_market(**params) + + def order_market_sell(self, **params): + """Send in a new market sell order + + :param symbol: required + :type symbol: 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 + + :returns: API response + + See order endpoint for full response options + + :raises: BinanceResponseException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException + + """ + params.update({ + 'side': self.SIDE_SELL + }) + return self.order_market(**params) + + 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 + :param type: required + :type type: enum + :param timeInForce: required if limit order + :type timeInForce: enum + :param quantity: required + :type quantity: decimal + :param price: required + :type price: decimal + :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 + :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 + + + """ + 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 + :type orderId: int + :param origClientOrderId: optional + :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, + "clientOrderId": "myOrder1", + "price": "0.1", + "origQty": "1.0", + "executedQty": "0.0", + "status": "NEW", + "timeInForce": "GTC", + "type": "LIMIT", + "side": "BUY", + "stopPrice": "0.0", + "icebergQty": "0.0", + "time": 1499827319559 + } + + :raises: BinanceResponseException, 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 + :type orderId: int + :param limit: Default 500; max 500. + :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", + "orderId": 1, + "clientOrderId": "myOrder1", + "price": "0.1", + "origQty": "1.0", + "executedQty": "0.0", + "status": "NEW", + "timeInForce": "GTC", + "type": "LIMIT", + "side": "BUY", + "stopPrice": "0.0", + "icebergQty": "0.0", + "time": 1499827319559 + } + ] + + :raises: BinanceResponseException, 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 + :type orderId: int + :param origClientOrderId: optional + :type origClientOrderId: str + :param newClientOrderId: Used to uniquely identify this cancel. Automatically generated by default. + :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 + + """ + 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 + :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", + "orderId": 1, + "clientOrderId": "myOrder1", + "price": "0.1", + "origQty": "1.0", + "executedQty": "0.0", + "status": "NEW", + "timeInForce": "GTC", + "type": "LIMIT", + "side": "BUY", + "stopPrice": "0.0", + "icebergQty": "0.0", + "time": 1499827319559 + } + ] + + :raises: BinanceResponseException, 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, + "buyerCommission": 0, + "sellerCommission": 0, + "canTrade": true, + "canWithdraw": true, + "canDeposit": true, + "balances": [ + { + "asset": "BTC", + "free": "4723846.89208129", + "locked": "0.00000000" + }, + { + "asset": "LTC", + "free": "4763368.68006011", + "locked": "0.00000000" + } + ] + } + + :raises: BinanceResponseException, BinanceAPIException + + """ + return self._get('account', True, data=params) + + 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. + :type limit: int + :param fromId: TradeId to fetch from. Default gets most recent trades. + :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, + "price": "4.00000100", + "qty": "12.00000000", + "commission": "10.10000000", + "commissionAsset": "BNB", + "time": 1499865549590, + "isBuyer": true, + "isMaker": false, + "isBestMatch": true + } + ] + + :raises: BinanceResponseException, BinanceAPIException + + """ + return self._get('myTrades', True, data=params) + + # 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 + :param amount: required + :type amount: decimal + :param name: Description of the address - optional + :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 + } + + :raises: BinanceResponseException, BinanceAPIException, BinanceWithdrawException + + """ + res = self._request_withdraw_api('post', 'withdraw.html', True, data=params) + if not res['success']: + raise BinanceWithdrawException(res['msg']) + return res + + 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 + :type status: int + :param startTime: optional + :type startTime: long + :param endTime: optional + :type endTime: long + :param recvWindow: the number of milliseconds the request is valid for + :type recvWindow: int + + :returns: API response + + .. code-block:: python + + { + "depositList": [ + { + "insertTime": 1508198532000, + "amount": 0.04670582, + "asset": "ETH", + "status": 1 + } + ], + "success": true + } + + :raises: BinanceResponseException, 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 + :type status: int + :param startTime: optional + :type startTime: long + :param endTime: optional + :type endTime: long + :param recvWindow: the number of milliseconds the request is valid for + :type recvWindow: int + + :returns: API response + + .. code-block:: python + + { + "withdrawList": [ + { + "amount": 1, + "address": "0x6915f16f8791d0a1cc2bf47c13a6b2a92000504b", + "asset": "ETH", + "applyTime": 1508198532000 + "status": 4 + }, + { + "amount": 0.005, + "address": "0x6915f16f8791d0a1cc2bf47c13a6b2a92000504b", + "txId": "0x80aaabed54bdab3f6de5868f89929a2371ad21d666f20f7393d1a3389fad95a1", + "asset": "ETH", + "applyTime": 1508198532000, + "status": 4 + } + ], + "success": true + } + + :raises: BinanceResponseException, 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 + + """ + return self._request_withdraw_api('get', 'depositAddress.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 + + """ + res = self._post('userDataStream', False, data={}) + return res['listenKey'] + + def stream_keepalive(self, **params): + """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 + + """ + return self._put('userDataStream', False, data=params) + + def stream_close(self, **params): + """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 + + :returns: API response + + .. code-block:: python + + {} + + :raises: BinanceResponseException, BinanceAPIException + + """ + return self._delete('userDataStream', False, data=params) diff --git a/vnpy/api/binance/exceptions.py b/vnpy/api/binance/exceptions.py new file mode 100644 index 00000000..23bef301 --- /dev/null +++ b/vnpy/api/binance/exceptions.py @@ -0,0 +1,80 @@ +# encoding: UTF-8 + + +class BinanceAPIException(Exception): + + LISTENKEY_NOT_EXIST = '-1125' + + def __init__(self, response): + json_res = response.json() + self.status_code = response.status_code + self.response = response + self.code = json_res['code'] + self.message = json_res['msg'] + self.request = getattr(response, 'request', None) + + def __str__(self): # pragma: no cover + return 'APIError(code=%s): %s' % (self.code, self.message) + + +class BinanceRequestException(Exception): + def __init__(self, message): + self.message = message + + def __str__(self): + return 'BinanceRequestException: %s' % self.message + + +class BinanceOrderException(Exception): + + def __init__(self, code, message): + self.code = code + self.message = message + + def __str__(self): + return 'BinanceOrderException(code=%s): %s' % (self.code, self.message) + + +class BinanceOrderMinAmountException(BinanceOrderException): + + def __init__(self, value): + message = "Amount must be a multiple of %s" % value + super(BinanceOrderMinAmountException, self).__init__(-1013, message) + + +class BinanceOrderMinPriceException(BinanceOrderException): + + def __init__(self, value): + message = "Price must be at least %s" % value + super(BinanceOrderMinPriceException, self).__init__(-1013, message) + + +class BinanceOrderMinTotalException(BinanceOrderException): + + def __init__(self, value): + message = "Total must be at least %s" % value + super(BinanceOrderMinTotalException, self).__init__(-1013, message) + + +class BinanceOrderUnknownSymbolException(BinanceOrderException): + + def __init__(self, value): + message = "Unknown symbol %s" % value + super(BinanceOrderUnknownSymbolException, self).__init__(-1013, message) + + +class BinanceOrderInactiveSymbolException(BinanceOrderException): + + def __init__(self, value): + message = "Attempting to trade an inactive symbol %s" % value + super(BinanceOrderInactiveSymbolException, self).__init__(-1013, message) + + +class BinanceWithdrawException(Exception): + def __init__(self, message): + if message == u'参数异常': + message = 'Withdraw to this address through the website first' + self.message = message + + def __str__(self): + return 'BinanceWithdrawException: %s' % self.message diff --git a/vnpy/api/binance/readme.txt b/vnpy/api/binance/readme.txt new file mode 100644 index 00000000..13725129 --- /dev/null +++ b/vnpy/api/binance/readme.txt @@ -0,0 +1,150 @@ +参考了 https://github.com/wudian/marketMaker 的代码 +表示感谢! + +以及 https://github.com/sammchardy/python-binance 的代码 + +API 文档 +https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md +https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md + + +------------------------------------------------------------------------ + + +准备采用的方案 +1、Individual Symbol Ticker Streams +24hr Ticker statistics for a single symbol pushed every second + +Stream Name: @ticker + +Payload: + +{ + "e": "24hrTicker", // Event type + "E": 123456789, // Event time + "s": "BNBBTC", // Symbol + "p": "0.0015", // Price change + "P": "250.00", // Price change percent + "w": "0.0018", // Weighted average price + "x": "0.0009", // Previous day's close price + "c": "0.0025", // Current day's close price + "Q": "10", // Close trade's quantity + "b": "0.0024", // Best bid price + "B": "10", // Bid bid quantity + "a": "0.0026", // Best ask price + "A": "100", // Best ask quantity + "o": "0.0010", // Open price + "h": "0.0025", // High price + "l": "0.0010", // Low price + "v": "10000", // Total traded base asset volume + "q": "18", // Total traded quote asset volume + "O": 0, // Statistics open time + "C": 86400000, // Statistics close time + "F": 0, // First trade ID + "L": 18150, // Last trade Id + "n": 18151 // Total number of trades +} + +2、All Market Tickers Stream +24hr Ticker statistics for all symbols in an array pushed every second + +Stream Name: !ticker@arr + +Payload: + +[ + { + // Same as @ticker payload + } +] + + + +3、 +Account Update +Account state is updated with the outboundAccountInfo event. + +Payload: + +{ + "e": "outboundAccountInfo", // Event type + "E": 1499405658849, // Event time + "m": 0, // Maker commission rate (bips) + "t": 0, // Taker commission rate (bips) + "b": 0, // Buyer commission rate (bips) + "s": 0, // Seller commission rate (bips) + "T": true, // Can trade? + "W": true, // Can withdraw? + "D": true, // Can deposit? + "u": 1499405658848, // Time of last account update + "B": [ // Balances array + { + "a": "LTC", // Asset + "f": "17366.18538083", // Free amount + "l": "0.00000000" // Locked amount + }, + { + "a": "BTC", + "f": "10537.85314051", + "l": "2.19464093" + }, + { + "a": "ETH", + "f": "17902.35190619", + "l": "0.00000000" + }, + { + "a": "BNC", + "f": "1114503.29769312", + "l": "0.00000000" + }, + { + "a": "NEO", + "f": "0.00000000", + "l": "0.00000000" + } + ] +} +Order Update +Orders are updated with the executionReport event. Check the API documentation and below for relevant enum definitions. + +Payload: + +{ + "e": "executionReport", // Event type + "E": 1499405658658, // Event time + "s": "ETHBTC", // Symbol + "c": "mUvoqJxFIILMdfAW5iGSOW", // Client order ID + "S": "BUY", // Side + "o": "LIMIT", // Order type + "f": "GTC", // Time in force + "q": "1.00000000", // Order quantity + "p": "0.10264410", // Order price + "P": "0.00000000", // Stop price + "F": "0.00000000", // Iceberg quantity + "g": -1, // Ignore + "C": "null", // Original client order ID; This is the ID of the order being canceled + "x": "NEW", // Current execution type + "X": "NEW", // Current order status + "r": "NONE", // Order reject reason; will be an error code. + "i": 4293153, // Order ID + "l": "0.00000000", // Last executed quantity + "z": "0.00000000", // Cumulative filled quantity + "L": "0.00000000", // Last executed price + "n": "0", // Commission amount + "N": null, // Commission asset + "T": 1499405658657, // Transaction time + "t": -1, // Trade ID + "I": 8641984, // Ignore + "w": true, // Is the order working? Stops will have + "m": false, // Is this trade the maker side? + "M": false // Ignore +} +Execution types: + +NEW +CANCELED +REPLACED (currently unused) +REJECTED +TRADE +EXPIRED \ No newline at end of file diff --git a/vnpy/api/binance/run.bat b/vnpy/api/binance/run.bat new file mode 100644 index 00000000..7bcebd67 --- /dev/null +++ b/vnpy/api/binance/run.bat @@ -0,0 +1 @@ +python test4.py \ No newline at end of file diff --git a/vnpy/api/binance/run_debug.bat b/vnpy/api/binance/run_debug.bat new file mode 100644 index 00000000..37f0862e --- /dev/null +++ b/vnpy/api/binance/run_debug.bat @@ -0,0 +1 @@ +python test5.py \ No newline at end of file diff --git a/vnpy/api/binance/test.py b/vnpy/api/binance/test.py new file mode 100644 index 00000000..95c890d3 --- /dev/null +++ b/vnpy/api/binance/test.py @@ -0,0 +1,26 @@ +# encoding: UTF-8 + +from vnpy.vnbinance import * + + +# api = BINANCE_Sub_Spot_Api() + +# api.connect_Subpot(apiKey , secretKey) +# api.subscribeSpotTicker("BNBBTC".lower()) + +url = "wss://stream.binance.com:9443/ws/bnbbtc@kline_1m" + +# print BINANCE_STREAM_URL +factory = BinanceClientFactory( url ) +factory.protocol = BinanceClientProtocol +factory.callback = process_message +context_factory = ssl.ClientContextFactory() +connectWS(factory, context_factory) + +reactor.run( installSignalHandlers=False ) +input() + +# bm = BinanceSocketManager(None) +# #bm.start_aggtrade_socket(symbol='BNBBTC' , callback=process_message) +# bm.start_symbol_ticker_socket("BNBBTC" , callback=process_message) +# bm.start() \ No newline at end of file diff --git a/vnpy/api/binance/test2.py b/vnpy/api/binance/test2.py new file mode 100644 index 00000000..abd43b4c --- /dev/null +++ b/vnpy/api/binance/test2.py @@ -0,0 +1,28 @@ +# encoding: UTF-8 +import hashlib +import zlib +import json +from time import sleep +from threading import Thread + +from autobahn.twisted.websocket import WebSocketClientFactory, \ + WebSocketClientProtocol, \ + connectWS +from twisted.internet import reactor, ssl +from twisted.internet.protocol import ReconnectingClientFactory +from twisted.internet.error import ReactorAlreadyRunning +import threading + + +import websocket + +def onMessage(ws, data): + print(ws ,data) + +BINANCE_STREAM_URL = 'wss://stream.binance.com:9443/ws/bnbbtc@ticker' + +ss = websocket.WebSocketApp(BINANCE_STREAM_URL, + on_message=onMessage) + +reactor.run(installSignalHandlers=False) +input() \ No newline at end of file diff --git a/vnpy/api/binance/test3.py b/vnpy/api/binance/test3.py new file mode 100644 index 00000000..a26a135c --- /dev/null +++ b/vnpy/api/binance/test3.py @@ -0,0 +1,14 @@ +# encoding: UTF-8 + +from vnbinance import * + + +apiKey = 'xxxx' +secretKey = 'xxxx' + +aa = BinanceSpotApi() +aa.connect_Subpot( apiKey , secretKey) + +# aa.subcribeSymbol( "bnb_btc") + +input() \ No newline at end of file diff --git a/vnpy/api/binance/test4.py b/vnpy/api/binance/test4.py new file mode 100644 index 00000000..1271f91c --- /dev/null +++ b/vnpy/api/binance/test4.py @@ -0,0 +1,46 @@ +# encoding: UTF-8 + +from vnpy.api.binance.vnbinance import * + +apiKey = "xxx" +secretKey = "xxxx" + +ap = BinanceSpotApi() + +ap.connect_Subpot( apiKey , secretKey) + + +# ap.subscribeSpotDepth( "BNBBTC" ) + +# ap.spotListAllOrders("ETCBTC") + +# id 5509642 + +# ap.spotListOpenOrders() +# ap.spotAccountInfo() + +# ap.subscribeAllTicker() + +# print "ap.spotExchangeInfo()" +# ap.spotExchangeInfo() + +# print ap.spotCancelOrder( "ETCBTC" , "5509642" ) +# ap.subcribeSymbol( "bnb_btc") + + +# ap.spotListAllOrders("ETCBTC") + +print ("ap.subscribeSpotTicker") +# ap.subscribeSpotTicker("bnb_btc") + +price_f = "0.00000325" +# ap.spotTrade( symbol_pair = "ETCBTC" , type_ = "sell" , price = 0.1 , amount = 0.01) + +ap.spotTrade( symbol_pair = "TNBBTC" , type_ = "buy" , price = price_f , amount = 400) + +# ap.spotListAllOrdesrs("TNBBTC") + +# ap.spotCancelOrder( "TNBBTC" , "5047104" ) +# ap.spotListAllOrders("ETCBTC") + +#input() diff --git a/vnpy/api/binance/test5.py b/vnpy/api/binance/test5.py new file mode 100644 index 00000000..6ebdd443 --- /dev/null +++ b/vnpy/api/binance/test5.py @@ -0,0 +1,74 @@ +from vnpy.api.binance.client import Client + +apiKey = "xxx" +secretKey = "xxxx" + +client = Client(apiKey, secretKey) + +# # get market depth +# depth = client.get_order_book(symbol='BNBBTC') + +# # place a test market buy order, to place an actual order use the create_order function +# order = client.create_test_order( +# symbol='BNBBTC', +# side=Client.SIDE_BUY, +# type=Client.ORDER_TYPE_MARKET, +# quantity=100) + +# # get all symbol prices +# prices = client.get_all_tickers() + +# # withdraw 100 ETH +# # check docs for assumptions around withdrawals +# from binance.exceptions import BinanceAPIException, BinanceWithdrawException +# try: +# result = client.withdraw( +# asset='ETH', +# address='', +# amount=100) +# except BinanceAPIException as e: +# print(e) +# except BinanceWithdrawException as e: +# print(e) +# else: +# print("Success") + +# # fetch list of withdrawals +# withdraws = client.get_withdraw_history() + +# # fetch list of ETH withdrawals +# eth_withdraws = client.get_withdraw_history(asset='ETH') + +# # get a deposit address for BTC +# address = client.get_deposit_address(asset='BTC') + +# # start aggregated trade websocket for BNBBTC +def process_message(msg): + print("message type: {}".format(msg['e'])) + print(msg) + # do something + +#from vnpy.api.binance.websockets import BinanceSocketManager +#bm = BinanceSocketManager(client) +#bm.start_aggtrade_socket('BNBBTC', process_message) +#bm.start() + +# get historical kline data from any date range + +from datetime import datetime,timedelta +# fetch 1 minute klines for the last day up until now +start_time = datetime.now() +start_time = start_time - timedelta(hours=1) +t =int(start_time.timestamp()*1000) + +# startTime =t +klines = client.get_klines(symbol="BTCUSDT",interval= Client.KLINE_INTERVAL_1MINUTE) + +for kl in klines: + print(datetime.fromtimestamp(kl[0]/1e3)) + +# fetch 30 minute klines for the last month of 2017 +# klines = client.get_historical_klines("ETHBTC", Client.KLINE_INTERVAL_30MINUTE, "1 Dec, 2017", "1 Jan, 2018") + +# fetch weekly klines since it listed +# klines = client.get_historical_klines("NEOBTC", KLINE_INTERVAL_1WEEK, "1 Jan, 2017") \ No newline at end of file diff --git a/vnpy/api/binance/vnbinance.py b/vnpy/api/binance/vnbinance.py new file mode 100644 index 00000000..64f744f3 --- /dev/null +++ b/vnpy/api/binance/vnbinance.py @@ -0,0 +1,470 @@ +# encoding: UTF-8 + +import sys +import hashlib +import zlib +import json +from time import sleep +from threading import Thread + +from autobahn.twisted.websocket import WebSocketClientFactory, \ + WebSocketClientProtocol, \ + connectWS +from twisted.internet import reactor, ssl +from twisted.internet.protocol import ReconnectingClientFactory +from twisted.internet.error import ReactorAlreadyRunning +import threading +import traceback + +from vnpy.api.binance.exceptions import BinanceAPIException, BinanceRequestException, BinanceWithdrawException +from vnpy.api.binance.websockets import BinanceSocketManager +from vnpy.api.binance.client import Client +import urllib, requests +import urllib.parse + +# 请求指令 +FUNCTIONCODE_GET_ACCOUNT_BINANCE = 'get_account' +# FUNCTIONCODE_GET_ORDER = 'get_order' # 不准备实现 +FUNCTIONCODE_GET_OPEN_ORDERS = 'get_open_orders' # 获得所有的 open orders +FUNCTIONCODE_GET_ALL_ORDERS = "get_all_orders" # 获得所有的 orders +FUNCTIONCODE_BUY_ORDER_BINANCE = 'buy' +FUNCTIONCODE_SELL_ORDER_BINANCE = 'sell' +FUNCTIONCODE_CANCEL_ORDER_BINANCE = 'cancel_order' +FUNCTIONCODE_GET_EXCHANGE_INFO = "get_exchange_info" + +# 印射关系 +binance_exchanges_dict = { + "bcc" : "bch", + "bccusdt": "bchusdt", + "bccbtc" : "bchbtc", + "bccbnb" : "bchbnb", + "bcceth" : "bcheth", + "BCC" : "BCH", + "BCCUSDT": "BCHUSDT", + "BCCBTC" : "BCHBTC", + "BCCBNB" : "BCHBNB", + "BCCETH" : "BCHETH" +} + +exchanges_biannce_dict = {v: k for k, v in binance_exchanges_dict.items()} + +''' +将 币安的某些symbol_pair 与 其他交易所统一, 比如 bcc 换成 bch +''' +def symbolFromBinanceToOtherExchanges(symbol_pair): + global binance_exchanges_dict + if symbol_pair in binance_exchanges_dict.keys(): + return binance_exchanges_dict[symbol_pair] + return symbol_pair + +''' +将 其他交易所的symbol 印射到 币安的实际symbol +''' +def symbolFromOtherExchangesToBinance(symbol_pair): + global exchanges_biannce_dict + if symbol_pair in exchanges_biannce_dict.keys(): + return exchanges_biannce_dict[symbol_pair] + return symbol_pair + +class BinanceSpotApi(object): + """基于Websocket的API对象""" + #---------------------------------------------------------------------- + def __init__(self): + """Constructor""" + self.apiKey = '' # 用户名 + self.secretKey = '' # 密码 + + self.client = None # 客户端管理器 + self.bm = None # sockets 管理器 + + self.reqID = 0 # 请求编号 + # self.reqQueue = Queue() # 请求队列 + self.reqQueue = [] # 请求的队列 + self.reqThread = Thread(target=self.processQueue) # 请求处理线程 + + self.active = False # API工作状态 + + self.DEBUG = True + #---------------------------------------------------------------------- + def connect_Subpot(self, apiKey , secretKey ): + self.apiKey = apiKey + self.secretKey = secretKey + + self.client = Client( apiKey , secretKey) + + self.bm = BinanceSocketManager(self.client) + + self.bm.start_user_socket( self.onMessage) + + self.bm.start() + + self.active = True + self.reqThread.start() + + + #---------------------------------------------------------------------- + def processQueue(self): + """处理请求队列中的请求""" + while self.active: + try: + #req = self.reqQueue.get(block=True, timeout=0.001) # 获取请求的阻塞为一秒 + if len(self.reqQueue) == 0: + continue + (Type , req) = self.reqQueue[0] + + callback = req['callback'] + reqID = req['reqID'] + + try: + data = self.processRequest(req) + # 请求成功 + if data != None : + if self.DEBUG: + print(callback.__name__) + callback(data, req, reqID) + except Exception as ex: + print(u'processQueue exception:{},{}'.format(str(ex), traceback.format_exc()), file=sys.stderr) + self.reqQueue.pop(0) + sleep(0.1) + except Exception as ex: + print(u'processQueue exception:{},{}'.format(str(ex), traceback.format_exc()), file=sys.stderr) + + #---------------------------------------------------------------------- + def processRequest(self, req): + """ + 处理各类请求 + :param req: + :return: + """ + try: + method = req['method'] + reqID = req["reqID"] + callback = req['callback'] + data = None + + # 请求账号信息 + if method == FUNCTIONCODE_GET_ACCOUNT_BINANCE: + data = self.client.get_account() + # 替换 一些 symbol + for b_symbol_dic in data["balances"]: + b_symbol_dic["asset"] = symbolFromBinanceToOtherExchanges(b_symbol_dic["asset"]) + + self.onGetAccount(data , req , reqID) + + # 请求委托单 + elif method == FUNCTIONCODE_GET_OPEN_ORDERS: + kwargs = req["kwargs"] + symbol = kwargs["symbol"] + + if symbol != None: + data = self.client.get_open_orders(symbol = symbol) + # update symbol + for dic in data: + dic["symbol"] = symbolFromBinanceToOtherExchanges( dic["symbol"] ) + callback(data , req , reqID) + else: + data = self.client.get_open_orders() + # update symbol + for dic in data: + dic["symbol"] = symbolFromBinanceToOtherExchanges( dic["symbol"] ) + callback(data , req , reqID) + + # 请求所有委托单 + elif method == FUNCTIONCODE_GET_ALL_ORDERS: + kwargs = req["kwargs"] + symbol = kwargs["symbol"] + #if '_' not in symbol and len(symbol) in [3,4]: + # return None + data = self.client.get_all_orders( symbol = symbol ) + # update symbol + for dic in data: + dic["symbol"] = symbolFromBinanceToOtherExchanges( dic["symbol"] ) + + callback(data , req , reqID) + # 买入现货 + elif method == FUNCTIONCODE_BUY_ORDER_BINANCE: + kwargs = req["kwargs"] + symbol = self.legalSymbolUpper(kwargs["symbol"]) + quantity = float(kwargs["quantity"]) + price = float(kwargs["price"]) + price = str('%.8f' % float(price)) + data = self.client.order_limit_buy( symbol=symbol, price = price ,quantity=quantity) + # update symbol + data["symbol"] = symbolFromBinanceToOtherExchanges( data["symbol"] ) + callback(data , req , reqID) + + # 卖出现货 + elif method == FUNCTIONCODE_SELL_ORDER_BINANCE: + kwargs = req["kwargs"] + symbol = self.legalSymbolUpper(kwargs["symbol"]) + quantity = float(kwargs["quantity"]) + price = float(kwargs["price"]) + price = str('%.8f' % float(price)) + data = self.client.order_limit_sell( symbol=symbol, price = price ,quantity=quantity) + # update symbol + data["symbol"] = symbolFromBinanceToOtherExchanges( data["symbol"] ) + callback(data , req , reqID) + + # 取消订单 + elif method == FUNCTIONCODE_CANCEL_ORDER_BINANCE: + kwargs = req["kwargs"] + symbol = self.legalSymbolUpper(kwargs["symbol"]) + orderId = int(kwargs["orderId"]) + data = self.client.cancel_order( symbol = symbol , orderId = orderId ) + # update symbol + data["symbol"] = symbolFromBinanceToOtherExchanges( data["symbol"] ) + + callback(data , req , reqID) + + # 获取交易所信息 + elif method == FUNCTIONCODE_GET_EXCHANGE_INFO: + data = self.client.get_exchange_info() + # update symbol + for symbol_dic in data["symbols"]: + symbol_dic["symbol"] = symbolFromBinanceToOtherExchanges( symbol_dic["symbol"] ) + symbol_dic["baseAsset"] = symbolFromBinanceToOtherExchanges( symbol_dic["baseAsset"] ) + + callback(data , req , reqID) + + except Exception as ex: + print(u'processQueue exception:{},{}'.format(str(ex), traceback.format_exc()), file=sys.stderr) + # pass + + #---------------------------------------------------------------------- + def legalSymbolLower(self, symbol): + symbol = symbol.lower() + symbol = ''.join(symbol.split('_')) + return symbol + + #---------------------------------------------------------------------- + def legalSymbolUpper(self, symbol): + symbol = symbol.upper() + symbol = ''.join(symbol.split('_')) + return symbol + + #---------------------------------------------------------------------- + def exit(self): + """退出""" + self.active = False + if self.bm != None: + self.bm.close() + + if self.reqThread.isAlive(): + self.reqThread.join() + + #----------------------------------------------------------------------- + def sendTradingRequest(self, method , callback , kwargs = None,optional=None): + # 请求编号加1 + self.reqID += 1 + # 生成请求字典并放入队列中 + req = {} + req['method'] = method + req['callback'] = callback + req['optional'] = optional + 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: + if use_method == method: + flag = True + break + if False == flag: + self.reqQueue.append( (method , req)) + else: + self.reqQueue.append( (method , req)) + + # 返回请求编号 + return self.reqID + + #---------------------------------------------------------------------- + def spotTrade(self, symbol_pair, type_, price, amount): + """现货委托""" + symbol_pair = self.legalSymbolUpper(symbol_pair) + symbol_pair = symbolFromOtherExchangesToBinance(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": + return self.sendTradingRequest( method = FUNCTIONCODE_SELL_ORDER_BINANCE , callback = self.onTradeOrder , kwargs = {"symbol":symbol_pair, "price":price,"quantity":amount} , optional=None) + + return None + + #---------------------------------------------------------------------- + def spotCancelOrder(self, symbol_pair, orderid): + """现货委托撤单""" + symbol_pair = self.legalSymbolUpper(symbol_pair) + symbol_pair = symbolFromOtherExchangesToBinance(symbol_pair) + return self.sendTradingRequest( method = FUNCTIONCODE_CANCEL_ORDER_BINANCE , callback = self.onGetCancelOrder , kwargs = {"symbol":symbol_pair,"orderId":int(orderid)} , optional=None) + + #---------------------------------------------------------------------- + def spotAccountInfo(self): + """列出账户""" + return self.sendTradingRequest( method = FUNCTIONCODE_GET_ACCOUNT_BINANCE , callback = self.onGetAccount , kwargs = {} , optional = None) + + #---------------------------------------------------------------------- + def spotListOpenOrders(self , symbol = None): + """列出所有的 orders""" + if symbol != None: + symbol = self.legalSymbolUpper(symbol) + symbol = symbolFromOtherExchangesToBinance(symbol) + return self.sendTradingRequest( method = FUNCTIONCODE_GET_OPEN_ORDERS , callback = self.onGetOpenOrders , kwargs = {"symbol":symbol} , optional=None) + + #---------------------------------------------------------------------- + def spotListAllOrders( self , symbol): + symbol = self.legalSymbolUpper(symbol) + symbol = symbolFromOtherExchangesToBinance(symbol) + return self.sendTradingRequest( method = FUNCTIONCODE_GET_ALL_ORDERS , callback = self.onGetAllOrders , kwargs = {"symbol":symbol} , optional=None) + + #---------------------------------------------------------------------- + def spotExchangeInfo(self): + return self.sendTradingRequest( method = FUNCTIONCODE_GET_EXCHANGE_INFO , callback = self.onExchangeInfo , kwargs = {} , optional = None) + + #---------------------------------------------------------------------- + # def subcribeSymbol(self , symbol): + # symbol = self.legalSymbolLower(symbol) + # symbol = symbolFromOtherExchangesToBinance(symbol) + # #self.bm.start_symbol_ticker_socket( symbol , self.onTick) + # self.bm.start_multiplex_socket([symbol + "@ticker" , symbol + "@depth5" , symbol + "@trade"] , callback=self.onMessage) + + #---------------------------------------------------------------------- + def subscribeAllTicker(self): + self.bm.start_ticker_socket( callback = self.onPreDealAllTicker) + + #---------------------------------------------------------------------- + def subscribeSpotTicker(self , symbol): + if self.bm != None: + # print "self.bm != None:" + symbol = self.legalSymbolLower(symbol) + symbol = symbolFromOtherExchangesToBinance(symbol) + self.bm.start_symbol_ticker_socket( symbol , self.onPreDealTicker) + + #---------------------------------------------------------------------- + def subscribeSpotDepth(self, symbol): + if self.bm != None: + symbol = self.legalSymbolLower(symbol) + symbol = symbolFromOtherExchangesToBinance(symbol) + self.bm.start_depth_socket(symbol , self.onPreDealDepth) + + #---------------------------------------------------------------------- + def subscribeSpotTrades(self, symbol): + if self.bm != None: + symbol = self.legalSymbolLower(symbol) + symbol = symbolFromOtherExchangesToBinance(symbol) + self.bm.start_trade_socket(symbol , self.onPreDealTrades) + + #---------------------------------------------------------------------- + def http_get_request(self, url, params, add_to_headers=None): + headers = { + "Content-type": "application/x-www-form-urlencoded", + 'User-Agent':'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0' + } + if add_to_headers: + headers.update(add_to_headers) + postdata = urllib.parse.urlencode(params) + try: + #response = requests.get(url, postdata, headers=headers, timeout=5) + response = requests.get(url ) + if response.status_code == 200: + return response.json() + else: + return {"status":"fail"} + except Exception as e: + print("httpGet failed, detail is:%s" %e) + return {"status":"fail","msg":e} + + # limit in [5, 10, 20, 50, 100, 500, 1000] + #---------------------------------------------------------------------- + def getDepthSymbol(self, symbol , limit = 10): + symbol = self.legalSymbolUpper(symbol) + symbol = symbolFromOtherExchangesToBinance(symbol) + url = "https://www.binance.com/api/v1/depth?symbol=%s&limit=%s" % ( symbol , str(limit)) + data = self.http_get_request( url , "") + return data + + #---------------------------------------------------------------------- + def onMessage(self, msg): + if 's' in msg.keys(): + msg['s'] = symbolFromBinanceToOtherExchanges(msg["s"]) + if "lastUpdateId" in msg.keys(): + self.onDepth( msg) + else: + if msg["e"] == "trade": + self.onTrades(msg) + elif "ticker" in msg["e"]: + self.onTick(msg) + + #---------------------------------------------------------------------- + def onAllError(self, ex , req , reqID): + print( "onAllError" + str(ex)) + + #---------------------------------------------------------------------- + def onPreDealAllTicker(self, msg): + for dic in msg: + dic["s"] = symbolFromBinanceToOtherExchanges(dic["s"]) + self.onAllTicker(msg) + + #---------------------------------------------------------------------- + def onAllTicker(self,msg): + """币安支持所有 ticker 同时socket过来""" + print(u'onAllTicker:'.format(msg)) + + # ---------------------------------------------------------------------- + def onPreDealTicker(self, msg): + if 's' in msg.keys(): + msg['s'] = symbolFromBinanceToOtherExchanges(msg["s"]) + self.onTick(msg) + + #---------------------------------------------------------------------- + def onTick(self, msg): + print(u'onTick:'.format(msg)) + + # ---------------------------------------------------------------------- + def onPreDealDepth(self, msg): + if 's' in msg.keys(): + msg['s'] = symbolFromBinanceToOtherExchanges(msg["s"]) + self.onDepth(msg) + #---------------------------------------------------------------------- + def onDepth(self, msg): + print(u'onDepth:'.format(msg)) + + # ---------------------------------------------------------------------- + def onPreDealTrades(self, msg): + if 's' in msg.keys(): + msg['s'] = symbolFromBinanceToOtherExchanges(msg["s"]) + self.onTrades(msg) + #---------------------------------------------------------------------- + def onTrades(self, msg): + print(u'onTrades:{}'.format(msg)) + + #---------------------------------------------------------------------- + def onGetAccount(self, data, req, reqID): + print(u'onGetAccount:'.format(data, req, reqID)) + + #---------------------------------------------------------------------- + def onGetOpenOrders(self, data, req, reqID): + print(u'onGetOpenOrders:ata:{}, req:{}, reqID:{}'.format(data, req, reqID)) + + #---------------------------------------------------------------------- + def onGetAllOrders(self, data, req, reqID): + print(u'onGetAllOrders:data:{}, req:{}, reqID:{}'.format(data, req, reqID)) + + #---------------------------------------------------------------------- + def onGetBuyOrder(self, data, req, reqID): + print(u'onGetBuyOrder:data:{}, req:{}, reqID:{}'.format(data, req, reqID)) + + #---------------------------------------------------------------------- + def onGetSellOrder(self, data, req, reqID): + print(u'onGetSellOrder:data:{}, req:{}, reqID:{}'.format(data, req, reqID)) + + #---------------------------------------------------------------------- + def onGetCancelOrder(self, data, req, reqID): + print(u'onGetCancelOrder:data:{},req:{},reqId:{}'.format(data, req, reqID)) + + #---------------------------------------------------------------------- + def onExchangeInfo(self, data, req, reqID): + print(u'onExchangeInfo:data:{},req:{},reqId:{}'.format(data, req, reqID)) + + # ---------------------------------------------------------------------- + def onTradeOrder(self, data, req, reqID): + print (u'onTradeOrder:{}'.format(data)) \ No newline at end of file diff --git a/vnpy/api/binance/websockets.py b/vnpy/api/binance/websockets.py new file mode 100644 index 00000000..0c497e2d --- /dev/null +++ b/vnpy/api/binance/websockets.py @@ -0,0 +1,460 @@ +#!/usr/bin/env python +# coding=utf-8 + +import json +import threading +import sys + +from autobahn.twisted.websocket import WebSocketClientFactory, \ + WebSocketClientProtocol, \ + connectWS +from twisted.internet import reactor, ssl +from twisted.internet.protocol import ReconnectingClientFactory +from twisted.internet.error import ReactorAlreadyRunning + +from vnpy.api.binance.client import Client + +from datetime import datetime + +class BinanceClientProtocol(WebSocketClientProtocol): + + def onConnect(self, response): + # reset the delay after reconnecting + self.factory.resetDelay() + + def onMessage(self, payload, isBinary): + if not isBinary: + try: + payload_obj = json.loads(payload.decode('utf8')) + except ValueError: + pass + else: + self.factory.callback(payload_obj) + + +class BinanceReconnectingClientFactory(ReconnectingClientFactory): + + # set initial delay to a short time + initialDelay = 0.1 + + maxDelay = 10 + + maxRetries = 5 + + +class BinanceClientFactory(WebSocketClientFactory, BinanceReconnectingClientFactory): + + protocol = BinanceClientProtocol + + def clientConnectionFailed(self, connector, reason): + self.retry(connector) + + def clientConnectionLost(self, connector, reason): + # check if closed cleanly + if reason.getErrorMessage() != 'Connection was closed cleanly.': + self.retry(connector) + + +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 + + def __init__(self, client): + """Initialise the BinanceSocketManager + + :param client: Binance API client + :type client: binance.Client + + """ + threading.Thread.__init__(self) + self._conns = {} + self._user_timer = None + self._user_listen_key = None + self._user_callback = None + self._client = client + + def _start_socket(self, path, callback, prefix='ws/'): + if path in self._conns: + return False + + factory_url = self.STREAM_URL + prefix + path + factory = BinanceClientFactory(factory_url) + factory.protocol = BinanceClientProtocol + factory.callback = callback + 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 + + 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 + + :returns: connection key string if successful, False otherwise + + 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 + [ + "0.10376590", # price (need to update the quantity on this price) + "59.15767010", # quantity + [] # can be ignored + ], + ], + "a": [ # ask depth delta + [ + "0.10376586", # price (need to update the quantity on this price) + "159.15767010", # quantity + [] # can be ignored + ], + [ + "0.10383109", + "345.86845230", + [] + ], + [ + "0.10490700", + "0.00000000", # quantity=0 means remove this level + [] + ] + ] + } + """ + socket_name = symbol.lower() + '@depth' + if depth != self.WEBSOCKET_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 + + :returns: connection key string if successful, False otherwise + + Message Format + + .. code-block:: python + + { + "e": "kline", # event type + "E": 1499404907056, # event time + "s": "ETHBTC", # symbol + "k": { + "t": 1499404860000, # start time of this bar + "T": 1499404919999, # end time of this bar + "s": "ETHBTC", # symbol + "i": "1m", # interval + "f": 77462, # first trade id + "L": 77465, # last trade id + "o": "0.10278577", # open + "c": "0.10278645", # close + "h": "0.10278712", # high + "l": "0.10278518", # low + "v": "17.47929838", # volume + "n": 4, # number of trades + "x": false, # whether this bar is final + "q": "1.79662878", # quote volume + "V": "2.34879839", # volume of active buy + "Q": "0.24142166", # quote volume of active buy + "B": "13279784.01349473" # can be ignored + } + } + """ + socket_name = '{}@kline_{}'.format(symbol.lower(), interval) + return self._start_socket(socket_name, 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 + "s": "BNBBTC", # Symbol + "t": 12345, # Trade ID + "p": "0.001", # Price + "q": "100", # Quantity + "b": 88, # Buyer order Id + "a": 50, # Seller order Id + "T": 123456785, # Trade time + "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 + "s": "ETHBTC", # symbol + "a": 70232, # aggregated tradeid + "p": "0.10281118", # price + "q": "8.15632997", # quantity + "f": 77489, # first breakdown trade id + "l": 77489, # last breakdown trade id + "T": 1499405254324, # trade time + "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 + "s": "BNBBTC", # Symbol + "p": "0.0015", # Price change + "P": "250.00", # Price change percent + "w": "0.0018", # Weighted average price + "x": "0.0009", # Previous day's close price + "c": "0.0025", # Current day's close price + "Q": "10", # Close trade's quantity + "b": "0.0024", # Best bid price + "B": "10", # Bid bid quantity + "a": "0.0026", # Best ask price + "A": "100", # Best ask quantity + "o": "0.0010", # Open price + "h": "0.0025", # High price + "l": "0.0010", # Low price + "v": "10000", # Total traded base asset volume + "q": "18", # Total traded quote asset volume + "O": 0, # Statistics open time + "C": 86400000, # Statistics close time + "F": 0, # First trade ID + "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, + 'o': '0.07393000', + 's': 'BCCBTC', + 'C': 1509622420916, + 'b': '0.07800800', + 'l': '0.07160300', + 'h': '0.08199900', + 'L': 287722, + 'P': '6.694', + 'Q': '0.10000000', + 'q': '1202.67106335', + 'p': '0.00494900', + 'O': 1509536020916, + 'a': '0.07887800', + 'n': 9113, + 'B': '1.00000000', + 'c': '0.07887900', + 'x': '0.07399600', + 'w': '0.07639068', + 'A': '2.41900000', + 'v': '15743.68900000' + } + ] + """ + return self._start_socket('!ticker@arr', callback) + + 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 + """ + 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_callback = callback + conn_key = self._start_socket(self._user_listen_key, callback) + if conn_key: + # start timer to keep socket alive + self._start_user_timer() + + return conn_key + + def _start_user_timer(self): + self._user_timer = threading.Timer(self._user_timeout, self._keepalive_user_socket) + self._user_timer.setDaemon(True) + self._user_timer.start() + + def _keepalive_user_socket(self): + try: + 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() + 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 + + self._conns[conn_key].disconnect() + del(self._conns[conn_key]) + + # check if we have a user stream socket + if len(conn_key) >= 60 and conn_key[:60] == self._user_listen_key: + self._stop_user_socket() + + def _stop_user_socket(self): + if not self._user_listen_key: + return + # 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): + try: + reactor.run(installSignalHandlers=False) + except ReactorAlreadyRunning: + # Ignore error about reactor already running + pass + + def close(self): + """Close all connections + + """ + keys = set(self._conns.keys()) + for key in keys: + self.stop_socket(key) + + self._conns = {}