CTP 学习笔记

前言

综合交易平台(Comprehensive Transaction Platform,CTP)是专门为期货公司开发的一套期货经纪业务管理系统,由交易、风险控制和结算三大系统组成。

前后研究了两个多星期 CTP,各种查资料,感觉总算是基本搞清楚了 CTP 是个什么东西(鬼知道我为什么要搞 CTP),说多了都是泪。本文主要通过对 CTP 简单案例的实现,对 CTP 进行简单的讲解,以及本人学习过程中遇到的一些坑。

下载安装

你可以在 上期技术官网 里面下载最新的 API 接口以及说明文档,或者也可以在 simnow 官网 下载,都是一样的,只不过上期技术官网会时不时抽风。

下载完成之后,我们得到了下面这些文件,以 win64 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 结构类型定义
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 分为「行情部分」和「交易部分」

  • 行情部分:在行情部分中,我们可以实现「订阅行情」,从而获取「深度行情」,也就是最新价、成交量、持仓量等合约的信息,这也被称为 tick 数据。
  • 交易部分:在交易部分中,我们可以实现查询合约,查询仓位,下单,退单等交易的功能。

对于 API 的更多信息,你可以在官网上下载的 API 接口说明 中找到所有 API 和 SPI 的函数原型信息。(也可以点击这里下载

CTP 的所有接口都分为 Spi 和 Api 两种,这里对其简单说明

API:Api 类提供了交易/行情的各种功能,但这些需要我们主动对服务器发出的请求

SPI:Spi 类提供了交易/行情相关的回调接口,我们需要继承该类并重载这些接口,以获取响应数据。

一般来说,API 和 SPI 都是配对出现的。

举个例子,我们为了实现登录,需要我们主动调用 API 函数 ReqUserLogin,我们暂且忽略参数,就像下面这个样子:

1
m_mdApi->ReqUserLogin(&t, 1)

在执行上面这条命令之后,我们向服务器发送 登录请求,服务器收到我们的请求之后,向我们发送 登录成功消息

之后对应的 SPI 回调函数 OnRspUserLogin 就会被调用。

1
2
3
4
void MdSpi::OnRspUserLogin(...)
{
cout << "账户登录成功" << endl;
}

「行情部分」实现

在这里,我们以「行情部分」为例进行实现,「交易部分」的逻辑也是类似的。

继承 CThostFtdcMdSpi 类

首先,我们需要自己创建一个类,并继承 CThostFtdcMdSpi 类,同时需要我们自己重载 CThostFtdcMdSpi 中的回调函数,以便实现自己需要的功能。

我们新建一个名为 CTPDemo 的项目,并创建一个名为 MdSpi 的类,并继承 CThostFtdcMdSpi

这里我用的是 Visual Studio 2019

注意设置好库目录,在链接器中引入静态库 thostmduserapi_se.lib

在头文件 MdSpi.h 中,我们写入如下代码:

1
2
3
4
5
6
7
8
// MdSpi.h
#include "ThostFtdcMdApi.h"
class MdSpi : public CThostFtdcMdSpi
{
public:
// 当客户端与交易后台建立起通信连接时(还未登录前),该方法被调用。
void OnFrontConnected();
};

在源文件 MdSpi.cpp 中,我们对 OnFrontConnected 进行重载:

1
2
3
4
5
6
7
8
9
// MdSpi.cpp
#include <iostream>
#include "MdSpi.h"
using namespace std;

void MdSpi::OnFrontConnected()
{
cout << "建立网络连接成功" << endl;
}

在上面的过程中,我们做的事情很简单,新建一个名为 MdSpi 的类,并继承 CThostFtdcMdSpi;然后对 OnFrontConnected 方法进行了重载,为了简单,这里只重载了一个函数,关于其他函数,下面会陆续实现。

初始化行情接口

然后,在 CTPDemo.cppmain() 中初始化行情接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#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;
}

在写完了上面这些代码之后,让我们回过头来看看上面的这些代码干了些什么事

1
2
3
4
#include <iostream>
#include "MdSpi.h"
using namespace std;
#pragma comment (lib, "thostmduserapi_se.lib") // 链接库

第 1 - 4 行,我们导入了头文件 MdSpi.h,并连接了静态库 thostmduserapi_se.lib,做好准备工作。

1
2
3
4
CThostFtdcMdApi* g_pMdUserApi = nullptr;					// 行情指针
...
...
g_pMdUserApi = CThostFtdcMdApi::CreateFtdcMdApi(); // 创建行情实例

第 6 行和第 12 行,我们创建了一个全局变量,名为 g_pMdUserApi 的指针,它的类型为 CThostFtdcMdApi,在 main 通过 CreateFtdcMdApi 创建了 Api 实例,并将其赋值给 g_pMdUserApi

1
2
3
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 行,我们注册了前置地址以便与服务器连接。

1
g_pMdUserApi->Init();									        // 连接运行

第 16 行调用 Init() 函数开始正式初始化 api,也就是说前面的工作只是准备工作,到了这里 api 才真正开始工作。此时 api 会向之前注册的地址发起与 CTP 前置的连接。

接着,生成并执行 CTPDemo.exe,你就会发现黑框中出现「建立网络连接成功」字样,说明我们与行情服务器连接成功!

行情接口工作原理

