参考:用Python的交易员
【前言】从本篇教程开始,所有的开发都会在Python环境中进行(谢天谢地可以和C++说再见了)。
一、底层接口简介
1、通常情况下,一个交易程序的架构会由以下三个部分组成:
- 底层接口:负责对接行情和交易API,将数据推送到系统核心中,以及发送指令(下单、数据请求等)
- 中层引擎:用于整合程序中的各个组件(包括底层接口、数据库接口等等)到一个对象中,便于顶层UI调用
- 顶层GUI:用于显示数据和调用中层引擎暴露的主动函数,实现各项具体功能
2、上面这张图展示的是国外的一款开源交易平台AlgoTrader的架构:
- 两边的Adapters代表的是底层接口(左边行情数据,右边交易)
- 红色圆柱形中包括的是中层引擎架构,事件驱动方面使用了Esper复杂事件处理(CEP)引擎,同时内置了一些常用的功能引擎,如期权定价引擎、外汇对冲模块、投资组合管理模块等
- 上方的Strategy1、2等代表的是顶层应用(算法策略、GUI界面等),通过调用中层引擎的功能来实现用户所需的业务
3、vn.py和AlgoTrader的比较:
这里对两个项目做一个简单的比较。
vn.py优势:
- 语言易用:Java语言比Python啰嗦
- 架构简洁:Java的编程理念(纯面向对象,大量使用框架等)更是比Python的编程理念(人生苦短,我们的目标是搞定问题)繁琐
- 事件驱动引擎:AlgoTrader使用的Esper引擎尽管功能强大,使用起来也过于复杂,对于国内绝大部分的量化业务而言完全用不着
- 本地化:vn.py完全为中国市场设计,在功能设计上更符合国人的使用习惯,而AlgoTrader则是针对欧美市场设计
AlgoTrader优势:
- 静态语言:Java在开发时可以进行静态检查,同时相对较低的灵活性使得其更适合大型团队使用(即每个成员未必对项目整体非常了解)
- 国外接口:已有大量的国外经纪商和行情提供商接口,如果用户主要做欧美市场基本可以开箱即用
- 成熟度:AlgoTrader从2009发布至今已经有6年的历史,同时有着相当数量的机构客户
两个项目的相似之处:
- 作者在最初都是为了交易某种期权策略而开发了该项目
- 整体框架设计类似(底层接口、中层引擎、顶层GUI)
- 都可以非常方便的开发全自动交易策略
- 都是开源项目,目前托管在Github上,用户可以根据自己的需求随意定制相关功能
- 都可以应用于高频交易(毫秒级延迟),不适用于超高频交易(微秒级延迟)
4、教程说明
本篇教程将会介绍底层接口的开发,后面的若干篇则是关于中层引擎和各种顶层GUI组件。相关的示例都是基于vn.demo中的LTS接口DEMO,发布在:https://github.com/vnpy/vnpy/tree/master/vn.demo/ltsdemo
二、底层接口对接
通过前面的教程,我们已经获得了和原生C++ API功能完全相同的Python封装API。通常情况下,为了将某个API对接到我们的程序中,需要以下两步:
- 将API的回调函数收到的数据推送到程序的中层引擎中,等待处理
- 将API的主动函数进行一定的简化封装,便于中层引擎调用
vn.lts中的API接口在使用时需要由用户继承后实现回调函数对应的具体功能,下面的内容以行情接口为例。
########################################################################
class DemoMdApi(MdApi):
"""
Demo中的行情API封装
封装后所有数据自动推送到事件驱动引擎中,由其负责推送到各个监听该事件的回调函数上
对用户暴露的主动函数包括:
登陆 login
订阅合约 subscribe
"""
#----------------------------------------------------------------------
def __init__(self, eventEngine):
"""
API对象的初始化函数
"""
super(DemoMdApi, self).__init__()
# 事件引擎,所有数据都推送到其中,再由事件引擎进行分发
self.__eventEngine = eventEngine
# 请求编号,由api负责管理
self.__reqid = 0
# 以下变量用于实现连接和重连后的自动登陆
self.__userid = ''
self.__password = ''
self.__brokerid = ''
# 以下集合用于重连后自动订阅之前已订阅的合约,使用集合为了防止重复
self.__setSubscribed = set()
# 初始化.con文件的保存目录为\mdconnection,注意这个目录必须已存在,否则会报错
self.createFtdcMdApi(os.getcwd() + '\\mdconnection\\')
- DemoMdApi类继承自MdApi类,并实现了回调函数的具体功能。
- 创建DemoMdApi的对象时,用户需要传入的参数是事件驱动引擎对象eventEngine。
- 每次调用API的主动函数时,需要传入一个reqid的参数,作为本次请求的唯一标识,绝大部分情况下我们不需要在意每个请求的标识情况,因此选择将该参数交给DemoMdApi对象来维护,每次调用主动函数时自动加1。
- 我们在DemoMdApi的对象中保存用户名、密码和经纪商编号,用于前置机连接完成后的自动登录功能,以及断线重连相关的操作。
- __setSubscribed对应的是一个Python集合,用于保存我们通过订阅函数订阅过的合约,在断线重连后自动进行订阅,之所以选择set而不是list是为了保证合约的唯一性,避免重复订阅(尽管重复订阅也没影响)。
- 在创建对象DemoMdApi对象的同时,自动调用createFtdcMdApi来初始化连接接口,选择使用当前目录下的mdconnection文件夹来保存.con通讯文件。
1、回调函数
#----------------------------------------------------------------------
def onFrontConnected(self):
"""服务器连接"""
event = Event(type_=EVENT_LOG)
event.dict_['log'] = u'行情服务器连接成功'
self.__eventEngine.put(event)
# 如果用户已经填入了用户名等等,则自动尝试连接
if self.__userid:
req = {}
req['UserID'] = self.__userid
req['Password'] = self.__password
req['BrokerID'] = self.__brokerid
self.__reqid = self.__reqid + 1
self.reqUserLogin(req, self.__reqid)
...
#----------------------------------------------------------------------
def onRspUserLogin(self, data, error, n, last):
"""登陆回报"""
event = Event(type_=EVENT_LOG)
if error['ErrorID'] == 0:
log = u'行情服务器登陆成功'
else:
log = u'登陆回报,错误代码:' + unicode(error['ErrorID']) + u',' + u'错误信息:' + error['ErrorMsg'].decode('gbk')
event.dict_['log'] = log
self.__eventEngine.put(event)
# 重连后自动订阅之前已经订阅过的合约
if self.__setSubscribed:
for instrument in self.__setSubscribed:
self.subscribe(instrument[0], instrument[1])
...
#----------------------------------------------------------------------
def onRtnDepthMarketData(self, data):
"""行情推送"""
# 行情推送收到后,同时触发常规行情事件,以及特定合约行情事件,用于满足不同类型的监听
# 常规行情事件
event1 = Event(type_=EVENT_MARKETDATA)
event1.dict_['data'] = data
self.__eventEngine.put(event1)
# 特定合约行情事件
event2 = Event(type_=(EVENT_MARKETDATA_CONTRACT+data['InstrumentID']))
event2.dict_['data'] = data
self.__eventEngine.put(event2)
...
- 通过回调函数收到API的数据推送后,创建不同类型的事件Event对象(来自于事件驱动引擎模块),在事件对象的数据字典dict_中保存需要具体推送的数据,然后推送到事件驱动引擎中,由其负责处理。
- 回调函数收到的数据中,data和error分别对应的是保存主要数据(如行情)和错误信息的字典,n是该回调函数对应的请求号(即调用主动函数时的reqid),last是一个布尔值,代表是否为该次调用的最后返回信息。
- 我们主要对data字典感兴趣,因此选择在事件中整体推送。
- 而error字典每次收到后应当立即检查是否包含错误信息(因为即使没有发生错误也会推送),若有则自动保存为一个日志事件(通过日志监控控件显示出来)。
- 服务器连接完成后(onFrontConnected),检查是否已经填入了用户名等登录信息,若有则自动登录(请参考后面主动函数中的示例)。
- 登陆完成后(onRspUserLogin),自动订阅__setSubscribed中之前已经订阅过的合约。
- 收到行情推送后(onRtnDepthMarketData),我们选择创建两种事件,一种是常规行情事件(通常适用于市场行情监控GUI等对所有行情推送都关注的组件),另一种是特定合约行情事件(通常适用于算法等仅关注特定合约行情的组件)。
- 当我们调用会有返回信息的主动函数时,需要传入本次请求的编号,此时我们先将__reqid自加1,再作为参数传入主动函数中。
2、主动函数
#----------------------------------------------------------------------
def login(self, address, userid, password, brokerid):
"""连接服务器"""
self.__userid = userid
self.__password = password
self.__brokerid = brokerid
# 注册服务器地址
self.registerFront(address)
# 初始化连接,成功会调用onFrontConnected
self.init()
#----------------------------------------------------------------------
def subscribe(self, instrumentid, exchangeid):
"""订阅合约"""
req = {}
req['InstrumentID'] = instrumentid
req['ExchangeID'] = exchangeid
self.subscribeMarketData(req)
instrument = (instrumentid, exchangeid)
self.__setSubscribed.add(instrument)
- 主动函数仅封装了两个功能,登录login和订阅合约subscribe。这里假设通常我们不会做登出(直接杀进程)和退订合约(不一定)之类的操作,有需求的话可以自行封装对应的函数。
- 对于登录函数login而言,传入4个参数包括服务器前置机地址address,用户名userid,密码password以及经纪商代码brokerid。函数调用后,我们先将userid,password和brokerid保存下来,然后注册服务器地址registerFront,并初始化连接init。连接完成后,onFrontConnected回调函数会被自动调用,然后发生的操作请参考前一段落的回调函数工作流程。
- LTS的API在订阅行情时,需要传入合约的代码以及合约所在的交易所(因为存在两个证券交易所相同代码的情况),而CTP的API在期货方面则不存在该问题,只需传入合约代码。发送订阅请求后,将该订阅请求保存在__setSubscribed集合中,使得断线重连时可以自动重新订阅。
三、总结
在交易程序的开发中,所有的API对接原理均大同小异,除了类CTP API以外,国内的恒生接口、FIX引擎接口等等也可以同样遵照以上的原理进行对接设计。