【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


  事件引擎是vn.py的核心组件,通过对接底层接口和上层应用模块,维持整个交易系统的正常动作。与常见的时间驱动不同,事件引擎的设计基于事件驱动。

1 时间驱动

  时间驱动就是让计算机每隔一段时间固定运行一次脚本,脚本自身可以很长、包含非常多的步骤,但是这种程序的运行机制相对比较简单、容易理解。这里介绍两个时间驱动在量化交易领域应用的例子。
  例子1:每隔2分钟,获取交易账户的最新净值、持仓信息、委托状态等。
  例子2:每隔1秒钟,检查一次最新收到的股指期货Tick数据,更新K线和其他技术指标,检查是否满足趋势策略的下单条件,若满足则执行下单。
  但是对于速度要求较高的量化交易(如日内CTA策略、高频策略等),时间驱动程序却存在一个非常大的缺点:对数据信息在反应操作上的处理延时。在例子2中,在每次逻辑脚本运行完等待的那1秒钟里,程序对于接收的新数据信息(行情、成交推送等)是不会做出任何反应的,只有在等待时间结束后脚本再次运行时才会进行相关的计算处理,导致交易成本提高。
  若是缩小时间驱动程序的等待间隔,提高程序运行频率。尽管可以在一定程序上降低滑点费用,却会导致CPU计算资源的浪费,大大降低计算机运转性能。

2 事件驱动

  事件驱动,简单地说就是你点什么按钮(即产生什么事件),计算机执行什么操作(即调用什么函数)。当然,事件不仅限于用户的操作。事件驱动的核心自然是“事件”。从事件角度说,事件驱动程序的基本结构包括事件收集器事件发送器事件处理器。事件收集器专门负责收集所有事件,包括来自用户的(如鼠标、键盘事件等)、硬件的(如时钟事件等)和软件的(如操作系统、应用程序本身等)。事件发送器负责将收集到的事件分发到目标对象中。事件处理器做具体的事件响应工作,它往往要到实现阶段才完全确定,因而需要运用虚函数机制。
  在交易系统的设计中,事件驱动就是当某个新的事件被推送到程序中时(如API推送新的行情、成交等),程序立即调用和这个事件相对应的处理函数进行相关操作。若没有事件推送过去,该程序是不会被触发的。这就是由事件驱动。
  事件驱动设计的另一个优点是增强交易系统的扩展性,因为事件驱动引擎可以管理不同事件的事件监听函数并执行所有和事件驱动相关的操作。
  例如收到CTP API推送的最新股指期货Tick数据,针对这个数据,系统需要进行如下操作:

  • 更新行情监控表中股指期货的行情数据(表格更新)
  • 各个策略均需要运行一次内部算法,检查该数据是否会触发策略执行下单(运算,下单, on_tick)
  • 风控系统需要检查最新行情价格是否会导致账户的整体风险超限,若超限需要进行报警(运算、报警)。

3 事件引擎工作流程

  事件驱动流程可以分为两个方面:一方面是用户在主动订阅端通过事件引擎把订阅请求发送到API接口;另一方面是回调推送把最新的Tick行情数据由事件引擎推送到具体的交易策略中进行运算。
在这里插入图片描述

  1. 主动订阅端(图中最下面那个框)
  • 用户发起调用mainEngine.subscribe函数
  • mainEngine.subscribe中调用ctpGateway.subscribe函数
  • ctpGateway.subscribe中调用ctpMdApi.subscribe函数
  • ctpMdApi.subscribe中调用C++封装的MdApi.subscribeMarketData函数,将订阅行情的请求最终通过C++ CTP API发出
  1. 回调推送端
  • ctaEngine对象向eventEngine中注册EVENT_TICK事件类型的处理函数句柄ctaEngine.processTickEvent
  • C++ CTP API收到Tick推送,自动回调MdApi.onRtnDepthMarketData函数推送行情数据字典
  • MdApi.onDepthMarketData将data里的数据读取并转化成Tick对象,并调用ctpGateway.onTick函数
  • ctpGateway.onTick函数将Tick对象包装成类型为EVENT_TICK的行情事件对象Event,并调用eventEngine.put函数,放入事件引擎的缓冲队列。
  • 事件引擎的工作线程,从缓冲队列中读取出最新的行情事件后,根据EVENT_TICK事件类型去查找缓存在内部字典中的处理函数列表,并将事件对象作为入参,遍历调用到列表中的处理函数ctaEngine.processTickEvent
  • ctaEngine.processTickEvent查看Tick的代码vtSymbol,并调用交易该代码合约的策略对象strategy.onTick函数,最终去运行策略中的逻辑。

4 事件引擎结构

  事件引擎位于vnpy/event/engine.py内EventEngine。整体来说,事件引擎主要由以下5部分组成:

  • 事件队列:_queue
  • 事件处理线程:_thread
  • 事件处理函数字典/通用事件处理函数列表:_handlers/_generalHandlers
  • 定时器:_timer
  • 引擎开头:_active

