Python量化交易平台开发教程系列4-事件驱动引擎原理和使用
前言
从这篇开始,后面的教程都会基于Python(终于可以跟C++说再见了)。
经过上一篇复杂繁琐的API编译后,我们已经有了一个可以在Python环境中用来收行情和发单的接口,但是尽管作者在Github上也放了简单的API功能测试代码作为接口使用方法的示例,绝大部分读者应该对于如何用这个接口去开发自己的交易系统毫无头绪。
类似的情况也常常发生于当我们从万得、恒生、网上的其他开源项目(比如pyctp)等等拿到开发接口和文档示例后:
看了半天觉得似乎上面讲的都懂。
但要写个自己的系统依旧不知道从何处下手。
所以在搞定交易接口后,我们开发交易系统的第一步就是要弄清楚系统的工作原理,在读完这篇教程后,你应该至少不会再对如何写一个交易系统茫然无措了。
事件驱动
计算机程序分类
所有的计算机程序都可以大致分为两类:脚本型(单次运行)和连续运行型(直到用户主动退出)。
脚本型
脚本型的程序包括最早的批处理文件以及使用Python做交易策略回测等等,这类程序的特点是在用户启动后会按照编程时设计好的步骤一步步运行,所有步骤运行完后自动退出。
连续运行型
连续运行型的程序包含了操作系统和绝大部分我们日常使用的软件等等,这类程序启动后会处于一个无限循环中连续运行,直到用户主动退出时才会结束。
连续运行型程序
我们要开发的交易系统就是属于连续运行型程序,而这种程序根据其计算逻辑的运行机制不同,又可以粗略的分为时间驱动和事件驱动两种。
时间驱动
时间驱动的程序逻辑相对容易设计,简单来说就是让电脑每隔一段时间自动做一些事情。这个事情本身可以很复杂、包括很多步骤,但这些步骤都是线性的,按照顺序一步步执行下来。
以下代码展示了一个非常简单的时间驱动的Python程序。
from time import sleep
def demo():
print u’时间驱动的程序每隔1秒运行demo函数’
while 1:
demo()
sleep(1.0)
时间驱动的程序本质上就是每隔一段时间固定运行一次脚本(上面代码中的demo函数)。尽管脚本自身可以很长、包含非常多的步骤,但是我们可以看出这种程序的运行机制相对比较简单、容易理解。
举一些量化交易相关的例子:
每隔5分钟,通过新浪财经网页的公开API读取一次沪深300成分股的价格,根据当日涨幅进行排序后输出到电脑屏幕上。
每隔1秒钟,检查一次最新收到的股指期货TICK数据,更新K线和其他技术指标,检查是否满足趋势策略的下单条件,若满足则执行下单。
对速度要求较高的量化交易方面(日内CTA策略、高频策略等等),时间驱动的程序会存在一个非常大的缺点:对数据信息在反应操作上的处理延时。例子2中,在每次逻辑脚本运行完等待的那1秒钟里,程序对于接收到的新数据信息(行情、成交推送等等)是不会做出任何反应的,只有在等待时间结束后脚本再次运行时才会进行相关的计算处理。而处理延时在量化交易中的直接后果就是money:市价单滑点、限价单错过本可成交的价格。
时间驱动的程序在量化交易方面还存在一些其他的缺点:如浪费CPU的计算资源、实现异步逻辑复杂度高等等。
事件驱动
与时间驱动对应的就是事件驱动的程序:当某个新的事件被推送到程序中时(如API推送新的行情、成交),程序立即调用和这个事件相对应的处理函数进行相关的操作。
上面例子2的事件驱动版:交易程序对股指TICK数据进行监听,当没有新的行情过来时,程序保持监听状态不进行任何操作;当收到新的数据时,数据处理函数立即更新K线和其他技术指标,并检查是否满足趋势策略的下单条件执行下单。
对于简单的程序,我们可以采用上面测试代码中的方案,直接在API的回调函数中写入相应的逻辑。但随着程序复杂度的增加,这种方案会变得越来越不可行。假设我们有一个带有图形界面的量化交易系统,系统在某一时刻接收到了API推送的股指期货行情数据,针对这个数据系统需要进行如下处理:
更新图表上显示的K线图形(绘图)
更新行情监控表中股指期货的行情数据(表格更新)
策略1需要运行一次内部算法,检查该数据是否会触发策略进行下单(运算、下单)
策略2同样需要运行一次内部算法,检查该数据是否会触发策略进行下单(运算、下单)
风控系统需要检查最新行情价格是否会导致账户的整体风险超限,若超限需要进行报警(运算、报警)
此时将上面所有的操作都写到一个回调函数中无疑变成了非常差的方案,代码过长容易出错不说,可扩展性也差,每添加一个策略或者功能则又需要修改之前的源代码(有经验的读者会知道,经常修改生产代码是一种非常危险的运营管理方法)。
为了解决这种情况,我们需要用到事件驱动引擎来管理不同事件的事件监听函数并执行所有和事件驱动相关的操作。
事件驱动引擎原理
vn.py框架中的vn.event模块包含了一个可扩展的事件驱动引擎。整个引擎的实现并不复杂,除去注释、空行后大概也就100行左右的代码:
encoding: UTF-8
系统模块
from Queue import Queue, Empty
from threading import Thread
第三方模块
from PyQt4.QtCore import QTimer
自己开发的模块
from eventType import *
#
class EventEngine:
“””
事件驱动引擎
事件驱动引擎中所有的变量都设置为了私有,这是为了防止不小心
从外部修改了这些变量的值或状态,导致bug。
变量说明
__queue:私有变量,事件队列
__active:私有变量,事件引擎开关
__thread:私有变量,事件处理线程
__timer:私有变量,计时器
__handlers:私有变量,事件处理函数字典
方法说明
__run: 私有方法,事件处理线程连续运行用
__process: 私有方法,处理事件,调用注册在引擎中的监听函数
__onTimer:私有方法,计时器固定事件间隔触发后,向事件队列中存入计时器事件
start: 公共方法,启动引擎
stop:公共方法,停止引擎
register:公共方法,向引擎中注册监听函数
unregister:公共方法,向引擎中注销监听函数
put:公共方法,向事件队列中存入新的事件
事件监听函数必须定义为输入参数仅为一个event对象,即:
函数
def func(event)
...
对象方法
def method(self, event)
...
"""
#----------------------------------------------------------------------
def __init__(self):
"""初始化事件引擎"""
# 事件队列
self.__queue = Queue()
# 事件引擎开关
self.__active = False
# 事件处理线程
self.__thread = Thread(target = self.__run)
# 计时器,用于触发计时器事件
self.__timer = QTimer()
self.__timer.timeout.connect(self.__onTimer)
# 这里的__handlers是一个字典,用来保存对应的事件调用关系
# 其中每个键对应的值是一个列表,列表中保存了对该事件进行监听的函数功能
self.__handlers = {}
#----------------------------------------------------------------------
def __run(self):
"""引擎运行"""
while self.__active == True:
try:
event = self.__queue.get(block = True, timeout = 1) # 获取事件的阻塞时间设为1秒
self.__process(event)
except Empty:
pass
#----------------------------------------------------------------------
def __process(self, event):
"""处理事件"""
# 检查是否存在对该事件进行监听的处理函数
if event.type_ in self.__handlers:
# 若存在,则按顺序将事件传递给处理函数执行
[handler(event) for handler in self.__handlers[event.type_]]
# 以上语句为Python列表解析方式的写法,对应的常规循环写法为:
#for handler in self.__handlers[event.type_]:
#handler(event)
#----------------------------------------------------------------------
def __onTimer(self):
"""向事件队列中存入计时器事件"""
# 创建计时器事件
event = Event(type_=EVENT_TIMER)
# 向队列中存入计时器事件
self.put(event)
#----------------------------------------------------------------------
def start(self):
"""引擎启动"""
# 将引擎设为启动
self.__active = True
# 启动事件处理线程
self.__thread.start()
# 启动计时器,计时器事件间隔默认设定为1秒
self.__timer.start(1000)
#----------------------------------------------------------------------
def stop(self):
"""停止引擎"""
# 将引擎设为停止
self.__active = False
# 停止计时器
self.__timer.stop()
# 等待事件处理线程退出
self.__thread.join()
#----------------------------------------------------------------------
def register(self, type_, handler):
"""注册事件处理函数监听"""
# 尝试获取该事件类型对应的处理函数列表,若无则创建
try:
handlerList = self.__handlers[type_]
except KeyError:
handlerList = []
self.__handlers[type_] = handlerList
# 若要注册的处理器不在该事件的处理器列表中,则注册该事件
if handler not in handlerList:
handlerList.append(handler)
#----------------------------------------------------------------------
def unregister(self, type_, handler):
"""注销事件处理函数监听"""
# 尝试获取该事件类型对应的处理函数列表,若无则忽略该次注销请求
try:
handlerList = self.handlers[type_]
# 如果该函数存在于列表中,则移除
if handler in handlerList:
handlerList.remove(handler)
# 如果函数列表为空,则从引擎中移除该事件类型
if not handlerList:
del self.handlers[type_]
except KeyError:
pass
#----------------------------------------------------------------------
def put(self, event):
"""向事件队列中存入事件"""
self.__queue.put(event)
初始化
当事件驱动引擎对象被创建时,初始化函数init会创建以下私有变量:
__queue:用来保存事件的队列
__active:用来控制引擎启动、停止的开关
__thread:负责处理事件、执行具体操作的线程
__timer:用来每隔一段时间触发定时事件的计时器
__handlers:用来保存不同类型事件所对应的事件处理函数的字典
注册事件处理函数
引擎提供了register方法,用来向引擎注册事件处理函数的监听,传入参数为
type_:表示事件类型的常量字符串,由用户自行定义,注意不同事件类型间不能重复
handler:当该类型的事件被触发时,用户希望进行相应操作的事件处理函数,函数的定义方法参考代码中的注释
当用户调用register方法注册事件处理函数时,引擎会尝试获取__handlers字典中该事件类型所对应的处理函数列表(若无则创建一个空列表),并向这个列表中添加该事件处理函数。使用了Python的列表对象,用户可以很容易的控制同一个事件类型下多个事件处理函数的工作顺序,因此对某些涉及多步操作的复杂算法可以保证按照正确的顺序执行,这点是相比于某些系统0消息机制(如Qt的Signal/Slot)最大的优势。
如当标的物行情发生变化时,期权高频套利算法需要执行以下操作:
使用定价引擎先计算新的期权理论价、希腊值
使用风控引擎对当前持仓的风险度汇总,并计算报价的中间价
使用套利引擎基于预先设定的价差、下单手数等参数,计算具体价格并发单
以上三步操作,只需在交易系统启动时按顺序注册监听到标的物行情事件上,就可以保证操作顺序的正确。
和register对应的是unregister方法,用于注销事件处理函数的监听,传入参数相同,具体原理请参照源代码。在实际应用中,用户可以动态的组合使用register和unregister方法,只在需要监听某些事件的时候监听,完成后取消监听,从而节省CPU资源。
这里让笔者吐槽一下某些国内的C++平台(当然不是指所有的),每个策略对系统里所有的订单回报进行监听,如果是自身相关的就处理,不相关的就PASS。这种写法,光是判断是否和自身相关就得多做多少无谓的判断、浪费多少CPU资源,随着策略数量的增加,浪费呈线性增加的趋势,这种平台还叫嚣做高频,唉……
触发事件
用户可以通过引擎的put方法向事件队列__queue中存入事件,等待事件处理线程来进行处理,事件类的实现如下:
#
class Event:
“”“事件对象”“”
#----------------------------------------------------------------------
def __init__(self, type_=None):
"""Constructor"""
self.type_ = type_ # 事件类型
self.dict_ = {} # 字典用于保存具体的事件数据
对象创建时用户可以选择传入事件类型字符串type_作为参数。dict_字典用于保存具体事件相关的数据信息,以供事件处理函数进行操作。
事件处理线程的连续运行
事件引擎的事件处理线程__thread中执行连续运行工作的函数为__run:当事件引擎的开关__active没有被关闭时,引擎尝试从事件队列中读取最新的事件,若读取成功则立即调用__process函数处理该事件,若无法读取(队列为空)则进入阻塞状态节省CPU资源,当阻塞时间(默认为1秒)结束时再次进入以上循环。
__process函数工作时,首先检查事件对象的事件类型在__handlers字典中是否存在,若存在(说明有事件处理函数在监听该事件)则按照注册顺序调用监听函数列表中的事件处理函数进行相关操作。
计时器
事件引擎中的__timer是一个PyQt中的QTimer对象,提供的功能非常简单:每隔一段时间(由用户设定)自动运行函数__onTimer。__onTimer函数会创建一个类型为EVENT_TIMER(在eventType.py文件中定义)的事件对象,并调用引擎的put方法存入到事件队列中。
敏感的读者可能已经意识到了,这个计时器本质上是一个由时间驱动的功能。尽管我们在前文中提到了事件驱动在量化交易平台开发中的重要性,但不可否认某些交易功能的实现必须基于时间驱动,例如:下单后若2秒不成交则立即撤单、每隔5分钟将当日的成交记录保存到数据库中等。这类功能在实现时就可以选择使用事件处理函数对EVENT_TIMER类型的计时器事件进行监听(参考下一章节“事件驱动引擎使用”中的示例)。
启动、停止
用户可以通过start和stop两个方法来启动和停止事件驱动引擎,原理很简单读者可以直接参考源代码。
当启动计时器时,事件间隔默认设定为了1秒(1000毫秒),这个参数用户可以视乎自己的需求进行调整。假设用户使用时间驱动的函数工作间隔为分钟级,则可以选择将参数设置为60秒(600000毫秒),以此类推。
事件驱动引擎使用
同样在eventEngine.py中,包含了一段测试代码test函数,用来展示事件驱动引擎的使用方法:
———————————————————————-
def test():
“”“测试函数”“”
import sys
from datetime import datetime
from PyQt4.QtCore import QCoreApplication
def simpletest(event):
print u'处理每秒触发的计时器事件:%s' % str(datetime.now())
app = QCoreApplication(sys.argv)
ee = EventEngine()
ee.register(EVENT_TIMER, simpletest)
ee.start()
app.exec_()
直接运行脚本可以进行测试
if name == ‘main‘:
test()
test函数整体上包含了这几步:
导入相关的包(sys、datetime、PyQt4),注意由于EventEngine的实现中使用了PyQt4的QTimer类,因此整个程序的运行必须包含在Qt事件循环中,即使用QCoreApplication(或者PyQt4.QtGui中的QApplication)的exec_()方法在程序主线程中启动事件循环。
定义一个简单的函数simpletest,该函数包含一个输入参数event对象,函数被调用后会打印一段字符以及当前的时间
创建QCoreApplication对象app
创建事件驱动引擎EventEngine对象ee
向引擎中注册simpletest函数对定时器事件EVENT_TIMER的监听
启动事件驱动引擎
启动Qt事件循环
整体上看,当用户开发自己的程序时,需要修改的只是第2步和第5步:创建自己的事件处理函数并将这些函数注册到相应的事件类型上进行监听。
总结
有了API接口和事件驱动引擎,接下来我们可以开始开发自己的平台了,后面的几篇教程将会一步步展示一个简单的LTS交易平台的开发过程。
转载请注明出处:用Python的交易员