【vn.py学习笔记(二)】vn.py底层接口 学习笔记


  笔者刚接触量化投资,对量化投资挺感兴趣,在闲暇时间进行量化投资的学习,只能进行少量资金进行量化实践。目前在进行基于vnpy的A股市场的量化策略学习,主要尝试攻克的技术难点在: A股市场日线数据的免费获取维护、自动下单交易、全市场选股程序、选股策略的回测程序、基于机器学习的股票趋势预测。
  书中介绍的的是vn.py v1.9.2LTS版本,笔者接下来的vn.py学习笔记将基于 vn.py最新的源码进行学习分享,有不对的地方欢迎大家指出,共同学习。
  欢迎志同道合的朋友加我QQ(1163962054)交流。
  github仓库: https://github.com/PanAndy/quant_share

1 CTP API的工作原理

1.1 CTP介绍

  CTP是由上海期货交易所的上海期货信息技术有限公司专门为期货公司开发的一套期货经纪业务管理系统,由交易系统、结算系统、风险控制系统三大系统组成。其中,交易系统主要负责订单管理、行情转发及银期转账业务;结算系统负责交易管理、账户管理、经纪人管理、资金管理、费率设置、日终结算、信息查询,以及报表管理等;风险控制系统则主要在盘中进行高速的实时试算,以及时提示并控制风险。
  CTP公开并对外开放交易系统接口,使用该接口可以接收交易所的行情数据和执行交易指令。该接口采用API的方式接入,使得用户可以随意开发自己的交易软件并直接连接到交易柜台上进行交易,在早期推动了国内量化交易的进程。CTP API这一设计模式在期货领域已成为行业标准,例如飞马、飞创Xspeed、华宝证券LTS、金仕达黄金和恒生UFT等都采用类CTP API的设计。

1.2 API功能介绍

  《CTP客户端开发指南》指出:CTP API包含交易接口(Trader API)、风控接口(Risk API),以及结算接口(CSV)。使用这三个接口都可以实现与上期所技术综合交易平台系统的对接,从而进行交易、风险控制,以及对每日结算数据的保存。应该注意的是,风控接口和结算接口只供给期货公司内部使用,并不对投资者开放,故针对投资者的CTP API仅仅指交易接口。
  交易接口主要用于获取交易所行情和下达交易指令,如订阅行情、下单、撤单、预埋单、银期转账、信息查询等。交易接口是三个接口中应用最广泛的接口,它的受众主要是:终端软件开发商(如快期)、对交易终端有特殊需求的个人、机构或自营单位投资者。

1.3 CTP API文件

  CTP API原生文件位于vnpy/api/ctp文件夹内,注意,以.dll和.lib为后缀名的文件都是编译好的二进制文件,无法打开。所以从用户角度只需关注后缀名为.h的文件。其中TraderApi定义交易相关的指令,MdAPI定义获取行情相关的指令,UserApiStruct包含了API中用到的结构体的定义,UserApiDataType包含了对API中用到的常量的定义。浏览了一下vnpy/api/ctp下的c++源码,基本所有代码都有,返回的数据也都有注释,有必要的话可以自行深度阅读一下数据类型,这对理解ctp_gateway的操作很有帮助。

文件名 详情
ThostFtdcTraderApi.h C++头文件;包含交易相关的指令,如报单
ThostFtdcApi.h C++头文件;包含行情获取相关的指令
ThostFtdcUserApiStruct.h 包含所用到的数据结构
ThostFtdcUserApiDataType.h 包含所有用到的数据类型
thosttraderapi.dll 交易部分的动态链接库
thosttraderapi.lib 交易部分的静态链接库
thostmduserapi.dll 行情部分的动态链接库
thosttraderapi.lib 行情部分的静态链接库
error.dtd 包含所有可能的错误信息
error.xml 包含所有可能的错误信息

1.4 API 通用规则

  1. 命名规则
    CTP API的命名规则如下表所示。
