class HuobiFutureMarket: """ Huobi Future Market Server. Attributes: kwargs: platform: Exchange platform name, must be `huobi_future`. wss: Exchange Websocket host address, default is `wss://www.hbdm.com`. symbols: symbol list, Huobi Future contract code list. channels: channel list, only `orderbook` to be enabled. orderbook_length: The length of orderbook's data to be published via OrderbookEvent, default is 10. """ def __init__(self, **kwargs): self._platform = kwargs["platform"] self._wss = kwargs.get("wss", "wss://www.hbdm.com") self._symbols = list(set(kwargs.get("symbols"))) self._channels = kwargs.get("channels") self._orderbook_length = kwargs.get("orderbook_length", 10) self._c_to_s = {} # {"channel": "symbol"} url = self._wss + "/ws" self._ws = Websocket(url, self.connected_callback, process_binary_callback=self.process_binary) self._ws.initialize() async def connected_callback(self): """ After create connection to Websocket server successfully, we will subscribe orderbook event. """ for ch in self._channels: if ch == "kline": for symbol in self._symbols: channel = self._symbol_to_channel(symbol, "kline") if not channel: continue kline = {"sub": channel} await self._ws.send(kline) elif ch == "orderbook": for symbol in self._symbols: channel = self._symbol_to_channel(symbol, "depth") if not channel: continue data = {"sub": channel} await self._ws.send(data) elif ch == "trade": for symbol in self._symbols: channel = self._symbol_to_channel(symbol, "trade") if not channel: continue data = {"sub": channel} await self._ws.send(data) else: logger.error("channel error! channel:", ch, caller=self) async def process_binary(self, msg): """ Process binary message that received from Websocket connection. Args: msg: Binary message. """ data = json.loads(gzip.decompress(msg).decode()) # logger.debug("data:", json.dumps(data), caller=self) channel = data.get("ch") if not channel: if data.get("ping"): hb_msg = {"pong": data.get("ping")} await self._ws.send(hb_msg) return symbol = self._c_to_s[channel] if channel.find("kline") != -1: d = data.get("tick") kline = { "platform": self._platform, "symbol": symbol, "open": d["open"], "high": d["high"], "low": d["low"], "close": d["close"], "volume": d["amount"], "timestamp": data.get("ts"), "kline_type": MARKET_TYPE_KLINE } EventKline(**kline).publish() logger.debug("symbol:", symbol, "kline:", kline, caller=self) elif channel.find("depth") != -1: tick = data.get("tick") asks = tick.get("asks")[:self._orderbook_length] bids = tick.get("bids")[:self._orderbook_length] timestamp = tick.get("ts") orderbook = { "platform": self._platform, "symbol": symbol, "asks": asks, "bids": bids, "timestamp": timestamp } EventOrderbook(**orderbook).publish() logger.debug("symbol:", symbol, "orderbook:", orderbook, caller=self) elif channel.find("trade") != -1: tick = data.get("tick") direction = tick["data"][0].get("direction") price = tick["data"][0].get("price") quantity = tick["data"][0].get("amount") trade = { "platform": self._platform, "symbol": symbol, "action": ORDER_ACTION_BUY if direction == "buy" else ORDER_ACTION_SELL, "price": price, "quantity": quantity, "timestamp": tick.get("ts") } EventTrade(**trade).publish() logger.debug("symbol:", symbol, "trade:", trade, caller=self) else: logger.error("event error! msg:", msg, caller=self) def _symbol_to_channel(self, symbol, channel_type): if channel_type == "kline": channel = "market.{s}.kline.1min".format(s=symbol) elif channel_type == "depth": channel = "market.{s}.depth.step6".format(s=symbol) elif channel_type == "trade": channel = "market.{s}.trade.detail".format(s=symbol) else: logger.error("channel type error! channel type:", channel_type, calle=self) return None self._c_to_s[channel] = symbol return channel
class KucoinMarket: """ Kucoin Market Server. Attributes: kwargs: platform: Exchange platform name, must be `kucoin`. host: Exchange Websocket host address, default is "https://openapi-v2.kucoin.com". symbols: symbol list, OKEx Future instrument_id list. channels: channel list, only `orderbook` , `kline` and `trade` to be enabled. orderbook_length: The length of orderbook's data to be published via OrderbookEvent, default is 20. orderbook_interval: The interval time to fetch a orderbook information, default is 2 seconds. """ def __init__(self, **kwargs): self._platform = kwargs["platform"] self._host = kwargs.get("host", "https://openapi-v2.kucoin.com") self._symbols = list(set(kwargs.get("symbols"))) self._channels = kwargs.get("channels") self._orderbook_length = kwargs.get("orderbook_length", 20) # only support for 20 or 100. self._orderbook_interval = kwargs.get("orderbook_interval", 2) if self._orderbook_length != 20: self._orderbook_length = 100 self._request_id = 0 # Unique request id for pre request. self._orderbooks = { } # Orderbook data, e.g. {"symbol": {"bids": {"price": quantity, ...}, "asks": {...}, timestamp: 123, "sequence": 123}} self._last_publish_ts = 0 # The latest publish timestamp for OrderbookEvent. self._ws = None # Websocket object. # REST API client. self._rest_api = KucoinRestAPI(self._host, None, None, None) SingleTask.run(self._initialize) async def _initialize(self): """Initialize.""" for channel in self._channels: if channel == "orderbook": LoopRunTask.register(self.do_orderbook_update, self._orderbook_interval) elif channel == "trade": # Create Websocket connection. success, error = await self._rest_api.get_websocket_token() if error: logger.error("get websocket token error!", caller=self) return url = "{}?token={}".format( success["instanceServers"][0]["endpoint"], success["token"]) self._ws = Websocket(url, self.connected_callback, self.process) self._ws.initialize() LoopRunTask.register(self.send_heartbeat_msg, 30) async def connected_callback(self): """ After create connection to Websocket server successfully, we will subscribe orderbook/kline/trade event. """ symbols = [] for s in self._symbols: t = s.replace("/", "-") symbols.append(t) if not symbols: logger.warn("symbols not found in config file.", caller=self) return if not self._channels: logger.warn("channels not found in config file.", caller=self) return for ch in self._channels: request_id = await self.generate_request_id() # if ch == "orderbook": # d = { # "id": request_id, # "type": "subscribe", # "topic": "/market/level2:" + ",".join(symbols), # "response": True # } # await self._ws.send(d) # logger.info("subscribe orderbook success.", caller=self) if ch == "trade": d = { "id": request_id, "type": "subscribe", "topic": "/market/match:" + ",".join(symbols), "privateChannel": False, "response": True } await self._ws.send(d) logger.info("subscribe trade success.", caller=self) else: logger.error("channel error! channel:", ch, caller=self) async def send_heartbeat_msg(self, *args, **kwargs): request_id = await self.generate_request_id() data = {"id": request_id, "type": "ping"} if not self._ws: logger.error("") return await self._ws.send(data) @async_method_locker("_generate_request_id") async def generate_request_id(self): self._request_id = tools.get_cur_timestamp_ms() return self._request_id async def process(self, msg): """ Process message that received from Websocket connection. Args: msg: Message received from Websocket connection. """ logger.debug("msg:", msg, caller=self) topic = msg.get("topic", "") if "level2" in topic: await self.process_orderbook_update(msg["data"]) elif "match" in topic: await self.process_trade_update(msg["data"]) @async_method_locker("process") async def process_orderbook_update(self, data): """ Deal with orderbook update message. """ symbol = data["symbol"].replace("-", "/") if symbol not in self._orderbooks: return if self._orderbooks[symbol]["sequence"] > data["sequenceStart"]: return self._orderbooks[symbol]["timestamp"] = tools.get_cur_timestamp_ms() for ask in data["changes"]["asks"]: price = float(ask[0]) quantity = float(ask[1]) if price == 0: continue if quantity == 0 and price in self._orderbooks[symbol]["asks"]: self._orderbooks[symbol]["asks"].pop(price) else: self._orderbooks[symbol]["asks"][price] = quantity for bid in data["changes"]["bids"]: price = float(bid[0]) quantity = float(bid[1]) if price == 0: continue if quantity == 0 and price in self._orderbooks[symbol]["bids"]: self._orderbooks[symbol]["bids"].pop(price) else: self._orderbooks[symbol]["bids"][price] = quantity if self._orderbooks[symbol][ "timestamp"] - self._last_publish_ts > 1000: # Only publish one per minute. self._last_publish_ts = self._orderbooks[symbol]["timestamp"] ob = copy.copy(self._orderbooks[symbol]) SingleTask.run(self.publish_orderbook, symbol, ob) async def publish_orderbook(self, symbol, data): """ Publish orderbook message to EventCenter via OrderbookEvent. """ if not data["asks"] or not data["bids"]: logger.warn("symbol:", symbol, "asks:", data["asks"], "bids:", data["bids"], caller=self) return ask_keys = sorted(list(data["asks"].keys())) bid_keys = sorted(list(data["bids"].keys()), reverse=True) if ask_keys[0] <= bid_keys[0]: logger.warn("symbol:", symbol, "ask1:", ask_keys[0], "bid1:", bid_keys[0], caller=self) return asks = [] for k in ask_keys[:self._orderbook_length]: price = "%.8f" % k quantity = str(data["asks"].get(k)) asks.append([price, quantity]) bids = [] for k in bid_keys[:self._orderbook_length]: price = "%.8f" % k quantity = str(data["bids"].get(k)) bids.append([price, quantity]) orderbook = { "platform": self._platform, "symbol": symbol, "asks": asks, "bids": bids, "timestamp": data["timestamp"] } EventOrderbook(**orderbook).publish() logger.info("symbol:", symbol, "orderbook:", orderbook, caller=self) async def process_trade_update(self, data): """ Deal with trade data, and publish trade message to EventCenter via TradeEvent. """ symbol = data["symbol"].replace("-", "/") if symbol not in self._symbols: return action = ORDER_ACTION_BUY if data[ "side"] == "buy" else ORDER_ACTION_SELL price = "%.8f" % float(data["price"]) quantity = "%.8f" % float(data["size"]) timestamp = int(int(data["time"]) / 1000000) # Publish EventTrade. trade = { "platform": self._platform, "symbol": symbol, "action": action, "price": price, "quantity": quantity, "timestamp": timestamp } EventTrade(**trade).publish() logger.info("symbol:", symbol, "trade:", trade, caller=self) async def do_orderbook_update(self, *args, **kwargs): """ Fetch orderbook information.""" for symbol in self._symbols: result, error = await self._rest_api.get_orderbook( symbol.replace("/", "-"), self._orderbook_length) if error: continue orderbook = { "platform": self._platform, "symbol": symbol, "asks": result["asks"], "bids": result["bids"], "timestamp": result["time"] } EventOrderbook(**orderbook).publish() logger.info("symbol:", symbol, "orderbook:", orderbook, caller=self) # await 0.1 second before next request. await asyncio.sleep(0.1)
class GateMarket: """ Gate.io Market Server. Attributes: kwargs: platform: Exchange platform name, must be `gate`. wss: Wss host, default is `wss://ws.gate.io`. symbols: Trade pair list, e.g. ["ETH/BTC"]. channels: What are channels to be subscribed, only support `orderbook` / `trade` / `kline`. orderbook_length: The length of orderbook"s data to be published via OrderbookEvent, default is 10. """ def __init__(self, **kwargs): self._platform = kwargs["platform"] self._wss = kwargs.get("wss", "wss://ws.gate.io") self._symbols = list(set(kwargs.get("symbols"))) self._channels = kwargs.get("channels") self._orderbook_length = kwargs.get("orderbook_length", 10) self._orderbook_price_precious = kwargs.get("orderbook_price_precious", "0.00000001") self._request_id = 0 # unique request id for per request message. self._orderbooks = {} # orderbook data, e.g. {"symbol": {"bids": {"price": quantity, ...}, "asks": {...}, "timestamp": 0}, ... } url = self._wss + "/v3/" self._ws = Websocket(url, connected_callback=self.connected_callback, process_callback=self.process) self._ws.initialize() LoopRunTask.register(self.send_heartbeat_msg, 10) async def connected_callback(self): """After create Websocket connection successfully, we will subscribing orderbook/trade/kline events.""" if not self._symbols: logger.warn("symbols not found in config file.", caller=self) return if not self._channels: logger.warn("channels not found in config file.", caller=self) return for ch in self._channels: if ch == "trade": params = [] for s in self._symbols: params.append(s.replace("/", "_")) request_id = await self._generate_request_id() d = {"id": request_id, "method": "trades.subscribe", "params": params} await self._ws.send(d) logger.info("subscribe trade success.", caller=self) elif ch == "orderbook": await self.subscribe_orderbook() elif ch == "kline": for s in self._symbols: params = [s.replace("/", "_"), 60] request_id = await self._generate_request_id() d = {"id": request_id, "method": "kline.subscribe", "params": params} await self._ws.send(d) logger.info("subscribe kline success.", caller=self) else: logger.error("channel error:", ch, caller=self) continue async def subscribe_orderbook(self): self._orderbooks = {} params = [] for s in self._symbols: params.append([s.replace("/", "_"), int(self._orderbook_length), str(self._orderbook_price_precious)]) request_id = await self._generate_request_id() d = {"id": request_id, "method": "depth.subscribe", "params": params} await self._ws.send(d) logger.info("subscribe orderbook success.", caller=self) async def unsubscribe_orderbook(self): request_id = await self._generate_request_id() d = {"id": request_id, "method": "depth.unsubscribe", "params": []} await self._ws.send(d) logger.info("unsubscribe orderbook success.", caller=self) async def send_heartbeat_msg(self, *args, **kwargs): request_id = await self._generate_request_id() data = { "id": request_id, "method": "server.ping", "params": [] } if not self._ws: logger.error("Websocket connection not yeah!", caller=self) return await self._ws.send(data) async def process(self, msg): """ Process message that received from Websocket connection. Args: msg: Message data that received from Websocket connection. """ logger.debug("msg:", msg, caller=self) if not isinstance(msg, dict): return method = msg.get("method") if method == "trades.update": await self.process_trade_update(msg["params"]) elif method == "depth.update": await self.process_orderbook_update(msg["params"]) elif method == "kline.update": await self.process_kline_update(msg["params"]) async def process_trade_update(self, data): """ Deal with trade data, and publish trade message to EventCenter via TradeEvent. Args: data: Newest trade data. """ symbol = data[0].replace("_", "/") for item in data[1]: action = ORDER_ACTION_BUY if item["type"] == "buy" else ORDER_ACTION_SELL price = "%.8f" % float(item["price"]) quantity = "%.8f" % float(item["amount"]) timestamp = int(item["time"] * 1000) # Publish EventTrade. trade = { "platform": self._platform, "symbol": symbol, "action": action, "price": price, "quantity": quantity, "timestamp": timestamp } EventTrade(**trade).publish() logger.info("symbol:", symbol, "trade:", trade, caller=self) @async_method_locker("process_orderbook_update") async def process_orderbook_update(self, data): """ Deal with orderbook message that updated. Args: data: Newest orderbook data. """ symbol = data[2].replace("_", "/") if symbol not in self._symbols: return if symbol not in self._orderbooks: self._orderbooks[symbol] = {"asks": {}, "bids": {}, "timestamp": 0} for item in data[1].get("asks", []): price = float(item[0]) quantity = float(item[1]) if quantity == 0: if price in self._orderbooks[symbol]["asks"]: self._orderbooks[symbol]["asks"].pop(price) else: self._orderbooks[symbol]["asks"][price] = quantity for item in data[1].get("bids", []): price = float(item[0]) quantity = float(item[1]) if quantity == 0: if price in self._orderbooks[symbol]["bids"]: self._orderbooks[symbol]["bids"].pop(price) else: self._orderbooks[symbol]["bids"][price] = quantity self._orderbooks[symbol]["timestamp"] = tools.get_cur_timestamp_ms() await self.publish_orderbook(symbol) async def publish_orderbook(self, symbol): """ Publish OrderbookEvent. """ ob = copy.copy(self._orderbooks[symbol]) if not ob["asks"] or not ob["bids"]: logger.warn("symbol:", symbol, "asks:", ob["asks"], "bids:", ob["bids"], caller=self) return ask_keys = sorted(list(ob["asks"].keys())) bid_keys = sorted(list(ob["bids"].keys()), reverse=True) if ask_keys[0] <= bid_keys[0]: logger.warn("symbol:", symbol, "ask1:", ask_keys[0], "bid1:", bid_keys[0], caller=self) await self.unsubscribe_orderbook() await self.subscribe_orderbook() return # asks asks = [] for k in ask_keys[:self._orderbook_length]: price = "%.8f" % k quantity = "%.8f" % ob["asks"].get(k) asks.append([price, quantity]) # bids bids = [] for k in bid_keys[:self._orderbook_length]: price = "%.8f" % k quantity = "%.8f" % ob["bids"].get(k) bids.append([price, quantity]) # Publish OrderbookEvent. orderbook = { "platform": self._platform, "symbol": symbol, "asks": asks, "bids": bids, "timestamp": ob["timestamp"] } EventOrderbook(**orderbook).publish() logger.info("symbol:", symbol, "orderbook:", orderbook, caller=self) async def process_kline_update(self, data): """ Deal with 1min kline data, and publish kline message to EventCenter via KlineEvent. Args: data: Newest kline data. """ for item in data: timestamp = item[0] * 1000 _open = "%.8f" % float(item[1]) close = "%.8f" % float(item[2]) high = "%.8f" % float(item[3]) low = "%.8f" % float(item[4]) volume = "%.8f" % float(item[6]) symbol = item[-1].replace("_", "/") # Publish EventKline kline = { "platform": self._platform, "symbol": symbol, "open": _open, "high": high, "low": low, "close": close, "volume": volume, "timestamp": timestamp, "kline_type": const.MARKET_TYPE_KLINE } EventKline(**kline).publish() logger.info("symbol:", symbol, "kline:", kline, caller=self) @async_method_locker("_generate_request_id") async def _generate_request_id(self): self._request_id += 1 return self._request_id
class HuobiFutureTrade: """ Huobi Future Trade module. You can initialize trade object with some attributes in kwargs. Attributes: account: Account name for this trade exchange. strategy: What's name would you want to created for you strategy. symbol: Symbol name for your trade. host: HTTP request host. default `https://api.hbdm.com"`. wss: Websocket address. default `wss://www.hbdm.com`. access_key: Account's ACCESS KEY. secret_key Account's SECRET KEY. asset_update_callback: You can use this param to specific a async callback function when you initializing Trade object. `asset_update_callback` is like `async def on_asset_update_callback(asset: Asset): pass` and this callback function will be executed asynchronous when received AssetEvent. order_update_callback: You can use this param to specific a async callback function when you initializing Trade object. `order_update_callback` is like `async def on_order_update_callback(order: Order): pass` and this callback function will be executed asynchronous when some order state updated. position_update_callback: You can use this param to specific a async callback function when you initializing Trade object. `position_update_callback` is like `async def on_position_update_callback(order: Position): pass` and this callback function will be executed asynchronous when some position state updated. init_success_callback: You can use this param to specific a async callback function when you initializing Trade object. `init_success_callback` is like `async def on_init_success_callback(success: bool, error: Error, **kwargs): pass` and this callback function will be executed asynchronous after Trade module object initialized successfully. """ def __init__(self, **kwargs): """Initialize.""" e = None if not kwargs.get("account"): e = Error("param account miss") if not kwargs.get("strategy"): e = Error("param strategy miss") if not kwargs.get("symbol"): e = Error("param symbol miss") if not kwargs.get("contract_type"): e = Error("param contract_type miss") if not kwargs.get("contract_code"): e = Error("param contract_code miss") if not kwargs.get("host"): kwargs["host"] = "https://api.hbdm.com" if not kwargs.get("wss"): kwargs["wss"] = "wss://api.hbdm.com" if not kwargs.get("access_key"): e = Error("param access_key miss") if not kwargs.get("secret_key"): e = Error("param secret_key miss") if e: logger.error(e, caller=self) if kwargs.get("init_success_callback"): SingleTask.run(kwargs["init_success_callback"], False, e) return self._account = kwargs["account"] self._strategy = kwargs["strategy"] self._platform = HUOBI_FUTURE self._symbol = kwargs["symbol"] self._contract_type = kwargs["contract_type"] self._contract_code = kwargs["contract_code"] self._host = kwargs["host"] self._wss = kwargs["wss"] self._access_key = kwargs["access_key"] self._secret_key = kwargs["secret_key"] self._asset_update_callback = kwargs.get("asset_update_callback") self._order_update_callback = kwargs.get("order_update_callback") self._position_update_callback = kwargs.get("position_update_callback") self._init_success_callback = kwargs.get("init_success_callback") url = self._wss + "/notification" self._ws = Websocket(url, self.connected_callback, process_binary_callback=self.process_binary) self._ws.initialize() self._assets = { } # Asset detail, {"BTC": {"free": "1.1", "locked": "2.2", "total": "3.3"}, ... }. self._orders = {} # Order objects, {"order_id": order, ...}. self._position = Position(self._platform, self._account, self._strategy, self._contract_code) self._order_channel = "orders.{symbol}".format( symbol=self._symbol.lower()) self._position_channel = "positions.{symbol}".format( symbol=self._symbol.lower()) self._subscribe_order_ok = False self._subscribe_position_ok = False self._rest_api = HuobiFutureRestAPI(self._host, self._access_key, self._secret_key) # Subscribe AssetEvent. if self._asset_update_callback: AssetSubscribe(self._platform, self._account, self.on_event_asset_update) @property def assets(self): return copy.copy(self._assets) @property def orders(self): return copy.copy(self._orders) @property def position(self): return copy.copy(self._position) @property def rest_api(self): return self._rest_api async def connected_callback(self): """After connect to Websocket server successfully, send a auth message to server.""" timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S") data = { "AccessKeyId": self._access_key, "SignatureMethod": "HmacSHA256", "SignatureVersion": "2", "Timestamp": timestamp } sign = self._rest_api.generate_signature("GET", data, "/notification") data["op"] = "auth" data["type"] = "api" data["Signature"] = sign await self._ws.send(data) async def auth_callback(self, data): if data["err-code"] != 0: e = Error( "Websocket connection authorized failed: {}".format(data)) logger.error(e, caller=self) SingleTask.run(self._init_success_callback, False, e) return # subscribe order data = { "op": "sub", "cid": tools.get_uuid1(), "topic": self._order_channel } await self._ws.send(data) # subscribe position data = { "op": "sub", "cid": tools.get_uuid1(), "topic": self._position_channel } await self._ws.send(data) async def sub_callback(self, data): if data["err-code"] != 0: e = Error("subscribe {} failed!".format(data["topic"])) logger.error(e, caller=self) SingleTask.run(self._init_success_callback, False, e) return if data["topic"] == self._order_channel: self._subscribe_order_ok = True elif data["topic"] == self._position_channel: self._subscribe_position_ok = True if self._subscribe_order_ok and self._subscribe_position_ok: success, error = await self._rest_api.get_open_orders(self._symbol) if error: e = Error("get open orders failed!") SingleTask.run(self._init_success_callback, False, e) for order_info in success["data"]["orders"]: order_info["ts"] = order_info["created_at"] self._update_order(order_info) SingleTask.run(self._init_success_callback, True, None) @async_method_locker("HuobiFutureTrade.process_binary.locker") async def process_binary(self, raw): """ 处理websocket上接收到的消息 @param raw 原始的压缩数据 """ data = json.loads(gzip.decompress(raw).decode()) logger.debug("data:", data, caller=self) op = data.get("op") if op == "ping": hb_msg = {"op": "pong", "ts": data.get("ts")} await self._ws.send(hb_msg) elif op == "auth": await self.auth_callback(data) elif op == "sub": await self.sub_callback(data) elif op == "notify": if data["topic"] == self._order_channel: self._update_order(data) elif data["topic"] == "positions": self._update_position(data) elif data["topic"] == self._position_channel: self._update_position(data) async def create_order(self, action, price, quantity, order_type=ORDER_TYPE_LIMIT, *args, **kwargs): """ Create an order. Args: action: Trade direction, BUY or SELL. price: Price of each contract. quantity: The buying or selling quantity. order_type: Order type, LIMIT or MARKET. kwargs: lever_rate: Leverage rate, 10 or 20. Returns: order_no: Order ID if created successfully, otherwise it's None. error: Error information, otherwise it's None. """ if int(quantity) > 0: if action == ORDER_ACTION_BUY: direction = "buy" offset = "open" elif action == ORDER_ACTION_SELL: direction = "sell" offset = "close" else: return None, "action error" else: if action == ORDER_ACTION_BUY: direction = "buy" offset = "close" elif action == ORDER_ACTION_SELL: direction = "sell" offset = "open" else: return None, "action error" lever_rate = kwargs.get("lever_rate", 20) if order_type == ORDER_TYPE_LIMIT: order_price_type = "limit" elif order_type == ORDER_TYPE_MARKET: order_price_type = "opponent" else: return None, "order type error" quantity = abs(int(quantity)) result, error = await self._rest_api.create_order( self._symbol, self._contract_type, self._contract_code, price, quantity, direction, offset, lever_rate, order_price_type) if error: return None, error return str(result["data"]["order_id"]), None async def revoke_order(self, *order_nos): """ Revoke (an) order(s). Args: order_nos: Order id list, you can set this param to 0 or multiple items. If you set 0 param, you can cancel all orders for this symbol(initialized in Trade object). If you set 1 param, you can cancel an order. If you set multiple param, you can cancel multiple orders. Do not set param length more than 100. Returns: Success or error, see bellow. """ # If len(order_nos) == 0, you will cancel all orders for this symbol(initialized in Trade object). if len(order_nos) == 0: success, error = await self._rest_api.revoke_order_all( self._symbol, self._contract_code, self._contract_type) if error: return False, error if success.get("errors"): return False, success["errors"] return True, None # If len(order_nos) == 1, you will cancel an order. if len(order_nos) == 1: success, error = await self._rest_api.revoke_order( self._symbol, order_nos[0]) if error: return order_nos[0], error if success.get("errors"): return False, success["errors"] else: return order_nos[0], None # If len(order_nos) > 1, you will cancel multiple orders. if len(order_nos) > 1: success, error = await self._rest_api.revoke_orders( self._symbol, order_nos) if error: return order_nos[0], error if success.get("errors"): return False, success["errors"] return success, error async def get_open_order_nos(self): """ Get open order id list. Args: None. Returns: order_nos: Open order id list, otherwise it's None. error: Error information, otherwise it's None. """ success, error = await self._rest_api.get_open_orders(self._symbol) if error: return None, error else: order_nos = [] for order_info in success["data"]["orders"]: if order_info["contract_code"] != self._contract_code: continue order_nos.append(str(order_info["order_id"])) return order_nos, None def _update_order(self, order_info): """ Order update. Args: order_info: Order information. """ if order_info["contract_code"] != self._contract_code: return order_no = str(order_info["order_id"]) status = order_info["status"] order = self._orders.get(order_no) if not order: if order_info["direction"] == "buy": if order_info["offset"] == "open": trade_type = TRADE_TYPE_BUY_OPEN else: trade_type = TRADE_TYPE_BUY_CLOSE else: if order_info["offset"] == "close": trade_type = TRADE_TYPE_SELL_CLOSE else: trade_type = TRADE_TYPE_SELL_OPEN info = { "platform": self._platform, "account": self._account, "strategy": self._strategy, "order_no": order_no, "action": ORDER_ACTION_BUY if order_info["direction"] == "buy" else ORDER_ACTION_SELL, "symbol": self._contract_code, "price": order_info["price"], "quantity": order_info["volume"], "trade_type": trade_type } order = Order(**info) self._orders[order_no] = order if status in [1, 2, 3]: order.status = ORDER_STATUS_SUBMITTED elif status == 4: order.status = ORDER_STATUS_PARTIAL_FILLED order.remain = int(order.quantity) - int( order_info["trade_volume"]) elif status == 6: order.status = ORDER_STATUS_FILLED order.remain = 0 elif status in [5, 7]: order.status = ORDER_STATUS_CANCELED order.remain = int(order.quantity) - int( order_info["trade_volume"]) else: return order.avg_price = order_info["trade_avg_price"] order.ctime = order_info["created_at"] order.utime = order_info["ts"] SingleTask.run(self._order_update_callback, copy.copy(order)) # Delete order that already completed. if order.status in [ ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED ]: self._orders.pop(order_no) def _update_position(self, data): """ Position update. Args: position_info: Position information. Returns: None. """ for position_info in data["data"]: if position_info["contract_code"] != self._contract_code: return if position_info["direction"] == "buy": self._position.long_quantity = int(position_info["volume"]) self._position.long_avg_price = position_info["cost_hold"] else: self._position.short_quantity = int(position_info["volume"]) self._position.short_avg_price = position_info["cost_hold"] # self._position.liquid_price = None self._position.utime = data["ts"] SingleTask.run(self._position_update_callback, copy.copy(self._position)) async def on_event_asset_update(self, asset: Asset): """ Asset event data callback. Args: asset: Asset object callback from EventCenter. Returns: None. """ self._assets = asset SingleTask.run(self._asset_update_callback, asset)
class Huobi: """ Huobi Market Server. Attributes: kwargs: platform: Exchange platform name, must be `huobi`. wss: Exchange Websocket host address, default is "wss://api.huobi.pro". symbols: Trade pair list, e.g. ["ETH/BTC"]. channels: channel list, only `orderbook`, `kline` and `trade` to be enabled. orderbook_length: The length of orderbook's data to be published via OrderbookEvent, default is 10. price_precious: The price precious, default is 8. """ def __init__(self, **kwargs): self._platform = kwargs["platform"] self._wss = kwargs.get("wss", "wss://api.huobi.pro") self._symbols = list(set(kwargs.get("symbols"))) self._channels = kwargs.get("channels") self._orderbook_length = kwargs.get("orderbook_length", 10) self._price_precious = kwargs.get("price_precious", 8) self._c_to_s = {} # {"channel": "symbol"} url = self._wss + "/ws" self._ws = Websocket(url, connected_callback=self.connected_callback, process_binary_callback=self.process_binary) self._ws.initialize() async def connected_callback(self): """After create Websocket connection successfully, we will subscribing orderbook/trade/kline events.""" for ch in self._channels: if ch == "kline": for symbol in self._symbols: channel = self._symbol_to_channel(symbol, "kline") if not channel: continue kline = {"sub": channel} await self._ws.send(kline) elif ch == "orderbook": for symbol in self._symbols: channel = self._symbol_to_channel(symbol, "depth") if not channel: continue data = {"sub": channel} await self._ws.send(data) elif ch == "trade": for symbol in self._symbols: channel = self._symbol_to_channel(symbol, "trade") if not channel: continue data = {"sub": channel} await self._ws.send(data) else: logger.error("channel error! channel:", ch, caller=self) async def process_binary(self, msg): """ Process binary message that received from Websocket connection. Args: msg: Binary message received from Websocket connection. """ data = json.loads(gzip.decompress(msg).decode()) # logger.debug("data:", json.dumps(data), caller=self) channel = data.get("ch") if not channel: if data.get("ping"): hb_msg = {"pong": data.get("ping")} await self._ws.send(hb_msg) return symbol = self._c_to_s[channel] if channel.find("kline") != -1: d = data.get("tick") kline = { "platform": self._platform, "symbol": symbol, "open": "%.{}f".format(self._price_precious) % d["open"], "high": "%.{}f".format(self._price_precious) % d["high"], "low": "%.{}f".format(self._price_precious) % d["low"], "close": "%.{}f".format(self._price_precious) % d["close"], "volume": "%.{}f".format(self._price_precious) % d["amount"], "timestamp": int(data.get("ts")), "kline_type": MARKET_TYPE_KLINE } EventKline(**kline).publish() logger.debug("symbol:", symbol, "kline:", kline, caller=self) elif channel.find("depth") != -1: d = data.get("tick") asks, bids = [], [] for item in d.get("asks")[:self._orderbook_length]: price = "%.{}f".format(self._price_precious) % item[0] quantity = "%.{}f".format(self._price_precious) % item[1] asks.append([price, quantity]) for item in d.get("bids")[:self._orderbook_length]: price = "%.{}f".format(self._price_precious) % item[0] quantity = "%.{}f".format(self._price_precious) % item[1] bids.append([price, quantity]) orderbook = { "platform": self._platform, "symbol": symbol, "asks": asks, "bids": bids, "timestamp": d.get("ts") } EventOrderbook(**orderbook).publish() logger.debug("symbol:", symbol, "orderbook:", orderbook, caller=self) elif channel.find("trade") != -1: tick = data.get("tick") direction = tick["data"][0].get("direction") price = tick["data"][0].get("price") quantity = tick["data"][0].get("amount") trade = { "platform": self._platform, "symbol": symbol, "action": ORDER_ACTION_BUY if direction == "buy" else ORDER_ACTION_SELL, "price": "%.{}f".format(self._price_precious) % price, "quantity": "%.{}f".format(self._price_precious) % quantity, "timestamp": tick.get("ts") } EventTrade(**trade).publish() logger.debug("symbol:", symbol, "trade:", trade, caller=self) else: logger.error("event error! msg:", msg, caller=self) def _symbol_to_channel(self, symbol, channel_type): """ Convert symbol to channel. Args: symbol: Trade pair name. channel_type: channel name, kline / ticker / depth. """ if channel_type == "kline": channel = "market.{s}.kline.1min".format( s=symbol.replace("/", '').lower()) elif channel_type == "depth": channel = "market.{s}.depth.step0".format( s=symbol.replace("/", '').lower()) elif channel_type == "trade": channel = "market.{s}.trade.detail".format( s=symbol.replace("/", '').lower()) else: logger.error("channel type error! channel type:", channel_type, calle=self) return None self._c_to_s[channel] = symbol return channel
class BinanceFutureTrade: """ Binance Future Trade module. You can initialize trade object with some attributes in kwargs. Attributes: account: Account name for this trade exchange. strategy: What's name would you want to created for you strategy. symbol: Symbol name for your trade. host: HTTP request host. (default "https://fapi.binance.com") wss: Websocket address. (default "wss://fstream.binance.com") access_key: Account's ACCESS KEY. secret_key Account's SECRET KEY. asset_update_callback: You can use this param to specific a async callback function when you initializing Trade object. `asset_update_callback` is like `async def on_asset_update_callback(asset: Asset): pass` and this callback function will be executed asynchronous when received AssetEvent. order_update_callback: You can use this param to specific a async callback function when you initializing Trade object. `order_update_callback` is like `async def on_order_update_callback(order: Order): pass` and this callback function will be executed asynchronous when some order state updated. init_success_callback: You can use this param to specific a async callback function when you initializing Trade object. `init_success_callback` is like `async def on_init_success_callback(success: bool, error: Error, **kwargs): pass` and this callback function will be executed asynchronous after Trade module object initialized successfully. """ def __init__(self, **kwargs): """Initialize Trade module.""" e = None if not kwargs.get("account"): e = Error("param account miss") if not kwargs.get("strategy"): e = Error("param strategy miss") if not kwargs.get("symbol"): e = Error("param symbol miss") if not kwargs.get("host"): kwargs["host"] = "https://fapi.binance.com" if not kwargs.get("wss"): kwargs["wss"] = "wss://fstream.binance.com" if not kwargs.get("access_key"): e = Error("param access_key miss") if not kwargs.get("secret_key"): e = Error("param secret_key miss") if e: logger.error(e, caller=self) if kwargs.get("init_success_callback"): SingleTask.run(kwargs["init_success_callback"], False, e) return self._account = kwargs["account"] self._strategy = kwargs["strategy"] self._platform = BINANCE_FUTURE self._symbol = kwargs["symbol"] self._host = kwargs["host"] self._wss = kwargs["wss"] self._access_key = kwargs["access_key"] self._secret_key = kwargs["secret_key"] self._asset_update_callback = kwargs.get("asset_update_callback") self._order_update_callback = kwargs.get("order_update_callback") self._position_update_callback = kwargs.get("position_update_callback") self._init_success_callback = kwargs.get("init_success_callback") self._ok = False # Initialize successfully ? self._raw_symbol = self._symbol # Row symbol name, same as Binance Exchange. self._listen_key = None # Listen key for Websocket authentication. self._assets = { } # Asset data. e.g. {"BTC": {"free": "1.1", "locked": "2.2", "total": "3.3"}, ... } self._orders = {} # Order data. e.g. {order_no: order, ... } self._position = Position(self._platform, self._account, self._strategy, self._symbol) # 仓位 # Initialize our REST API client. self._rest_api = BinanceFutureRestAPI(self._host, self._access_key, self._secret_key) # Subscribe our AssetEvent. if self._asset_update_callback: AssetSubscribe(self._platform, self._account, self.on_event_asset_update) # Create a loop run task to reset listen key every 20 minutes. LoopRunTask.register(self._reset_listen_key, 60 * 20) # Create a loop run task to check position information per 1 second. LoopRunTask.register(self._check_position_update, 1) # Create a loop run task to send ping message to server per 30 seconds. # LoopRunTask.register(self._send_heartbeat_msg, 10) # Create a coroutine to initialize Websocket connection. SingleTask.run(self._init_websocket) @property def assets(self): return copy.copy(self._assets) @property def orders(self): return copy.copy(self._orders) @property def rest_api(self): return self._rest_api async def _init_websocket(self): """ Initialize Websocket connection. """ # Get listen key first. success, error = await self._rest_api.get_listen_key() if error: e = Error("get listen key failed: {}".format(error)) logger.error(e, caller=self) SingleTask.run(self._init_success_callback, False, e) return self._listen_key = success["listenKey"] uri = "/ws/" + self._listen_key url = urljoin(self._wss, uri) self._ws = Websocket(url, self.connected_callback, process_callback=self.process) self._ws.initialize() async def _reset_listen_key(self, *args, **kwargs): """ Reset listen key. """ if not self._listen_key: logger.error("listen key not initialized!", caller=self) return await self._rest_api.put_listen_key(self._listen_key) logger.info("reset listen key success!", caller=self) # async def _send_heartbeat_msg(self, *args, **kwargs): # """Send ping to server.""" # hb = {"ping": tools.get_cur_timestamp_ms()} # await self._ws.send(hb) async def connected_callback(self): """ After websocket connection created successfully, pull back all open order information. """ logger.info("Websocket connection authorized successfully.", caller=self) order_infos, error = await self._rest_api.get_open_orders( self._raw_symbol) if error: e = Error("get open orders error: {}".format(error)) SingleTask.run(self._init_success_callback, False, e) return for order_info in order_infos: order_no = "{}_{}".format(order_info["orderId"], order_info["clientOrderId"]) if order_info["status"] == "NEW": status = ORDER_STATUS_SUBMITTED elif order_info["status"] == "PARTIAL_FILLED": status = ORDER_STATUS_PARTIAL_FILLED elif order_info["status"] == "FILLED": status = ORDER_STATUS_FILLED elif order_info["status"] == "CANCELED": status = ORDER_STATUS_CANCELED elif order_info["status"] == "REJECTED": status = ORDER_STATUS_FAILED elif order_info["status"] == "EXPIRED": status = ORDER_STATUS_FAILED else: logger.warn("unknown status:", order_info, caller=self) continue info = { "platform": self._platform, "account": self._account, "strategy": self._strategy, "order_no": order_no, "action": order_info["side"], "order_type": order_info["type"], "symbol": self._symbol, "price": order_info["price"], "quantity": order_info["origQty"], "remain": float(order_info["origQty"]) - float(order_info["executedQty"]), "status": status, "trade_type": int(order_info["clientOrderId"][-1]), "ctime": order_info["updateTime"], "utime": order_info["updateTime"] } order = Order(**info) self._orders[order_no] = order SingleTask.run(self._order_update_callback, copy.copy(order)) self._ok = True SingleTask.run(self._init_success_callback, True, None) async def create_order(self, action, price, quantity, *args, **kwargs): """ Create an order. Args: action: Trade direction, BUY or SELL. price: Price of each contract. quantity: The buying or selling quantity. Returns: order_no: Order ID if created successfully, otherwise it's None. error: Error information, otherwise it's None. """ if float(quantity) > 0: if action == ORDER_ACTION_BUY: trade_type = TRADE_TYPE_BUY_OPEN else: trade_type = TRADE_TYPE_SELL_CLOSE else: if action == ORDER_ACTION_BUY: trade_type = TRADE_TYPE_BUY_CLOSE else: trade_type = TRADE_TYPE_SELL_OPEN quantity = abs(float(quantity)) price = tools.float_to_str(price) quantity = tools.float_to_str(quantity) client_order_id = tools.get_uuid1().replace("-", "")[:21] + str(trade_type) result, error = await self._rest_api.create_order( action, self._raw_symbol, price, quantity, client_order_id) if error: return None, error order_no = "{}_{}".format(result["orderId"], result["clientOrderId"]) return order_no, None async def revoke_order(self, *order_nos): """ Revoke (an) order(s). Args: order_nos: Order id list, you can set this param to 0 or multiple items. If you set 0 param, you can cancel all orders for this symbol(initialized in Trade object). If you set 1 param, you can cancel an order. If you set multiple param, you can cancel multiple orders. Do not set param length more than 100. Returns: Success or error, see bellow. """ # If len(order_nos) == 0, you will cancel all orders for this symbol(initialized in Trade object). if len(order_nos) == 0: order_infos, error = await self._rest_api.get_open_orders( self._raw_symbol) if error: return False, error for order_info in order_infos: _, error = await self._rest_api.revoke_order( self._raw_symbol, order_info["orderId"], order_info["clientOrderId"]) if error: return False, error return True, None # If len(order_nos) == 1, you will cancel an order. if len(order_nos) == 1: order_id, client_order_id = order_nos[0].split("_") success, error = await self._rest_api.revoke_order( self._raw_symbol, order_id, client_order_id) if error: return order_nos[0], error else: return order_nos[0], None # If len(order_nos) > 1, you will cancel multiple orders. if len(order_nos) > 1: success, error = [], [] for order_no in order_nos: order_id, client_order_id = order_no.split("_") _, e = await self._rest_api.revoke_order( self._raw_symbol, order_id, client_order_id) if e: error.append((order_no, e)) else: success.append(order_no) return success, error async def get_open_order_nos(self): """ Get open order no list. """ success, error = await self._rest_api.get_open_orders(self._raw_symbol) if error: return None, error order_nos = [] for order_info in success: order_no = "{}_{}".format(order_info["orderId"], order_info["clientOrderId"]) order_nos.append(order_no) return order_nos, None @async_method_locker("BinanceTrade.process.locker") async def process(self, msg): """ Process message that received from Websocket connection. Args: msg: message received from Websocket connection. """ logger.debug("msg:", json.dumps(msg), caller=self) e = msg.get("e") if e == "ORDER_TRADE_UPDATE": # Order update. self._update_order(msg["o"]) async def _check_position_update(self, *args, **kwargs): """Check position update.""" if not self._ok: return update = False success, error = await self._rest_api.get_position() if error: return position_info = None for item in success: if item["symbol"] == self._raw_symbol: position_info = item break if not self._position.utime: # Callback position info when initialized. update = True self._position.update() size = float(position_info["positionAmt"]) average_price = float(position_info["entryPrice"]) if size > 0: if self._position.long_quantity != size: update = True self._position.update(0, 0, size, average_price, 0) elif size < 0: if self._position.short_quantity != abs(size): update = True self._position.update(abs(size), average_price, 0, 0, 0) elif size == 0: if self._position.long_quantity != 0 or self._position.short_quantity != 0: update = True self._position.update() if update: await self._position_update_callback(copy.copy(self._position)) def _update_order(self, order_info): """ Order update. Args: order_info: Order information. Returns: Return order object if or None. """ if order_info["s"] != self._raw_symbol: return order_no = "{}_{}".format(order_info["i"], order_info["c"]) if order_info["X"] == "NEW": status = ORDER_STATUS_SUBMITTED elif order_info["X"] == "PARTIAL_FILLED": status = ORDER_STATUS_PARTIAL_FILLED elif order_info["X"] == "FILLED": status = ORDER_STATUS_FILLED elif order_info["X"] == "CANCELED": status = ORDER_STATUS_CANCELED elif order_info["X"] == "REJECTED": status = ORDER_STATUS_FAILED elif order_info["X"] == "EXPIRED": status = ORDER_STATUS_FAILED else: return order = self._orders.get(order_no) if not order: info = { "platform": self._platform, "account": self._account, "strategy": self._strategy, "order_no": order_no, "action": order_info["S"], "order_type": order_info["o"], "symbol": self._symbol, "price": order_info["p"], "quantity": order_info["q"], "ctime": order_info["T"] } order = Order(**info) self._orders[order_no] = order order.remain = float(order_info["q"]) - float(order_info["z"]) order.avg_price = order_info["L"] order.status = status order.utime = order_info["T"] order.trade_type = int(order_no[-1]) SingleTask.run(self._order_update_callback, copy.copy(order)) async def on_event_asset_update(self, asset: Asset): """ Asset data update callback. Args: asset: Asset object. """ self._assets = asset SingleTask.run(self._asset_update_callback, asset)
class CoinbaseMarket: """ Coinbase Market Server. Attributes: kwargs: platform: Exchange platform name, must be `coinbase`. wss: Exchange Websocket host address, default is "wss://ws-feed.pro.coinbase.com". symbols: Trade pair list, e.g. ["ETH/USD"]. channels: channel list, only `orderbook` and `trade` to be enabled. orderbook_length: The length of orderbook's data to be published via OrderbookEvent, default is 10. """ def __init__(self, **kwargs): self._platform = kwargs["platform"] self._wss = kwargs.get("wss", "wss://ws-feed.pro.coinbase.com") self._symbols = list(set(kwargs.get("symbols"))) self._channels = kwargs.get("channels") self._orderbook_length = kwargs.get("orderbook_length", 10) self._orderbooks = { } # orderbook datas {"symbol": {"bids": {"price": quantity, ...}, "asks": {...}}} self._symbols_map = { } # config symbol name to raw symbol name, e.g. {"BTC-USD": "BTC/USD"} url = self._wss self._ws = Websocket(url, connected_callback=self.connected_callback, process_callback=self.process) self._ws.initialize() async def connected_callback(self): """ After create Websocket connection successfully, we will subscribing orderbook/trade events. """ symbols = [] for s in self._symbols: t = s.replace("/", "-") symbols.append(t) self._symbols_map[t] = s if not symbols: logger.warn("symbols not found in config file.", caller=self) return if not self._channels: logger.warn("channels not found in config file.", caller=self) return channels = [] for ch in self._channels: if ch == "orderbook": sub = {"name": "level2", "product_ids": symbols} channels.append(sub) elif ch == "trade": sub = {"name": "ticker", "product_ids": symbols} channels.append(sub) else: logger.error("channel error! channel:", ch, caller=self) if channels: msg = {"type": "subscribe", "channels": channels} await self._ws.send(msg) logger.info("subscribe orderbook/trade success.", caller=self) async def process(self, msg): """ Process message that received from Websocket connection. Args: msg: Message data that received from Websocket connection. """ logger.debug("msg:", msg, caller=self) t = msg.get("type") if t == "snapshot": await self.process_orderbook_snapshot(msg) elif t == "l2update": await self.process_orderbook_update(msg) elif t == "ticker": await self.process_trade(msg) @async_method_locker("process_orderbook_update") async def process_orderbook_snapshot(self, msg): """ Deal with orderbook snapshot message. Args: msg: Orderbook snapshot message. """ symbol = msg["product_id"].replace("-", "/") if symbol not in self._symbols: return asks = msg.get("asks") bids = msg.get("bids") self._orderbooks[symbol] = {"asks": {}, "bids": {}, "timestamp": 0} for ask in asks[:200]: price = float(ask[0]) quantity = float(ask[1]) self._orderbooks[symbol]["asks"][price] = quantity for bid in bids[:200]: price = float(bid[0]) quantity = float(bid[1]) self._orderbooks[symbol]["bids"][price] = quantity @async_method_locker("process_orderbook_update") async def process_orderbook_update(self, msg): """ Deal with orderbook message that updated. Args: msg: Orderbook update message. """ symbol = msg["product_id"].replace("-", "/") if symbol not in self._symbols: return self._orderbooks[symbol]["timestamp"] = tools.utctime_str_to_mts( msg["time"][:23] + "Z") for item in msg["changes"]: side = item[0] price = float(item[1]) quantity = float(item[2]) if side == "sell": if quantity == 0: if price in self._orderbooks[symbol]["asks"]: self._orderbooks[symbol]["asks"].pop(price) else: self._orderbooks[symbol]["asks"][price] = quantity elif side == "buy": if quantity == 0: if price in self._orderbooks[symbol]["bids"]: self._orderbooks[symbol]["bids"].pop(price) else: self._orderbooks[symbol]["bids"][price] = quantity await self.publish_orderbook_event(symbol) async def publish_orderbook_event(self, symbol): """Publish OrderbookEvent.""" ob = copy.copy(self._orderbooks[symbol]) if not ob["asks"] or not ob["bids"]: logger.warn("symbol:", symbol, "asks:", ob["asks"], "bids:", ob["bids"], caller=self) return ask_keys = sorted(list(ob["asks"].keys())) bid_keys = sorted(list(ob["bids"].keys()), reverse=True) if ask_keys[0] <= bid_keys[0]: logger.warn("symbol:", symbol, "ask1:", ask_keys[0], "bid1:", bid_keys[0], caller=self) return # asks asks = [] for k in ask_keys[:self._orderbook_length]: price = "%.8f" % k quantity = "%.8f" % ob["asks"].get(k) asks.append([price, quantity]) # bids bids = [] for k in bid_keys[:self._orderbook_length]: price = "%.8f" % k quantity = "%.8f" % ob["bids"].get(k) bids.append([price, quantity]) # Publish OrderbookEvent. orderbook = { "platform": self._platform, "symbol": symbol, "asks": asks, "bids": bids, "timestamp": tools.get_cur_timestamp_ms() } EventOrderbook(**orderbook).publish() logger.info("symbol:", symbol, "orderbook:", orderbook, caller=self) async def process_trade(self, msg): """Process trade data and publish TradeEvent.""" symbol = msg["product_id"].replace("-", "/") if symbol not in self._symbols: return if not msg.get( "trade_id" ): # The first ticker message received from Websocket maybe no this field, dropped. return action = ORDER_ACTION_BUY if msg.get( "side") == "buy" else ORDER_ACTION_SELL price = "%.8f" % float(msg["price"]) quantity = "%.8f" % float(msg["last_size"]) timestamp = tools.utctime_str_to_mts(msg["time"][:23] + "Z") trade = { "platform": self._platform, "symbol": symbol, "action": action, "price": price, "quantity": quantity, "timestamp": timestamp } EventTrade(**trade).publish() logger.info("symbol:", symbol, "trade:", trade, caller=self)
class GeminiMarket: """ Gemini Market Server. Attributes: kwargs: platform: Exchange platform name, must be `gemini`. wss: Exchange Websocket host address, default is "wss://api.gemini.com". symbols: Trade pair list, e.g. ["ETH/BTC"]. channels: channel list, only `orderbook` to be enabled. orderbook_length: The length of orderbook's data to be published via OrderbookEvent, default is 10. """ def __init__(self, **kwargs): self._platform = kwargs["platform"] self._wss = kwargs.get("wss", "wss://api.gemini.com") self._symbols = list(set(kwargs.get("symbols"))) self._channels = kwargs.get("channels") self._orderbook_length = kwargs.get("orderbook_length", 10) self._orderbooks = { } # orderbook datas {"symbol": {"bids": {"price": quantity, ...}, "asks": {...}}} self._symbols_map = { } # config symbol name to raw symbol name, e.g. {"BTCUSD": "BTC/USD"} url = self._wss + "/v2/marketdata" self._ws = Websocket(url, connected_callback=self.connected_callback, process_callback=self.process) self._ws.initialize() async def connected_callback(self): """After create Websocket connection successfully, we will subscribing orderbook events.""" symbols = [] for s in self._symbols: t = s.replace("/", "") symbols.append(t) self._symbols_map[t] = s if not symbols: logger.warn("symbols not found in config file.", caller=self) return if not self._channels: logger.warn("channels not found in config file.", caller=self) return subscriptions = [] for ch in self._channels: if ch == "orderbook": sub = {"name": "l2", "symbols": symbols} subscriptions.append(sub) else: logger.error("channel error! channel:", ch, caller=self) if subscriptions: msg = {"type": "subscribe", "subscriptions": subscriptions} await self._ws.send(msg) logger.info("subscribe orderbook success.", caller=self) async def process(self, msg): """ Process message that received from Websocket connection. Args: msg: Message data that received from Websocket connection. """ logger.debug("msg:", msg, caller=self) t = msg.get("type") if t == "l2_updates": symbol = msg["symbol"] datas = msg["changes"] await self.process_orderbook_update(symbol, datas) @async_method_locker("process_orderbook_update") async def process_orderbook_update(self, symbol_raw, datas): """ Deal with orderbook message that updated. Args: symbol_raw: Trade pair raw name. datas: Newest orderbook data. """ if symbol_raw not in self._symbols_map: return symbol = self._symbols_map[symbol_raw] if symbol not in self._orderbooks: self._orderbooks[symbol] = {"asks": {}, "bids": {}} for item in datas: side = item[0] price = float(item[1]) quantity = float(item[2]) if side == "sell": if quantity == 0: if price in self._orderbooks[symbol]["asks"]: self._orderbooks[symbol]["asks"].pop(price) else: self._orderbooks[symbol]["asks"][price] = quantity elif side == "buy": if quantity == 0: if price in self._orderbooks[symbol]["bids"]: self._orderbooks[symbol]["bids"].pop(price) else: self._orderbooks[symbol]["bids"][price] = quantity await self.publish_orderbook_event(symbol) async def publish_orderbook_event(self, symbol): """Publish OrderbookEvent.""" ob = copy.copy(self._orderbooks[symbol]) if not ob["asks"] or not ob["bids"]: logger.warn("symbol:", symbol, "asks:", ob["asks"], "bids:", ob["bids"], caller=self) return ask_keys = sorted(list(ob["asks"].keys())) bid_keys = sorted(list(ob["bids"].keys()), reverse=True) if ask_keys[0] <= bid_keys[0]: logger.warn("symbol:", symbol, "ask1:", ask_keys[0], "bid1:", bid_keys[0], caller=self) return # asks asks = [] for k in ask_keys[:self._orderbook_length]: price = "%.8f" % k quantity = "%.8f" % ob["asks"].get(k) asks.append([price, quantity]) # bids bids = [] for k in bid_keys[:self._orderbook_length]: price = "%.8f" % k quantity = "%.8f" % ob["bids"].get(k) bids.append([price, quantity]) # Publish OrderbookEvent. orderbook = { "platform": self._platform, "symbol": symbol, "asks": asks, "bids": bids, "timestamp": tools.get_cur_timestamp_ms() } EventOrderbook(**orderbook).publish() logger.info("symbol:", symbol, "orderbook:", orderbook, caller=self)
class Binance: """ Binance Market Server. Attributes: kwargs: platform: Exchange platform name, must be `binance`. wss: Exchange Websocket host address, default is `wss://stream.binance.com:9443`. symbols: Symbol list. channels: Channel list, only `orderbook` / `trade` / `kline` to be enabled. orderbook_length: The length of orderbook's data to be published via OrderbookEvent, default is 10. """ def __init__(self, **kwargs): self._platform = kwargs["platform"] self._wss = kwargs.get("wss", "wss://stream.binance.com:9443") self._symbols = list(set(kwargs.get("symbols"))) self._channels = kwargs.get("channels") self._orderbook_length = kwargs.get("orderbook_length", 10) self._c_to_s = {} self._tickers = {} url = self._make_url() self._ws = Websocket(url, process_callback=self.process) self._ws.initialize() def _make_url(self): """Generate request url. """ cc = [] for ch in self._channels: if ch == "kline": for symbol in self._symbols: c = self._symbol_to_channel(symbol, "kline_1m") cc.append(c) elif ch == "orderbook": for symbol in self._symbols: c = self._symbol_to_channel(symbol, "depth20") cc.append(c) elif ch == "trade": for symbol in self._symbols: c = self._symbol_to_channel(symbol, "trade") cc.append(c) else: logger.error("channel error! channel:", ch, caller=self) url = self._wss + "/stream?streams=" + "/".join(cc) return url async def process(self, msg): """Process message that received from Websocket connection. Args: msg: Message received from Websocket connection. """ # logger.debug("msg:", msg, caller=self) if not isinstance(msg, dict): return channel = msg.get("stream") if channel not in self._c_to_s: logger.warn("unkown channel, msg:", msg, caller=self) return symbol = self._c_to_s[channel] data = msg.get("data") e = data.get("e") if e == "kline": await self.process_kline(symbol, data) elif channel.endswith("depth20"): await self.process_orderbook(symbol, data) elif e == "trade": await self.process_trade(symbol, data) async def process_kline(self, symbol, data): """Process kline data and publish KlineEvent.""" kline = { "platform": self._platform, "symbol": symbol, "open": data.get("k").get("o"), "high": data.get("k").get("h"), "low": data.get("k").get("l"), "close": data.get("k").get("c"), "volume": data.get("k").get("q"), "timestamp": data.get("k").get("t"), "kline_type": const.MARKET_TYPE_KLINE } EventKline(**kline).publish() logger.info("symbol:", symbol, "kline:", kline, caller=self) async def process_orderbook(self, symbol, data): """Process orderbook data and publish OrderbookEvent.""" bids = [] asks = [] for bid in data.get("bids")[:self._orderbook_length]: bids.append(bid[:2]) for ask in data.get("asks")[:self._orderbook_length]: asks.append(ask[:2]) orderbook = { "platform": self._platform, "symbol": symbol, "asks": asks, "bids": bids, "timestamp": tools.get_cur_timestamp_ms() } EventOrderbook(**orderbook).publish() logger.info("symbol:", symbol, "orderbook:", orderbook, caller=self) async def process_trade(self, symbol, data): """Process trade data and publish TradeEvent.""" trade = { "platform": self._platform, "symbol": symbol, "action": ORDER_ACTION_SELL if data["m"] else ORDER_ACTION_BUY, "price": data.get("p"), "quantity": data.get("q"), "timestamp": data.get("T") } EventTrade(**trade).publish() logger.info("symbol:", symbol, "trade:", trade, caller=self) def _symbol_to_channel(self, symbol, channel_type="ticker"): channel = "{x}@{y}".format(x=symbol.replace("/", "").lower(), y=channel_type) self._c_to_s[channel] = symbol return channel
class KrakenMarket: """ Kraken Market Server Attributes: kwargs: platform: Exchange platform name, must be `kraken`. host: HTTP host, default is `https://api.kraken.com`. wss: Wss host, default is `wss://ws.kraken.com`. symbols: Symbol name list, e.g. XTB/USD. (Trade pair name list) channels: What are channels to be subscribed, only support `orderbook` and `trade`. orderbook_interval: The interval time to fetch a orderbook information, default is 2 seconds. orderbook_length: The length of orderbook's data to be published via OrderbookEvent, default is 10. """ def __init__(self, **kwargs): """Initialize.""" self._platform = kwargs["platform"] self._host = kwargs.get("host", "https://api.kraken.com") self._wss = kwargs.get("wss", "wss://ws.kraken.com") self._symbols = list(set(kwargs.get("symbols"))) self._channels = kwargs.get("channels") self._orderbook_interval = kwargs.get("orderbook_interval", 2) self._orderbook_length = kwargs.get("orderbook_length", 10) self.heartbeat_msg = {} self._orderbooks = { } # orderbook data, e.g. {"symbol": {"bids": {"price": quantity, ...}, "asks": {...}, "timestamp": 0}, ... } self._rest_api = KrakenRestAPI(self._host, None, None) url = self._wss self._ws = Websocket(url, connected_callback=self.connected_callback, process_callback=self.process) self._ws.initialize() # Register a loop run task to reset heartbeat message request id. LoopRunTask.register(self.send_heartbeat_msg, 5) async def connected_callback(self): """After create Websocket connection successfully, we will subscribing orderbook/trade/kline.""" if not self._symbols: logger.warn("symbols not found in config file.", caller=self) return if not self._channels: logger.warn("channels not found in config file.", caller=self) return for ch in self._channels: if ch == "orderbook": # subscription = {"name": "book"} LoopRunTask.register(self.on_event_update_orderbook, self._orderbook_interval) continue elif ch == "trade": subscription = {"name": "trade"} # elif ch == "kline": # TODO: something wrong from exchange server? subscribe ohlc-1 but receive ohlc-5 ? # subscription = {"name": "ohlc", "interval": 1} else: logger.error("channel error:", ch, caller=self) continue d = { "event": "subscribe", "pair": self._symbols, "subscription": subscription } await self._ws.send(d) logger.info("subscribe", ch, "success.", caller=self) async def send_heartbeat_msg(self, *args, **kwargs): data = {"event": "ping", "reqid": tools.get_cur_timestamp()} if not self._ws: logger.error("Websocket connection not yeah!", caller=self) return await self._ws.send(data) async def process(self, msg): """ Process message that received from Websocket connection. """ logger.debug("msg:", msg, caller=self) if not isinstance(msg, list): return if msg[-2] == "book-10": await self.deal_orderbook_update(msg) elif msg[-2] == "trade": await self.deal_trade_update(msg) @async_method_locker("deal_orderbook_update") async def deal_orderbook_update(self, msg): """ Deal with orderbook message that updated. """ data = msg[1] symbol = msg[-1] if symbol not in self._symbols: return if symbol not in self._orderbooks: self._orderbooks[symbol] = {"asks": {}, "bids": {}, "timestamp": 0} a = data["as"] b = data["bs"] else: a = data.get("a", []) b = data.get("b", []) for item in a: price = float(item[0]) quantity = float(item[1]) if quantity == 0: if price in self._orderbooks[symbol]["asks"]: self._orderbooks[symbol]["asks"].pop(price) else: self._orderbooks[symbol]["asks"][price] = quantity self._orderbooks[symbol]["timestamp"] = int( float(item[2]) * 1000) for item in b: price = float(item[0]) quantity = float(item[1]) if quantity == 0: if price in self._orderbooks[symbol]["bids"]: self._orderbooks[symbol]["bids"].pop(price) else: self._orderbooks[symbol]["bids"][price] = quantity self._orderbooks[symbol]["timestamp"] = int( float(item[2]) * 1000) await self.publish_orderbook() async def publish_orderbook(self, *args, **kwargs): """ Publish OrderbookEvent. """ for symbol, data in self._orderbooks.items(): ob = copy.copy(data) if not ob["asks"] or not ob["bids"]: logger.warn("symbol:", symbol, "asks:", ob["asks"], "bids:", ob["bids"], caller=self) continue ask_keys = sorted(list(ob["asks"].keys())) bid_keys = sorted(list(ob["bids"].keys()), reverse=True) if ask_keys[0] <= bid_keys[0]: logger.warn("symbol:", symbol, "ask1:", ask_keys[0], "bid1:", bid_keys[0], caller=self) continue # asks asks = [] for k in ask_keys[:self._orderbook_length]: price = "%.8f" % k quantity = "%.8f" % ob["asks"].get(k) asks.append([price, quantity]) # bids bids = [] for k in bid_keys[:self._orderbook_length]: price = "%.8f" % k quantity = "%.8f" % ob["bids"].get(k) bids.append([price, quantity]) # Publish OrderbookEvent. orderbook = { "platform": self._platform, "symbol": symbol, "asks": asks, "bids": bids, "timestamp": ob["timestamp"] } EventOrderbook(**orderbook).publish() logger.info("symbol:", symbol, "orderbook:", orderbook, caller=self) async def deal_trade_update(self, msg): """ Deal with trade message that updated. """ data = msg[1] symbol = msg[-1] if symbol not in self._symbols: return for item in data: price = "%.8f" % float(item[0]) quantity = "%.8f" % float(item[1]) timestamp = int(float(item[2]) * 1000) action = ORDER_ACTION_BUY if item[3] == "b" else ORDER_ACTION_SELL # Publish TradeEvent. trade = { "platform": self._platform, "symbol": symbol, "action": action, "price": price, "quantity": quantity, "timestamp": timestamp } EventTrade(**trade).publish() logger.info("symbol:", symbol, "trade:", trade, caller=self) async def on_event_update_orderbook(self, *args, **kwargs): """ Loop run to fetch orderbook information. """ for symbol in self._symbols: SingleTask.run(self.get_newest_orderbook, symbol) async def get_newest_orderbook(self, symbol): """ Get the newest orderbook information. """ result, error = await self._rest_api.get_orderbook( symbol.replace("/", ""), self._orderbook_length) key = list(result.keys())[0] asks, bids = [], [] for item in result.get(key)["asks"]: asks.append(item[:2]) for item in result.get(key)["bids"]: bids.append(item[:2]) orderbook = { "platform": self._platform, "symbol": symbol, "asks": asks, "bids": bids, "timestamp": tools.get_cur_timestamp_ms() } EventOrderbook(**orderbook).publish() logger.info("symbol:", symbol, "orderbook:", orderbook, caller=self)
class OKExFuture: """ OKEx Future Market Server. Attributes: kwargs: platform: Exchange platform name, must be `okex_future`. wss: Exchange Websocket host address, default is "wss://real.okex.com:10442". symbols: symbol list, OKEx Future instrument_id list. channels: channel list, only `orderbook` , `kline` and `trade` to be enabled. orderbook_length: The length of orderbook's data to be published via OrderbookEvent, default is 10. """ def __init__(self, **kwargs): self._platform = kwargs["platform"] self._wss = kwargs.get("wss", "wss://real.okex.com:10442") self._host = kwargs.get("host", "https://www.okex.com") self._symbols = list(set(kwargs.get("symbols"))) self._channels = kwargs.get("channels") self._orderbook_length = kwargs.get("orderbook_length", 10) self._access_key = kwargs.get("access_key") self._secret_key = kwargs.get("secret_key") self._passphrase = kwargs.get("passphrase") self._orderbooks = { } # orderbook data, e.g. {"symbol": {"bids": {"price": quantity, ...}, "asks": {...}}} url = self._wss + "/ws/v3" self._ws = Websocket(url, connected_callback=self.connected_callback, process_binary_callback=self.process_binary) self._ws.initialize() LoopRunTask.register(self.send_heartbeat_msg, 5) self._rest_api = OKExFutureRestAPI(self._host, self._access_key, self._secret_key, self._passphrase) self._initialize() async def connected_callback(self): """After create connection to Websocket server successfully, we will subscribe orderbook/kline/trade event.""" ches = [] for ch in self._channels: if ch == "orderbook": for symbol in self._symbols: ch = "futures/depth:{s}".format(s=symbol) ches.append(ch) elif ch == "trade": for symbol in self._symbols: ch = "futures/trade:{s}".format(s=symbol.replace("/", '-')) ches.append(ch) elif ch == "kline": for symbol in self._symbols: ch = "futures/candle60s:{s}".format( s=symbol.replace("/", '-')) ches.append(ch) # else: # logger.error("channel error! channel:", ch, caller=self) if len(ches) > 0: msg = {"op": "subscribe", "args": ches} await self._ws.send(msg) logger.info("subscribe orderbook/kline/trade success.", caller=self) async def send_heartbeat_msg(self, *args, **kwargs): data = "ping" if not self._ws: logger.error("Websocket connection not yeah!", caller=self) return await self._ws.send(data) async def process_binary(self, raw): """ Process message that received from Websocket connection. Args: raw: Raw binary message received from Websocket connection. """ decompress = zlib.decompressobj(-zlib.MAX_WBITS) msg = decompress.decompress(raw) msg += decompress.flush() msg = msg.decode() if msg == "pong": # Heartbeat message. return msg = json.loads(msg) # logger.debug("msg:", msg, caller=self) table = msg.get("table") if table == "futures/depth": if msg.get("action") == "partial": for d in msg["data"]: await self.process_orderbook_partial(d) elif msg.get("action") == "update": for d in msg["data"]: await self.process_orderbook_update(d) elif table == "futures/trade": for d in msg["data"]: await self.process_trade(d) elif table == "futures/candle60s": for d in msg["data"]: await self.process_kline(d) async def process_orderbook_partial(self, data): """Deal with orderbook partial message.""" symbol = data.get("instrument_id") if symbol not in self._symbols: return asks = data.get("asks") bids = data.get("bids") self._orderbooks[symbol] = {"asks": {}, "bids": {}, "timestamp": 0} for ask in asks: price = float(ask[0]) quantity = int(ask[1]) self._orderbooks[symbol]["asks"][price] = quantity for bid in bids: price = float(bid[0]) quantity = int(bid[1]) self._orderbooks[symbol]["bids"][price] = quantity timestamp = tools.utctime_str_to_mts(data.get("timestamp")) self._orderbooks[symbol]["timestamp"] = timestamp async def process_orderbook_update(self, data): """Deal with orderbook update message.""" symbol = data.get("instrument_id") asks = data.get("asks") bids = data.get("bids") timestamp = tools.utctime_str_to_mts(data.get("timestamp")) if symbol not in self._orderbooks: return self._orderbooks[symbol]["timestamp"] = timestamp for ask in asks: price = float(ask[0]) quantity = int(ask[1]) if quantity == 0 and price in self._orderbooks[symbol]["asks"]: self._orderbooks[symbol]["asks"].pop(price) else: self._orderbooks[symbol]["asks"][price] = quantity for bid in bids: price = float(bid[0]) quantity = int(bid[1]) if quantity == 0 and price in self._orderbooks[symbol]["bids"]: self._orderbooks[symbol]["bids"].pop(price) else: self._orderbooks[symbol]["bids"][price] = quantity await self.publish_orderbook(symbol) async def publish_orderbook(self, symbol): """Publish orderbook message to EventCenter via OrderbookEvent.""" ob = copy.copy(self._orderbooks[symbol]) if not ob["asks"] or not ob["bids"]: logger.warn("symbol:", symbol, "asks:", ob["asks"], "bids:", ob["bids"], caller=self) return ask_keys = sorted(list(ob["asks"].keys())) bid_keys = sorted(list(ob["bids"].keys()), reverse=True) if ask_keys[0] <= bid_keys[0]: logger.warn("symbol:", symbol, "ask1:", ask_keys[0], "bid1:", bid_keys[0], caller=self) return asks = [] for k in ask_keys[:self._orderbook_length]: price = "%.8f" % k quantity = str(ob["asks"].get(k)) asks.append([price, quantity]) bids = [] for k in bid_keys[:self._orderbook_length]: price = "%.8f" % k quantity = str(ob["bids"].get(k)) bids.append([price, quantity]) orderbook = { "platform": self._platform, "symbol": symbol, "asks": asks, "bids": bids, "timestamp": ob["timestamp"] } EventOrderbook(**orderbook).publish() logger.info("symbol:", symbol, "orderbook:", orderbook, caller=self) async def process_trade(self, data): """Deal with trade data, and publish trade message to EventCenter via TradeEvent.""" symbol = data["instrument_id"] if symbol not in self._symbols: return action = ORDER_ACTION_BUY if data[ "side"] == "buy" else ORDER_ACTION_SELL price = "%.8f" % float(data["price"]) quantity = "%.8f" % float(data["qty"]) timestamp = tools.utctime_str_to_mts(data["timestamp"]) # Publish EventTrade. trade = { "platform": self._platform, "symbol": symbol, "action": action, "price": price, "quantity": quantity, "timestamp": timestamp } EventTrade(**trade).publish() logger.info("symbol:", symbol, "trade:", trade, caller=self) async def process_kline(self, data): """ Deal with 1min kline data, and publish kline message to EventCenter via KlineEvent. Args: data: Newest kline data. """ symbol = data["instrument_id"] if symbol not in self._symbols: return timestamp = tools.utctime_str_to_mts(data["candle"][0]) _open = "%.8f" % float(data["candle"][1]) high = "%.8f" % float(data["candle"][2]) low = "%.8f" % float(data["candle"][3]) close = "%.8f" % float(data["candle"][4]) volume = "%.8f" % float(data["candle"][5]) # Publish EventKline kline = { "platform": self._platform, "symbol": symbol, "open": _open, "high": high, "low": low, "close": close, "volume": volume, "timestamp": timestamp, "kline_type": const.MARKET_TYPE_KLINE } EventKline(**kline).publish() logger.info("symbol:", symbol, "kline:", kline, caller=self) def _initialize(self): """ Initialize.""" for channel in self._channels: if channel == const.MARKET_TYPE_KLINE_5M: heartbeat.register(self.create_kline_tasks, 1, const.MARKET_TYPE_KLINE_5M) elif channel == const.MARKET_TYPE_KLINE_15M: heartbeat.register(self.create_kline_tasks, 1, const.MARKET_TYPE_KLINE_15M) elif channel == const.MARKET_TYPE_KLINE_1H: heartbeat.register(self.create_kline_tasks, 1, const.MARKET_TYPE_KLINE_1H) # else: # logger.error("channel error! channel:", channel, caller=self) async def create_kline_tasks(self, kline_type, *args, **kwargs): """ Create some tasks to fetch kline information Args: kline_type: Type of line, kline or kline_5m or kline_15m NOTE: Because of REST API request limit, we only send one request pre minute. """ for index, symbol in enumerate(self._symbols): asyncio.get_event_loop().call_later(index, self.delay_kline_update, symbol, kline_type) def delay_kline_update(self, symbol, kline_type): """ Do kline update. """ asyncio.get_event_loop().create_task( self.do_kline_update(symbol, kline_type)) async def do_kline_update(self, symbol, kline_type): if kline_type == const.MARKET_TYPE_KLINE_5M: range_type = 5 * 60 elif kline_type == const.MARKET_TYPE_KLINE_15M: range_type = 15 * 60 elif kline_type == const.MARKET_TYPE_KLINE_1H: range_type = 60 * 60 else: return start = get_timestamp(datetime.datetime.now() - datetime.timedelta(days=5)) end = get_timestamp() result, error = await self._rest_api.get_kline(symbol, start, end, range_type) if error: return open, hig, low, close = [], [], [], [] if len(result): for item in result: open.append(item[1]) hig.append(item[2]) low.append(item[3]) close.append(item[4]) kline = { "platform": self._platform, "symbol": symbol, "open": open[::-1], #最近的数据在最后面 "high": hig[::-1], "low": low[::-1], "close": close[::-1], "kline_type": kline_type } print(kline) EventKline(**kline).publish() logger.info("symbol:", symbol, "kline:", kline, caller=self)
class HuobiFutureMarket: """ Huobi Future Market Server. Attributes: kwargs: platform: Exchange platform name, must be `huobi_future`. wss: Exchange Websocket host address, default is `wss://www.hbdm.com`. symbols: symbol list, Huobi Future contract code list. channels: channel list, only `orderbook` to be enabled. orderbook_length: The length of orderbook's data to be published via OrderbookEvent, default is 10. """ def __init__(self, **kwargs): self._platform = kwargs["platform"] self._wss = kwargs.get("wss", "wss://www.hbdm.com") self._host = kwargs.get("host", "https://api.hbdm.com") self._symbols = list(set(kwargs.get("symbols"))) self._channels = kwargs.get("channels") self._orderbook_length = kwargs.get("orderbook_length", 10) self._access_key = kwargs.get("access_key", "") self._secret_key = kwargs.get("secret_key", "") self._c_to_s = {} # {"channel": "symbol"} url = self._wss + "/ws" self._ws = Websocket(url, self.connected_callback, process_binary_callback=self.process_binary) self._ws.initialize() self._rest_api = HuobiFutureRestAPI(self._host, self._access_key, self._secret_key) self._initialize() async def connected_callback(self): """ After create connection to Websocket server successfully, we will subscribe orderbook event. """ for ch in self._channels: if ch == "kline": for symbol in self._symbols: channel = self._symbol_to_channel(symbol, "kline") if not channel: continue kline = {"sub": channel} await self._ws.send(kline) elif ch == "orderbook": for symbol in self._symbols: channel = self._symbol_to_channel(symbol, "depth") if not channel: continue data = {"sub": channel} await self._ws.send(data) elif ch == "trade": for symbol in self._symbols: channel = self._symbol_to_channel(symbol, "trade") if not channel: continue data = {"sub": channel} await self._ws.send(data) async def process_binary(self, msg): """ Process binary message that received from Websocket connection. Args: msg: Binary message. """ data = json.loads(gzip.decompress(msg).decode()) # logger.debug("data:", json.dumps(data), caller=self) channel = data.get("ch") if not channel: if data.get("ping"): hb_msg = {"pong": data.get("ping")} await self._ws.send(hb_msg) return symbol = self._c_to_s[channel] if channel.find("kline") != -1: d = data.get("tick") kline = { "platform": self._platform, "symbol": symbol, "open": d["open"], "high": d["high"], "low": d["low"], "close": d["close"], "volume": d["amount"], "timestamp": data.get("ts"), "kline_type": MARKET_TYPE_KLINE } EventKline(**kline).publish() logger.debug("symbol:", symbol, "kline:", kline, caller=self) elif channel.find("depth") != -1: tick = data.get("tick") asks = tick.get("asks")[:self._orderbook_length] bids = tick.get("bids")[:self._orderbook_length] timestamp = tick.get("ts") orderbook = { "platform": self._platform, "symbol": symbol, "asks": asks, "bids": bids, "timestamp": timestamp } EventOrderbook(**orderbook).publish() logger.debug("symbol:", symbol, "orderbook:", orderbook, caller=self) elif channel.find("trade") != -1: tick = data.get("tick") direction = tick["data"][0].get("direction") price = tick["data"][0].get("price") quantity = tick["data"][0].get("amount") trade = { "platform": self._platform, "symbol": symbol, "action": ORDER_ACTION_BUY if direction == "buy" else ORDER_ACTION_SELL, "price": price, "quantity": quantity, "timestamp": tick.get("ts") } EventTrade(**trade).publish() logger.debug("symbol:", symbol, "trade:", trade, caller=self) else: logger.error("event error! msg:", msg, caller=self) def _symbol_to_channel(self, symbol, channel_type): if channel_type == "kline": channel = "market.{s}.kline.1min".format(s=symbol) elif channel_type == "depth": channel = "market.{s}.depth.step6".format(s=symbol) elif channel_type == "trade": channel = "market.{s}.trade.detail".format(s=symbol) else: logger.error("channel type error! channel type:", channel_type, calle=self) return None self._c_to_s[channel] = symbol return channel def _initialize(self): """ Initialize.""" for channel in self._channels: if channel == const.MARKET_TYPE_KLINE_5M: heartbeat.register(self.create_kline_tasks, 1, const.MARKET_TYPE_KLINE_5M) elif channel == const.MARKET_TYPE_KLINE_15M: heartbeat.register(self.create_kline_tasks, 1, const.MARKET_TYPE_KLINE_15M) elif channel == const.MARKET_TYPE_KLINE_1H: heartbeat.register(self.create_kline_tasks, 1, const.MARKET_TYPE_KLINE_1H) # else: # logger.error("channel error! channel:", channel, caller=self) async def create_kline_tasks(self, kline_type, *args, **kwargs): """ Create some tasks to fetch kline information Args: kline_type: Type of line, kline or kline_5m or kline_15m NOTE: Because of REST API request limit, we only send one request pre minute. """ for index, symbol in enumerate(self._symbols): asyncio.get_event_loop().call_later(index, self.delay_kline_update, symbol, kline_type) def delay_kline_update(self, symbol, kline_type): """ Do kline update. """ asyncio.get_event_loop().create_task( self.do_kline_update(symbol, kline_type)) async def do_kline_update(self, symbol, kline_type): if kline_type == const.MARKET_TYPE_KLINE_5M: range_type = "5min" elif kline_type == const.MARKET_TYPE_KLINE_15M: range_type = "15min" elif kline_type == const.MARKET_TYPE_KLINE_1H: range_type = "60min" elif kline_type == const.MARKET_TYPE_KLINE_1H: range_type = "4hour" elif kline_type == const.MARKET_TYPE_KLINE_1H: range_type = "1day" else: return result, error = await self._rest_api.get_kline(symbol, size=130, period=range_type) if error: return open, hig, low, close = [], [], [], [] if len(result): for item in result["data"]: open.append(item["open"]) hig.append(item["high"]) low.append(item["low"]) close.append(item["close"]) kline = { "platform": self._platform, "symbol": symbol, "open": open, #最近的数据在最后面 "high": hig, "low": low, "close": close, "kline_type": kline_type } EventKline(**kline).publish() logger.info("symbol:", symbol, "kline_%s:" % range_type, kline, caller=self)
class BinanceTrade: """ Binance Trade module. You can initialize trade object with some attributes in kwargs. Attributes: account: Account name for this trade exchange. strategy: What's name would you want to created for you strategy. symbol: Symbol name for your trade. host: HTTP request host. (default "https://api.binance.com") wss: Websocket address. (default "wss://stream.binance.com:9443") access_key: Account's ACCESS KEY. secret_key Account's SECRET KEY. asset_update_callback: You can use this param to specific a async callback function when you initializing Trade object. `asset_update_callback` is like `async def on_asset_update_callback(asset: Asset): pass` and this callback function will be executed asynchronous when received AssetEvent. order_update_callback: You can use this param to specific a async callback function when you initializing Trade object. `order_update_callback` is like `async def on_order_update_callback(order: Order): pass` and this callback function will be executed asynchronous when some order state updated. init_success_callback: You can use this param to specific a async callback function when you initializing Trade object. `init_success_callback` is like `async def on_init_success_callback(success: bool, error: Error, **kwargs): pass` and this callback function will be executed asynchronous after Trade module object initialized successfully. """ def __init__(self, **kwargs): """Initialize Trade module.""" e = None if not kwargs.get("account"): e = Error("param account miss") if not kwargs.get("strategy"): e = Error("param strategy miss") if not kwargs.get("symbol"): e = Error("param symbol miss") if not kwargs.get("host"): kwargs["host"] = "https://api.binance.com" if not kwargs.get("wss"): kwargs["wss"] = "wss://stream.binance.com:9443" if not kwargs.get("access_key"): e = Error("param access_key miss") if not kwargs.get("secret_key"): e = Error("param secret_key miss") if e: logger.error(e, caller=self) SingleTask.run(kwargs["init_success_callback"], False, e) return self._account = kwargs["account"] self._strategy = kwargs["strategy"] self._platform = BINANCE self._symbol = kwargs["symbol"] self._host = kwargs["host"] self._wss = kwargs["wss"] self._access_key = kwargs["access_key"] self._secret_key = kwargs["secret_key"] self._asset_update_callback = kwargs.get("asset_update_callback") self._order_update_callback = kwargs.get("order_update_callback") self._init_success_callback = kwargs.get("init_success_callback") super(BinanceTrade, self).__init__(self._wss) self._raw_symbol = self._symbol.replace( "/", "") # Row symbol name, same as Binance Exchange. self._listen_key = None # Listen key for Websocket authentication. self._assets = { } # Asset data. e.g. {"BTC": {"free": "1.1", "locked": "2.2", "total": "3.3"}, ... } self._orders = {} # Order data. e.g. {order_no: order, ... } # Initialize our REST API client. self._rest_api = BinanceRestAPI(self._host, self._access_key, self._secret_key) # Subscribe our AssetEvent. if self._asset_update_callback: AssetSubscribe(self._platform, self._account, self.on_event_asset_update) # Create a loop run task to reset listen key every 30 minutes. LoopRunTask.register(self._reset_listen_key, 60 * 30) # Create a coroutine to initialize Websocket connection. SingleTask.run(self._init_websocket) @property def assets(self): return copy.copy(self._assets) @property def orders(self): return copy.copy(self._orders) @property def rest_api(self): return self._rest_api async def _init_websocket(self): """ Initialize Websocket connection. """ # Get listen key first. success, error = await self._rest_api.get_listen_key() if error: e = Error("get listen key failed: {}".format(error)) logger.error(e, caller=self) SingleTask.run(self._init_success_callback, False, e) return self._listen_key = success["listenKey"] uri = "/ws/" + self._listen_key url = urljoin(self._wss, uri) self._ws = Websocket(url, self.connected_callback, process_callback=self.process) self._ws.initialize() async def _reset_listen_key(self, *args, **kwargs): """ Reset listen key. """ if not self._listen_key: logger.error("listen key not initialized!", caller=self) return await self._rest_api.put_listen_key(self._listen_key) logger.info("reset listen key success!", caller=self) async def connected_callback(self): """ After websocket connection created successfully, pull back all open order information. """ logger.info("Websocket connection authorized successfully.", caller=self) order_infos, error = await self._rest_api.get_open_orders( self._raw_symbol) if error: e = Error("get open orders error: {}".format(error)) SingleTask.run(self._init_success_callback, False, e) return for order_info in order_infos: order_no = "{}_{}".format(order_info["orderId"], order_info["clientOrderId"]) if order_info["status"] == "NEW": status = ORDER_STATUS_SUBMITTED elif order_info["status"] == "PARTIALLY_FILLED": status = ORDER_STATUS_PARTIAL_FILLED elif order_info["status"] == "FILLED": status = ORDER_STATUS_FILLED elif order_info["status"] == "CANCELED": status = ORDER_STATUS_CANCELED elif order_info["status"] == "REJECTED": status = ORDER_STATUS_FAILED elif order_info["status"] == "EXPIRED": status = ORDER_STATUS_FAILED else: logger.warn("unknown status:", order_info, caller=self) continue info = { "platform": self._platform, "account": self._account, "strategy": self._strategy, "order_no": order_no, "action": order_info["side"], "order_type": order_info["type"], "symbol": self._symbol, "price": order_info["price"], "quantity": order_info["origQty"], "remain": float(order_info["origQty"]) - float(order_info["executedQty"]), "status": status, "ctime": order_info["time"], "utime": order_info["updateTime"] } order = Order(**info) self._orders[order_no] = order SingleTask.run(self._order_update_callback, copy.copy(order)) SingleTask.run(self._init_success_callback, True, None) async def create_order(self, action, price, quantity, *args, **kwargs): """ Create an order. Args: action: Trade direction, BUY or SELL. price: Price of each contract. quantity: The buying or selling quantity. Returns: order_no: Order ID if created successfully, otherwise it's None. error: Error information, otherwise it's None. """ price = tools.float_to_str(price) quantity = tools.float_to_str(quantity) client_order_id = kwargs.get("client_order_id") result, error = await self._rest_api.create_order( action, self._raw_symbol, price, quantity, client_order_id=client_order_id) if error: return None, error order_no = "{}_{}".format(result["orderId"], result["clientOrderId"]) return order_no, None async def revoke_order(self, *order_nos): """ Revoke (an) order(s). Args: order_nos: Order id list, you can set this param to 0 or multiple items. If you set 0 param, you can cancel all orders for this symbol(initialized in Trade object). If you set 1 param, you can cancel an order. If you set multiple param, you can cancel multiple orders. Do not set param length more than 100. Returns: Success or error, see bellow. """ # If len(order_nos) == 0, you will cancel all orders for this symbol(initialized in Trade object). if len(order_nos) == 0: order_infos, error = await self._rest_api.get_open_orders( self._raw_symbol) if error: return False, error for order_info in order_infos: _, error = await self._rest_api.revoke_order( self._raw_symbol, order_info["orderId"], order_info["clientOrderId"]) if error: return False, error return True, None # If len(order_nos) == 1, you will cancel an order. if len(order_nos) == 1: order_id, client_order_id = order_nos[0].split("_") success, error = await self._rest_api.revoke_order( self._raw_symbol, order_id, client_order_id) if error: return order_nos[0], error else: return order_nos[0], None # If len(order_nos) > 1, you will cancel multiple orders. if len(order_nos) > 1: success, error = [], [] for order_no in order_nos: order_id, client_order_id = order_no.split("_") _, e = await self._rest_api.revoke_order( self._raw_symbol, order_id, client_order_id) if e: error.append((order_no, e)) else: success.append(order_no) return success, error async def get_open_order_nos(self): """ Get open order no list. """ success, error = await self._rest_api.get_open_orders(self._raw_symbol) if error: return None, error else: order_nos = [] for order_info in success: order_no = "{}_{}".format(order_info["orderId"], order_info["clientOrderId"]) order_nos.append(order_no) return order_nos, None @async_method_locker("BinanceTrade.process.locker") async def process(self, msg): """ Process message that received from Websocket connection. Args: msg: message received from Websocket connection. """ logger.debug("msg:", json.dumps(msg), caller=self) e = msg.get("e") if e == "executionReport": # Order update. if msg["s"] != self._raw_symbol: return order_no = "{}_{}".format(msg["i"], msg["c"]) if msg["X"] == "NEW": status = ORDER_STATUS_SUBMITTED elif msg["X"] == "PARTIALLY_FILLED": status = ORDER_STATUS_PARTIAL_FILLED elif msg["X"] == "FILLED": status = ORDER_STATUS_FILLED elif msg["X"] == "CANCELED": status = ORDER_STATUS_CANCELED elif msg["X"] == "REJECTED": status = ORDER_STATUS_FAILED elif msg["X"] == "EXPIRED": status = ORDER_STATUS_FAILED else: logger.warn("unknown status:", msg, caller=self) return order = self._orders.get(order_no) if not order: info = { "platform": self._platform, "account": self._account, "strategy": self._strategy, "order_no": order_no, "client_order_id": msg["c"], "action": msg["S"], "order_type": msg["o"], "symbol": self._symbol, "price": msg["p"], "quantity": msg["q"], "ctime": msg["O"] } order = Order(**info) self._orders[order_no] = order order.remain = float(msg["q"]) - float(msg["z"]) order.status = status order.utime = msg["T"] SingleTask.run(self._order_update_callback, copy.copy(order)) async def on_event_asset_update(self, asset: Asset): """ Asset data update callback. Args: asset: Asset object. """ self._assets = asset SingleTask.run(self._asset_update_callback, asset)
class Bitmex: """ Bitmex Market Server. Attributes: kwargs: platform: Exchange platform name, must be `bitmex`. wss: Exchange Websocket host address, default is `wss://www.bitmex.com`. symbols: Symbol list. channels: Channel list, only `orderbook` / `trade` / `kline` to be enabled. """ def __init__(self, **kwargs): self._platform = kwargs["platform"] self._wss = kwargs.get("wss", "wss://www.bitmex.com") self._symbols = list(set(kwargs.get("symbols"))) self._channels = kwargs.get("channels") self._c_to_s = {} # {"channel": "symbol"} url = self._wss + "/realtime" self._ws = Websocket(url, self.connected_callback, process_callback=self.process) self._ws.initialize() async def connected_callback(self): """After create connection to Websocket server successfully, we will subscribe orderbook/trade/kline event.""" channels = [] for ch in self._channels: if ch == "orderbook": for symbol in self._symbols: channel = self._symbol_to_channel(symbol, "orderBook10") channels.append(channel) if ch == "trade": for symbol in self._symbols: channel = self._symbol_to_channel(symbol, "trade") channels.append(channel) if ch == "kline": for symbol in self._symbols: channel = self._symbol_to_channel(symbol, "tradeBin1m") channels.append(channel) if channels: data = {"op": "subscribe", "args": channels} await self._ws.send(data) logger.info("subscribe orderbook/trade/kline successfully.", caller=self) async def process(self, msg): """ Process message that received from Websocket connection. Args: msg: Message data received from Websocket connection. """ logger.debug("msg:", msg, caller=self) if not isinstance(msg, dict): return table = msg.get("table") if table == "orderBook10": await self.process_orderbook(msg["data"]) elif table == "trade": await self.process_trade(msg["data"]) elif table == "tradeBin1m": await self.process_kline(msg["data"]) async def process_orderbook(self, data): """Process orderbook data and publish OrderbookEvent.""" for item in data: symbol = item.get("symbol") orderbook = { "platform": self._platform, "symbol": symbol, "asks": item.get("asks"), "bids": item.get("bids"), "timestamp": tools.utctime_str_to_mts(item["timestamp"]) } EventOrderbook(**orderbook).publish() logger.info("symbol:", symbol, "orderbook:", orderbook, caller=self) async def process_kline(self, data): """Process kline data and publish KlineEvent.""" for item in data: symbol = item["symbol"] kline = { "platform": self._platform, "symbol": symbol, "open": "%.1f" % item["open"], "high": "%.1f" % item["high"], "low": "%.1f" % item["low"], "close": "%.1f" % item["close"], "volume": str(item["volume"]), "timestamp": tools.utctime_str_to_mts(item["timestamp"]), "kline_type": MARKET_TYPE_KLINE } EventKline(**kline).publish() logger.info("symbol:", symbol, "kline:", kline, caller=self) async def process_trade(self, data): """Process trade data and publish TradeEvent.""" for item in data: symbol = item["symbol"] trade = { "platform": self._platform, "symbol": symbol, "action": ORDER_ACTION_BUY if item["side"] else ORDER_ACTION_SELL, "price": "%.1f" % item["price"], "quantity": str(item["size"]), "timestamp": tools.utctime_str_to_mts(item["timestamp"]) } EventTrade(**trade).publish() logger.info("symbol:", symbol, "trade:", trade, caller=self) def _symbol_to_channel(self, symbol, channel_type): channel = "{channel_type}:{symbol}".format(channel_type=channel_type, symbol=symbol) self._c_to_s[channel] = symbol return channel
class Deribit: """ Deribit Market Server. Attributes: kwargs: platform: Exchange platform name, must be `deribit`. wss: Exchange Websocket host address, default is `wss://deribit.com`. symbols: Symbol list. channels: Channel list, only `orderbook` to be enabled. orderbook_length: The length of orderbook's data to be published via OrderbookEvent, default is 10. """ def __init__(self, **kwargs): self._platform = kwargs["platform"] self._wss = None self._symbols = list(set(kwargs.get("symbols"))) self._channels = kwargs.get("channels") self._orderbook_length = kwargs.get("orderbook_length", 10) self._access_key = None self._secret_key = None self._last_msg_ts = tools.get_cur_timestamp_ms() # 上次接收到消息的时间戳 for item in config.accounts: if item["platform"] == self._platform: self._wss = item.get("wss", "wss://deribit.com") self._access_key = item["access_key"] self._secret_key = item["secret_key"] if not self._wss or not self._access_key or not self._access_key: logger.error( "no find deribit account in ACCOUNTS from config file.", caller=self) return url = self._wss + "/ws/api/v1/" self._ws = Websocket(url, connected_callback=self.connected_callback, process_callback=self.process) self._ws.initialize() LoopRunTask.register(self.send_heartbeat_msg, 10) async def connected_callback(self): """After create connection to Websocket server successfully, we will subscribe orderbook/trade/kline event.""" nonce = tools.get_cur_timestamp_ms() uri = "/api/v1/private/subscribe" params = {"instrument": self._symbols, "event": ["order_book"]} sign = self.deribit_signature(nonce, uri, params, self._access_key, self._secret_key) data = { "id": "thenextquant", "action": uri, "arguments": params, "sig": sign } await self._ws.send(data) logger.info("subscribe orderbook success.", caller=self) async def send_heartbeat_msg(self, *args, **kwargs): data = {"action": "/api/v1/public/ping"} if not self._ws: logger.error("Websocket connection not yeah!", caller=self) return await self._ws.send(data) async def process(self, msg): """ Process message that received from Websocket connection. Args: msg: Message data received from Websocket connection. """ # logger.debug("msg:", msg, caller=self) if tools.get_cur_timestamp_ms() <= self._last_msg_ts: return if not isinstance(msg, dict): return notifications = msg.get("notifications") if not notifications: return message = notifications[0].get("message") if message != "order_book_event": return symbol = notifications[0].get("result").get("instrument") bids = [] for item in notifications[0].get("result").get( "bids")[:self._orderbook_length]: b = [item.get("price"), item.get("quantity")] bids.append(b) asks = [] for item in notifications[0].get("result").get( "asks")[:self._orderbook_length]: a = [item.get("price"), item.get("quantity")] asks.append(a) self._last_msg_ts = tools.get_cur_timestamp_ms() orderbook = { "platform": self._platform, "symbol": symbol, "asks": asks, "bids": bids, "timestamp": self._last_msg_ts } EventOrderbook(**orderbook).publish() logger.info("symbol:", symbol, "orderbook:", orderbook, caller=self) def deribit_signature(self, nonce, uri, params, access_key, access_secret): """ 生成signature """ sign = "_=%s&_ackey=%s&_acsec=%s&_action=%s" % (nonce, access_key, access_secret, uri) for key in sorted(params.keys()): sign += "&" + key + "=" + "".join(params[key]) return "%s.%s.%s" % ( access_key, nonce, base64.b64encode(hashlib.sha256(sign.encode()).digest()).decode())
class OKEx: """ OKEx Market Server. Attributes: kwargs: platform: Exchange platform name, must be `okex` or `okex_margin`. host: Exchange Websocket host address, default is `wss://real.okex.com:8443`. symbols: symbol list, OKEx Future instrument_id list. channels: channel list, only `orderbook`, `kline` and `trade` to be enabled. orderbook_length: The length of orderbook's data to be published via OrderbookEvent, default is 10. """ def __init__(self, **kwargs): self._platform = kwargs["platform"] self._wss = kwargs.get("wss", "wss://real.okex.com:8443") self._symbols = list(set(kwargs.get("symbols"))) self._channels = kwargs.get("channels") self._orderbook_length = kwargs.get("orderbook_length", 10) self._orderbooks = {} # 订单薄数据 {"symbol": {"bids": {"price": quantity, ...}, "asks": {...}}} url = self._wss + "/ws/v3" self._ws = Websocket(url, connected_callback=self.connected_callback, process_binary_callback=self.process_binary) self._ws.initialize() LoopRunTask.register(self.send_heartbeat_msg, 5) async def connected_callback(self): """After create Websocket connection successfully, we will subscribing orderbook/trade/kline.""" ches = [] for ch in self._channels: if ch == "orderbook": for symbol in self._symbols: ch = "spot/depth:{s}".format(s=symbol.replace("/", '-')) ches.append(ch) elif ch == "trade": for symbol in self._symbols: ch = "spot/trade:{s}".format(s=symbol.replace("/", '-')) ches.append(ch) elif ch == "kline": for symbol in self._symbols: ch = "spot/candle60s:{s}".format(s=symbol.replace("/", '-')) ches.append(ch) else: logger.error("channel error! channel:", ch, caller=self) if ches: msg = { "op": "subscribe", "args": ches } await self._ws.send(msg) logger.info("subscribe orderbook/trade/kline success.", caller=self) async def send_heartbeat_msg(self, *args, **kwargs): data = "ping" if not self._ws: logger.error("Websocket connection not yeah!", caller=self) return await self._ws.send(data) async def process_binary(self, raw): """ Process binary message that received from Websocket connection. Args: raw: Raw message that received from Websocket connection. """ decompress = zlib.decompressobj(-zlib.MAX_WBITS) msg = decompress.decompress(raw) msg += decompress.flush() msg = msg.decode() # logger.debug("msg:", msg, caller=self) if msg == "pong": return msg = json.loads(msg) table = msg.get("table") if table == "spot/depth": if msg.get("action") == "partial": for d in msg["data"]: await self.process_orderbook_partial(d) elif msg.get("action") == "update": for d in msg["data"]: await self.deal_orderbook_update(d) else: logger.warn("unhandle msg:", msg, caller=self) elif table == "spot/trade": for d in msg["data"]: await self.process_trade(d) elif table == "spot/candle60s": for d in msg["data"]: await self.process_kline(d) async def process_orderbook_partial(self, data): """Process orderbook partical data.""" symbol = data.get("instrument_id").replace("-", "/") if symbol not in self._symbols: return asks = data.get("asks") bids = data.get("bids") self._orderbooks[symbol] = {"asks": {}, "bids": {}, "timestamp": 0} for ask in asks: price = float(ask[0]) quantity = float(ask[1]) self._orderbooks[symbol]["asks"][price] = quantity for bid in bids: price = float(bid[0]) quantity = float(bid[1]) self._orderbooks[symbol]["bids"][price] = quantity timestamp = tools.utctime_str_to_mts(data.get("timestamp")) self._orderbooks[symbol]["timestamp"] = timestamp async def deal_orderbook_update(self, data): """Process orderbook update data.""" symbol = data.get("instrument_id").replace("-", "/") asks = data.get("asks") bids = data.get("bids") timestamp = tools.utctime_str_to_mts(data.get("timestamp")) if symbol not in self._orderbooks: return self._orderbooks[symbol]["timestamp"] = timestamp for ask in asks: price = float(ask[0]) quantity = float(ask[1]) if quantity == 0 and price in self._orderbooks[symbol]["asks"]: self._orderbooks[symbol]["asks"].pop(price) else: self._orderbooks[symbol]["asks"][price] = quantity for bid in bids: price = float(bid[0]) quantity = float(bid[1]) if quantity == 0 and price in self._orderbooks[symbol]["bids"]: self._orderbooks[symbol]["bids"].pop(price) else: self._orderbooks[symbol]["bids"][price] = quantity await self.publish_orderbook(symbol) async def publish_orderbook(self, symbol): """Publish OrderbookEvent.""" ob = copy.copy(self._orderbooks[symbol]) if not ob["asks"] or not ob["bids"]: logger.warn("symbol:", symbol, "asks:", ob["asks"], "bids:", ob["bids"], caller=self) return ask_keys = sorted(list(ob["asks"].keys())) bid_keys = sorted(list(ob["bids"].keys()), reverse=True) if ask_keys[0] <= bid_keys[0]: logger.warn("symbol:", symbol, "ask1:", ask_keys[0], "bid1:", bid_keys[0], caller=self) return asks = [] for k in ask_keys[:self._orderbook_length]: price = "%.8f" % k quantity = "%.8f" % ob["asks"].get(k) asks.append([price, quantity]) bids = [] for k in bid_keys[:self._orderbook_length]: price = "%.8f" % k quantity = "%.8f" % ob["bids"].get(k) bids.append([price, quantity]) orderbook = { "platform": self._platform, "symbol": symbol, "asks": asks, "bids": bids, "timestamp": ob["timestamp"] } EventOrderbook(**orderbook).publish() logger.debug("symbol:", symbol, "orderbook:", orderbook, caller=self) async def process_trade(self, data): """Process trade data and publish TradeEvent.""" symbol = data.get("instrument_id").replace("-", "/") if symbol not in self._symbols: return action = ORDER_ACTION_BUY if data["side"] == "buy" else ORDER_ACTION_SELL price = "%.8f" % float(data["price"]) quantity = "%.8f" % float(data["size"]) timestamp = tools.utctime_str_to_mts(data["timestamp"]) trade = { "platform": self._platform, "symbol": symbol, "action": action, "price": price, "quantity": quantity, "timestamp": timestamp } EventTrade(**trade).publish() logger.debug("symbol:", symbol, "trade:", trade, caller=self) async def process_kline(self, data): """Process kline data and publish KlineEvent.""" symbol = data["instrument_id"].replace("-", "/") if symbol not in self._symbols: return timestamp = tools.utctime_str_to_mts(data["candle"][0]) _open = "%.8f" % float(data["candle"][1]) high = "%.8f" % float(data["candle"][2]) low = "%.8f" % float(data["candle"][3]) close = "%.8f" % float(data["candle"][4]) volume = "%.8f" % float(data["candle"][5]) kline = { "platform": self._platform, "symbol": symbol, "open": _open, "high": high, "low": low, "close": close, "volume": volume, "timestamp": timestamp, "kline_type": const.MARKET_TYPE_KLINE } EventKline(**kline).publish() logger.debug("symbol:", symbol, "kline:", kline, caller=self)
class HuobiTrade: """ huobi Trade模块 """ def __init__(self, **kwargs): """ 初始化 """ e = None if not kwargs.get("account"): e = Error("param account miss") if not kwargs.get("strategy"): e = Error("param strategy miss") if not kwargs.get("symbol"): e = Error("param symbol miss") if not kwargs.get("host"): kwargs["host"] = "https://api.huobi.pro" if not kwargs.get("wss"): kwargs["wss"] = "wss://api.huobi.pro" if not kwargs.get("access_key"): e = Error("param access_key miss") if not kwargs.get("secret_key"): e = Error("param secret_key miss") if e: logger.error(e, caller=self) SingleTask.run(kwargs["init_success_callback"], False, e) return self._account = kwargs["account"] self._strategy = kwargs["strategy"] self._platform = HUOBI self._symbol = kwargs["symbol"] self._host = kwargs["host"] self._wss = kwargs["wss"] self._access_key = kwargs["access_key"] self._secret_key = kwargs["secret_key"] self._asset_update_callback = kwargs.get("asset_update_callback") self._order_update_callback = kwargs.get("order_update_callback") self._init_success_callback = kwargs.get("init_success_callback") self._raw_symbol = self._symbol.replace("/", "").lower() # 转换成交易所对应的交易对格式 self._order_channel = "orders.{}".format(self._raw_symbol) # 订阅订单更新频道 url = self._wss + "/ws/v1" self._ws = Websocket(url, self.connected_callback, process_binary_callback=self.process_binary) self._ws.initialize() self._assets = { } # 资产 {"BTC": {"free": "1.1", "locked": "2.2", "total": "3.3"}, ... } self._orders = {} # 订单 # 初始化 REST API 对象 self._rest_api = HuobiRestAPI(self._host, self._access_key, self._secret_key) # 初始化资产订阅 if self._asset_update_callback: AssetSubscribe(self._platform, self._account, self.on_event_asset_update) @property def assets(self): return copy.copy(self._assets) @property def orders(self): return copy.copy(self._orders) @property def rest_api(self): return self._rest_api async def connected_callback(self): """ 建立连接之后,授权登陆,然后订阅order和position """ # 身份验证 timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S") params = { "AccessKeyId": self._access_key, "SignatureMethod": "HmacSHA256", "SignatureVersion": "2", "Timestamp": timestamp } signature = self._rest_api.generate_signature("GET", params, "api.huobi.pro", "/ws/v1") params["op"] = "auth" params["Signature"] = signature await self._ws.send(params) async def _auth_success_callback(self): """ 授权成功之后回调 """ # 获取当前未完成订单 success, error = await self._rest_api.get_open_orders(self._raw_symbol) if error: e = Error("get open orders error: {}".format(error)) SingleTask.run(self._init_success_callback, False, e) return for order_info in success: data = { "order-id": order_info["id"], "order-type": order_info["type"], "order-state": order_info["state"], "unfilled-amount": float(order_info["amount"]) - float(order_info["filled-amount"]), "order-price": float(order_info["price"]), "price": float(order_info["price"]), "order-amount": float(order_info["amount"]), "created-at": order_info["created-at"], "utime": order_info["created-at"], } self._update_order(data) # 订阅订单更新数据 params = {"op": "sub", "topic": self._order_channel} await self._ws.send(params) @async_method_locker("HuobiTrade.process_binary.locker") async def process_binary(self, raw): """ 处理websocket上接收到的消息 @param raw 原始的压缩数据 """ msg = json.loads(gzip.decompress(raw).decode()) logger.debug("msg:", msg, caller=self) op = msg.get("op") if op == "auth": # 授权 if msg["err-code"] != 0: e = Error( "Websocket connection authorized failed: {}".format(msg)) logger.error(e, caller=self) SingleTask.run(self._init_success_callback, False, e) return logger.info("Websocket connection authorized successfully.", caller=self) await self._auth_success_callback() elif op == "ping": # ping params = {"op": "pong", "ts": msg["ts"]} await self._ws.send(params) elif op == "sub": # 订阅频道返回消息 if msg["topic"] != self._order_channel: return if msg["err-code"] != 0: e = Error("subscribe order event error: {}".format(msg)) SingleTask.run(self._init_success_callback, False, e) else: SingleTask.run(self._init_success_callback, True, None) elif op == "notify": # 订单更新通知 if msg["topic"] != self._order_channel: return data = msg["data"] data["utime"] = msg["ts"] self._update_order(data) async def create_order(self, action, price, quantity, order_type=ORDER_TYPE_LIMIT, *args, **kwargs): """ 创建订单 @param action 交易方向 BUY / SELL @param price 委托价格 @param quantity 委托数量 @param order_type 委托类型 LIMIT / MARKET """ if action == ORDER_ACTION_BUY: if order_type == ORDER_TYPE_LIMIT: t = "buy-limit" elif order_type == ORDER_TYPE_MARKET: t = "buy-market" else: logger.error("order_type error! order_type:", order_type, caller=self) return None, "order type error" elif action == ORDER_ACTION_SELL: if order_type == ORDER_TYPE_LIMIT: t = "sell-limit" elif order_type == ORDER_TYPE_MARKET: t = "sell-market" else: logger.error("order_type error! order_type:", order_type, caller=self) return None, "order type error" else: logger.error("action error! action:", action, caller=self) return None, "action error" price = tools.float_to_str(price) quantity = tools.float_to_str(quantity) result, error = await self._rest_api.create_order( self._raw_symbol, price, quantity, t) return result, error async def revoke_order(self, *order_nos): """ 撤销订单 @param order_nos 订单号列表,可传入任意多个,如果不传入,那么就撤销所有订单 """ # 如果传入order_nos为空,即撤销全部委托单 if len(order_nos) == 0: order_nos, error = await self.get_open_order_nos() if error: return [], error if not order_nos: return [], None # 如果传入order_nos为一个委托单号,那么只撤销一个委托单 if len(order_nos) == 1: success, error = await self._rest_api.revoke_order(order_nos[0]) if error: return order_nos[0], error else: return order_nos[0], None # 如果传入order_nos数量大于1,那么就批量撤销传入的委托单 if len(order_nos) > 1: s, e = await self._rest_api.revoke_orders(order_nos) if e: return [], e success = s["success"] error = s["failed"] return success, error async def get_open_order_nos(self): """ 获取未完全成交订单号列表 """ success, error = await self._rest_api.get_open_orders(self._raw_symbol) if error: return None, error else: order_nos = [] for order_info in success: order_nos.append(order_info["id"]) return order_nos, None def _update_order(self, order_info): """ 更新订单信息 @param order_info 订单信息 * NOTE: order-state: 订单状态, submitting , submitted 已提交, partial-filled 部分成交, partial-canceled 部分成交撤销, filled 完全成交, canceled 已撤销 """ order_no = str(order_info["order-id"]) action = ORDER_ACTION_BUY if order_info["order-type"] in [ "buy-market", "buy-limit" ] else ORDER_ACTION_SELL state = order_info["order-state"] remain = "%.8f" % float(order_info["unfilled-amount"]) avg_price = "%.8f" % float(order_info["price"]) ctime = order_info["created-at"] utime = order_info["utime"] if state == "canceled": status = ORDER_STATUS_CANCELED elif state == "partial-canceled": status = ORDER_STATUS_CANCELED elif state == "submitting": status = ORDER_STATUS_SUBMITTED elif state == "submitted": status = ORDER_STATUS_SUBMITTED elif state == "partial-filled": status = ORDER_STATUS_PARTIAL_FILLED elif state == "filled": status = ORDER_STATUS_FILLED else: logger.error("status error! order_info:", order_info, caller=self) return None order = self._orders.get(order_no) if not order: info = { "platform": self._platform, "account": self._account, "strategy": self._strategy, "order_no": order_no, "action": action, "symbol": self._symbol, "price": "%.8f" % float(order_info["order-price"]), "quantity": "%.8f" % float(order_info["order-amount"]), "remain": remain, "status": status } order = Order(**info) self._orders[order_no] = order order.remain = remain order.status = status order.avg_price = avg_price order.ctime = ctime order.utime = utime SingleTask.run(self._order_update_callback, copy.copy(order)) if status in [ ORDER_STATUS_FAILED, ORDER_STATUS_CANCELED, ORDER_STATUS_FILLED ]: self._orders.pop(order_no) async def on_event_asset_update(self, asset: Asset): """ 资产数据更新回调 """ self._assets = asset SingleTask.run(self._asset_update_callback, asset)