C++ API 回测
一、系统概述
本文主要为对接闪策回测系统的终端厂商、策略开发人员以及个人或机构投资者提供帮助指引
支持文件单回测和策略回测
支持期货和证券的回测(以及支持期货与证券同时进行回测)
支持模拟撮合成交模式和概率成交模式
支持回测结果落地
二、接口开发说明
2.1 文件列表
用户应关注的文件内容如下:
backtestapi
├── backtest_runtime
│ ├── conf
│ │ └── db_connection.json -- 回测数据库配置
│ ├── lib
│ │ ├── libscbacktest.so -- 闪策回测动态库
│ │ └── ... -- 其他依赖项
│ ├── scripts
│ │ ├── generateBacktestConfig.py -- 将用户配置文件转为回测配置文件
│ │ └── ... -- 其他脚本
│ └── shared/
├── conf
│ ├── initConfig.json -- 用户配置文件样例
├── include -- 公共头文件
│ ├── SCOMSApiCommon.h -- 闪策技术数据结构类型;交易所格式合约编码与闪策合约数据结构转换的工具;常用工具
│ ├── SCOMSApiData.h -- 闪策消息数据结构类型(详细解释见附录:消息格式内容说明)
│ ├── SCOMSApi.h -- 闪策回测通用接口
│ └── SCOMSApiType.h -- 闪策回测通用类型
├── src
│ └── Strategy.cc -- 策略代码示例
├── Makefile -- Makefile
├── README.md
└── run.sh -- 运行脚本
2.2 配置文件说明
见 回测配置文件说明
2.3 Demo 示例
2.3.1 开发环境
Linux 开发环境:g++ >= 7.4.0 ( API Demo 在 Ubuntu 18.04 / CentOS 7 环境中经过测试,使用 g++ 7.4.0 进行编译运行 )
2.3.2 运行方法
# 以 Ubuntu 系统为例
cd /path/to/backtestapi
make && cd build # Makefile 内部会在当前目录创建一个 build 子目录,编译产物均放在 build 子目录下
LD_LIBRARY_PATH=../lib ./backtestdemo /path/to/your/config.json
2.4 接口描述
逻辑上接口分为 SCOMSApi 和 SCOMSSpi 两部分:
SCOMSApi类中定义了用户行为相关接口,该部分接口应由策略根据需要调用SCOMSSpi类中定义了响应函数,用于返回回测对于请求的处理结果,开发者应继承该类并根据需要重载相关回调函数,当有响应返回时由 API 主动调用回调函数
功能上 SCOMSApi 类中定义的请求接口分为:
2.4.1 创建 API 实例对象(CreateOMSApi)
函数原型:
static SCOMSApi* CreateOMSApi( const char* pConfigPath )输入参数:
参数名称 |
参数说明 |
用法 |
|---|---|---|
const char* pConfigPath |
配置文件路径 |
输入 |
返回: 返回 API 实例对象指针(成功),返回空指针则表示失败
用法说明: 创建 API 实例对象
2.4.2 获取 API 版本信息(GetApiVersion)
函数原型:
static const char* GetApiVersion()输入参数:
参数名称 |
参数说明 |
用法 |
|---|---|---|
无 |
返回: 返回版本信息的 char* 字符串
用法说明: 输出该接口返回的字符串内容判断版本信息
2.4.3 注册回调函数(RegisterSpi)
函数原型:
virtual void RegisterSpi( SCOMSSpi* spi ) = 0输入参数:
参数名称 |
参数说明 |
用法 |
|---|---|---|
SCOMSSpi* spi |
接口基类的指针 |
传入由自己实现的派生类指针 |
返回: 无
用法说明: 传入的类所实现的回调函数,将会在响应到来时触发
2.4.4 API 释放(Release)
函数原型:
virtual void Release() = 0输入参数:
参数名称 |
参数说明 |
用法 |
|---|---|---|
无 |
返回: 无
用法说明: 在策略结束后手动释放资源
2.4.5 资源初始化(Init)
函数原型:
virtual uint32_t Init() = 0输入参数:
参数名称 |
参数说明 |
用法 |
|---|---|---|
无 |
返回: 返回 SCOMS_SUCC 表示成功,否则失败。错误代码详见 SCOMSError:
enum SCOMSError : uint32_t
{
SCOMS_SUCC = 0,
SCOMS_ERR_UNKNOWN = 1,
SCOMS_ERR_CONNECTION_FAIL = 2,
SCOMS_ERR_UNSUPPORTED = 3,
SCOMS_ERR_PARAM = 4,
SCOMS_ERR_NO_DATA = 5
};
用法说明: 在回测启动前初始化资源
2.4.6 请求事件(PublishEvent)
函数原型:
virtual uint32_t PublishEvent( const char* data, uint32_t len ) = 0输入参数:
参数名称 |
参数说明 |
用法 |
|---|---|---|
const char* data |
闪策消息数据结构的指针,见 SCOMSApiData.h 内容 |
输入 |
uint32_t len |
传入的数据结构体的长度 |
输入 |
返回: 返回 SCOMS_SUCC 表示成功,否则失败
用法说明: 发布请求,例如发送订单、撤单。该接口应在回测启动后才可调用
2.4.7 订阅事件(SubscribeEvent)
函数原型:
virtual uint32_t SubscribeEvent( uint32_t message_type ) = 0输入参数:
参数名称 |
参数说明 |
用法 |
|---|---|---|
uint32_t message_type |
事件类型,见 mtype_t 定义 |
输入 |
返回: 返回 SCOMS_SUCC 表示成功,否则失败 用法说明: 订阅事件。被订阅的事件在回测收到响应时,将触发 onEvent 函数,使用 sc::message_hdr_t 类型转换 onEvent 参数中的 data 参数,hdr 中的 mtype 即表示触发的事件类型。 详见 2.4.14 事件触发回调说明
2.4.8 注册定时器(SubscribeTimer)
函数原型:
virtual uint32_t SubscribeTimer( uint32_t interval, const sc::timeval_t& startTime = sc::timeval_t::earliest, const sc::timeval_t& endTime = sc::timeval_t::latest ) = 0输入参数:
参数名称 |
参数说明 |
用法 |
|---|---|---|
uint32_t interval |
间隔时间,单位为毫秒 |
输入 |
const sc::timeval_t& startTime |
定时器开始时间,默认为 sc::timeval_t::earliest,表示回测开始事件 |
输入 |
const sc::timeval_t& endTime |
定时器结束时间,默认为 sc::timeval_t::latest,表示回测结束时间 |
输入 |
返回: 返回 uint32_t 表示定时器 id,大于 0 表示注册成功;反之失败。该 id 对应 onTimer 参数中的 id
用法说明: 注册定时器。被注册的定时器会从
startTime时间开始,每interval毫秒触发一次,直到endTime时间结束。该方法注册的定时器,其时间由回测内部维护,时间流速和真实世界的时间流速不一定相同,所以设置的间隔时间,是表示回测时间线中过去的时间,而非真实世界中过去的时间。我们更推荐用户使用 API 中的方法来注册定时器,而非自己维护定时器的实现与触发
2.4.9 注册特定时刻定时器(SubscribeSpecifiedTimer)
函数原型:
virtual uint32_t SubscribeSpecifiedTimer( const sc::timeval_t& specifiedTime ) = 0输入参数:
参数名称 |
参数说明 |
用法 |
|---|---|---|
const sc::timeval_t& specifiedTime |
指定时间 |
输入 |
返回: 返回 uint32_t 表示定时器 id,大于 0 表示注册成功;反之失败。该 id 对应 onTimer 参数中的 id
用法说明: 注册指定时间的定时器。当定时器到达固定时刻时,会触发一次定时器回调函数
2.4.10 取消订阅事件(UnSubscribeEvent)
函数原型:
virtual void UnSubscribeEvent( uint32_t message_type ) = 0输入参数:
参数名称 |
参数说明 |
用法 |
|---|---|---|
uint32_t message_type |
事件类型 |
输入 |
返回: 无
用法说明: 用于取消订阅事件
2.4.11 取消注册定时器(UnSubscribeTimer)
函数原型:
virtual void UnSubscribeTimer( uint32_t id ) = 0输入参数:
参数名称 |
参数说明 |
用法 |
|---|---|---|
uint32_t id |
定时器 id |
输入 |
返回: 无
用法说明: 输入 SubscribeTimer / SubscribeSpecifiedTimer 返回的定时器 id,取消该定时器
2.4.12 获取当前时间(GetCurrentTime)
函数原型:
virtual sc::timeval_t GetCurrentTime() = 0输入参数:
参数名称 |
参数说明 |
用法 |
|---|---|---|
无 |
返回: 返回 sc::timeval_t 表示回测时间线中的当前时间
用法说明: 用于获取回测当中的当前时间
2.4.13 启动函数(run)
函数原型:
virtual void run() = 0输入参数:
参数名称 |
参数说明 |
用法 |
|---|---|---|
无 |
返回: 无
用法说明: 在初始化相关资源后,启动回测
2.4.14 事件触发回调(onEvent)
函数原型
virtual void onEvent( const char* data, int32_t len ) {}回调参数:
参数名称 |
参数说明 |
用法 |
|---|---|---|
const char* data |
事件内容,是包括消息头加消息体的完整数据,需要先对消息头做解析,再解析后续消息体 |
回调 |
int32_t len |
数据长度,是包括消息头加消息体的完整长度 |
回调 |
返回: 无
用法说明: 当订阅过的事件有更新时,该接口将会被回调,通过将
data数据转为 sc::message_hdr_t 类型,就可以知道回调的是什么事件
// 对数据做类型转换
auto msgHdr = reinterpret_cast<const sc::message_hdr_t*>( data );
闪策的消息格式简称 BB MSG,在传输时,均以一个消息头加一个消息体的格式进行传输(格式参考 struct sc::common::CompactMsg),所以在解析消息头获取了对应事件类型时,将消息体转为对应结构即可读取其中内容(不同事件的具体内容见:附录-消息格式内容说明)
if ( msgHdr->mtype == sc::MSG_SC_SNAPSHOT_DATA )
{
auto msgBody = reinterpret_cast<const sc::msg_sc_snapshot_data*>( data + sizeof( sc::message_hdr_t ) );
// ... onMarketData( msgBody );
}
2.4.15 定时器触发回调(onTimer)
函数原型
virtual void onTimer( uint32_t id ) {}回调参数:
参数名称 |
参数说明 |
用法 |
|---|---|---|
uint32_t id |
定时器 id |
回调 |
返回: 无
用法说明: 每当有一个定时器被触发时,该函数都会被回调,参数中的 id 表示触发的是哪个定时器,所以用户在使用
SubscribeTimer或SubscribeSpecifiedTimer注册的定时器,应保存其返回的定时器 id,以用于在onTimer中进行分辨
2.5 编写策略
前言
Linux 环境:使用
g++的-I选项指定头文件路径;-L选项指定动态库路径;-l选项指定动态库;编译示例:
g++ -o backtestdemo Strategy.cc -I./include -L./lib -lscbacktest -lscchinainstrid
引入公共头文件
#include "SCOMSApi.h"
#include "SCOMSApiData.h"
编写派生类继承 SCOMSSpi 类,并根据需要实现相应函数
class Strategy : public SCOMSSpi
{
public:
Strategy( SCOMSApi* api_ )
: api( api_ )
{
}
~Strategy()
{
}
void onEvent( const char* data, int32_t len ) override
{
auto hdr = reinterpret_cast<const sc::message_hdr_t*>( data );
std::cout << "Event " << hdr->mtype << " is triggered" << std::endl;
switch( hdr->mtype ) // 判断事件类型,分别处理
{
case sc::MSG_OMS_STATUS_CHANGE:
onStatusChange( hdr );
break;
case sc::MSG_OMS_FILL_NEW:
onFillNew( hdr );
break;
case sc::MSG_SC_SNAPSHOT_DATA:
onMarketData( hdr );
break;
}
}
void onTimer( uint32_t id ) override
{
std::cout << "Timer " << id << " is triggered" << std::endl;
if( id == xxx )
{
onXXXTimer();
}
}
SCOMSApi* api;
};
2.6 创建对象、初始化、注册回调函数
int main( int argc, const char* argv[] )
{
// 通过命令行参数获取配置文件路径
if( argc < 2 )
{
std::cout << "usage: " << argv[0] << " <config.json>" << std::endl;
exit( EXIT_FAILURE );
}
std::string configFile = argv[1];
// 创建对象
// SCOMSApi::CreateOMSApi 接收配置文件的路径,返回 API 实例对象;当创建失败时,返回空指针
auto api = SCOMSApi::CreateOMSApi( configFile.c_str() );
if( api == nullptr )
{
std::cout << "create oms api fail" << std::endl;
exit( EXIT_FAILURE );
}
// 初始化
// Init 在初始化成功时返回 SCOMS_SUCC;失败则反之
if( SCOMS_SUCC != api->Init() )
{
std::cout << "oms api init fail" << std::endl;
exit( EXIT_FAILURE );
}
// 创建策略对象,注意需要保证 Strategy 必须保持存活直到回测的 run() 运行结束,Release() 返回
Strategy st( api );
// 注册回调函数
api->RegisterSpi( &st );
// ...
}
2.7 订阅事件、注册定时器
需要注意,只有使用 SubscribeEvent 订阅了的事件,回调函数中才会接收到
// 订阅订单状态变化
api->SubscribeEvent( sc::MSG_OMS_STATUS_CHANGE );
// 订阅成交推送
api->SubscribeEvent( sc::MSG_OMS_FILL_NEW );
// 订阅期货快照行情
api->SubscribeEvent( sc::MSG_SC_SNAPSHOT_DATA );
// 订阅快照委托队列数据
api->SubscribeEvent( sc::MSG_SC_SNAPSHOT_DATA_OLIST );
// 逐笔委托数据
api->SubscribeEvent( sc::MSG_SC_MARKET_TBT );
// 逐笔成交数据
api->SubscribeEvent( sc::MSG_SC_TICK_TBT );
// 注册一个每分钟触发一次的定时器,定时器在触发时参数中会携带一个 id,当 id 等于此处的 timerId 时,表示触发的是该定时器
uint32_t timerId = api->SubscribeTimer( 60 * 1000 );
2.8 启动回测
注意 启动函数 run 为阻塞函数,当调用该函数后,当前线程会被阻塞,直到回测结束,或异常抛出
api->run();
2.9 对触发的事件响应做处理
在上面编写的策略中,onEvent 对不同事件的更新,分别调用了几个函数:
onStatusChange( const sc::message_hdr_t* hdr )
{
// 该事件表示订单状态发生变化
auto msgBody = reinterpret_cast<const sc::msg_oms_status_change*>( hdr + 1 ); // hdr + 1 表示取消息头后跟随的消息体
if( sc::STAT_TRANSIT == msgBody.newstatus )
{
// 表示该订单到达柜台,正在发往交易所。在回测中,表示该订单到达总线,正在发往撮合模块
// 时序流转详见:附录 - 闪策 OMS 交易协议规范
}
if( sc::STAT_OPEN == msgBody.newstatus )
{
// 当 newstatus 为 STAT_OPEN 时,我们应判断这是委托的响应还是撤单的响应
if( sc::STAT_TRANSIT == msgBody.oldstatus )
{
// 表示是委托响应,且该订单已被交易所接受。在回测中,表示该订单已被撮合模块接受
}
if( sc::STAT_OPEN == msgBody.oldstatus )
{
// 表示是撤单响应,且该撤单已发往交易所。在回测中,表示该撤单已发往撮合模块
}
}
if( sc::STAT_DONE == msgBody.newstatus )
{
// 表示该订单已结束,我们可以查看 reason 获取结束的原因
if( sc::R_FILL == msgBody.reason )
{
// 表示该笔订单已全部成交
}
if( sc::R_CANCEL == msgBody.reason )
{
if( sc::CXLSTATUS_CONFIRMED == msgBody.cxlstatus )
{
// 表示该笔订单被成功撤单
}
}
if( sc::R_REJECT == msgBody.reason )
{
// 表示该笔订单被拒绝,我们可以查看 reject_text 获取被拒绝的原因
std::cout << "Order was rejected because: " << msgBody.reject_text.data() << std::endl;
}
}
}
onFillNew( const sc::message_hdr_t* hdr )
{
// 该事件表示有订单发生了成交
auto msgBody = reinterpret_cast<const sc::msg_oms_fill_new*>( hdr + 1 );
// 获取成交时间
std::cout << msgBody.exch_time << std::endl;
// 获取成交数量
std::cout << msgBody.size << std::endl;
}
onMarketData( const sc::message_hdr_t* hdr )
{
// 该事件表示期货快照行情推送
auto msgBody = reinterpret_cast<const sc::msg_sc_snapshot_data*>( hdr + 1 );
}
2.10 回测结束时释放资源
注意 在回测结束前,不可调用该接口
api->Release();
三、注意事项
1. 回测系统内部时间管理
timeval_t::now() 接口和 SCOMSApi::GetCurrentTime() 的作用都是获取当前时间,但是:
在程序调用
api->Init()前,timeval_t::now()接口返回的是真实的系统当前时间,SCOMSApi::GetCurrentTime返回值未定义当程序调用
api->Init()后,二者均返回回测时间轴中的当前时间
2. 线程安全
回测本身使用单线程,使用 SCOMSApi 中的接口无需担心线程安全问题,包括在 SCOMSSpi 的回调函数中使用
四、常见问题
Q1:为什么使用 printMsg 打印消息内容,出来的 symbol、instr 内容均是 SYM_UNKNOWN ?
检查配置文件中的
reference_data.symbol_file_path是否配置正确,且配置的文件是否真实存在。回测内部需要根据该文件内容解析具体品种,才能够转换为正确内容
Q2:为什么在回测中,收不到某个合约的行情?
检查配置文件中的
instruments配置内容和marketdata相关配置内容,是否包含指定的合约(例如:au2505.SHFE)。对未包含的合约,回测并不会读取其行情源
Q3:回测 API 支持添加底仓吗?没有底仓我也能直接进行平仓吗?
暂不支持添加底仓信息。回测本身不关注实际仓位,只关注客户订单和交易所行情中的订单能否进行撮合,所以只要订单有效,即使没有仓位也可以进行撮合
Q4:我注册了一个固定时刻定时器,但它没有触发,为什么?
确认该定时器设置的时间,是否早于回测开始时间,或者晚于回测结束时间。对于此类定时器,回测内部会直接跳过