代码定义如下:

    def __init__(self, interval: int = 1):
        """
        Timer event is generated every 1 second by default, if
        interval not specified.
        """
        self._interval: int = interval
        self._queue: Queue = Queue()
        self._active: bool = False
        self._thread: Thread = Thread(target=self._run)
        self._timer: Thread = Thread(target=self._run_timer)
        self._handlers: defaultdict = defaultdict(list)
        self._general_handlers: List = []

4.1 事件队列

  事件队列(_queue)是一个队列(Queue)对象。通过事件引擎的put(event)方法,用户可向事件队列中传入事件(Event)对象,事件引擎开始运行后(self._run)会不断尝试从队列中取出事件对象、从事件处理函数字典中寻找对应该事件类型的一个或多个事件处理函数并依次调用(self._process)之。引擎调用监听对应事件类型的事件处理函数的过程,就是引擎被事件“驱动”的过程。
  事件引擎线程运行和事件处理函数调用的源码如下:

    def put(self, event: Event) -> None:
        """
        Put an event object into event queue.
        """
        self._queue.put(event)

    def register(self, type: str, handler: HandlerType) -> None:
        """
        Register a new handler function for a specific event type. Every
        function can only be registered once for each event type.
        为特定事件类型注册新的处理函数。 对于每种事件类型,每个函数只能注册一次。
        """
        handler_list = self._handlers[type]
        if handler not in handler_list:
            handler_list.append(handler)

    def unregister(self, type: str, handler: HandlerType) -> None:
        """
        Unregister an existing handler function from event engine.
        """
        handler_list = self._handlers[type]

        if handler in handler_list:
            handler_list.remove(handler)

        if not handler_list:
            self._handlers.pop(type)

    def register_general(self, handler: HandlerType) -> None:
        """
        Register a new handler function for all event types. Every
        function can only be registered once for each event type.
        """
        if handler not in self._general_handlers:
            self._general_handlers.append(handler)

    def unregister_general(self, handler: HandlerType) -> None:
        """
        Unregister an existing general handler function.
        """
        if handler in self._general_handlers:
            self._general_handlers.remove(handler)

    def _run(self) -> None:
        """
        Get event from queue and then process it.
        """
        while self._active:
            try:
                event = self._queue.get(block=True, timeout=1)
                self._process(event)
            except Empty:
                pass

    def _process(self, event: Event) -> None:
        """
        First ditribute event to those handlers registered listening
        to this type.
        首先将事件分发给已注册侦听此类型的那些处理程序

        Then distrubute event to those general handlers which listens
        to all types.
        将事件分发给那些处理所有类型的通常处理程序。
        """
        if event.type in self._handlers:
            [handler(event) for handler in self._handlers[event.type]]

        if self._general_handlers:
            [handler(event) for handler in self._general_handlers]

4.2 事件处理线程

  事件处理线程(_thread)是一个线程(Thread)对象,该线程中运行的是_run()函数,即对事件队列的轮询及事件处理函数的调用都运行在该线程中。将事件处理放到一个单独的线程中是必要的,最重要的原因是当创建事件引擎并调用start函数将之启动的时候,函数可以及时返回而保持事件引擎的连续运行,不会因为事件引擎的行动而阻塞。

4.3 事件处理函数字典/通用事件处理函数列表

  事件处理函数字典(_handlers)是一个字典(dict),用于存储事件类型和事件处理函数的监听关系。事件处理函数字典的Key一般是事先定义好的常量,用来表示单一的事件类型,字典的Value是一个列表(list),用来存储处理对应事件类型的处理函数(即一个事件类型可以被多个处理函数监听)。用户可以通过register和unregister这两个函数向事件处理字典中对应事件类型的函数列表中添加或移除事件处理函数,即注册和注销事件处理函数。
  通用事件处理函数(_generalHandlers)是一个列表,用于存储一系列监听所有事件的函数。用户可以通过registerGeneralHandler和unregisterGeneralHandler这两个函数注册或注销通用事件处理函数。

4.4 定时器

  定时器(_timer)是一个运行在单独线程中的函数(_runTimer()),在定时器开头(_timerActive)打开后,该函数会定期(间隔_timerSleep秒)向事件队列中插入一个类型为EVENT_TIMER的事件。用户可以通过注册对该事件类型的事件处理函数,实现函数的定期运行。如vntrader便是通过注册了监听EVENT_TIMER的事件处理函数实现了对持仓和账户信息的定期查询和实时更新。也就是说,每隔一段时间,监听EVENT_TIMER的事件处理函数都会被执行一次。

4.5 引擎开关

  引擎开头(_active)是一个布尔型(bool)变量,只有当该变量值为True的时候,事件引擎才会开始轮询,用户通过引擎的start和stop函数修改该变量的值来实现控制事件引擎的启动和终止。

学习资料

猜你喜欢

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