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 接口描述

逻辑上接口分为 SCOMSApiSCOMSSpi 两部分:

  • 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 表示触发的是哪个定时器,所以用户在使用 SubscribeTimerSubscribeSpecifiedTimer 注册的定时器,应保存其返回的定时器 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:我注册了一个固定时刻定时器,但它没有触发,为什么?

  • 确认该定时器设置的时间,是否早于回测开始时间,或者晚于回测结束时间。对于此类定时器,回测内部会直接跳过