综合交易平台(Comprehensive Transaction Platform,CTP)是专门为期货公司开发的一套期货经纪业务管理系统,由交易、风险控制和结算三大系统组成。
前后研究了两个多星期 CTP,各种查资料,感觉总算是基本搞清楚了 CTP 是个什么东西(鬼知道我为什么要搞 CTP),说多了都是泪。本文主要通过对 CTP 简单案例的实现,对 CTP 进行简单的讲解,以及本人学习过程中遇到的一些坑。
你可以在 上期技术官网 里面下载最新的 API 接口以及说明文档,或者也可以在 simnow 官网 下载,都是一样的,只不过上期技术官网会时不时抽风。
下载完成之后,我们得到了下面这些文件,以 win64 为例:
// 结构类型定义
ThostFtdcUserApiStruct.h // 定义了客户端接口使用的业务数据结构
ThostFtdcUserApiDataType.h // 定义了客户端接口使用的业务数据类型
// 行情部分
ThostFtdcMdApi.h // 定义了客户端接口
thostmduserapi_se.dll // 行情官方动态库
thostmduserapi_se.lib // 行情官方静态库
// 交易部分
ThostFtdcTraderApi.h // 定义了客户端接口
thosttraderapi_se.dll // 交易官方动态库
thosttraderapi_se.lib // 交易官方静态库
// 其他
error.dtd // 错误信息
error.xml // 错误信息
总的来说,我们可以将 CTP 分为「行情部分」和「交易部分」
对于 API 的更多信息,你可以在官网上下载的 API 接口说明 中找到所有 API 和 SPI 的函数原型信息。(也可以点击这里下载)
{% note info %}
CTP 的所有接口都分为 Spi 和 Api 两种,这里对其简单说明
API:Api 类提供了交易/行情的各种功能,但这些需要我们主动对服务器发出的请求。
SPI:Spi 类提供了交易/行情相关的回调接口,我们需要继承该类并重载这些接口,以获取响应数据。
一般来说,API 和 SPI 都是配对出现的。
举个例子,我们为了实现登录,需要我们主动调用 API 函数 ReqUserLogin
,我们暂且忽略参数,就像下面这个样子:
m_mdApi->ReqUserLogin(&t, 1)
在执行上面这条命令之后,我们向服务器发送 登录请求,服务器收到我们的请求之后,向我们发送 登录成功消息;
之后对应的 SPI 回调函数 OnRspUserLogin
就会被调用。
void MdSpi::OnRspUserLogin(...)
{
cout << "账户登录成功" << endl;
}
{% endnote %}
在这里,我们以「行情部分」为例进行实现,「交易部分」的逻辑也是类似的。
首先,我们需要自己创建一个类,并继承 CThostFtdcMdSpi
类,同时需要我们自己重载 CThostFtdcMdSpi
中的回调函数,以便实现自己需要的功能。
我们新建一个名为 CTPDemo
的项目,并创建一个名为 MdSpi
的类,并继承 CThostFtdcMdSpi
。
{% note info %}
这里我用的是 Visual Studio 2019
注意设置好库目录,在链接器中引入静态库 thostmduserapi_se.lib
{% endnote %}
在头文件 MdSpi.h
中,我们写入如下代码:
// MdSpi.h
#include "ThostFtdcMdApi.h"
class MdSpi : public CThostFtdcMdSpi
{
public:
// 当客户端与交易后台建立起通信连接时(还未登录前),该方法被调用。
void OnFrontConnected();
};
在源文件 MdSpi.cpp
中,我们对 OnFrontConnected
进行重载:
// MdSpi.cpp
#include
#include "MdSpi.h"
using namespace std;
void MdSpi::OnFrontConnected()
{
cout << "建立网络连接成功" << endl;
}
在上面的过程中,我们做的事情很简单,新建一个名为 MdSpi
的类,并继承 CThostFtdcMdSpi
;然后对 OnFrontConnected
方法进行了重载,为了简单,这里只重载了一个函数,关于其他函数,下面会陆续实现。
然后,在 CTPDemo.cpp
的 main()
中初始化行情接口
#include
#include "MdSpi.h"
using namespace std;
#pragma comment (lib, "thostmduserapi_se.lib") // 链接库
CThostFtdcMdApi* g_pMdUserApi = nullptr; // 行情指针
int main()
{
// 初始化行情线程
g_pMdUserApi = CThostFtdcMdApi::CreateFtdcMdApi(); // 创建行情实例
CThostFtdcMdSpi* pMdUserSpi = new MdSpi; // 创建行情回调实例
g_pMdUserApi->RegisterSpi(pMdUserSpi); // 注册事件类
g_pMdUserApi->RegisterFront((char*)"tcp://180.168.146.187:10211"); // 设置行情前置地址
g_pMdUserApi->Init(); // 连接运行
// 等到线程退出
g_pMdUserApi->Join();
return 0;
}
在写完了上面这些代码之后,让我们回过头来看看上面的这些代码干了些什么事
#include
#include "MdSpi.h"
using namespace std;
#pragma comment (lib, "thostmduserapi_se.lib") // 链接库
第 1 - 4 行,我们导入了头文件 MdSpi.h
,并连接了静态库 thostmduserapi_se.lib
,做好准备工作。
CThostFtdcMdApi* g_pMdUserApi = nullptr; // 行情指针
...
...
g_pMdUserApi = CThostFtdcMdApi::CreateFtdcMdApi(); // 创建行情实例
第 6 行和第 12 行,我们创建了一个全局变量,名为 g_pMdUserApi
的指针,它的类型为 CThostFtdcMdApi
,在 main
通过 CreateFtdcMdApi
创建了 Api 实例,并将其赋值给 g_pMdUserApi
CThostFtdcMdSpi* pMdUserSpi = new MdSpi; // 创建行情回调实例
g_pMdUserApi->RegisterSpi(pMdUserSpi); // 注册事件类
g_pMdUserApi->RegisterFront((char*)"tcp://180.168.146.187:10211"); // 设置行情前置地址
第 13 - 14 行,我们创建了一个 Spi 实例,并通过 RegisterSpi
将 Api 与 Spi 进行绑定;第 15 行,我们注册了前置地址以便与服务器连接。
g_pMdUserApi->Init(); // 连接运行
第 16 行调用 Init()
函数开始正式初始化 api,也就是说前面的工作只是准备工作,到了这里 api 才真正开始工作。此时 api 会向之前注册的地址发起与 CTP 前置的连接。
接着,生成并执行 CTPDemo.exe
,你就会发现黑框中出现「建立网络连接成功」字样,说明我们与行情服务器连接成功!
行情接口的具体工作原理可以参考官方说明文档,首先可以尝试着理解,即在之后实现的过程中的流程问题
在上面,我们实现了一个最简单的 CTP 使用样例,接下来,我们在此基础上实现登录和获取行情信息
在进行登录操作之间,你需要在 sinmow 中注册一个模拟交易账户
SimNow 是上海期货交易所全资子公司上期技术公司专为投资者打造的期货模拟仿真交易平台,为上海期货交易所投资者教育网认证的期货模拟仿真系统。该产品仿真各交易所的交易及结算规则研发,目前已经支持国内各期货交易所的商品期货业务。
注册完成之后,请关注 investorId
和 brokerId
。
首先,让我们来看看登录时都需要提供些什么,打开 API 接口说明文档,找到函数 ReqUserLogin
ReqUserLogin
函数原型如下。我们发现,请求登录需要两个参数,一个为 CThostFtdcReqUserLoginField
,另一个为 int
类型的请求编号。
virtual int ReqUserLogin(CThostFtdcReqUserLoginField *pReqUserLoginField, int nRequestID) = 0;
现在让我们来看看 CThostFtdcReqUserLoginField
是什么东西。打开 ThostFtdcUserApiStruct.h
,就在前面几行,我们很容易找到「用户登录请求」的结构体,在这里,主要截取了几个关键字段,也就是登录时我们需要提供的。
///用户登录请求
struct CThostFtdcReqUserLoginField
{
...
///经纪公司代码
TThostFtdcBrokerIDType BrokerID;
///用户代码
TThostFtdcUserIDType UserID;
///密码
TThostFtdcPasswordType Password;
...
};
在 MdSpi.cpp
中写入如下代码
// MdSpi.cpp
#include
#include "MdSpi.h"
using namespace std;
extern CThostFtdcMdApi* g_pMdUserApi; // 行情指针
void MdSpi::OnFrontConnected()
{
cout << "建立网络连接成功" << endl;
// 开始登录
CThostFtdcReqUserLoginField loginReq;
memset(&loginReq, 0, sizeof(loginReq));
strcpy(loginReq.BrokerID, "9999");
strcpy(loginReq.UserID, "000000");
strcpy(loginReq.Password, "123456");
static int requestID = 0;
int result = g_pMdUserApi->ReqUserLogin(&loginReq, ++requestID);
if (!result)
cout << "发送登录请求成功" << endl;
else
cerr << "发送登录请求失败" << endl;
}
在之前代码的基础上,我们又添加了一些代码。
在这里,我将登录代码直接写在了 OnFrontConnected
中,那么这也就意味着,一旦建立网络连接成功,调用 OnFrontConnected
之后便会进行登录操作。
extern CThostFtdcMdApi* g_pMdUserApi; // 行情指针
在第 6 行中,我们声明全局变量 g_pMdUserApi
,也就是我们之前创建的 Api 实例
CThostFtdcReqUserLoginField loginReq;
memset(&loginReq, 0, sizeof(loginReq));
strcpy(loginReq.BrokerID, "9999");
strcpy(loginReq.UserID, "000000");
strcpy(loginReq.Password, "123456");
第 12 - 16 行,我们创建了一个 CThostFtdcReqUserLoginField
类型的对象,并写入 BrokerID
,UserID
和 Password
。注意,请替换为你自己的用户名和密码。
static int requestID = 0;
int result = g_pMdUserApi->ReqUserLogin(&loginReq, ++requestID);
if (!result)
cout << "发送登录请求成功" << endl;
else
cerr << "发送登录请求失败" << endl;
第 17- 22 行,将 loginReq
和 requestID
传入 ReqUserLogin
,并输出结果。
接下来,我们尝试生成并执行 CTPDemo.exe
,发现有发送登录请求成功字样,说明发送登录请求成功。
{% note info %}
ReqUserLogin
返回值
0,代表成功。
-1,表示网络连接失败;
-2,表示未处理请求超过许可数;
-3,表示每秒发送请求数超过许可数。
{% endnote %}
在发送登录请求之后,正如之前提到的,我们还需要重载回调函数 OnRspUserLogin
接收登陆信息。
在 MdSpi.h
中声明回调函数 OnRspUserLogin
// MdSpi.h
#include "ThostFtdcMdApi.h"
class MdSpi : public CThostFtdcMdSpi
{
public:
// 当客户端与交易后台建立起通信连接时(还未登录前),该方法被调用。
void OnFrontConnected();
///登录请求响应
void OnRspUserLogin(CThostFtdcRspUserLoginField* pRspUserLogin, CThostFtdcRspInfoField* pRspInfo, int nRequestID, bool bIsLast);
};
在 MdSpi.cpp
中添加如下代码
// MdSpi.cpp
void MdSpi::OnRspUserLogin(CThostFtdcRspUserLoginField* pRspUserLogin, CThostFtdcRspInfoField* pRspInfo, int nRequestID, bool bIsLast)
{
bool bResult = pRspInfo && (pRspInfo->ErrorID != 0);
if (!bResult) {
cout << "账户登录成功" << endl;
cout << "交易日: " << pRspUserLogin->TradingDay << endl;
}
else
cerr << "返回错误--->>> ErrorID=" << pRspInfo->ErrorID << ", ErrorMsg=" << pRspInfo->ErrorMsg << endl;
}
在回调函数 OnRspUserLogin
中,我们首先判断了错误是否出现,若登陆成功,则打印交易日信息。
{% note info %}
你可以在说明文档中找到响应信息 pRspInfo
的结构
struct CThostFtdcRspInfoField
{
///错误代码
TThostFtdcErrorIDType ErrorID;
///错误信息
TThostFtdcErrorMsgType ErrorMsg;
};
{% endnote %}
最后,生成并执行 CTPDemo.exe
,可以看到如下信息,说明我们登陆成功
在上面的操作中,我们实现了用户登陆,接下来开始尝试获取行情信息
同样的,我们在说明文档中找到 Api,SubscribeMarketData
,发现他需要两个参数,一个是需要订阅的合约列表,另一个是合约数组的数量,具体的使用方法也可以在调用示例中找到。
virtual int SubscribeMarketData(char *ppInstrumentID[], int nCount) = 0;
同样的,我们需要在 MdSpi.h
中声明其回调函数
// MdSpi.h
#include "ThostFtdcMdApi.h"
class MdSpi : public CThostFtdcMdSpi
{
public:
// 当客户端与交易后台建立起通信连接时(还未登录前),该方法被调用。
void OnFrontConnected();
///登录请求响应
void OnRspUserLogin(CThostFtdcRspUserLoginField* pRspUserLogin, CThostFtdcRspInfoField* pRspInfo, int nRequestID, bool bIsLast);
///订阅行情应答
void OnRspSubMarketData(CThostFtdcSpecificInstrumentField* pSpecificInstrument, CThostFtdcRspInfoField* pRspInfo, int nRequestID, bool bIsLast);
};
另一方面,在 MdSpi.cpp
中的完整代码如下
// MdSpi.cpp
#include
#include "MdSpi.h"
using namespace std;
extern CThostFtdcMdApi* g_pMdUserApi;
void MdSpi::OnFrontConnected()
{
cout << "建立网络连接成功" << endl;
// 开始登录
CThostFtdcReqUserLoginField loginReq;
memset(&loginReq, 0, sizeof(loginReq));
strcpy(loginReq.BrokerID, "9999");
strcpy(loginReq.UserID, "000000");
strcpy(loginReq.Password, "123456");
static int requestID = 0;
int result = g_pMdUserApi->ReqUserLogin(&loginReq, ++requestID);
if (!result)
cout << "发送登录请求成功" << endl;
else
cerr << "发送登录请求失败" << endl;
}
void MdSpi::OnRspUserLogin(CThostFtdcRspUserLoginField* pRspUserLogin, CThostFtdcRspInfoField* pRspInfo, int nRequestID, bool bIsLast)
{
bool bResult = pRspInfo && (pRspInfo->ErrorID != 0);
if (!bResult) {
cout << "账户登录成功" << endl;
cout << "交易日: " << pRspUserLogin->TradingDay << endl;
// 开始订阅行情 // 新增
char** ppInstrumentID = new char* [50]; // 新增
ppInstrumentID[0] = "cu2108"; // 新增
g_pMdUserApi->SubscribeMarketData(ppInstrumentID, 1); // 新增
}
else
cerr << "返回错误--->>> ErrorID=" << pRspInfo->ErrorID << ", ErrorMsg=" << pRspInfo->ErrorMsg << endl;
}
void MdSpi::OnRspSubMarketData(CThostFtdcSpecificInstrumentField* pSpecificInstrument, CThostFtdcRspInfoField* pRspInfo, int nRequestID, bool bIsLast)
{
bool bResult = pRspInfo && (pRspInfo->ErrorID != 0);
if (!bResult) {
cout << "订阅行情成功" << endl;
cout << "合约代码: " << pSpecificInstrument->InstrumentID << endl;
}
else
cerr << "返回错误--->>> ErrorID=" << pRspInfo->ErrorID << ", ErrorMsg=" << pRspInfo->ErrorMsg << endl;
}
第 32 - 35 行,我们在登陆的回调函数 OnRspUserLogin
中,添加了订阅行情请求,指定合约 cu2108
。
第 41 - 51 行,我们添加了回调函数 OnRspSubMarketData
,返回订阅行情的响应信息。
生成并执行 CTPDemo.exe
,可以看到如下信息,说明我们订阅行情成功
正如我们在 行情接口工作原理 中看到的,订阅合约之后,我们还需要持续接收合约的具体行情信息
在说明文档中找到对应的回调函数 OnRtnDepthMarketData
virtual void OnRtnDepthMarketData(CThostFtdcDepthMarketDataField *pDepthMarketData) {};
在 MdSpi.h
中对其进行声明
// MdSpi.h
#include "ThostFtdcMdApi.h"
class MdSpi : public CThostFtdcMdSpi
{
public:
// 当客户端与交易后台建立起通信连接时(还未登录前),该方法被调用。
void OnFrontConnected();
///登录请求响应
void OnRspUserLogin(CThostFtdcRspUserLoginField* pRspUserLogin, CThostFtdcRspInfoField* pRspInfo, int nRequestID, bool bIsLast);
///订阅行情应答
void OnRspSubMarketData(CThostFtdcSpecificInstrumentField* pSpecificInstrument, CThostFtdcRspInfoField* pRspInfo, int nRequestID, bool bIsLast);
///深度行情通知
void OnRtnDepthMarketData(CThostFtdcDepthMarketDataField* pDepthMarketData);
};
在 MdSpi.cpp
中实现 OnRtnDepthMarketData
void MdSpi::OnRtnDepthMarketData(CThostFtdcDepthMarketDataField* pDepthMarketData)
{
cout << "=====获得深度行情成功=====" << endl;
cout << "合约代码:" << pDepthMarketData->InstrumentID << endl;
cout << "最新价:" << pDepthMarketData->LastPrice << endl;
cout << "成交量:" << pDepthMarketData->Volume << endl;
}
最后,生成并执行 CTPDemo.exe
,可以不断看到最新的行情信息。
至此,我们简单实现了「行情部分」的「登录」和「获取行情」,总的来说,重点是理解 CTP 的工作原理以及流程。对于「交易部分」的实现也是类似的,这里就不再赘述。
你可以在这里找到完整的代码:https://github.com/EmoryHuang/CTPDemo