예제 #1
0
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()
예제 #2
0
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)
예제 #3
0
파일: futuGateway.py 프로젝트: roccox/vnpy
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)
예제 #4
0
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)
예제 #5
0
파일: futu_gateway.py 프로젝트: shhuiw/vnpy
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)
예제 #6
0
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