瞅着周末了来整理一下 CTP 的 demo 和相关的开发资料。


目录

  • 行情接口的初始化
  • 交易接口的初始化
  • Spi 异步调用 Api

行情接口的初始化

  1. 继承 CThostFtdcMdSpi 类并实现接口
  2. 创建 MdApi 和 MdSpi 的实例并将 MdSpi 注册到 MdApi 中
  3. 注册行情服务器前置地址并启动 MdApi 线程
  4. 登录行情账号
  5. 订阅合约
  6. 接收行情数据

继承 CThostFtdcMdSpi 类并实现接口

  • 首先创建一个头文件,例如我们起名为mdspi.h,然后包含 MdApi 的三个头文件
#include "ctpapi/ThostFtdcMdApi.h"
#include "ctpapi/ThostFtdcUserApiStruct.h"
#include "ctpapi/ThostFtdcUserApiDataType.h"
  • 继承 CThostFtdcMdSpi 类
class MdSpi : public CThostFtdcMdSpi
{
public:
    MdSpi(....);
    ......
};

MdSpi 的回调结果往往关联着 MdApi 的下一个动作,例如 MdSpi::OnFrontConnected 被调用后,我们就可以通过 MdApi 登录行情账号了。因此 MdSpi 通常要维持一种方式与 MdApi 进行直接或间接的通信。换言之我们可以在构造 MdSpi 时向里面传参来实现通信。例如:

MdSpi(CThostFtdcMdApi *mdApi);  // 传入一个 mdApi 的指针作为构造函数的参数

本文后面提供两种方式,分别是同步和异步的处理方法。

  • 实现 MdSpi 中的接口

MdSpi 是 MdApi 的回调,即当 MdApi 要通知我们完成了什么事情时,是通过调用 MdSpi 中对应的函数来实现的。例如,MdApi 连接上前置服务器后,通过调用 MdSpi 的 OnFrontConnected 来告诉我们已经连接上服务器了,这个时候我们要做的事情就是登录行情账号。

创建 MdApi 和 MdSpi 的实例并将 MdSpi 注册到 MdApi 中

实现了 MdSpi 中的接口后,下一步就是使用它,例如:

CThostFtdcMdApi *mdApi = CThostFtdcMdApi::CreateFtdcMdApi();
MdSpi *mdSpi = new MdSpi(....);
mdApi->RegisterSpi(mdSpi);

注册行情服务器前置地址并启动 MdApi 线程

mdApi->RegisterFront(....);
mdApi->Init();

登录行情账号

当 MdSpi::OnFrontConnected 被调用后,我们就能知道 MdApi 已经连接到了前置服务器,可以执行登录操作了,同步的登录方式是直接在 MdSpi 中调用 MdApi 的登录函数:

void MdSpi::OnFrontConnected()
{
    CThostFtdcReqUserLoginField reqUserLogin;
    memset(&reqUserLogin, 0, sizeof(reqUserLogin));
    strcpy(reqUserLogin.BrokerID, sBrokerID);
    strcpy(reqUserLogin.UserID, sUserID);
    strcpy(reqUserLogin.Password, sPassword);
    pMdApi->ReqUserLogin(&reqUserLogin, 1);
}

订阅合约

当 MdSpi::OnRspUserLogin 被调用后,我们就可以订阅合约了:

void MdSpi::OnRspUserLogin(CThostFtdcRspUserLoginField *pRspUserLogin, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast)
{
    char* x[2];
    x[0] = new char[10];
    x[1] = new char[10];
    strcpy(x[0], "rb1901");
    strcpy(x[1], "rb1905");
    pMdApi->SubscribeMarketData(x, 2);
}

行情订阅的成功与否都会通过接口 MdSpi::OnRspSubMarketData 通知。

接收行情数据

行情通过 MdSpi::OnRtnDepthMarketData 返回,例如:

void MdSpi::OnRtnDepthMarketData(CThostFtdcDepthMarketDataField *pDepthMarketData)
{
    std::cout << "=====获得深度行情=====" << std::endl;
    std::cout << "交易日: " << pDepthMarketData->TradingDay << std::endl;
    std::cout << "交易所代码: " << pDepthMarketData->ExchangeID << std::endl;
    std::cout << "合约代码: " << pDepthMarketData->InstrumentID << std::endl;
    std::cout << "合约在交易所的代码: " << pDepthMarketData->ExchangeInstID << std::endl;
    std::cout << "最新价: " << pDepthMarketData->LastPrice << std::endl;
    std::cout << "数量: " << pDepthMarketData->Volume << std::endl;
    ///TODO......
}

交易接口的初始化

  1. 继承 CThostFtdcTradeSpi 类并实现接口
  2. 创建实例完成初始化并启动 TradeApi 线程
  3. 执行客户端验证
  4. 登录交易账号
  5. 执行投资者结果确认

继承 CThostFtdcTradeSpi 类并实现接口

  • 首先创建一个头文件,例如我们起名为mdspi.h,然后包含 MdApi 的三个头文件
#include "ctpapi/ThostFtdcTradeApi.h"
#include "ctpapi/ThostFtdcUserApiStruct.h"
#include "ctpapi/ThostFtdcUserApiDataType.h"
  • 继承 CThostFtdcTradeSpi 类
