class Subscriber: def __init__(self): self._status = False self.sub_dict = None self.quote_ctx = None self.lock = Lock() def __del__(self): if self.quote_ctx is not None: self.quote_ctx.close() def start(self, host = ct.FUTU_HOST, port = ct.FUTU_PORT): with self.lock: if self.quote_ctx is None: self.quote_ctx = OpenQuoteContext(host, port) else: self.quote_ctx.start() self.sub_dict = self.get_subscribed_dict() logger.debug("self.sub_dict:%s" % self.sub_dict) self._status = True def stop(self): self.quote_ctx.stop() self._status = False logger.debug("stop success") def close(self): self.quote_ctx.close() logger.debug("close success") def status(self): with self.lock: return self._status def get_order_book_data(self, code): ret, data = self.quote_ctx.get_order_book(code) return ret, data def get_tick_data(self, code): ret, data = self.quote_ctx.get_rt_ticker(code) return ret, data def get_quote_data(self, code): ret, data = self.quote_ctx.get_stock_quote(code) return ret, data def subscribe(self, code_list, dtype, callback = None): if dtype in self.sub_dict and set(code_list).issubset(set(self.sub_dict[dtype])): return 0 if callback is not None: self.quote_ctx.set_handler(callback) ret, msg = self.quote_ctx.subscribe(code_list, dtype) if 0 == ret: if dtype not in self.sub_dict: self.sub_dict[dtype] = list() self.sub_dict[dtype].extend(code_list) else: logger.error("%s subscrbe failed, msg:%s, dtype:%s" % (code, msg, dtype)) return ret def unsubscribe(self, code_list, subtype): ''' code_list – 取消订阅的股票代码列表 subtype_list – 取消订阅的类型,参见SubType ''' ret, msg = self.quote_ctx.unsubscribe(code_list, subtype) if 0 == ret: if subtype in self.sub_dict and code in self.sub_dict[subtype]: self.sub_dict[subtype].remove(code) else: logger.error(msg) return ret def get_subscribed_dict(self): ret, data = self.quote_ctx.query_subscription() if 0 != ret: logger.error(msg) return None return data['sub_dict'] if 'sub_dict' in data else dict()
class FutuGateway(BaseGateway): """""" default_setting = { "密码": "", "地址": "127.0.0.1", "端口": 11111, "市场": ["HK", "US"], "环境": [TrdEnv.REAL, TrdEnv.SIMULATE], } exchanges = list(EXCHANGE_FUTU2VT.values()) def __init__(self, event_engine): """Constructor""" super(FutuGateway, self).__init__(event_engine, "FUTU") self.quote_ctx = None self.trade_ctx = None self.host = "" self.port = 0 self.market = "" self.password = "" self.env = TrdEnv.SIMULATE self.ticks = {} self.trades = set() self.contracts = {} self.thread = Thread(target=self.query_data) # For query function. self.count = 0 self.interval = 1 self.query_funcs = [self.query_account, self.query_position] def connect(self, setting: dict): """""" self.host = setting["地址"] self.port = setting["端口"] self.market = setting["市场"] self.password = setting["密码"] self.env = setting["环境"] self.connect_quote() self.connect_trade() self.thread.start() def query_data(self): """ Query all data necessary. """ sleep(2.0) # Wait 2 seconds till connection completed. self.query_contract() self.query_trade() self.query_order() self.query_position() self.query_account() # Start fixed interval query. self.event_engine.register(EVENT_TIMER, self.process_timer_event) def process_timer_event(self, event): """""" self.count += 1 if self.count < self.interval: return self.count = 0 func = self.query_funcs.pop(0) func() self.query_funcs.append(func) def connect_quote(self): """ Connect to market data server. """ self.quote_ctx = OpenQuoteContext(self.host, self.port) class QuoteHandler(StockQuoteHandlerBase): gateway = self def on_recv_rsp(self, rsp_str): ret_code, content = super(QuoteHandler, self).on_recv_rsp(rsp_str) if ret_code != RET_OK: return RET_ERROR, content self.gateway.process_quote(content) return RET_OK, content class OrderBookHandler(OrderBookHandlerBase): gateway = self def on_recv_rsp(self, rsp_str): ret_code, content = super(OrderBookHandler, self).on_recv_rsp(rsp_str) if ret_code != RET_OK: return RET_ERROR, content self.gateway.process_orderbook(content) return RET_OK, content self.quote_ctx.set_handler(QuoteHandler()) self.quote_ctx.set_handler(OrderBookHandler()) self.quote_ctx.start() self.write_log("行情接口连接成功") def connect_trade(self): """ Connect to trade server. """ # Initialize context according to market. if self.market == "US": self.trade_ctx = OpenUSTradeContext(self.host, self.port) else: self.trade_ctx = OpenHKTradeContext(self.host, self.port) # Implement handlers. class OrderHandler(TradeOrderHandlerBase): gateway = self def on_recv_rsp(self, rsp_str): ret_code, content = super(OrderHandler, self).on_recv_rsp(rsp_str) if ret_code != RET_OK: return RET_ERROR, content self.gateway.process_order(content) return RET_OK, content class DealHandler(TradeDealHandlerBase): gateway = self def on_recv_rsp(self, rsp_str): ret_code, content = super(DealHandler, self).on_recv_rsp(rsp_str) if ret_code != RET_OK: return RET_ERROR, content self.gateway.process_deal(content) return RET_OK, content # Unlock to allow trading. code, data = self.trade_ctx.unlock_trade(self.password) if code == RET_OK: self.write_log("交易接口解锁成功") else: self.write_log(f"交易接口解锁失败,原因:{data}") # Start context. self.trade_ctx.set_handler(OrderHandler()) self.trade_ctx.set_handler(DealHandler()) self.trade_ctx.start() self.write_log("交易接口连接成功") def subscribe(self, req: SubscribeRequest): """""" for data_type in ["QUOTE", "ORDER_BOOK"]: futu_symbol = convert_symbol_vt2futu(req.symbol, req.exchange) code, data = self.quote_ctx.subscribe(futu_symbol, data_type, True) if code: self.write_log(f"订阅行情失败:{data}") def send_order(self, req: OrderRequest): """""" side = DIRECTION_VT2FUTU[req.direction] futu_order_type = OrderType.NORMAL # Only limit order is supported. # Set price adjustment mode to inside adjustment. if req.direction is Direction.LONG: adjust_limit = 0.05 else: adjust_limit = -0.05 futu_symbol = convert_symbol_vt2futu(req.symbol, req.exchange) code, data = self.trade_ctx.place_order( req.price, req.volume, futu_symbol, side, futu_order_type, trd_env=self.env, adjust_limit=adjust_limit, ) if code: self.write_log(f"委托失败:{data}") return "" for ix, row in data.iterrows(): orderid = str(row["order_id"]) order = req.create_order_data(orderid, self.gateway_name) self.on_order(order) return order.vt_orderid def cancel_order(self, req: CancelRequest): """""" code, data = self.trade_ctx.modify_order(ModifyOrderOp.CANCEL, req.orderid, 0, 0, trd_env=self.env) if code: self.write_log(f"撤单失败:{data}") def query_contract(self): """""" for product, futu_product in PRODUCT_VT2FUTU.items(): code, data = self.quote_ctx.get_stock_basicinfo( self.market, futu_product) if code: self.write_log(f"查询合约信息失败:{data}") return for ix, row in data.iterrows(): symbol, exchange = convert_symbol_futu2vt(row["code"]) contract = ContractData( symbol=symbol, exchange=exchange, name=row["name"], product=product, size=1, pricetick=0.001, net_position=True, gateway_name=self.gateway_name, ) self.on_contract(contract) self.contracts[contract.vt_symbol] = contract self.write_log("合约信息查询成功") def query_account(self): """""" code, data = self.trade_ctx.accinfo_query(trd_env=self.env, acc_id=0) if code: self.write_log(f"查询账户资金失败:{data}") return for ix, row in data.iterrows(): account = AccountData( accountid=f"{self.gateway_name}_{self.market}", balance=float(row["total_assets"]), frozen=(float(row["total_assets"]) - float(row["avl_withdrawal_cash"])), gateway_name=self.gateway_name, ) self.on_account(account) def query_position(self): """""" code, data = self.trade_ctx.position_list_query(trd_env=self.env, acc_id=0) if code: self.write_log(f"查询持仓失败:{data}") return for ix, row in data.iterrows(): symbol, exchange = convert_symbol_futu2vt(row["code"]) pos = PositionData( symbol=symbol, exchange=exchange, direction=Direction.NET, volume=row["qty"], frozen=(float(row["qty"]) - float(row["can_sell_qty"])), price=float(row["cost_price"]), pnl=float(row["pl_val"]), gateway_name=self.gateway_name, ) self.on_position(pos) def query_order(self): """""" code, data = self.trade_ctx.order_list_query("", trd_env=self.env) if code: self.write_log(f"查询委托失败:{data}") return self.process_order(data) self.write_log("委托查询成功") def query_trade(self): """""" code, data = self.trade_ctx.deal_list_query("", trd_env=self.env) if code: self.write_log(f"查询成交失败:{data}") return self.process_deal(data) self.write_log("成交查询成功") def close(self): """""" if self.quote_ctx: self.quote_ctx.close() if self.trade_ctx: self.trade_ctx.close() def get_tick(self, code): """ Get tick buffer. """ tick = self.ticks.get(code, None) symbol, exchange = convert_symbol_futu2vt(code) if not tick: tick = TickData( symbol=symbol, exchange=exchange, datetime=datetime.now(CHINA_TZ), gateway_name=self.gateway_name, ) self.ticks[code] = tick contract = self.contracts.get(tick.vt_symbol, None) if contract: tick.name = contract.name return tick def process_quote(self, data): """报价推送""" for ix, row in data.iterrows(): symbol = row["code"] date = row["data_date"].replace("-", "") time = row["data_time"] dt = datetime.strptime(f"{date} {time}", "%Y%m%d %H:%M:%S") dt = CHINA_TZ.localize(dt) tick = self.get_tick(symbol) tick.datetime = dt tick.open_price = row["open_price"] tick.high_price = row["high_price"] tick.low_price = row["low_price"] tick.pre_close = row["prev_close_price"] tick.last_price = row["last_price"] tick.volume = row["volume"] if "price_spread" in row: spread = row["price_spread"] tick.limit_up = tick.last_price + spread * 10 tick.limit_down = tick.last_price - spread * 10 self.on_tick(copy(tick)) def process_orderbook(self, data): """""" symbol = data["code"] tick = self.get_tick(symbol) d = tick.__dict__ for i in range(5): bid_data = data["Bid"][i] ask_data = data["Ask"][i] n = i + 1 d["bid_price_%s" % n] = bid_data[0] d["bid_volume_%s" % n] = bid_data[1] d["ask_price_%s" % n] = ask_data[0] d["ask_volume_%s" % n] = ask_data[1] if tick.datetime: self.on_tick(copy(tick)) def process_order(self, data): """ Process order data for both query and update. """ for ix, row in data.iterrows(): # Ignore order with status DELETED if row["order_status"] == OrderStatus.DELETED: continue symbol, exchange = convert_symbol_futu2vt(row["code"]) order = OrderData( symbol=symbol, exchange=exchange, orderid=str(row["order_id"]), direction=DIRECTION_FUTU2VT[row["trd_side"]], price=float(row["price"]), volume=row["qty"], traded=row["dealt_qty"], status=STATUS_FUTU2VT[row["order_status"]], datetime=generate_datetime(row["create_time"]), gateway_name=self.gateway_name, ) self.on_order(order) def process_deal(self, data): """ Process trade data for both query and update. """ for ix, row in data.iterrows(): tradeid = str(row["deal_id"]) if tradeid in self.trades: continue self.trades.add(tradeid) symbol, exchange = convert_symbol_futu2vt(row["code"]) trade = TradeData( symbol=symbol, exchange=exchange, direction=DIRECTION_FUTU2VT[row["trd_side"]], tradeid=tradeid, orderid=row["order_id"], price=float(row["price"]), volume=row["qty"], datetime=generate_datetime(row["create_time"]), gateway_name=self.gateway_name, ) self.on_trade(trade)
class FutuGateway(VtGateway): """富途接口""" #---------------------------------------------------------------------- def __init__(self, eventEngine, gatewayName='FUTU'): """Constructor""" super(FutuGateway, self).__init__(eventEngine, gatewayName) self.quoteCtx = None self.tradeCtx = None self.host = '' self.ip = 0 self.market = '' self.password = '' self.env = TrdEnv.SIMULATE # 默认仿真交易 self.fileName = self.gatewayName + '_connect.json' self.filePath = getJsonPath(self.fileName, __file__) self.tickDict = {} self.tradeSet = set() # 保存成交编号的集合,防止重复推送 self.qryEnabled = True self.qryThread = Thread(target=self.qryData) #---------------------------------------------------------------------- def writeLog(self, content): """输出日志""" log = VtLogData() log.gatewayName = self.gatewayName log.logContent = content self.onLog(log) #---------------------------------------------------------------------- def writeError(self, code, msg): """输出错误""" error = VtErrorData() error.gatewayName = self.gatewayName error.errorID = code error.errorMsg = msg self.onError(error) #---------------------------------------------------------------------- def connect(self): """连接""" # 载入配置 try: f = open(self.filePath) setting = json.load(f) self.host = setting['host'] self.port = setting['port'] self.market = setting['market'] self.password = setting['password'] self.env = setting['env'] except: self.writeLog(u'载入配置文件出错') return self.connectQuote() self.connectTrade() self.qryThread.start() #---------------------------------------------------------------------- def qryData(self): """初始化时查询数据""" # 等待2秒保证行情和交易接口启动完成 sleep(2.0) # 查询合约、成交、委托、持仓、账户 self.qryContract() self.qryTrade() self.qryOrder() self.qryPosition() self.qryAccount() # 启动循环查询 self.initQuery() #---------------------------------------------------------------------- def connectQuote(self): """连接行情功能""" self.quoteCtx = OpenQuoteContext(self.host, self.port) # 继承实现处理器类 class QuoteHandler(StockQuoteHandlerBase): """报价处理器""" gateway = self # 缓存Gateway对象 def on_recv_rsp(self, rsp_str): ret_code, content = super(QuoteHandler, self).on_recv_rsp(rsp_str) if ret_code != RET_OK: return RET_ERROR, content self.gateway.processQuote(content) return RET_OK, content class OrderBookHandler(OrderBookHandlerBase): """订单簿处理器""" gateway = self def on_recv_rsp(self, rsp_str): ret_code, content = super(OrderBookHandler, self).on_recv_rsp(rsp_str) if ret_code != RET_OK: return RET_ERROR, content self.gateway.processOrderBook(content) return RET_OK, content # 设置回调处理对象 self.quoteCtx.set_handler(QuoteHandler()) self.quoteCtx.set_handler(OrderBookHandler()) # 启动行情 self.quoteCtx.start() self.writeLog(u'行情接口连接成功') #---------------------------------------------------------------------- def connectTrade(self): """连接交易功能""" # 连接交易接口 if self.market == 'US': self.tradeCtx = OpenUSTradeContext(self.host, self.port) else: self.tradeCtx = OpenHKTradeContext(self.host, self.port) # 继承实现处理器类 class OrderHandler(TradeOrderHandlerBase): """委托处理器""" gateway = self # 缓存Gateway对象 def on_recv_rsp(self, rsp_str): ret_code, content = super(OrderHandler, self).on_recv_rsp(rsp_str) if ret_code != RET_OK: return RET_ERROR, content self.gateway.processOrder(content) return RET_OK, content class DealHandler(TradeDealHandlerBase): """订单簿处理器""" gateway = self def on_recv_rsp(self, rsp_str): ret_code, content = super(DealHandler, self).on_recv_rsp(rsp_str) if ret_code != RET_OK: return RET_ERROR, content self.gateway.processDeal(content) return RET_OK, content # 只有港股实盘交易才需要解锁 code, data = self.tradeCtx.unlock_trade(self.password) if code == RET_OK: self.writeLog(u'交易接口解锁成功') else: self.writeLog(u'交易接口解锁失败,原因:%s' %data) # 设置回调处理对象 self.tradeCtx.set_handler(OrderHandler()) self.tradeCtx.set_handler(DealHandler()) # 启动交易接口 self.tradeCtx.start() self.writeLog(u'交易接口连接成功') #---------------------------------------------------------------------- def subscribe(self, subscribeReq): """订阅行情""" for data_type in ['QUOTE', 'ORDER_BOOK']: code, data = self.quoteCtx.subscribe(subscribeReq.symbol, data_type, True) if code: self.writeError(code, u'订阅行情失败:%s' %data) #---------------------------------------------------------------------- def sendOrder(self, orderReq): """发单""" side = directionMap[orderReq.direction] priceType = OrderType.NORMAL # 只支持限价单 # 设置价格调整模式为向内调整(即买入调整后价格比原始价格低) if orderReq.direction == DIRECTION_LONG: adjustLimit = 0.05 else: adjustLimit = -0.05 code, data = self.tradeCtx.place_order(orderReq.price, orderReq.volume, orderReq.symbol, side, priceType, trd_env=self.env, adjust_limit=adjustLimit) if code: self.writeError(code, u'委托失败:%s' %data) return '' for ix, row in data.iterrows(): orderID = str(row['order_id']) vtOrderID = '.'.join([self.gatewayName, orderID]) return vtOrderID #---------------------------------------------------------------------- def cancelOrder(self, cancelOrderReq): """撤单""" code, data = self.tradeCtx.modify_order(ModifyOrderOp.CANCEL, cancelOrderReq.orderID, 0, 0, trd_env=self.env) if code: self.writeError(code, u'撤单失败:%s' %data) return #---------------------------------------------------------------------- def qryContract(self): """查询合约""" for vtProductClass, product in productMap.items(): code, data = self.quoteCtx.get_stock_basicinfo(self.market, product) if code: self.writeError(code, u'查询合约信息失败:%s' %data) return for ix, row in data.iterrows(): contract = VtContractData() contract.gatewayName = self.gatewayName contract.symbol = row['code'] contract.vtSymbol = contract.symbol contract.name = row['name'] contract.productClass = vtProductClass contract.size = int(row['lot_size']) contract.priceTick = 0.001 self.onContract(contract) self.writeLog(u'合约信息查询成功') #---------------------------------------------------------------------- def qryAccount(self): """查询账户资金""" code, data = self.tradeCtx.accinfo_query(trd_env=self.env, acc_id=0) if code: self.writeError(code, u'查询账户资金失败:%s' %data) return for ix, row in data.iterrows(): account = VtAccountData() account.gatewayName = self.gatewayName account.accountID = '%s_%s' %(self.gatewayName, self.market) account.vtAccountID = '.'.join([self.gatewayName, account.accountID]) account.balance = float(row['total_assets']) account.available = float(row['avl_withdrawal_cash']) self.onAccount(account) #---------------------------------------------------------------------- def qryPosition(self): """查询持仓""" code, data = self.tradeCtx.position_list_query(trd_env=self.env, acc_id=0) if code: self.writeError(code, u'查询持仓失败:%s' %data) return for ix, row in data.iterrows(): pos = VtPositionData() pos.gatewayName = self.gatewayName pos.symbol = row['code'] pos.vtSymbol = pos.symbol pos.direction = DIRECTION_LONG pos.vtPositionName = '.'.join([pos.vtSymbol, pos.direction]) pos.position = float(row['qty']) pos.price = float(row['cost_price']) pos.positionProfit = float(row['pl_val']) pos.frozen = float(row['qty']) - float(row['can_sell_qty']) if pos.price < 0: pos.price = 0 if pos.positionProfit > 100000000: pos.positionProfit = 0 self.onPosition(pos) #---------------------------------------------------------------------- def qryOrder(self): """查询委托""" code, data = self.tradeCtx.order_list_query("", trd_env=self.env) if code: self.writeError(code, u'查询委托失败:%s' %data) return self.processOrder(data) self.writeLog(u'委托查询成功') #---------------------------------------------------------------------- def qryTrade(self): """查询成交""" code, data = self.tradeCtx.deal_list_query("", trd_env=self.env) if code: self.writeError(code, u'查询成交失败:%s' %data) return self.processDeal(data) self.writeLog(u'成交查询成功') #---------------------------------------------------------------------- def close(self): """关闭""" if self.quoteCtx: self.quoteCtx.close() if self.tradeCtx: self.tradeCtx.close() #---------------------------------------------------------------------- def initQuery(self): """初始化连续查询""" if self.qryEnabled: # 需要循环的查询函数列表 self.qryFunctionList = [self.qryAccount, self.qryPosition] self.qryCount = 0 # 查询触发倒计时 self.qryTrigger = 2 # 查询触发点 self.qryNextFunction = 0 # 上次运行的查询函数索引 self.startQuery() #---------------------------------------------------------------------- def query(self, event): """注册到事件处理引擎上的查询函数""" self.qryCount += 1 if self.qryCount > self.qryTrigger: # 清空倒计时 self.qryCount = 0 # 执行查询函数 function = self.qryFunctionList[self.qryNextFunction] function() # 计算下次查询函数的索引,如果超过了列表长度,则重新设为0 self.qryNextFunction += 1 if self.qryNextFunction == len(self.qryFunctionList): self.qryNextFunction = 0 #---------------------------------------------------------------------- def startQuery(self): """启动连续查询""" self.eventEngine.register(EVENT_TIMER, self.query) #---------------------------------------------------------------------- def setQryEnabled(self, qryEnabled): """设置是否要启动循环查询""" self.qryEnabled = qryEnabled #---------------------------------------------------------------------- def processQuote(self, data): """报价推送""" for ix, row in data.iterrows(): symbol = row['code'] tick = self.tickDict.get(symbol, None) if not tick: tick = VtTickData() tick.symbol = symbol tick.vtSymbol = tick.symbol tick.gatewayName = self.gatewayName self.tickDict[symbol] = tick tick.date = row['data_date'].replace('-', '') tick.time = row['data_time'] tick.datetime = datetime.strptime(' '.join([tick.date, tick.time]), '%Y%m%d %H:%M:%S') tick.openPrice = row['open_price'] tick.highPrice = row['high_price'] tick.lowPrice = row['low_price'] tick.preClosePrice = row['prev_close_price'] tick.lastPrice = row['last_price'] tick.volume = row['volume'] if 'price_spread' in row: spread = row['price_spread'] tick.upperLimit = tick.lastPrice + spread * 10 tick.lowerLimit = tick.lastPrice - spread * 10 newTick = copy(tick) self.onTick(newTick) #---------------------------------------------------------------------- def processOrderBook(self, data): """订单簿推送""" symbol = data['code'] tick = self.tickDict.get(symbol, None) if not tick: tick = VtTickData() tick.symbol = symbol tick.vtSymbol = tick.symbol tick.gatewayName = self.gatewayName self.tickDict[symbol] = tick d = tick.__dict__ for i in range(5): bidData = data['Bid'][i] askData = data['Ask'][i] n = i + 1 d['bidPrice%s' %n] = bidData[0] d['bidVolume%s' %n] = bidData[1] d['askPrice%s' %n] = askData[0] d['askVolume%s' %n] = askData[1] if tick.datetime: newTick = copy(tick) self.onTick(newTick) #---------------------------------------------------------------------- def processOrder(self, data): """处理委托推送""" for ix, row in data.iterrows(): # 如果状态是已经删除,则直接忽略 if row['order_status'] == OrderStatus.DELETED: continue print(row['order_status']) order = VtOrderData() order.gatewayName = self.gatewayName order.symbol = row['code'] order.vtSymbol = order.symbol order.orderID = str(row['order_id']) order.vtOrderID = '.'.join([self.gatewayName, order.orderID]) order.price = float(row['price']) order.totalVolume = float(row['qty']) order.tradedVolume = float(row['dealt_qty']) order.orderTime = row['create_time'].split(' ')[-1] order.status = statusMapReverse.get(row['order_status'], STATUS_UNKNOWN) order.direction = directionMapReverse[row['trd_side']] self.onOrder(order) #---------------------------------------------------------------------- def processDeal(self, data): """处理成交推送""" for ix, row in data.iterrows(): tradeID = str(row['deal_id']) if tradeID in self.tradeSet: continue self.tradeSet.add(tradeID) trade = VtTradeData() trade.gatewayName = self.gatewayName trade.symbol = row['code'] trade.vtSymbol = trade.symbol trade.tradeID = tradeID trade.vtTradeID = '.'.join([self.gatewayName, trade.tradeID]) trade.orderID = row['order_id'] trade.vtOrderID = '.'.join([self.gatewayName, trade.orderID]) trade.price = float(row['price']) trade.volume = float(row['qty']) trade.direction = directionMapReverse[row['trd_side']] trade.tradeTime = row['create_time'].split(' ')[-1] self.onTrade(trade)
class FutuGateway(VtGateway): """富途接口""" #---------------------------------------------------------------------- def __init__(self, eventEngine, gatewayName='FUTU'): """Constructor""" super(FutuGateway, self).__init__(eventEngine, gatewayName) self.quoteCtx = None self.tradeCtx = None self.host = '' self.ip = 0 self.market = '' self.password = '' self.env = TrdEnv.SIMULATE # 默认仿真交易 self.fileName = self.gatewayName + '_connect.json' self.filePath = getJsonPath(self.fileName, __file__) self.tickDict = {} self.tradeSet = set() # 保存成交编号的集合,防止重复推送 self.qryEnabled = True self.qryThread = Thread(target=self.qryData) #---------------------------------------------------------------------- def writeLog(self, content): """输出日志""" log = VtLogData() log.gatewayName = self.gatewayName log.logContent = content self.onLog(log) #---------------------------------------------------------------------- def writeError(self, code, msg): """输出错误""" error = VtErrorData() error.gatewayName = self.gatewayName error.errorID = code error.errorMsg = msg self.onError(error) #---------------------------------------------------------------------- def connect(self): """连接""" # 载入配置 try: f = open(self.filePath) setting = json.load(f) self.host = setting['host'] self.port = setting['port'] self.market = setting['market'] self.password = setting['password'] self.env = setting['env'] except: self.writeLog(u'载入配置文件出错') return self.connectQuote() self.connectTrade() self.qryThread.start() #---------------------------------------------------------------------- def qryData(self): """初始化时查询数据""" # 等待2秒保证行情和交易接口启动完成 sleep(2.0) # 查询合约、成交、委托、持仓、账户 self.qryContract() self.qryTrade() self.qryOrder() self.qryPosition() self.qryAccount() # 启动循环查询 self.initQuery() #---------------------------------------------------------------------- def connectQuote(self): """连接行情功能""" self.quoteCtx = OpenQuoteContext(self.host, self.port) # 继承实现处理器类 class QuoteHandler(StockQuoteHandlerBase): """报价处理器""" gateway = self # 缓存Gateway对象 def on_recv_rsp(self, rsp_str): ret_code, content = super(QuoteHandler, self).on_recv_rsp(rsp_str) if ret_code != RET_OK: return RET_ERROR, content self.gateway.processQuote(content) return RET_OK, content class OrderBookHandler(OrderBookHandlerBase): """订单簿处理器""" gateway = self def on_recv_rsp(self, rsp_str): ret_code, content = super(OrderBookHandler, self).on_recv_rsp(rsp_str) if ret_code != RET_OK: return RET_ERROR, content self.gateway.processOrderBook(content) return RET_OK, content # 设置回调处理对象 self.quoteCtx.set_handler(QuoteHandler()) self.quoteCtx.set_handler(OrderBookHandler()) # 启动行情 self.quoteCtx.start() self.writeLog(u'行情接口连接成功') #---------------------------------------------------------------------- def connectTrade(self): """连接交易功能""" # 连接交易接口 if self.market == 'US': self.tradeCtx = OpenUSTradeContext(self.host, self.port) else: self.tradeCtx = OpenHKTradeContext(self.host, self.port) # 继承实现处理器类 class OrderHandler(TradeOrderHandlerBase): """委托处理器""" gateway = self # 缓存Gateway对象 def on_recv_rsp(self, rsp_str): ret_code, content = super(OrderHandler, self).on_recv_rsp(rsp_str) if ret_code != RET_OK: return RET_ERROR, content self.gateway.processOrder(content) return RET_OK, content class DealHandler(TradeDealHandlerBase): """订单簿处理器""" gateway = self def on_recv_rsp(self, rsp_str): ret_code, content = super(DealHandler, self).on_recv_rsp(rsp_str) if ret_code != RET_OK: return RET_ERROR, content self.gateway.processDeal(content) return RET_OK, content # 只有港股实盘交易才需要解锁 code, data = self.tradeCtx.unlock_trade(self.password) if code == RET_OK: self.writeLog(u'交易接口解锁成功') else: self.writeLog(u'交易接口解锁失败,原因:%s' % data) # 设置回调处理对象 self.tradeCtx.set_handler(OrderHandler()) self.tradeCtx.set_handler(DealHandler()) # 启动交易接口 self.tradeCtx.start() self.writeLog(u'交易接口连接成功') #---------------------------------------------------------------------- def subscribe(self, subscribeReq): """订阅行情""" for data_type in ['QUOTE', 'ORDER_BOOK']: code, data = self.quoteCtx.subscribe(subscribeReq.symbol, data_type, True) if code: self.writeError(code, u'订阅行情失败:%s' % data) #---------------------------------------------------------------------- def sendOrder(self, orderReq): """发单""" side = directionMap[orderReq.direction] priceType = OrderType.NORMAL # 只支持限价单 # 设置价格调整模式为向内调整(即买入调整后价格比原始价格低) if orderReq.direction == DIRECTION_LONG: adjustLimit = 0.05 else: adjustLimit = -0.05 code, data = self.tradeCtx.place_order(orderReq.price, orderReq.volume, orderReq.symbol, side, priceType, trd_env=self.env, adjust_limit=adjustLimit) if code: self.writeError(code, u'委托失败:%s' % data) return '' for ix, row in data.iterrows(): orderID = str(row['order_id']) vtOrderID = '.'.join([self.gatewayName, orderID]) return vtOrderID #---------------------------------------------------------------------- def cancelOrder(self, cancelOrderReq): """撤单""" code, data = self.tradeCtx.modify_order(ModifyOrderOp.CANCEL, cancelOrderReq.orderID, 0, 0, trd_env=self.env) if code: self.writeError(code, u'撤单失败:%s' % data) return #---------------------------------------------------------------------- def qryContract(self): """查询合约""" for vtProductClass, product in productMap.items(): code, data = self.quoteCtx.get_stock_basicinfo( self.market, product) if code: self.writeError(code, u'查询合约信息失败:%s' % data) return for ix, row in data.iterrows(): contract = VtContractData() contract.gatewayName = self.gatewayName contract.symbol = row['code'] contract.vtSymbol = contract.symbol contract.name = row['name'] contract.productClass = vtProductClass contract.size = int(row['lot_size']) contract.priceTick = 0.001 self.onContract(contract) self.writeLog(u'合约信息查询成功') #---------------------------------------------------------------------- def qryAccount(self): """查询账户资金""" code, data = self.tradeCtx.accinfo_query(trd_env=self.env, acc_id=0) if code: self.writeError(code, u'查询账户资金失败:%s' % data) return for ix, row in data.iterrows(): account = VtAccountData() account.gatewayName = self.gatewayName account.accountID = '%s_%s' % (self.gatewayName, self.market) account.vtAccountID = '.'.join( [self.gatewayName, account.accountID]) account.balance = float(row['total_assets']) account.available = float(row['avl_withdrawal_cash']) self.onAccount(account) #---------------------------------------------------------------------- def qryPosition(self): """查询持仓""" code, data = self.tradeCtx.position_list_query(trd_env=self.env, acc_id=0) if code: self.writeError(code, u'查询持仓失败:%s' % data) return for ix, row in data.iterrows(): pos = VtPositionData() pos.gatewayName = self.gatewayName pos.symbol = row['code'] pos.vtSymbol = pos.symbol pos.direction = DIRECTION_LONG pos.vtPositionName = '.'.join([pos.vtSymbol, pos.direction]) pos.position = float(row['qty']) pos.price = float(row['cost_price']) pos.positionProfit = float(row['pl_val']) pos.frozen = float(row['qty']) - float(row['can_sell_qty']) if pos.price < 0: pos.price = 0 if pos.positionProfit > 100000000: pos.positionProfit = 0 self.onPosition(pos) #---------------------------------------------------------------------- def qryOrder(self): """查询委托""" code, data = self.tradeCtx.order_list_query("", trd_env=self.env) if code: self.writeError(code, u'查询委托失败:%s' % data) return self.processOrder(data) self.writeLog(u'委托查询成功') #---------------------------------------------------------------------- def qryTrade(self): """查询成交""" code, data = self.tradeCtx.deal_list_query("", trd_env=self.env) if code: self.writeError(code, u'查询成交失败:%s' % data) return self.processDeal(data) self.writeLog(u'成交查询成功') #---------------------------------------------------------------------- def close(self): """关闭""" if self.quoteCtx: self.quoteCtx.close() if self.tradeCtx: self.tradeCtx.close() #---------------------------------------------------------------------- def initQuery(self): """初始化连续查询""" if self.qryEnabled: # 需要循环的查询函数列表 self.qryFunctionList = [self.qryAccount, self.qryPosition] self.qryCount = 0 # 查询触发倒计时 self.qryTrigger = 2 # 查询触发点 self.qryNextFunction = 0 # 上次运行的查询函数索引 self.startQuery() #---------------------------------------------------------------------- def query(self, event): """注册到事件处理引擎上的查询函数""" self.qryCount += 1 if self.qryCount > self.qryTrigger: # 清空倒计时 self.qryCount = 0 # 执行查询函数 function = self.qryFunctionList[self.qryNextFunction] function() # 计算下次查询函数的索引,如果超过了列表长度,则重新设为0 self.qryNextFunction += 1 if self.qryNextFunction == len(self.qryFunctionList): self.qryNextFunction = 0 #---------------------------------------------------------------------- def startQuery(self): """启动连续查询""" self.eventEngine.register(EVENT_TIMER, self.query) #---------------------------------------------------------------------- def setQryEnabled(self, qryEnabled): """设置是否要启动循环查询""" self.qryEnabled = qryEnabled #---------------------------------------------------------------------- def processQuote(self, data): """报价推送""" for ix, row in data.iterrows(): symbol = row['code'] tick = self.tickDict.get(symbol, None) if not tick: tick = VtTickData() tick.symbol = symbol tick.vtSymbol = tick.symbol tick.gatewayName = self.gatewayName self.tickDict[symbol] = tick tick.date = row['data_date'].replace('-', '') tick.time = row['data_time'] tick.datetime = datetime.strptime(' '.join([tick.date, tick.time]), '%Y%m%d %H:%M:%S') tick.openPrice = row['open_price'] tick.highPrice = row['high_price'] tick.lowPrice = row['low_price'] tick.preClosePrice = row['prev_close_price'] tick.lastPrice = row['last_price'] tick.volume = row['volume'] if 'price_spread' in row: spread = row['price_spread'] tick.upperLimit = tick.lastPrice + spread * 10 tick.lowerLimit = tick.lastPrice - spread * 10 newTick = copy(tick) self.onTick(newTick) #---------------------------------------------------------------------- def processOrderBook(self, data): """订单簿推送""" symbol = data['code'] tick = self.tickDict.get(symbol, None) if not tick: tick = VtTickData() tick.symbol = symbol tick.vtSymbol = tick.symbol tick.gatewayName = self.gatewayName self.tickDict[symbol] = tick d = tick.__dict__ for i in range(5): bidData = data['Bid'][i] askData = data['Ask'][i] n = i + 1 d['bidPrice%s' % n] = bidData[0] d['bidVolume%s' % n] = bidData[1] d['askPrice%s' % n] = askData[0] d['askVolume%s' % n] = askData[1] if tick.datetime: newTick = copy(tick) self.onTick(newTick) #---------------------------------------------------------------------- def processOrder(self, data): """处理委托推送""" for ix, row in data.iterrows(): # 如果状态是已经删除,则直接忽略 if row['order_status'] == OrderStatus.DELETED: continue print(row['order_status']) order = VtOrderData() order.gatewayName = self.gatewayName order.symbol = row['code'] order.vtSymbol = order.symbol order.orderID = str(row['order_id']) order.vtOrderID = '.'.join([self.gatewayName, order.orderID]) order.price = float(row['price']) order.totalVolume = float(row['qty']) order.tradedVolume = float(row['dealt_qty']) order.orderTime = row['create_time'].split(' ')[-1] order.status = statusMapReverse.get(row['order_status'], STATUS_UNKNOWN) order.direction = directionMapReverse[row['trd_side']] self.onOrder(order) #---------------------------------------------------------------------- def processDeal(self, data): """处理成交推送""" for ix, row in data.iterrows(): tradeID = str(row['deal_id']) if tradeID in self.tradeSet: continue self.tradeSet.add(tradeID) trade = VtTradeData() trade.gatewayName = self.gatewayName trade.symbol = row['code'] trade.vtSymbol = trade.symbol trade.tradeID = tradeID trade.vtTradeID = '.'.join([self.gatewayName, trade.tradeID]) trade.orderID = row['order_id'] trade.vtOrderID = '.'.join([self.gatewayName, trade.orderID]) trade.price = float(row['price']) trade.volume = float(row['qty']) trade.direction = directionMapReverse[row['trd_side']] trade.tradeTime = row['create_time'].split(' ')[-1] self.onTrade(trade)
class FutuGateway(BaseGateway): """""" default_setting = { "password": "", "host": "127.0.0.1", "port": 11111, "market": ["HK", "US"], "env": [TrdEnv.REAL, TrdEnv.SIMULATE], } def __init__(self, event_engine): """Constructor""" super(FutuGateway, self).__init__(event_engine, "FUTU") self.quote_ctx = None self.trade_ctx = None self.host = "" self.port = 0 self.market = "" self.password = "" self.env = TrdEnv.SIMULATE self.ticks = {} self.trades = set() self.contracts = {} self.thread = Thread(target=self.query_data) # For query function. self.count = 0 self.interval = 1 self.query_funcs = [self.query_account, self.query_position] def connect(self, setting: dict): """""" self.host = setting["host"] self.port = setting["port"] self.market = setting["market"] self.password = setting["password"] self.env = setting["env"] self.connect_quote() self.connect_trade() self.thread.start() def query_data(self): """ Query all data necessary. """ sleep(2.0) # Wait 2 seconds till connection completed. self.query_contract() self.query_trade() self.query_order() self.query_position() self.query_account() # Start fixed interval query. self.event_engine.register(EVENT_TIMER, self.process_timer_event) def process_timer_event(self, event): """""" self.count += 1 if self.count < self.interval: return self.count = 0 func = self.query_funcs.pop(0) func() self.query_funcs.append(func) def connect_quote(self): """ Connect to market data server. """ self.quote_ctx = OpenQuoteContext(self.host, self.port) class QuoteHandler(StockQuoteHandlerBase): gateway = self def on_recv_rsp(self, rsp_str): ret_code, content = super(QuoteHandler, self).on_recv_rsp( rsp_str ) if ret_code != RET_OK: return RET_ERROR, content self.gateway.process_quote(content) return RET_OK, content class OrderBookHandler(OrderBookHandlerBase): gateway = self def on_recv_rsp(self, rsp_str): ret_code, content = super(OrderBookHandler, self).on_recv_rsp( rsp_str ) if ret_code != RET_OK: return RET_ERROR, content self.gateway.process_orderbook(content) return RET_OK, content self.quote_ctx.set_handler(QuoteHandler()) self.quote_ctx.set_handler(OrderBookHandler()) self.quote_ctx.start() self.write_log("行情接口连接成功") def connect_trade(self): """ Connect to trade server. """ # Initialize context according to market. if self.market == "US": self.trade_ctx = OpenUSTradeContext(self.host, self.port) else: self.trade_ctx = OpenHKTradeContext(self.host, self.port) # Implement handlers. class OrderHandler(TradeOrderHandlerBase): gateway = self def on_recv_rsp(self, rsp_str): ret_code, content = super(OrderHandler, self).on_recv_rsp( rsp_str ) if ret_code != RET_OK: return RET_ERROR, content self.gateway.process_order(content) return RET_OK, content class DealHandler(TradeDealHandlerBase): gateway = self def on_recv_rsp(self, rsp_str): ret_code, content = super(DealHandler, self).on_recv_rsp( rsp_str ) if ret_code != RET_OK: return RET_ERROR, content self.gateway.process_deal(content) return RET_OK, content # Unlock to allow trading. code, data = self.trade_ctx.unlock_trade(self.password) if code == RET_OK: self.write_log("交易接口解锁成功") else: self.write_log(f"交易接口解锁失败,原因:{data}") # Start context. self.trade_ctx.set_handler(OrderHandler()) self.trade_ctx.set_handler(DealHandler()) self.trade_ctx.start() self.write_log("交易接口连接成功") def subscribe(self, req: SubscribeRequest): """""" for data_type in ["QUOTE", "ORDER_BOOK"]: futu_symbol = convert_symbol_vt2futu(req.symbol, req.exchange) code, data = self.quote_ctx.subscribe(futu_symbol, data_type, True) if code: self.write_log(f"订阅行情失败:{data}") def send_order(self, req: OrderRequest): """""" side = DIRECTION_VT2FUTU[req.direction] price_type = OrderType.NORMAL # Only limit order is supported. # Set price adjustment mode to inside adjustment. if req.direction is Direction.LONG: adjust_limit = 0.05 else: adjust_limit = -0.05 futu_symbol = convert_symbol_vt2futu(req.symbol, req.exchange) code, data = self.trade_ctx.place_order( req.price, req.volume, futu_symbol, side, price_type, trd_env=self.env, adjust_limit=adjust_limit, ) if code: self.write_log(f"委托失败:{data}") return "" for ix, row in data.iterrows(): orderid = str(row["order_id"]) order = req.create_order_data(orderid, self.gateway_name) self.on_order(order) return order.vt_orderid def cancel_order(self, req: CancelRequest): """""" code, data = self.trade_ctx.modify_order( ModifyOrderOp.CANCEL, req.orderid, 0, 0, trd_env=self.env ) if code: self.write_log(f"撤单失败:{data}") def query_contract(self): """""" for product, futu_product in PRODUCT_VT2FUTU.items(): code, data = self.quote_ctx.get_stock_basicinfo( self.market, futu_product ) if code: self.write_log(f"查询合约信息失败:{data}") return for ix, row in data.iterrows(): symbol, exchange = convert_symbol_futu2vt(row["code"]) contract = ContractData( symbol=symbol, exchange=exchange, name=row["name"], product=product, size=1, pricetick=0.001, gateway_name=self.gateway_name, ) self.on_contract(contract) self.contracts[contract.vt_symbol] = contract self.write_log("合约信息查询成功") def query_account(self): """""" code, data = self.trade_ctx.accinfo_query(trd_env=self.env, acc_id=0) if code: self.write_log(f"查询账户资金失败:{data}") return for ix, row in data.iterrows(): account = AccountData( accountid=f"{self.gateway_name}_{self.market}", balance=float(row["total_assets"]), frozen=(float(row["total_assets"]) - float(row["avl_withdrawal_cash"])), gateway_name=self.gateway_name, ) self.on_account(account) def query_position(self): """""" code, data = self.trade_ctx.position_list_query( trd_env=self.env, acc_id=0 ) if code: self.write_log(f"查询持仓失败:{data}") return for ix, row in data.iterrows(): symbol, exchange = convert_symbol_futu2vt(row["code"]) pos = PositionData( symbol=symbol, exchange=exchange, direction=Direction.LONG, volume=float(row["qty"]), frozen=(float(row["qty"]) - float(row["can_sell_qty"])), price=float(row["pl_val"]), pnl=float(row["cost_price"]), gateway_name=self.gateway_name, ) self.on_position(pos) def query_order(self): """""" code, data = self.trade_ctx.order_list_query("", trd_env=self.env) if code: self.write_log(f"查询委托失败:{data}") return self.process_order(data) self.write_log("委托查询成功") def query_trade(self): """""" code, data = self.trade_ctx.deal_list_query("", trd_env=self.env) if code: self.write_log(f"查询成交失败:{data}") return self.process_deal(data) self.write_log("成交查询成功") def close(self): """""" if self.quote_ctx: self.quote_ctx.close() if self.trade_ctx: self.trade_ctx.close() def get_tick(self, code): """ Get tick buffer. """ tick = self.ticks.get(code, None) symbol, exchange = convert_symbol_futu2vt(code) if not tick: tick = TickData( symbol=symbol, exchange=exchange, datetime=datetime.now(), gateway_name=self.gateway_name, ) self.ticks[code] = tick contract = self.contracts.get(tick.vt_symbol, None) if contract: tick.name = contract.name return tick def process_quote(self, data): """报价推送""" for ix, row in data.iterrows(): symbol = row["code"] tick = self.get_tick(symbol) date = row["data_date"].replace("-", "") time = row["data_time"] tick.datetime = datetime.strptime( f"{date} {time}", "%Y%m%d %H:%M:%S") tick.open_price = row["open_price"] tick.high_price = row["high_price"] tick.low_price = row["low_price"] tick.pre_close = row["prev_close_price"] tick.last_price = row["last_price"] tick.volume = row["volume"] if "price_spread" in row: spread = row["price_spread"] tick.limit_up = tick.last_price + spread * 10 tick.limit_down = tick.last_price - spread * 10 self.on_tick(copy(tick)) def process_orderbook(self, data): """""" symbol = data["code"] tick = self.get_tick(symbol) d = tick.__dict__ for i in range(5): bid_data = data["Bid"][i] ask_data = data["Ask"][i] n = i + 1 d["bid_price_%s" % n] = bid_data[0] d["bid_volume_%s" % n] = bid_data[1] d["ask_price_%s" % n] = ask_data[0] d["ask_volume_%s" % n] = ask_data[1] if tick.datetime: self.on_tick(copy(tick)) def process_order(self, data): """ Process order data for both query and update. """ for ix, row in data.iterrows(): # Ignore order with status DELETED if row["order_status"] == OrderStatus.DELETED: continue symbol, exchange = convert_symbol_futu2vt(row["code"]) order = OrderData( symbol=symbol, exchange=exchange, orderid=str(row["order_id"]), direction=DIRECTION_FUTU2VT[row["trd_side"]], price=float(row["price"]), volume=float(row["qty"]), traded=float(row["dealt_qty"]), status=STATUS_FUTU2VT[row["order_status"]], time=row["create_time"].split(" ")[-1], gateway_name=self.gateway_name, ) self.on_order(order) def process_deal(self, data): """ Process trade data for both query and update. """ for ix, row in data.iterrows(): tradeid = str(row["deal_id"]) if tradeid in self.trades: continue self.trades.add(tradeid) symbol, exchange = convert_symbol_futu2vt(row["code"]) trade = TradeData( symbol=symbol, exchange=exchange, direction=DIRECTION_FUTU2VT[row["trd_side"]], tradeid=tradeid, orderid=row["order_id"], price=float(row["price"]), volume=float(row["qty"]), time=row["create_time"].split(" ")[-1], gateway_name=self.gateway_name, ) self.on_trade(trade)
class FutuAPI: # DATA_PATH = "/Users/joseph/Dropbox/code/stat-arb/data" DATA_PATH = "/home/atabet/projects/data" HK_EQUITY_AM_START = timer(9, 30, 0) HK_EQUITY_AM_END = timer(12, 0, 0) HK_EQUITY_PM_START = timer(13, 0, 0) HK_EQUITY_PM_END = timer(16, 0, 0) def __init__(self): self.subscribe_data = dict() self.broker_queue = dict() def __enter__(self): self.quote_ctx = OpenQuoteContext(host='127.0.0.1', port=11111) self.connect_quote() return self def __exit__(self, type, value, trace): self.quote_ctx.close() # 结束后记得关闭当条连接,防止连接条数用尽 print("Close quote context!") def subscribe(self, codes: List[str], sub_types: List[SubType], sub_push: bool): ret_sub, err_message = self.quote_ctx.subscribe( codes, sub_types, subscribe_push=sub_push) if ret_sub == RET_ERROR: raise ValueError(f"subscribe error: {err_message}") def get_global_state(self) -> dict: """ 获取全局市场状态 https://openapi.futunn.com/futu-api-doc/quote/get-global-state.html :return: (0, {'market_sz': 'CLOSED', 'market_us': 'PRE_MARKET_BEGIN', 'market_sh': 'CLOSED', 'market_hk': 'CLOSED', 'market_hkfuture': 'FUTURE_DAY_OPEN', 'market_usfuture': 'FUTURE_OPEN', 'server_ver': '217', 'trd_logined': True, 'timestamp': '1602491044', 'qot_logined': True, 'local_timestamp': 1602491044.555623, 'program_status_type': 'READY', 'program_status_desc': ''}) """ ret, data = self.quote_ctx.get_global_state() if ret == RET_OK: return data raise ValueError(f"get_global_state error: {data}") def get_capital_distribution(self, code: str = "HK.00700"): """ 获取资金分布 :param code: 股票代号 :return: """ ret, data = self.quote_ctx.get_capital_distribution(code) if ret == RET_OK: return data else: print('error:', data) def get_capital_flow(self, code: str = "HK.00700"): """ 获取资金流向 :param code: 股票代号 :return: """ ret, data = self.quote_ctx.get_capital_flow(code) if ret == RET_OK: return data else: print('error:', data) def get_broker_queue(self, code: str = "HK.00700"): """ 获取实时经纪队列 :param code: :return: """ # 如果通过推送获取数据,直接在缓存里提取最新的copy if code in self.broker_queue: return self.broker_queue[code] # 否则通过富途服务器获取。先订阅经纪队列类型。订阅成功后FutuOpenD将持续收到服务器的推送,False代表暂时不需要推送给脚本 if (code not in self.subscribe_data): ret_sub, err_message = self.quote_ctx.subscribe( [code], [SubType.BROKER], subscribe_push=False) if ret_sub != RET_OK: print(f"获取实时经纪队列(get_broker_queue)失败, {err_message}") self.subscribe_data[code] = dict() self.subscribe_data[code][SubType.BROKER] = True elif (SubType.BROKER not in self.subscribe_data[code]): ret_sub, err_message = self.quote_ctx.subscribe( [code], [SubType.BROKER], subscribe_push=False) if ret_sub != RET_OK: print(f"获取实时经纪队列(get_broker_queue)失败, {err_message}") self.subscribe_data[code][SubType.BROKER] = True ret, bid_frame_table, ask_frame_table = self.quote_ctx.get_broker_queue( code) # 获取一次经纪队列数据 if ret == RET_OK: return bid_frame_table, ask_frame_table else: print('error:', bid_frame_table) def process_broker_queue(self, data: pd.DataFrame): bid_broker, ask_broker = data codes = list(bid_broker["code"].unique()) assert len(codes) == 1, f"broker_queue pushback 不是合法的数据:{codes}" code = codes[0] self.broker_queue[code] = (bid_broker, ask_broker) def connect_quote(self): class BrokerQueueHandler(BrokerHandlerBase): api = self def on_recv_rsp(self, rsp_pb): ret_code, err_or_stock_code, data = super( BrokerQueueHandler, self).on_recv_rsp(rsp_pb) if ret_code != RET_OK: print( "BrokerTest: error, msg: {}".format(err_or_stock_code)) return RET_ERROR, data api.process_broker_queue(data) # BrokerQueueHandler自己的处理逻辑 return RET_OK, data self.quote_ctx.set_handler(BrokerQueueHandler()) self.quote_ctx.start() def get_history_kl_quota(self, get_detail: bool = False): """ 获取历史 K 线额度使用明细 接口限制 ------ 我们会根据您账户的资产和交易的情况,下发历史 K 线额度。因此,30 天内您只能获取有限只股票的历史 K 线数据。具体规则参见 API 用户额度 。 您当日消耗的历史 K 线额度,会在 30 天后自动释放。 https://openapi.futunn.com/futu-api-doc/quote/get-history-kl-quota.html :param get_detail: 设置True代表需要返回详细的拉取历史K 线的记录 :return: 例子 (1, 99, [{'code': 'HK.00700', 'request_time': '2020-03-27 19:15:57'}]) """ ret, data = self.quote_ctx.get_history_kl_quota(get_detail=get_detail) if ret == RET_OK: return data else: print('error:', data) def request_history_kline(self, code: str, start: str, end: str, ktype: KLType = KLType.K_DAY, autype: AuType = AuType.QFQ, fields: List[KL_FIELD] = [KL_FIELD.ALL], max_count: int = 500, extended_time: bool = False): """ 获取历史 K 线 接口限制 ------- 我们会根据您账户的资产和交易的情况,下发历史 K 线额度。因此,30 天内您只能获取有限只股票的历史 K 线数据。具体规则参见 API 用户额度 。您当日消耗的 历史 K 线额度,会在 30 天后自动释放。 每 30 秒内最多请求 60 次历史 K 线接口。注意:如果您是分页获取数据,此限频规则仅适用于每只股票的首页,后续页请求不受限频规则的限制。 分 K 提供最近 2 年数据,日 K 及以上提供最近 10 年的数据。 美股盘前和盘后 K 线仅支持 60 分钟及以下级别。由于美股盘前和盘后时段为非常规交易时段,此时段的 K 线数据可能不足 2 年。 https://openapi.futunn.com/futu-api-doc/quote/request-history-kline.html :param code: 'HK.00700' :param start: '2019-09-11' :param end: '2019-09-18' :param ktype: KLType.K_DAY, :param autype: AuType.QFQ, :param fields: [KL_FIELD.ALL], :param max_count: 500, :param extended_time: False :return: """ ret, data, page_req_key = self.quote_ctx.request_history_kline( code=code, start=start, end=end, ktype=ktype, autype=autype, fields=fields, max_count=max_count, extended_time=extended_time, ) # 每页max_count个,请求第一页 if ret == RET_OK: yield data else: print('error:', data) return while page_req_key != None: # 请求后面的所有结果 print('*************************************') ret, data, page_req_key = self.quote_ctx.request_history_kline( code=code, start=start, end=end, ktype=ktype, autype=autype, fields=fields, max_count=max_count, extended_time=extended_time, page_req_key=page_req_key) # 请求翻页后的数据 if ret == RET_OK: yield data else: print('error:', data) return def get_rehab(self, code: str = "HK.00700"): """ 获取复权因子 接口限制 ------ 每 30 秒内最多请求 60 次获取复权因子接口。 https://openapi.futunn.com/futu-api-doc/quote/get-rehab.html :param market: :param security: :return: """ ret, data = self.quote_ctx.get_rehab(code) if ret == RET_OK: return data else: print('error:', data) def get_cur_kline(self, code_list: List[str] = ["00700.HK"], ktype_list: List[SubType] = [SubType.K_1M], num: int = 1000, autype=AuType.QFQ): """ 获取实时 K 线 :param code_list: 股票代码列表 :param ktype_list: K 线类型列表 :param num: K 线数据个数,最多 1000 根 :param autype: 复权类型 :return: """ ret_sub, err_message = self.quote_ctx.subscribe(code_list, ktype_list, subscribe_push=False) # 先订阅K 线类型。订阅成功后FutuOpenD将持续收到服务器的推送,False代表暂时不需要推送给脚本 if ret_sub == RET_OK: # 订阅成功 ret_data = [] for code, ktype in zip(code_list, ktype_list): ret, data = self.quote_ctx.get_cur_kline( code, num, ktype, autype) # 获取港股00700最近2个K线数据 if ret == RET_OK: ret_data.append(data) else: print('error:', data) ret_data.append(None) return ret_data else: print('subscription failed', err_message) def record_cur_kline(self, code_list: List[str] = ["00700.HK"], ktype_list: List[SubType] = [SubType.K_1M], record_time: int = 28800): # 3600*8 """ 记录实时 K 线 (非futu原有api) :param market: 市场 :param security: 股票代码 :param ktype: K 线类型 :return: """ handler = CurKlineHandler() self.quote_ctx.set_handler(handler) # 设置实时摆盘回调 self.quote_ctx.subscribe(code_list, ktype_list) # 订阅K线数据类型,FutuOpenD开始持续收到服务器的推送 time.sleep(record_time) # 设置脚本接收FutuOpenD的推送持续时间 self.quote_ctx.unsubscribe(code_list, ktype_list) # 反订阅K线数据类型(1分钟后生效) def query_subscription(self, is_all_conn: bool = True): """ :param is_all_conn: 是否返回所有连接的订阅状态。True:返回所有连接的订阅状态;False:只返回当前连接的订阅状态 :return: """ ret, data = self.quote_ctx.query_subscription(is_all_conn=is_all_conn) if ret == RET_OK: return data else: print('error:', data) def get_owner_plate(self, code_list: List): """ 获取股票所属板块 :param code_list: :return: """ ret, data = self.quote_ctx.get_owner_plate(code_list) if ret == RET_OK: return data else: print('error:', data) def get_plate_list(self, market: Market, plate_class: Plate): """ 获取板块列表 :param market: :param plate_class: :return: """ ret, data = self.quote_ctx.get_plate_list(market, plate_class) if ret == RET_OK: return data else: print('error:', data) def get_plate_stock(self, plate_code: str, sort_field: SortField = SortField.CODE, ascend: bool = True): """ 获取板块内股票列表 :param plate_code: :param sort_field: :param ascend: :return: """ ret, data = self.quote_ctx.get_plate_stock(plate_code) if ret == RET_OK: return data else: raise ValueError(f'error: {data}') def get_stock_filter(self, market: Market, filter_list: List[Union[SimpleFilter, AccumulateFilter, FinancialFilter]], plate_code: str = None, begin: int = 0, num: int = 200): """ 条件选股 :param market: :param filter_list: :param plate_code: :param begin: :param num: :return: """ ret, ls = self.quote_ctx.get_stock_filter(market, filter_list, plate_code=plate_code, begin=begin, num=num) # 对香港市场的股票做简单筛选 if ret == RET_OK: # last_page, all_count, ret_list = ls # print(len(ret_list), all_count, ret_list) # for item in ret_list: # print(item.stock_code) # 取其中的股票代码 return ls else: raise ValueError(f'error: {ls}') def save_history_kline(self, code: str, start: str = None, end: str = None, ktype: KLType = KLType.K_DAY, autype: AuType = AuType.QFQ, fields: List[KL_FIELD] = [KL_FIELD.ALL], max_count: int = 5000, extended_time: bool = False): # 检查开始和结束时间 now = datetime.now() if end is None: end_dt = now end = datetime.strftime(end_dt, "%Y-%m-%d") else: end_dt = datetime.strptime(end, "%Y-%m-%d") if start is None: start_dt = now - relativedelta(months=25) start = datetime.strftime(start_dt, "%Y-%m-%d") else: start_dt = datetime.strptime(start, "%Y-%m-%d") assert start_dt < end_dt, "start>=end, invalid time input!" history_kline = self.request_history_kline( code=code, start=start, end=end, ktype=ktype, max_count=max_count, fields=fields, # [KL_FIELD.DATE_TIME, KL_FIELD.CLOSE] ) for n, kl in enumerate(history_kline): if "klines" not in locals(): klines = kl else: klines = klines.append(kl, ignore_index=True) dts = list(set([t.split(" ")[0] for t in klines["time_key"]])) dts.sort() if len(dts) == 0: print(f"No data available for {code}") break if len(dts) > 1: for dt in dts[:-1]: df = klines[(klines["time_key"] >= f"{dt} 00:00:00") & (klines["time_key"] <= f"{dt} 23:59:59")] if not os.path.exists( f"{self.DATA_PATH}/k_line/{str(ktype)}/{code}"): os.mkdir( f"{self.DATA_PATH}/k_line/{str(ktype)}/{code}") df.to_csv( f"{self.DATA_PATH}/k_line/{str(ktype)}/{code}/{dt}.csv", index=False) print(f"{code}/{dt}.csv saved.") klines = klines[klines["time_key"] >= f"{dts[-1]} 00:00:00"] if not klines.empty: klines.to_csv( f"{self.DATA_PATH}/k_line/{str(ktype)}/{code}/{dts[-1]}.csv", index=False) print(f"{code}/{dts[-1]}.csv saved.") def is_hk_equity_market_time(self, cur_datetime: datetime) -> bool: """If current datetime is within market open time""" return (self.HK_EQUITY_AM_START <= cur_datetime.time() <= self.HK_EQUITY_AM_END or self.HK_EQUITY_PM_START <= cur_datetime.time() <= self.HK_EQUITY_PM_END) def next_hk_equity_market_time(self, cur_datetime: datetime) -> datetime: """Return next market open time""" if cur_datetime.time() < self.HK_EQUITY_AM_START: return datetime( cur_datetime.year, cur_datetime.month, cur_datetime.day, self.HK_EQUITY_AM_START.hour, self.HK_EQUITY_AM_START.minute, self.HK_EQUITY_AM_START.second, ) elif cur_datetime.time() < self.HK_EQUITY_PM_START: return datetime( cur_datetime.year, cur_datetime.month, cur_datetime.day, self.HK_EQUITY_PM_START.hour, self.HK_EQUITY_PM_START.minute, self.HK_EQUITY_PM_START.second, ) else: return None def get_stock_basicinfo(self) -> pd.DataFrame: """stock info""" ret_code, data = self.quote_ctx.get_stock_basicinfo( Market.HK, SecurityType.STOCK) if ret_code: print(f"Fail to get stock basicinfo: {data}") return return data