1. CTP介绍
1.1 CTP简介
综合交易平台CTP(Comprehensive Transaction Platform)是由上海期货信息技术有限公司(上海期货交易所的全资子公司)开发的期货交易平台,CTP平台以“新一代交易所系统”的核心技术为基础,构建了稳定、高速、开放式的接口,适合程序化交易软件运用和短线炒单客户使用。投资者可直接用CTP的API开发交易程序,连到期货公司的CTP系统交易。
1.2 使用前的准备
1.2.1 登录信息
想要在客户端调用API接入CTP柜台进行程序化交易,需要以下基本信息才能登录(使用行情API只需要IP地址信息,其他均可为空):
- TradeFront / MarketFront:前者是CTP系统的交易前置IP地址,用来下单撤单等;后者是行情前置IP地址,用来订阅接收行情
- BrokerID:经纪商代码,指期货公司在CTP系统上的编码,四位数。如海通期货是8000
- AppID:客户终端软件代码
- AuthCode:客户终端软件认证码
- InvestorID(UserID,InvestUnitID):投资者代码,指客户在CTP系统上的唯一ID,在期货公司开户后由期货公司分配得到。UserID是操作员代码,InvestUnitID是投资单元代码,普通投资者直接填InvestorID即可
- Password:开户时设置的密码,需要注意的是开户完首次登录CTP系统需要修改密码
1.2.2 simnow模拟交易
没有期货账户的用户可以在simnow平台【白天才能上】进行模拟开户,其中InvestorID和Password在官网注册后获得,其他登录需要的信息如下:
- IP地址第一组(交易时段):Trade Front:180.168.146.187:10201,Market Front:180.168.146.187:10211
IP地址第二组(交易时段):Trade Front:180.168.146.187:10202,Market Front:180.168.146.187:10212
IP地址第三组(交易时段):Trade Front:218.202.237.33:10203,Market Front:218.202.237.33:10213
IP地址(非交易时段):Trade Front:180.168.146.187:10130,Market Front:180.168.146.187:10131- BrokerID:9999
- APPID:simnow_client_test
- AuthCode:0000000000000000
1.3 API介绍
1.3.1 API文件
Windows版本的API有8个文件,作用分别为:
- ThostFtdcUserApiDataType.h:定义业务数据类型,用
typedef
为现有类型创建同义字 - ThostFtdcUserApiStruct.h:使用上一个API文件中的数据类型定义业务数据结构,调用api时需要传入这些数据结构
- ThostFtdcMdApi.h、thostmduserapi_se.lib、thostmduserapi_se.dll:用于获取行情的API,md表示market data
- ThostFtdcTraderApi.h、thosttraderapi_se.lib、thosttraderapi_se.dll:用于交易操作的API
1.3.2 API内容
- 行情API和交易API的文件中各有两个类,xxxSpi和xxxApi,分别代表着回调和调用,每个Api都有一个与之绑定的Spi
- 每个Api函数对应固定的请求,在客户端主动调用,交易所会根据收到的请求回调与之对应的Spi函数
- 每个Spi函数对应固定的响应,在客户端需要进行重写,回调函数中的参数携带了交易所返回的信息
2. 基本使用
2.1 行情API使用
在主函数中创建行情API对象,并为其注册MdSpi对象,连接到行情IP地址后登录,登录完成后即可订阅行情(代码中只展示了部分.cpp文件的内容)。
// main.cpp
int main(int argc, char* argv[]) {
CThostFtdcMdApi* mdapi = CThostFtdcMdApi::CreateFtdcMdApi("./md_file/");
MdSpi* mdspi = new MdSpi(mdapi);
mdapi->RegisterSpi(mdspi);
mdapi->RegisterFront("tcp://180.168.146.187:10131");
mdapi->Init();
mdapi->Join();
return 0;
}
//MdSpi.cpp
void MdSpi::OnFrontConnected() {
cout << "=========已连接上,请求登录==========" << endl;
loginField = new CThostFtdcReqUserLoginField();
strcpy(loginField->BrokerID, BROKER_ID.c_str());
strcpy(loginField->UserID, NULL_STR.c_str());
strcpy(loginField->Password, NULL_STR.c_str());
mdapi->ReqUserLogin(loginField, loginRequestID++);
}
void MdSpi::OnRspUserLogin(CThostFtdcRspUserLoginField* pRspUserLogin, CThostFtdcRspInfoField* pRspInfo,int nRequestID, bool bIsLast) {
cout << "==============登录请求响应===============" << endl;
if (pRspInfo != nullptr || pRspInfo->ErrorID == 0) {
cout << "登陆成功!!!" << endl;
cout << "交易日:" << pRspUserLogin->TradingDay << endl;
cout << "登陆成功时间:" << pRspUserLogin->LoginTime << endl;
cout << "经纪公司代码:" << pRspUserLogin->BrokerID << endl;
cout << "用户代码:" << pRspUserLogin->UserID << endl;
//订阅行情
char* instrumentID[] = { "au2110" }; //订阅一个合约所以数量为1
mdapi->SubscribeMarketData(instrumentID, 1);
}
}
void MdSpi::OnRtnDepthMarketData(CThostFtdcDepthMarketDataField* pDepthMarketData) {
cout << "==============深度行情通知==============" << endl;
//cout << " 交易日:" << pDepthMarketData->TradingDay << endl;
//cout << "合约代码:" << pDepthMarketData->InstrumentID << endl;
//cout << "最新价:" << pDepthMarketData->LastPrice << endl;
//cout << "最后修改时间" << pDepthMarketData->UpdateTime << endl;
//cout << "最后修改毫秒" << pDepthMarketData->UpdateMillisec << endl;
//cout << "申买价一:" << pDepthMarketData->BidPrice1 << endl;
//cout << "申买量一:" << pDepthMarketData->BidVolume1 << endl;
//cout << "申卖价一:" << pDepthMarketData->AskPrice1 << endl;
//cout << "申卖量一:" << pDepthMarketData->AskVolume1 << endl;
//cout << "本次结算价格:" << pDepthMarketData->SettlementPrice << endl;
//cout << "成交金额:" << pDepthMarketData->Turnover << endl;
//cout << "持仓量:" << pDepthMarketData->OpenInterest << endl;
}
2.2 交易API使用
在主函数中创建交易API对象,并为其注册TdSpi对象,订阅共有流和私有流,连接到交易IP地址后,依次进行验证、登录、结算确认,完成后即可发出交易API请求(代码中只展示了部分.cpp文件的内容)。
// main.cpp
int main(int argc, char* argv[]) {
TdApi* tdApi = new TdApi();
while (true)
{
int x;
cout << "0退出;1验证;2登录;3结算确认;4查询持仓;5查询资金;6查询报单;7查询成交;8查询手续费;9报单;10撤单" << endl;
cout << "输入:" << endl;
cin >> x;
if (x == 0) break;
else if (x == 1) tdApi->Authenticate();
else if (x == 2) tdApi->Login();
else if (x == 3) tdApi->Confirm();
else if (x == 4) tdApi->QueryPosition();
else if (x == 5) tdApi->QueryAccount();
else if (x == 6) tdApi->QueryOrder();
else if (x == 7) tdApi->QueryTrade();
else if (x == 8) {
cout << "输入合约代码:";
string instrumentID;
cin >> instrumentID;
tdApi->QueryCommission(instrumentID);
}
else if (x == 9) {
cout << "输入合约代码:";
string instrumentID;
cin >> instrumentID;
cout << "输入报单价格:";
double limitPrice;
cin >> limitPrice;
cout << "输入报单数量:";
int volume;
cin >> volume;
tdApi->InsertOrder(instrumentID, limitPrice, volume);
}
else if (x == 10) {
tdApi->WithdrawOrder();
}
}
return 0;
}
//TdApi.cpp
TdApi::TdApi()
{
tdapi = CThostFtdcTraderApi::CreateFtdcTraderApi("./trader_file/");
tdspi = new TdSpi();
tdapi->RegisterSpi(tdspi);
tdapi->SubscribePublicTopic(THOST_TERT_RESTART);
tdapi->SubscribePrivateTopic(THOST_TERT_RESTART);
//tdapi->RegisterFront("tcp://218.202.237.33:10203");
tdapi->RegisterFront("tcp://180.168.146.187:10130");
tdapi->Init();
}
TdApi::~TdApi()
{
tdapi->RegisterSpi(nullptr);
tdapi->Release();
tdapi = nullptr;
delete tdspi;
tdspi = nullptr;
}
int TdApi::Authenticate()
{
CThostFtdcReqAuthenticateField* pReqAuthenticateField = new CThostFtdcReqAuthenticateField();
strcpy(pReqAuthenticateField->BrokerID, BROKER.c_str());
strcpy(pReqAuthenticateField->UserID, USER_ID.c_str());
strcpy(pReqAuthenticateField->AuthCode, AUTH_CODE.c_str());
strcpy(pReqAuthenticateField->AppID, APP_ID.c_str());
cout << "RequestID:" << nRequestID << endl;
return tdapi->ReqAuthenticate(pReqAuthenticateField, nRequestID++);
}
int TdApi::Login()
{
CThostFtdcReqUserLoginField* pReqUserLoginField = new CThostFtdcReqUserLoginField();
strcpy(pReqUserLoginField->BrokerID, BROKER.c_str());
strcpy(pReqUserLoginField->UserID, USER_ID.c_str());
strcpy(pReqUserLoginField->Password, PASS.c_str());
cout << "RequestID:" << nRequestID << endl;
return tdapi->ReqUserLogin(pReqUserLoginField, nRequestID++);
}
int TdApi::Confirm()
{
CThostFtdcSettlementInfoConfirmField* pSettlementInfoConfirm = new CThostFtdcSettlementInfoConfirmField();
strcpy(pSettlementInfoConfirm->BrokerID, BROKER.c_str());
strcpy(pSettlementInfoConfirm->InvestorID, USER_ID.c_str());
cout << "RequestID:" << nRequestID << endl;
return tdapi->ReqSettlementInfoConfirm(pSettlementInfoConfirm, nRequestID++);
}
int TdApi::InsertOrder(string instrumentID, double limitPrice, int volume)
{
CThostFtdcInputOrderField* pInputOrder = new CThostFtdcInputOrderField();
strcpy_s(pInputOrder->BrokerID, BROKER.c_str());
strcpy_s(pInputOrder->InvestorID, USER_ID.c_str());
strcpy_s(pInputOrder->ExchangeID, "SHFE");
strcpy_s(pInputOrder->InstrumentID, instrumentID.c_str());
pInputOrder->OrderPriceType = THOST_FTDC_OPT_LimitPrice;//限价
pInputOrder->Direction = THOST_FTDC_D_Buy;//买
pInputOrder->CombOffsetFlag[0] = THOST_FTDC_OF_Open;//开
pInputOrder->CombHedgeFlag[0] = THOST_FTDC_HF_Speculation;//投机
pInputOrder->LimitPrice = limitPrice;
pInputOrder->VolumeTotalOriginal = volume;
pInputOrder->TimeCondition = THOST_FTDC_TC_GFD;///当日有效
pInputOrder->VolumeCondition = THOST_FTDC_VC_AV;///任意数量
pInputOrder->MinVolume = 1;
pInputOrder->ContingentCondition = THOST_FTDC_CC_Immediately;
pInputOrder->StopPrice = 300.0;
pInputOrder->ForceCloseReason = THOST_FTDC_FCC_NotForceClose;
pInputOrder->IsAutoSuspend = 0;
pInputOrder->IsSwapOrder = 0;
cout << "RequestID:" << nRequestID << endl;
return tdapi->ReqOrderInsert(pInputOrder, nRequestID++);
}
2.3 API使用中的常见问题
- 客户端程序至少由两个线程组成,一个是应用程序主线程,一个是CTP API工作线程(行情API和交易API为不同的线程),程序与交易系统的通讯是由API工作线程驱动的
- CTP API提供的接口是线程安全的,可以有多个应用程序线程同时发出请求。SPI提供的接口回调是由API工作线程驱动,即SPI回调函数与API调用函数都在同一个线程中运行
- 使用行情API时登录的账号密码可都为空,登录后即可使用;使用交易API时需要先验证才能登录,登录后需要进行结算确认才能下单
- 使用行情API时需要订阅公有流和私有流。公有流用于接收公有数据,如合约在场上的交易状态。私有流用于接收私有数据,如报单回报。两种流的默认模式都是是从上次断开连接处继续收取交易所发布数据(Resume ),还可以指定全部重新获取(Restart),或从登陆后获取(Quick)
- 交易SPI的查询回调函数中,pRspInfo==nullptr或pRspInfo->ErrorID==0都代表查询成功,如果查询记录为空在调用字段的属性时会报空指针异常
- 报单时有些交易所只支持限价单,有效期类型只支持当日有效,若填别的无任何返回;且有些必填字段并不是必填
- 不同交易所的合约命名规则不统一:上期所/能源所/大商所(小写+4个数字);中金所(大写+4个数字);郑商所(大写+3个数字)。合约填写的格式错误或合约不存在则无返回结果
- 交易所代码:上期所(SHFE),大商所(DCE),郑商所(CZCE),中金所(FFEX),能源中心(INE)
- 下单和撤单成功(仅代表报单操作成功,并不代表一定执行)都会执行OnRtnOrder回调函数,有单成交时执行OnRtnTrade回调函数
- 有多个字段组合可以唯一确定一笔报单,推荐通过OrderSysID字段,OrderSysID是报单报入到交易所时交易所给编的唯一编号,该字段加上ExchangeID也可以用来确定唯一一笔报单
- 每次下单可能会收到多次OnRtnOrder函数响应,其流程如下图所示,CTP风控通过引起的OnRtnOrder回调,OrderSysID字段为空,因为订单还没有到达交易所
3. TIP
3.1 确定报单的唯一性
- FrontID:CTP后台前置编号
- SessionID:本次链接编号。登录成功回报会返回上述两个字段的值,这两个编号在本次连接中保持不变
- OrderRef:报单引用,可自定义或不填。上述三个字段可唯一确定一笔报单
- ExchangeID:交易所代码
- OrderSysID:报单报入到交易所时交易所给的唯一编号。上述两个字段也可唯一确定一笔报单
3.2 报单状态编号OrderStatus
报单的状态编号由交易所返回,由枚举值标识,常见状态有:
- a:“未知”,表示CTP已收到客户端报单,正在转发至交易所,对应第一个OnRtnOrder(第二个也可能出现)
- 3:“未成交还在队列中”,表示报单已通过交易所风险检查,对应第二个OnRtnOrder
- 1:“部分成交还在队列中”,表示订单部分成交,对应第三个OnRtnOrder
- 0:“全部成交”,表示订单全部成交,对应第三个OnRtnOrder
- 5:“撤单”,主动撤单或被动撤单时OnRtnOrder返回的状态,需配合OrderSubmitStatus字段区分是否主动撤单,该字段返回3时代表主动撤单,返回4代表被动撤单
3.3 查询与交易流控
使用客户端进行CTP交易时,可能会受到【CTP底层动态库、CTP柜台系统、交易所】对查询和交易的频率限制。
3.3.1 CTP底层动态库流控
目前底层动态库中的流控只针对查询,即API中所有以ReqQry开头的函数,此处流控分为两种:
- 未处理请求超过许可数:此时调用函数返回值为-2。这是因为目前动态库底层设置每次只允许1个查询请求在途,即当前这笔查询请求发出后,在未收到响应前,不能发起下一笔查询请求。该流控使得不能在回调函数中调用多次查询函数
- 每秒发送请求数超过许可数:此时调用函数返回值为-3。在穿透式版本之前,CTP动态库底层设定了API每秒只允许发出1次查询请求。升级新版本后,每秒允许查询次数由后台设定,在登录回报中传给动态库。该流控是session层面的,也就是说是每个链接内的查询次数受该流控,多个链接的查询次数互不影响,所以是可以多次CreateFtdcTraderApi创建多个session来实现1秒内多次查询
3.3.2 CTP柜台系统流控
- 请求数流控:该流控是针对单个session的所有请求(包括查询和交易)。该流控不会有错误信息或错误返回,具体设置只能询问期货公司
- 报撤单流控:该流控指调用报单、撤单函数时每秒内允许的最大笔数,这两个函数流控是分开计算的。如果触发该流控,则会在相应的错误回调函数中返回“CTP:下单频率限制”。该流控是投资者InvestorID级别的,所以多开session也没法。如果策略高频,真触发该风控,那只能分账户来解决该问题了。不过目前该风控是分交易所和品种的,也就是说如果该值设6,那客户1秒内同时报上期所5笔,大商所5笔,是不会触发到该流控的。
- 查询流控:查询比较耗时且占资源,所以除了在CTP底层动态库设置了流控,同样也在CTP后台设置了流控。如果触发后台的查询流控,则会回调OnRspError,并提示:“CTP:查询未就绪,请稍后重试”
- 前置连接数流控:指在CTP中每秒允许的最大API连接请求数。如果超过该流控则会被主动断开连接,回调OnFrontDisconnected。因此用户如果发现自己的程序一连接前置就被断开,则除了版本问题外,有可能是遇到了连接数流控。该流控是针对不同前置地址的
- 同一用户最大允许在线会话数:这是指同一个UserID在CTP中同时登录在线的最大允许session数,登录成功在线即算1个session,用其他客户端登录在线也计算在内。触发该流控则会在OnRspUserLogin回调中返回“CTP:用户在线会话超出上限”
3.3.3 交易所流控
一般是报单时可能会触发该流控,该流控是区分交易所的,有的交易所会有此流控,有的交易所没有,遇到得比较少。受到交易所流控后会触发OnRtnOrder,在StatusMsg中报“CTP:交易所每秒发送请求数超过许可数”或者“CTP:交易所未处理请求超过许可数”。