转自:https://zhuanlan.zhihu.com/p/20031646?columnSlug=vn-py
转载请注明出处:用Python的交易员
类CTP交易API简介
国内程序化交易技术的爆发式发展几乎就是起源于上期技术公司基于CTP柜台推出了交易API,使得用户可以随意开发自己的交易软件直接连接到交易柜台上进行交易,同时CTP API的设计模式也成为了许多其他柜台上交易API的设计标准,本人已知的类CTP交易API包括:
- 上期CTP
- 飞马
- 华宝证券LTS
- 飞创Xspeed
- 金仕达
- 恒生UFT
所以这个教程系列选择从类CTP交易API中的LTS API开始来介绍API的Python封装方法,真正掌握了以后想要做其他类型API(比如恒生的T2)的封装也只是大同小异而已。
LTS API文件说明
通常当用户从网上下载API的压缩包,解压后会看到以下的文件:
- .h文件:C++的头文件,包含了API的内部结构信息,开发C++程序时需要包含在项目内
- .dll文件:windows下的动态链接库文件,API的实体,开发C++程序编译和链接时用,使用开发好的程序时也必须放在程序的文件夹内
- .lib文件:windows下的库文件,编译和链接时用,程序开发好后无需放在程序的文件夹内
- .so文件:linux下的动态链接库文件,其他同.dll文件
找不到压缩包的读者可以在这里直接看vnpy/vn.lts/ltsapi at master · vnpy/vnpy · GitHub。
.h头文件介绍
.dll、.lib、.so文件都是编译好的二进制文件,无法打开,所以从用户角度我们只需关注.h文件中的内容。对于不同的API而言,.h文件的前缀可能有所区别,如LTS是SecurityFtdc,CTP是ThostFtdc,下面分别介绍这4个.h文件。
ApiDataType.h
该文件中包含了对API中用到的常量的定义,如以下代码定义了一个产品类型常量对应的字符:
#define SECURITY_FTDC_PC_Futures '1'
以及类型的定义,如以下代码定义了产品名称类型是一个长度为21个字符的字符串:
typedef char TSecurityFtdcProductNameType[21];
ApiStruct.h
该文件中包含了API中用到的结构体的定义,如以下代码定义了交易所这个结构体的构成:
///交易所
struct CSecurityFtdcExchangeField
{
///交易所代码
TSecurityFtdcExchangeIDType ExchangeID;
///交易所名称
TSecurityFtdcExchangeNameType ExchangeName;
///交易所属性
TSecurityFtdcExchangePropertyType ExchangeProperty;
};
例如TSecurityFtdcExchangeIDType这个类型的定义,可以在ApiDataType.h中找到。
MdApi.h
该文件中包含了API中的行情相关组件的定义,文件通常开头会有一段这样的内容:
#if !defined(SECURITY_FTDCMDAPI_H)
#define SECURITY_FTDCMDAPI_H
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#include "SecurityFtdcUserApiStruct.h"
#if defined(ISLIB) && defined(WIN32)
#ifdef LIB_MD_API_EXPORT
#define MD_API_EXPORT __declspec(dllexport)
#else
#define MD_API_EXPORT __declspec(dllimport)
#endif
#else
#define MD_API_EXPORT
#endif
这些内容主要是一些和操作系统、编译环境相关的定义,一般用户忽略就好(作者其实也不太懂…)。
然后是两个类CSecurityFtdcMdSpi和CSecurityFtdcMdApi的定义。
CSecurityFtdcMdSpi
MdSpi类中包含了行情功能相关的回调函数接口,什么是回调函数呢?简单来说就是由于柜台端向用户端发送信息后才会被系统自动调用的函数(非用户主动调用),对应的主动函数会在下面介绍。CSecurityFtdcMdSpi大概看起来是这么个样子:
class CSecurityFtdcMdSpi
{
public:
......
///登录请求响应
virtual void OnRspUserLogin(CSecurityFtdcRspUserLoginField *pRspUserLogin, CSecurityFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast) {};
......
///深度行情通知
virtual void OnRtnDepthMarketData(CSecurityFtdcDepthMarketDataField *pDepthMarketData) {};
};
……省略了部分代码。从上面的代码中可以注意到:
- 回调函数都是以On开头。
- 柜台端向用户端发送的信息经过API处理后,传给我们的是一个结构体的指针,如CSecurityFtdcRspUserLoginField *pRspUserLogin,这里的pRspUserLogin就是一个C++的指针类型,其指向的结构体对象是CSecurityFtdcRspUserLoginField结构的,而该结构的定义可以在ApiStruct.h中找到。
- 不同的回调函数,传过来的参数数量是不同的,OnRspUserLogin中传入的参数包括两个结构体指针,以及一个整数(代表该响应对应的用户请求号)和一个布尔值(该响应是否是这个请求号的最后一次响应)。
CSecurityFtdcMdApi
MdApi类中包含了行情功能相关的主动函数结构,顾名思义,主动函数指的是由用户负责进行调用的函数,用于向柜台端发送各种请求和指令,大概样子如下:
class MD_API_EXPORT CSecurityFtdcMdApi
{
public:
///创建MdApi
///@param pszFlowPath 存贮订阅信息文件的目录,默认为当前目录
///@return 创建出的UserApi
///modify for udp marketdata
static CSecurityFtdcMdApi *CreateFtdcMdApi(const char *pszFlowPath = "");
......
///注册回调接口
///@param pSpi 派生自回调接口类的实例
virtual void RegisterSpi(CSecurityFtdcMdSpi *pSpi) = 0;
///订阅行情。
///@param ppInstrumentID 合约ID
///@param nCount 要订阅/退订行情的合约个数
///@remark
virtual int SubscribeMarketData(char *ppInstrumentID[], int nCount, char* pExchageID) = 0;
......
///用户登录请求
virtual int ReqUserLogin(CSecurityFtdcReqUserLoginField *pReqUserLoginField, int nRequestID) = 0;
......
};
以上代码中,需要注意的重点包括:
- MdApi对象不应该直接创建,而应该通过调用类的静态方法CreateFtdcMdApi创建,传入参数为你希望保存API的通讯用的.con文件的目录(可以选择留空,则.con文件会被放在程序所在的文件夹下)。
- 创建MdSpi对象后,需要使用MdApi对象的RegisterSpi方法将该MdSpi对象的指针注册到MdApi上,也就是告诉MdApi从柜台端收到数据后应该通过哪个对象的回调函数推送给用户。从API的这个设计上作者猜测MdApi中后包含了和柜台端通讯、接收和发送数据包的功能,而MdSpi仅仅是用来实现一个通过回调函数向用户程序推送数据的接口。
- 绝大部分主动函数(以Req开头)在调用时都会用到一个整数类型的参数nRequestID,该参数在整个API的调用中应当保持递增唯一性,从而在收到回调函数推送的数据时,可以知道是由哪次操作引起的。
TraderApi.h
该文件中包含了API中的交易相关组件的定义,文件同样以一段看不懂的定义开头,然后包含了两个类CSecurityFtdcTraderSpi和CSecurityFtdcTraderApi,这两个类和MdApi中的两个类在结构上非常接近,区别仅仅在于类包含的方法函数上。
CSecurityFtdcTraderSpi
class CSecurityFtdcTraderSpi
{
public:
///当客户端与交易后台建立起通信连接时(还未登录前),该方法被调用。
virtual void OnFrontConnected(){};
...
///错误应答
virtual void OnRspError(CSecurityFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast) {};
///登录请求响应
virtual void OnRspUserLogin(CSecurityFtdcRspUserLoginField *pRspUserLogin, CSecurityFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast) {};
...
///报单通知
virtual void OnRtnOrder(CSecurityFtdcOrderField *pOrder) {};
...
///报单录入错误回报
virtual void OnErrRtnOrderInsert(CSecurityFtdcInputOrderField *pInputOrder, CSecurityFtdcRspInfoField *pRspInfo) {};
...
};
Spi(包括MdSpi和TraderSpi)类的回调函数基本上可以分为以下四种:
- 以On…开头,这种回调函数通常是返回API连接相关的信息内容,与业务逻辑无关,返回值(即回调函数的参数)通常为空或是简单的整数类型。
- 以OnRsp…开头,这种回调函数通常是针对用户的某次特定业务逻辑操作返回信息内容,返回值通常会包括4个参数:业务逻辑相关结构体的指针,错误信息结构体的指针,本次操作的请求号整数,是否是本次操作最后返回信息的布尔值。其中OnRspError主要用于一些通用错误信息的返回,因此返回的值中不包含业务逻辑相关结构体指针,只有3个返回值。
- 以OnRtn…开头,这种回调函数返回的通常是由柜台向用户主动推送的信息内容,如客户报单状态的变化、成交情况的变化、市场行情等等,因此返回值通常只有1个参数,为推送信息内容结构体的指针。
- 以OnErrRtn…开头,这种回调函数通常由于用户进行的某种业务逻辑操作请求(挂单、撤单等等)在交易所端触发了错误,如用户发出撤单指令、但是该订单在交易所端已经成交,返回值通常是2个参数,即业务逻辑相关结构体的指针和错误信息的指针。
CSecurityFtdcTraderApi
class TRADER_API_EXPORT CSecurityFtdcTraderApi
{
public:
///创建TraderApi
///@param pszFlowPath 存贮订阅信息文件的目录,默认为当前目录
///@return 创建出的UserApi
static CSecurityFtdcTraderApi *CreateFtdcTraderApi(const char *pszFlowPath = "");
...
///初始化
///@remark 初始化运行环境,只有调用后,接口才开始工作
virtual void Init() = 0;
...
///用户登录请求
virtual int ReqUserLogin(CSecurityFtdcReqUserLoginField *pReqUserLoginField, int nRequestID) = 0;
...
};
Api类包括的主动函数通常分为以下三种:
- Create…,类的静态方法,用于创建API对象,传入参数是用来保存API通讯.con文件的文件夹路径。
- Req…开头的函数,可以由用户主动调用的业务逻辑请求,传入参数通常包括2个:业务请求结构体指针和一个请求号的整数。
- 其他非Req…开头的函数,包括初始化、订阅数据流等等参数较为简单的功能,传入参数的数量和类型视乎函数功能不一定。
API工作流程
简单介绍一下MdApi和TraderApi的一般工作流程,这里不会包含太多细节,仅仅是让读者有一个概念。
MdApi
- 创建MdSpi对象
- 调用MdApi类以Create开头的静态方法,创建MdApi对象
- 调用MdApi对象的RegisterSpi方法注册MdSpi对象的指针
- 调用MdApi对象的RegisterFront方法注册行情柜台的前置机地址
- 调用MdApi对象的Init方法初始化到前置机的连接,连接成功后会通过MdSpi对象的OnFrontConnected回调函数通知用户
- 等待连接成功的通知后,可以调用MdApi的ReqUserLogin方法登陆,登陆成功后会通过MdSpi对象的OnRspUserLogin通知用户
- 登陆成功后就可以开始订阅合约了,使用MdApi对象的SubscribeMarketData方法,传入参数为想要订阅的合约的代码
- 订阅成功后,当合约有新的行情时,会通过MdApi的OnRtnDepthMarketData回调函数通知用户
- 用户的某次请求发生错误时,会通过OnRspError通知用户。
- MdApi同样提供了退订合约、登出的功能,一般退出程序时就直接杀进程(不太安全)
TraderApi
- TraderApi和MdApi类似,以下仅仅介绍不同点
- 注册TraderSpi对象的指针后,需要调用TraderApi对象的SubscribePrivateTopic和SubscribePublicTopic方法去选择公开和私有数据流的重传方法(这一步MdApi没有)
- 对于期货柜台而言(CTP、恒生UFT期货等),在每日第一次登陆成功后需要先查询前一日的结算单,等待结算单查询结果返回后,确认结算单,才可以进行后面的操作;而证券柜台LTS无此要求
- 上一步完成后,用户可以调用ReqQryInstrument的方法查询柜台上所有可以交易的合约信息(包括代码、中文名、涨跌停、最小价位变动、合约乘数等大量细节),一般是在这里获得合约信息列表后,再去MdApi中订阅合约;经常有人问为什么在MdApi中找不到查询可供订阅的合约代码的函数,这里尤其要注意,必须通过TraderApi来获取
- 当用户的报单、成交状态发生变化时,TraderApi会自动通过OnRtnOrder、OnRtnTrade通知用户,无需额外订阅
总结
第一篇教程到这里已经接近结束了,如果你是一个没有任何交易API开发经验的读者,并且坚持看了下来,此时你心中很可能有这么个想法:我X,API开发这么复杂???!!!
相信我,这是人之常情(某些读者如果觉得很好理解那作者真是佩服你了),作者刚开始的时候大概在CTP API的头文件和网上的教程资料、示例中纠结了3个多月而不得入门,当时也没有任何C++的开发经验(我是金融工程出生,大学里编程只学了VBA和Matlab,还几乎都是些算法方面的内容),边学语言边研究怎么开发,真心痛苦。
在这里,我想告诉读者的一个好消息是:还剩两篇教程,我们基本就可以和C++ say goodbye,进入Python灵活快速开发的世界了。同时对于绝大部分不打算自己去封装API的读者,这三篇文章可以走马观花的过一遍,不会影响任何你未来对于vn.py框架的使用。
当然,对于有恒心和毅力的读者,100%自己掌握API的封装技术是一项绝对值得投入时间和精力的事情。在很多人的观念中Python并不适合用来开发低延迟的交易平台,这里作者可以用亲身经验告诉你:那只是在纯用Python的情况下。作为一门胶水语言,Python最大的特点之一就是易于通过混合编程来进行拓展,用户可以在真正需要优化的地方进行最深度的定制优化,把自己有限的时间、精力花在刀刃上。在交易API层面,可以定制的地方包括C++层面的数据结构改变、数据预处理、回调函数传递顺序调整等等诸多的优化,这些只有在你完全掌握API的封装后才能办得到。
Python量化交易平台开发教程系列2-类CTP交易API的Python封装设计
原创文章,转载请注明出处:用Python的交易员
(本篇教程包含的内容太多也太复杂,有不少读者反应看不懂,因为本身也不是使用vn.py必须掌握的知识,这篇教程暂时处于半完成状态,等多收集些读者的建议后会再做一个比较大的修订)
为什么要封装API
直接原因就是C++的API没法直接在Python里用,不过这个回答有点太简单,这里我们稍微做一些拓展解释:
- C++ API中很多函数的调用参数是ApiStruct.h(参见上一篇)中定义的结构体,而在Python中我们既无法直接创建这些结构体(主动函数),也无法提取结构体中包含的数据(回调函数)。
- Python虚拟机是基于C语言实现的,所有的Python对象,哪怕只是一个整数或者字符串,在C的环境中都是一个PyObject对象(好吧,我知道C里没有对象,只有结构体,但估计90%的读者都不在乎这个区别)。用户如果在Python中直接传递一个参数到C++环境里,C++是无法识别的(Python:买入1手股指, C++:你要买入多少?)。
- Python只能加载封装为PyObject对象的模块,因此原生C++的API在Python中连加载都加载不了。
封装后API的工作流程
主动函数
- 用户在Python程序中调用封装API的主动函数,并直接传入Python变量(PyObject对象)作为参数。
- 封装API将Python变量转换成C++变量。
- 封装API调用原生API的主动函数,并传入C++变量作为参数。
回调函数
- 交易柜台通过原生API的C++回调函数推送数据信息,传入参数为C++变量
- 封装API将C++变量转换为Python变量
- 封装API调用封装后的回调函数向用户的Python程序中推送数据,传入参数为Python变量
名词定义
- 封装API:指的是经过封装后,可以直接在Python中使用的API
- 原生API:指的是由软件公司提供,在C++中使用的API
- Python变量:包含Python中的数字、字符串、对象等等
- C++变量:包含C++中的内置数据类型和结构体等
从Python的角度看原生API的一些问题
上一篇教程后读者应该对C++ API的结构和使用方法有了基础的了解,这篇教程主要介绍的是对原生的C++ API进行Python封装时的设计和思路,这些构成了vn.py开源项目中vn.lts(华宝证券LTS柜台API封装)的基础。首先让我们来从Python的角度看看原生API的一些问题:
- 原生的API中每个功能分为了两个类:分别是包含回调函数的Spi类和主动函数的Api类,这种设计能让用户更好的分清不同的功能。但是从面向对象的角度,把两个类封装到一起更为方便,实际使用中绝大部分C++的用户也会将接口整合到一个类里面(可以参见网上很多CTP开发的示例代码),因此Python的API中,我们也会将Spi和Api两个类的功能封装到一个类中。
- 原生的API中回调函数被触发后必须快速返回,否则会导致其他数据的推送被阻塞,阻塞时间长了还有可能导致API发生崩溃,因此回调函数中不适合包含耗时较长的计算逻辑。例如某个TICK行情推送后,如果用户在回调函数中写了一些比较复杂的计算(循环计算等等),耗时超过3秒(这个数字只是笔者的一个经验),则在这个3秒中,其他的行情推送用户是收不到的(被阻塞了),且很可能3秒后会出现API崩溃(程序死掉)。这里的解决方案是使用生产者-消费者模型,在API中包含一个缓冲队列,当回调函数收到新的数据信息时只是简单存入缓冲队列中并立即返回,而数据信息的处理和向Python中的推送则由另一个工作线程来执行。
- API的函数中使用了大量的结构体用于数据传送,这在C++而言是非常自然的设计,但是对Python封装会造成不小的麻烦,所有的结构体都要封装成对应的Python类,工作量太大也非常容易出错。这点我们可以利用Python相对于C++更为高级的数据结构来解决,Python中的dict字典本质是一个哈希表,但是同一个字典内键和值的类型允许不同,这个特性使得字典可以非常方便的用来代替C++的结构体。
明确了以上的问题后,我们就可以开始着手设计Python API的结构了。
Python API的结构设计
这里使用行情API作为示例。
…表示省略的代码。
//API的继承实现
class MdApi : public CSecurityFtdcMdSpi
{
private:
CSecurityFtdcMdApi* api; //API对象
thread *task_thread; //工作线程指针(向python中推送数据)
ConcurrentQueue<Task> task_queue; //任务队列
public:
MdApi()
{
function0<void> f = boost::bind(&MdApi::processTask, this);
thread t(f);
this->task_thread = &t;
};
~MdApi()
{
};
...
//登录请求响应
virtual void OnRspUserLogin(CSecurityFtdcRspUserLoginField *pRspUserLogin, CSecurityFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast);
...
//数据任务处理函数(在工作线程中运行)
void processTask();
...
//处理登陆请求响应
void processRspUserLogin(Task task);
...
//该回调函数在Python中继承
virtual void onRspUserLogin(dict data, dict error, int id, bool last) {};
...
//请求登陆的主动函数
int reqUserLogin(dict req, int nRequestID);
...
};
(以上代码截取自vnltsmd.h文件)
注意原生API的函数名开头都是大写字母,为了便于分辨以及符合Python的PEP8编码规则,作者的函数都以小写字母开头。上面的代码中采用的示例是用户登陆UserLogin这个功能。
封装中的类和函数命名规则
- 封装后的Python API的类取名为MdApi,注意这个不是原生API中的CSecurityFtdcMdApi。
- 原生API中以On开头的回调函数(如OnRspUserLogin)对应的Python API的回调函数直接改为以on开头(如onRspUserLogin)。
- 原生API中的主动函数(如ReqUserLogin)对应的封装后API中的主动函数改为首字母小写(如reqUserLogin)。
MdApi的成员变量
- api:原生API中的CSecurityFtdcMdApi对象,用于实现主动函数的调用。
- task_thread:一个boost线程指针,用于实现任务线程的工作。
- task_queue:一个线程安全的任务队列。
工作步骤(后面会有具体函数的实现细节)
- 用户在Python中调用reqUserLogin函数,传入参数为包含登陆信息(用户名、密码)的字典req以及本次请求号nRequestID,该函数自动将字典中的信息提取并创建原生API使用的结构体后,调用原生API的主动函数ReqUserLogin来进行登录。
- 登录成功后,原生API会调用OnRspUserLogin的回调函数返回登录信息(注意这里On是大写),在回调函数里,只是简单的把结构体数据保存到一个任务对象Task中,并推送到任务队列里。
- 工作线程中运行的函数是processTask,该函数负责检查任务队列中是否有新的任务,如果有则调用对应的process函数进行处理,如果没有则阻塞等待。
- processTask函数检查到任务队列中OnRspUserLogin推送的一个任务后,调用processRspUserLogin函数进行处理。该函数首先从结构体中提取数据并转换为Python字典,然后调用onRspUserLogin函数(这里的on是小写)推送到Python环境中,onRspUserLogin函数由用户在Python中继承实现。
Python API的函数实现
仍然使用之前的示例进行函数实现的讲解,包括MdApi的构造、析构函数,主动函数(reqUserLogin等),原生API回调函数(OnRspUserLogin等)和任务处理函数(processTask和processRspUserLogin等)。
构造、析构函数
MdApi()
{
function0<void> f = boost::bind(&MdApi::processTask, this);
thread t(f);
this->task_thread = &t;
};
~MdApi()
{
};
(以上代码截取自vnltsmd.h文件)
构造函数中仅包含了创建一个工作函数为processTask的工作线程,并将该线程的指针绑定到task_thread上。
析构函数为空,用户在退出前应当主动调用安全退出函数(参见源代码中的exit)。
主动函数
int MdApi::reqUserLogin(dict req, int nRequestID)
{
//创建原生API函数调用需要的结构体
CSecurityFtdcReqUserLoginField myreq = CSecurityFtdcReqUserLoginField();
//初始化这个结构体的内存
memset(&myreq, 0, sizeof(myreq));
//提取字典中的内容并复制到结构体中
getChar(req, "MacAddress", myreq.MacAddress);
getChar(req, "UserProductInfo", myreq.UserProductInfo);
getChar(req, "UserID", myreq.UserID);
getChar(req, "AuthCode", myreq.AuthCode);
getChar(req, "TradingDay", myreq.TradingDay);
getChar(req, "InterfaceProductInfo", myreq.InterfaceProductInfo);
getChar(req, "BrokerID", myreq.BrokerID);
getChar(req, "ClientIPAddress", myreq.ClientIPAddress);
getChar(req, "OneTimePassword", myreq.OneTimePassword);
getChar(req, "ProtocolInfo", myreq.ProtocolInfo);
getChar(req, "Password", myreq.Password);
//将结构体的指针和代表请求编号的整数作为参数调用原生API的函数
int i = this->api->ReqUserLogin(&myreq, nRequestID);
//返回原生API函数的调用结果
return i;
};
(以上代码截取自vnltsmd.cpp文件)
原生API中的请求登录函数为ReqUserLogin,传入的参数一共包含两个:一个CSecurityFtdcReqUserLoginField 结构体的指针,一个代表请求编号的整数。
封装后的API函数为reqUserLogin,传入参数同样为两个:一个Python字典对象、一个整数。reqUserLogin函数会从Python字典对象中根据键值依次提取结构体中对应的数据。如结构体中有一个成员叫做BrokerID,则使用getChar函数从字典对象中提取”BrokerID”键对应的值。
getChar函数的实现如下:
//d为Python字典对象
//key为d中想要提取的数据的键名
//value为最终需要这个数据的结构体成员的指针
void getChar(dict d, string key, char *value)
{
//首先检查字典中是否存在key这个键
if (d.has_key(key))
{
//提取key这个键对应的值,即Python对象o
object o = d[key];
//生成从o中提取std::string类的提取器
extract<string> x(o);
//检查提取器是否能提取出数据
if (x.check())
{
//执行解包器,提取string对象s
string s = x();
//从s中获取字符串指针buffer
const char *buffer = s.c_str();
//将字符串指针指向的字符串数组复制到结构体成员的指针上
//对字符串指针赋值必须使用strcpy_s, vs2013使用strcpy编译通不过
//+1应该是因为C++字符串的结尾符号?不是特别确定,不加这个1会出错
strcpy_s(value, strlen(buffer) + 1, buffer);
}
}
};
(以上代码截取自vnltsmd.cpp文件)
由于原生API中用到的底层数据类型主要包括四种:char字符、char[]字符串数组、int整数、double浮点数,可以对应的Python中的数据类型为:string、int、float。因此设计了三个函数getChar、getInt、getDouble来从Python对象中提取所需的C++数据,getInt、getDouble请参见源代码。
原生API的回调函数
void MdApi::OnRspUserLogin(CSecurityFtdcRspUserLoginField *pRspUserLogin, CSecurityFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast)
{
Task task = Task();
task.task_name = ONRSPUSERLOGIN;
task.task_data = *pRspUserLogin;
if (pRspInfo)
{
task.task_error = *pRspInfo;
}
else
{
CSecurityFtdcRspInfoField empty_error = CSecurityFtdcRspInfoField();
memset(&empty_error, 0, sizeof(empty_error));
task.task_error = empty_error;
}
task.task_id = nRequestID;
task.task_last = bIsLast;
this->task_queue.push(task);
};
(以上代码截取自vnltsmd.cpp文件)
当登录成功后,原生API中的回调函数OnRspUserLogin会被自动调用,通知用户登录相关的信息,传入参数包括四个,分别为CSecurityFtdcRspUserLoginField结构体指针(用户本次登录的相关信息)pRspUserLogin,CSecurityFtdcRspInfoField结构体指针(登录是否存在错误的相关信息)pRspInfo,整数(登陆请求编号)nRequestID和布尔值(是否为该请求的最后一次通知)bIsLast。
在回调函数中,我们通过创建一个Task对象来保存这些信息,并推入task_queue中,等待工作线程的提取处理。其中,由于pRspInfo可能存在空指针的情况,所以需要进行判断,若指针为空,则在Task对象上绑定一个内容为空的CSecurityFtdcRspInfoField结构体(这步等于一个异常情况的处理)。ONRSPUSERLOGIN是一个整数常量(在头文件中定义),用于标识该Task对象包含的是哪个回调函数返回的信息。
Task对象的定义如下:
//任务结构体
struct Task
{
int task_name; //回调函数名称对应的常量
any task_data; //数据结构体
any task_error; //错误结构体
int task_id; //请求id
bool task_last; //是否为最后返回
};
(以上代码截取自vnltsmd.h文件)
其中any是boost库中的any类,作用是定义一个可以存放任意类型数据的变量(有点类似于Python里的变量),但是当用户尝试从该变量中获取原本的数据时,需要知道原本数据的类型。原生API中不同回调函数返回的参数类型是不同的,因此为了提高代码的简洁性选择使用boost.any这个泛型类。
任务处理函数
首先是负责从任务队列中提取任务,并根据任务名称的不同使用对应的函数进行处理的processTask函数:
///-------------------------------------------------------------------------------------
///工作线程从队列中取出数据,转化为python对象后,进行推送
///-------------------------------------------------------------------------------------
void MdApi::processTask()
{
while (1)
{
Task task = this->task_queue.wait_and_pop();
switch (task.task_name)
{
...
case ONRSPUSERLOGIN:
{
this->processRspUserLogin(task);
break;
}
...
};
}
};
(以上代码截取自vnltsmd.cpp文件)
使用while (1)的方式让processTask处于无限循环中不断运行,从task_queue队列中提取任务对象task后,使用swtich根据任务的回调函数名称task_name,调用对应的函数处理该任务。上面的例子中,当程序检查task_name是ONRSPUSERLOGIN这个常量值后,就会调用processRspUserLogin函数进行处理,其代码如下:
void MdApi::processRspUserLogin(Task task)
{
CSecurityFtdcRspUserLoginField task_data = any_cast<CSecurityFtdcRspUserLoginField>(task.task_data);
dict data;
data["MaxOrderRef"] = task_data.MaxOrderRef;
data["UserID"] = task_data.UserID;
data["TradingDay"] = task_data.TradingDay;
data["SessionID"] = task_data.SessionID;
data["SystemName"] = task_data.SystemName;
data["FrontID"] = task_data.FrontID;
data["BrokerID"] = task_data.BrokerID;
data["LoginTime"] = task_data.LoginTime;
CSecurityFtdcRspInfoField task_error = any_cast<CSecurityFtdcRspInfoField>(task.task_error);
dict error;
error["ErrorMsg"] = task_error.ErrorMsg;
error["ErrorID"] = task_error.ErrorID;
this->onRspUserLogin(data, error, task.task_id, task.task_last);
};
(以上代码截取自vnltsmd.cpp文件)
any_cast函数由boost.any库提供,作用之前提到的从any变量中提取出用户需要的数据类型来。dict类由boost.python库提供,使用dict可以直接创建Python环境中的字典,同时当我们使用d[key] = value这种语句进行赋值时,dict中的key和value均会自动转换为对应的Python对象。当我们将返回的业务信息CSecurityFtdcRspUserLoginField结构体和错误信息结构体CSecurityFtdcRspInfoField分别转换为data和error这两个Python字典后,我们就可以通过onRspUserLogin回调函数推送到Python环境中了。
总结
之前几段示例代码展示的是用户登陆这个简单业务操作,包括了从用户在Python中调用主动函数到柜台通过回调函数返回信息再推送到Python中的全过程。文章主要是对源代码中的注释起到一个更为细致的解释作用。同样这篇内容对于想用vn.py做量化平台开发的用户而言不是必须掌握的东西,放在这里主要是考虑教程的完整性,看不懂的就先跳过吧。
下一章是vn.py平台中API部分的编译方法,github上项目里的.pyd文件可能由于你的操作系统或者编译器和作者本人的不同没法直接使用,必须自行编译,整个教程会包含一步步的截图和说明,包教包会(还不会的可以到github上提问 :p )。
2015/4/20 项目开发日志
目前项目状态
差不多一个多月的时间,完成了CTP API的vn.ctp封装,以及用于展示如何使用vn.py框架开发的vn.demo,两者均已发布到github上。
接下来将会发布几篇和vn.demo相关的教程。
vn.demo介绍
该demo主要用于展示如何使用vn.py框架开发交易平台,使用了vn.event和vn.lts模块。
如何使用
常规用户可以直接运行exe文件夹下的demoMain.exe。
对开发有兴趣的用户需要根据http://vnpy.org的教程3安装相关的开发环境,然后通过demoMain.py和demoMain.pyw(无cmd界面)运行。
实现功能
demo的实现参考了盈佳和尔易的LTS交易平台,功能如下:
- 行情、持仓、账户、成交、报单的监控
- 平台的日志记录
- 下单交易,实现了LTS提供的所有订单类型和交易类型
- 双击报单监控中的单元格撤单,以及下单交易组件一键全撤
文件说明
- demoApi.py主要包含了程序的底层接口,对vn.lts中的API进行了简化封装
- demoEngine.py主要包含了程序的中间层,负责调用底层接口
- demoUi.py主要包含了用于数据监控和主动函数调用相关的GUI组件
- demoMain.py包含了程序的主函数入口,双击运行
- demoMain.pyw功能和demoMain.py一样,双击时会自动调用pythonw.exe运行(无cmd界面)
nuitka编译说明
要执行nuikta编译,请在本文件夹下打开cmd,并输入以下命令:
nuitka —recurse-all —windows-disable-console —icon=C:\vn.demo\vnpy.ico demoMain.py
其中C:\vn.demo\vnpy.ico需要修改为用户vn.demo文件夹的路径。
LTS测试服务器地址
行情
tcp://211.144.195.163:34513
交易
tcp://211.144.195.163:34505