class TradeSpi : public CThostFtdcTradeSpi
{
public:
    TradeSpi(....);
    ......
};

创建实例完成初始化并启动 TradeApi 线程

实现了 TradeSpi 中的接口后,在主线程创建实例并初始化,例如:

CThostFtdcTraderApi *tradeApi = CThostFtdcTraderApi::CreateFtdcTraderApi();
TradeSpi *tradeSpi = new TradeSpi(....);
tradeApi->RegisterSpi(tradeSpi);
tradeApi->RegisterFront(....);
tradeApi->SubscribePublicTopic(THOST_TERT_RESTART);
tradeApi->SubscribePrivateTopic(THOST_TERT_RESTART);
tradeApi->Init();

执行客户端验证

交易接口需要进行客户端的验证,这一步通常在 TradeApi 连接到前置主机之后进行。因此我们可以简单的将执行验证的步骤写到 TradeSpi::OnFrontConnected 中。到目前为止 simnow 仿真账号都不需要进行验证,所以可以在 TradeSpi::OnFrontConnected 中直接进行交易账号的登录。

void TradeSpi::OnFrontConnected()
{
    CThostFtdcReqAuthenticateField req = { 0 };
    strcpy(req.AuthCode, authCode);
    strcpy(req.UserProductInfo, productName);
    strcpy(req.BrokerID, sBrokerID);
    strcpy(req.UserID, sUserID);
    tradeApi->ReqAuthenticate(&req, ++nRequestID);
}

登录交易账号

登录交易账号通常放在验证成功之后,如果不进行客户端验证,也可以放在连接成功之后。

void TradeSpi::OnRspAuthenticate(CThostFtdcRspAuthenticateField *pRspAuthenticateField, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast)
{
    CThostFtdcReqUserLoginField req = { 0 };
    strcpy(req.UserID, sUserID);
    strcpy(req.BrokerID, sBrokerID);
    strcpy(req.Password, sPassword);
    tradeApi->ReqUserLogin(&req, ++nRequestID);
}

执行投资者结果确认

每个交易日都必须要进行一次投资者结算结果确认,只有完成确认后才能执行其他操作。官方文档是这样写的:

为了让投资者了解当前的交易风险。每天,终端程序第一次登入 Thost 成功后,必须查询投资者结算结果(ReqQrySettlementInfo)和确认投资者结算结果(ReqSettlementInfoConfirm),才能正常发送交易指令,包括报单、撤单、服务器预埋单等指令。在一天中,如果投资者中已经确认了结算结果,以后登入 Thost,就不再必须确认结算结果,就可以直接发送交易指令了。

投资者结算结果确认可以简单的分为三个步骤进行:

ReqQrySettlementInfoConfirm 请求查询结算信息确认
ReqQrySettlementInfo 请求查询投资者结算结果
ReqSettlementInfoConfirm 投资者结算结果确认

这里我们可以在交易账户登陆成功后进行投资者结算结果确认。

void TradeSpi::OnRspUserLogin(CThostFtdcRspUserLoginField *pRspUserLogin, CThostFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast)
{
    CThostFtdcQrySettlementInfoConfirmField req1 = { 0 };
    strcpy(req1.BrokerID, sBrokerID);
    strcpy(req1.InvestorID, sUserID);
    tradeApi->ReqQrySettlementInfoConfirm(&req1, ++nRequestID);
    Sleep(1000);
    CThostFtdcQrySettlementInfoField req2 = { 0 };
    strcpy(req2.BrokerID, sBrokerID);
    strcpy(req2.InvestorID, sUserID);
    tradeApi->ReqQrySettlementInfo(&req2, ++nRequestID);
    Sleep(1000);
    CThostFtdcSettlementInfoConfirmField req3 = { 0 };
    strcpy(req3.BrokerID, sBrokerID);
    strcpy(req3.InvestorID, sUserID);
    tradeApi->ReqSettlementInfoConfirm(&req3, ++nRequestID);
}

注意:两条请求之间需要隔一段时间,官方给的资料里有说一秒钟不能超过两条请求,否则请求将执行失败。上面的例子执行投资者结算结果确认中,三条请求之间都 Sleep 了 1000 毫秒。


Spi 异步调用 Api

上面的例子使用的都是同步调用,这种方式存在两个问题:

  • 如果调用 Api 的接口导致 Spi 阻塞,将降低 Api 的处理效率;
  • 不能很好的协调多个请求之间的时间间隔问题。

解决方法是使用异步调用,例如生产者消费者模型。Spi 以及上层的策略组作为生产者,将请求插入到队列中;一个线程负责从队列中取出请求,并根据请求的类型和附带的参数调用 Api 执行。

异步也有几种方式,这里主要介绍共享内存的方法。我们将插入请求的角色作为生产者;将执行请求的角色作为消费者。方式一是两类角色之间通过第三者传递数据;方式二是生产者通过消费者提供的一个接口将数据直接传递给消费者。这两者本质上都是共享内存的传递方式。

文档和资料:https://github.com/optsp/ctp-archives

(写不动了,完)