消息 格式 示例
请求 Req---- ReqUserLogin
响应 OnRsp---- OnRspUserLogin
查询 ReqQry---- ReqQryInstrument
查询请求的响应 OnRspQry---- OnRspQryInstrument
回报 OnRtn---- OnRtnOrder
错误回报 OnErrRtn---- OnErrRtnOrderInsert
  1. API分类
    CTP API分为两种,Spi类和Api类。
  • Spi类(如ThostFtdcTraderApi.h的CthostFtdcTraderSpi类)包含所有的响应和回报函数,用于接收CTP发送的信息。开发者需要继承该接口类,并实现其中相应的虚函数。
  • Api类(如ThostFtdcTraderApi.h的CThostFtdcTraderApi类)包含主动主动发起请求和订阅的接口函数,开发者直接调用即可。
  1. 通用参数
  • nRequestID:客户端发送请求时要为该请求指定一个请求编号。交易接口会在响应或回报中返回与该请求相同的请求编号。当客户端频繁操作时,很有可能会造成同一个响应函数被调用多次,在这种情况下,能将请求与响应关联起来的纽带就是请求编号。
  • IsLast:当响应函数需要携带的数据包过大时,该数据包会被分割成数个小的数据包并按顺序逐次发送,在这种情况下同一个响应函数会被调用多次,而参数IsLast就是用于描述当前收到的响应数据包是不是所有数据包中的最后一个。
  • RspInfo:该参数用于描述请求执行过程中是否出现错误。该数据结构中的属性ErrorID如果是0,则说明该请求被交易核心认可通过,否则,该参数将描述交易核心返回的错误信息。
  • error.xml:些文件中包含所有可能的错误信息。
  1. 接口初始化
    通过创建和初始化行情接口和交易接口,开启交易接口的工作流程有以下几步。
    在这里插入图片描述
  2. 行情接口的工作原理
    下图展示了行情接口的工作原理,可以对API的工作原理有直观的印象。对着下图,可以对vnpy/gateway/ctp/ctp_gateway.py有更好的理解。
    在这里插入图片描述

2 CTP API的Python封装设计

2.1 封装设计思路

  在Python的API中,会把Spi和Api两个类的功能封装到一个类中。
  原生API的回调函数被触发后必须快速返回,否则会导致其他数据的推送被阻塞,阻塞时间长了还有可能导致API崩溃,因此回调函数中不适合包含耗时比较长的计算逻辑。例如,某个tick行情推送后,如果用户在回调函数中写了一些比较复杂的计算,如循环计算,耗时超过3秒钟,则在这个3秒钟中,用户是收不到其他行情的,且很可能3秒钟后出现API崩溃。这里的解决方案是使用生产者-消费者模型,在API中包含一个缓冲队列,当回调函数收到新的数据信息时,只是简单存入缓存队列中并立即返回,而数据信息的处理及向Python中的推送则由另一个工作线程来执行。
  基于C++的函数中使用了大量的结构体用于数据传送,若所有结构体都要封装成对应的Python类,工作量太大也非常容易出错。解决方案是使用Python中的Dict字典。
  原生API的函数名开头都是大写字母,为了便于分辨及符合Python的PEP8编码规则,Python封装后的函数都以小写字母开头。例如:原生API中以On开头的回调函数(如OnRspUserLogin)对应的PythonAPI的回调函数直接改为以on开头的函数(如onRspUserLogin)。
  相关的封装实现位于vnpy/api/ctp/vnctp中。

2.2 封装后API工作流程

  1. 主动函数
  • 用户在Python程序中调用封装API的主动函数,并直接传入Python变量(PyObject对象)作为参数。
  • 封装API将Python变量转换成C++变量。
  • 封装API调用原生API的主动函数,并传入C++变量作为参数。
  1. 回调函数
  • 交易柜台通过原生API的C++回调函数推送数据信息,传入参数为C++变量。
  • 封装API将C++变量转换为Python变量
  • 封装API调用封装后的回调函数向用户的Python程序中推送程序,并传入Python变量作为参数。