行情接口的具体工作原理可以参考官方说明文档,首先可以尝试着理解,即在之后实现的过程中的流程问题

实现登录并获取行情信息

在上面,我们实现了一个最简单的 CTP 使用样例,接下来,我们在此基础上实现登录和获取行情信息

登录

登录前的准备

在进行登录操作之间,你需要在 sinmow 中注册一个模拟交易账户

SimNow 是上海期货交易所全资子公司上期技术公司专为投资者打造的期货模拟仿真交易平台,为上海期货交易所投资者教育网认证的期货模拟仿真系统。该产品仿真各交易所的交易及结算规则研发,目前已经支持国内各期货交易所的商品期货业务。

注册完成之后,请关注 investorIdbrokerId

用户登录请求

首先,让我们来看看登录时都需要提供些什么,打开 API 接口说明文档,找到函数 ReqUserLogin

ReqUserLogin 函数原型如下。我们发现,请求登录需要两个参数,一个为 CThostFtdcReqUserLoginField ,另一个为 int 类型的请求编号。

1
virtual int ReqUserLogin(CThostFtdcReqUserLoginField *pReqUserLoginField, int nRequestID) = 0;

现在让我们来看看 CThostFtdcReqUserLoginField 是什么东西。打开 ThostFtdcUserApiStruct.h,就在前面几行,我们很容易找到「用户登录请求」的结构体,在这里,主要截取了几个关键字段,也就是登录时我们需要提供的。

1
2
3
4
5
6
7
8
9
10
11
12
///用户登录请求
struct CThostFtdcReqUserLoginField
{
...
///经纪公司代码
TThostFtdcBrokerIDType BrokerID;
///用户代码
TThostFtdcUserIDType UserID;
///密码
TThostFtdcPasswordType Password;
...
};
登录实现

MdSpi.cpp 中写入如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// MdSpi.cpp
#include <iostream>
#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 之后便会进行登录操作。

1
extern CThostFtdcMdApi* g_pMdUserApi;				// 行情指针

在第 6 行中,我们声明全局变量 g_pMdUserApi,也就是我们之前创建的 Api 实例

1
2
3
4
5
CThostFtdcReqUserLoginField loginReq;
memset(&loginReq, 0, sizeof(loginReq));
strcpy(loginReq.BrokerID, "9999");
strcpy(loginReq.UserID, "000000");
strcpy(loginReq.Password, "123456");

第 12 - 16 行,我们创建了一个 CThostFtdcReqUserLoginField 类型的对象,并写入 BrokerIDUserIDPassword注意,请替换为你自己的用户名和密码。

1
2
3
4
5
6
static int requestID = 0;
int result = g_pMdUserApi->ReqUserLogin(&loginReq, ++requestID);
if (!result)
cout << "发送登录请求成功" << endl;
else
cerr << "发送登录请求失败" << endl;

第 17- 22 行,将 loginReqrequestID 传入 ReqUserLogin,并输出结果。

接下来,我们尝试生成并执行 CTPDemo.exe ,发现有发送登录请求成功字样,说明发送登录请求成功。

ReqUserLogin 返回值

0,代表成功。

-1,表示网络连接失败;

-2,表示未处理请求超过许可数;

-3,表示每秒发送请求数超过许可数。

登录请求响应

在发送登录请求之后,正如之前提到的,我们还需要重载回调函数 OnRspUserLogin 接收登陆信息。

MdSpi.h 中声明回调函数 OnRspUserLogin

1
2
3
4
5
6
7
8
9
10
11
// MdSpi.h
#include "ThostFtdcMdApi.h"
class MdSpi : public CThostFtdcMdSpi
{
public:
// 当客户端与交易后台建立起通信连接时(还未登录前),该方法被调用。
void OnFrontConnected();

///登录请求响应
void OnRspUserLogin(CThostFtdcRspUserLoginField* pRspUserLogin, CThostFtdcRspInfoField* pRspInfo, int nRequestID, bool bIsLast);
};

MdSpi.cpp 中添加如下代码

1
2
3
4
5
6
7
8
9
10
11
// 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 中,我们首先判断了错误是否出现,若登陆成功,则打印交易日信息。

你可以在说明文档中找到响应信息 pRspInfo 的结构

1
2
3
4
5
6
7
struct CThostFtdcRspInfoField
{
///错误代码
TThostFtdcErrorIDType ErrorID;
///错误信息
TThostFtdcErrorMsgType ErrorMsg;
};

最后,生成并执行 CTPDemo.exe ,可以看到如下信息,说明我们登陆成功

获取行情信息

订阅行情

在上面的操作中,我们实现了用户登陆,接下来开始尝试获取行情信息

同样的,我们在说明文档中找到 Api,SubscribeMarketData,发现他需要两个参数,一个是需要订阅的合约列表,另一个是合约数组的数量,具体的使用方法也可以在调用示例中找到。

1
virtual int SubscribeMarketData(char *ppInstrumentID[], int nCount) = 0;
订阅行情实现

同样的,我们需要在 MdSpi.h 中声明其回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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 中的完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// MdSpi.cpp
#include <iostream>
#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

1
virtual void OnRtnDepthMarketData(CThostFtdcDepthMarketDataField *pDepthMarketData) {};

MdSpi.h 中对其进行声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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

1
2
3
4
5
6
7
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

参考资料

  • CTP 客户端开发指南
  • API 接口说明