3 CTP API对接中层引擎原理

  当原生C++的CTP API封装成功后,就可以对接到中层引擎,对接流程分为两步:
  (1)将API的回调函数收到的数据推送到程序的中层引擎中,等待处理。
  (2)将API的主动函数进行一定的简化封装,便于中层引擎调用。
  对接的具体代码位于:vnpy/gateway/ctp/ctp_gateway.py,它继承于vnpy/trader/gateway.py,代码阅读时可以一起看。
  注意,书中所介绍的版本这一块和最新的版本已经不同了。书中的代码将很多功能直接写在回调函数的实现里了,最新的代码对接口又进行了抽象,通过实现抽象的接口来调用封装后的API实现对CTP API的调用,最新的代码模块化更强。

3.1 回调函数

  通过回调函数收到API的数据推送后,创建不同类型的Event对象(来自事件引擎模块),在事件对象的data中保存需要具体推送的数据,然后推送到事件驱动引擎中,由其负责处理。
  在回调函数收到的数据中,data和error分别对应的是保存主要数据(如行情)和错误信息的字典,n是该回调函数对应的请求号(即调用主动函数时的reqid),last是一个布尔值,代表是否为该次调用的最后返回信息。注意,这几个字段并非每个回调函数都会返回,只是统一在这里说明。
  而error字典每次收到后应当立即检查是否包含错误信息(因为即使没有发生错误也会推送),若有则自动保存为一个日志事件(通过日志监控控件显示出来)。
  服务器连接完成后,检查是否已经填入了用户名等登录信息,若有则自动登录。
  登录完成后,自动订阅之前已经订阅过的合约。
  收到行情推送后,创造特定合约行情事件,通常适用于算法等仅关注特定合约行情的组件。
  当调用有返回信息的主动函数时,需要传入本次请求的编号,此时先将reqid加1,再作为参数传入主动函数中。

class CtpMdApi(MdApi):
    """ """

    def __init__(self, gateway):
        """Constructor"""
        super(CtpMdApi, self).__init__()

        self.gateway = gateway
        self.gateway_name = gateway.gateway_name

        self.reqid = 0

        self.connect_status = False
        self.login_status = False
        self.subscribed = set()

        self.userid = ""
        self.password = ""
        self.brokerid = ""

        self.current_date = datetime.now().strftime("%Y%m%d")

    def onFrontConnected(self):
        """
        Callback when front server is connected.
        """
        self.gateway.write_log("行情服务器连接成功")
        self.login()

    def onFrontDisconnected(self, reason: int):
        """
        Callback when front server is disconnected.
        """
        self.login_status = False
        self.gateway.write_log(f"行情服务器连接断开,原因{reason}")

    def onRspUserLogin(self, data: dict, error: dict, reqid: int, last: bool):
        """
        Callback when user is logged in.
        """
        if not error["ErrorID"]:
            self.login_status = True
            self.gateway.write_log("行情服务器登录成功")

            for symbol in self.subscribed:
                self.subscribeMarketData(symbol)
        else:
            self.gateway.write_error("行情服务器登录失败", error)

    def onRspError(self, error: dict, reqid: int, last: bool):
        """
        Callback when error occured.
        """
        self.gateway.write_error("行情接口报错", error)

    def onRspSubMarketData(self, data: dict, error: dict, reqid: int, last: bool):
        """ """
        if not error or not error["ErrorID"]:
            return

        self.gateway.write_error("行情订阅失败", error)

    def onRtnDepthMarketData(self, data: dict):
        """
        Callback of tick data update.
        """
        # Filter data update with no timestamp
        if not data["UpdateTime"]:
            return

        symbol = data["InstrumentID"]
        exchange = symbol_exchange_map.get(symbol, "")
        if not exchange:
            return

        timestamp = f"{self.current_date} {data['UpdateTime']}.{int(data['UpdateMillisec']/100)}"
        dt = datetime.strptime(timestamp, "%Y%m%d %H:%M:%S.%f")
        dt = CHINA_TZ.localize(dt)

        tick = TickData(
            symbol=symbol,
            exchange=exchange,
            datetime=dt,
            name=symbol_name_map[symbol],
            volume=data["Volume"],
            open_interest=data["OpenInterest"],
            last_price=adjust_price(data["LastPrice"]),
            limit_up=data["UpperLimitPrice"],
            limit_down=data["LowerLimitPrice"],
            open_price=adjust_price(data["OpenPrice"]),
            high_price=adjust_price(data["HighestPrice"]),
            low_price=adjust_price(data["LowestPrice"]),
            pre_close=adjust_price(data["PreClosePrice"]),
            bid_price_1=adjust_price(data["BidPrice1"]),
            ask_price_1=adjust_price(data["AskPrice1"]),
            bid_volume_1=data["BidVolume1"],
            ask_volume_1=data["AskVolume1"],
            gateway_name=self.gateway_name
        )

        if data["BidVolume2"] or data["AskVolume2"]:
            tick.bid_price_2 = adjust_price(data["BidPrice2"])
            tick.bid_price_3 = adjust_price(data["BidPrice3"])
            tick.bid_price_4 = adjust_price(data["BidPrice4"])
            tick.bid_price_5 = adjust_price(data["BidPrice5"])

            tick.ask_price_2 = adjust_price(data["AskPrice2"])
            tick.ask_price_3 = adjust_price(data["AskPrice3"])
            tick.ask_price_4 = adjust_price(data["AskPrice4"])
            tick.ask_price_5 = adjust_price(data["AskPrice5"])

            tick.bid_volume_2 = data["BidVolume2"]
            tick.bid_volume_3 = data["BidVolume3"]
            tick.bid_volume_4 = data["BidVolume4"]
            tick.bid_volume_5 = data["BidVolume5"]

            tick.ask_volume_2 = data["AskVolume2"]
            tick.ask_volume_3 = data["AskVolume3"]
            tick.ask_volume_4 = data["AskVolume4"]
            tick.ask_volume_5 = data["AskVolume5"]

        self.gateway.on_tick(tick)

    def connect(self, address: str, userid: str, password: str, brokerid: int):
        """
        Start connection to server.
        """
        self.userid = userid
        self.password = password
        self.brokerid = brokerid

        # If not connected, then start connection first.
        if not self.connect_status:
            path = get_folder_path(self.gateway_name.lower())
            self.createFtdcMdApi((str(path) + "\\Md").encode("GBK"))

            self.registerFront(address)
            self.init()

            self.connect_status = True
        # If already connected, then login immediately.
        elif not self.login_status:
            self.login()

    def login(self):
        """
        Login onto server.
        """
        req = {
    
    
            "UserID": self.userid,
            "Password": self.password,
            "BrokerID": self.brokerid
        }

        self.reqid += 1
        self.reqUserLogin(req, self.reqid)

    def subscribe(self, req: SubscribeRequest):
        """
        Subscribe to tick data update.
        """
        if self.login_status:
            self.subscribeMarketData(req.symbol)
        self.subscribed.add(req.symbol)

    def close(self):
        """
        Close the connection.
        """
        if self.connect_status:
            self.exit()

    def update_date(self):
        """"""
        self.current_date = datetime.now().strftime("%Y%m%d")

3.2 主动函数

  主动函数仅封装了两个功能:登录和订阅合约。
  对于登录函数而言,函数调用后,初始化连接。连接完成后,onFrontconnected回调函数会被自动调用。
  订阅合约方面,CTP API在期货方只需要传入合约代码,但是证券类的接口需要同时传入合约的代码和交易所(因为存在两个证券交易所相同代码的情况)。发送订阅请求后,将该订阅请求保存在subscribed集合中,使得断开重连时可以自动重新订阅。
  

参考资料

猜你喜欢

转载自blog.csdn.net/PAN_Andy/article/details/114271612