时序数据库DolphinDB历史数据回放教程

一个量化策略在用于实际交易时,处理实时数据的程序通常为事件驱动。而研发量化策略时,需要使用历史数据进行回测,这时的程序通常不是事件驱动。因此同一个策略需要编写两套代码,不仅耗时而且容易出错。在 DolphinDB database 中,用户可将历史数据按照时间顺序以“实时数据”的方式导入流数据表中,这样就可以使用同一套代码进行回测和实盘交易。

DolphinDB的流数据处理框架采用发布-订阅-消费的模式。数据生产者将实时数据继续地以流的形式发布给所有数据订阅者。订阅者收到消息以后,可使用自定义函数或者DolphinDB内置的聚合引擎来处理消息。DolphinDB流数据接口支持多种语言的API,包括C++, C#, Java, 和Python等。用户可以使用这些API来编写更加复杂的处理逻辑,更好地与实际生产环境相结合。详情请参考DolphinDB流数据教程

本文介绍replay和replayDS函数,然后使用金融数据展示数据回放的过程与应用场景。

1. 函数介绍

replay

replay(inputTables, outputTables, [dateColumn], [timeColumn], [replayRate], [parallelLevel=1])

replay函数的作用是将若干表或数据源同时回放到相应的输出表中。用户需要指定输入的数据表或数据源、输出表、日期列、时间列、回放速度以及并行度。

replay函数参数概念如下:

  • inputTables: 单个表或包含若干表或数据源(见replayDS介绍)的元组。
  • outputTables: 单个表或包含若干个表的元组,这些表通常为流数据表。输入表和输出表的个数一致,且一一对应,每对输入、输出表的结构相同。
  • dateColumn, timeColumn: string, 表示输入表的日期和时间列,若不指定则默认第一列为日期列。若输入表中时间列同时包含日期和时间,需要将dateColumn和timeColumn设为同一列。回放时,系统将根据dateColumn和timeColumn的设定,决定回放的最小时间精度。在此时间精度下,同一时刻的数据将在相同批次输出。比如一张表同时有日期列和时间列,但是replay函数只设置了dateColumn,那么同一天的所有数据会在一个批次输出。
  • replayRate: 整数, 表示每秒钟回放的数据条数。由于回放时同一个时刻数据在同一批次输出,因此当replayRate小于一个批次的行数时,实际输出的速率会大于replayRate。
  • parallelLevel: 整数, 表示读取数据的并行度。当源数据大小超过内存大小的时候,需要使用replayDS函数将源数据划分为若干个小的数据源,依次从磁盘中读取数据并回放。指定多个读取数据的线程数可提升数据读取速度。

replayDS

replayDS(sqlObj, [dateColumn], [timeColumn], [timeRepartitionSchema])

replayDS函数可以将输入的SQL查询转化为数据源,结合replay函数使用。其作用是根据输入表的分区以及timeRepartitionSchema,将原始的SQL查询按照时间顺序拆分成若干小的SQL查询。

replayDS函数参数概念如下:

  • sqlObj: SQL元代码,表示回放的数据,如, `date, `time, 08:00:00.000 + (1..10) * 3600000) replay(inputDS, outputTable, `date, `time, 1000, 2)

    使用data source的多表回放

    replay也支持多张表的同时回放,只需要将多张输入表以元组的方式传给replay,并且分别指定输出表即可。这里输出表和输入表应该一一对应,每一对都必须有相同的表结构。如果指定了日期列或时间列,那么所有表中都应当有存在相应的列。

    ds1 = replayDS(, `date, `time, 08:00:00.000 + (1..10) * 3600000)
    ds3 = replayDS(, `date, `time,  trs);

    (2)定义输出表outQuotes,一般为流数据表。

    share streamTable(100:0, sch.name,sch.type) as outQuotes

    (3)定义股票权重字典weights以及聚合函数etfVal,用于计算ETF价值。在本例中,我们仅计算AAPL、IBM、MSFT、NTES、AMZN、GOOG这几只股票的ETF价值。

    defg etfVal(weights,sym, price) {
        return wsum(price, weights[sym])
    }
    weights = dict(STRING, DOUBLE)
    weights[`AAPL] = 0.1
    weights[`IBM] = 0.1
    weights[`MSFT] = 0.1
    weights[`NTES] = 0.1
    weights[`AMZN] = 0.1
    weights[`GOOG] = 0.5

    (4)创建流聚合引擎,并订阅数据回放的输出表outQuotes。订阅outQuotes表时,我们指定了发布表的过滤条件,只有symbol为AAPL、IBM、MSFT、NTES、AMZN、GOOG的数据才会发布到横截面聚合引擎,减少不必要的网络开销和数据传输。

    setStreamTableFilterColumn(outQuotes, `symbol)
    outputTable = table(1:0, `time`etf, [TIMESTAMP,DOUBLE])
    tradesCrossAggregator=createCrossSectionalAggregator("etfvalue", <[etfVal{weights}(symbol, ofr)]>, quotes, outputTable, `symbol, `perBatch)
    subscribeTable(,"outQuotes","tradesCrossAggregator",-1,append!{tradesCrossAggregator},true,,,,,`AAPL`IBM`MSFT`NTES`AMZN`GOOG) 

    (5)开始回放,设定每秒回放10万条数据,聚合引擎则会实时地对回放的数据进行消费。

    submitJob("replay_quotes", "replay_quotes_stream",  replay,  [rds],  [`outQuotes], `date, `time,100000,4)

    (6)查看不同时间点下我们选择的股票的ETF价值。

    //查看outputTable表内前15行的数据,其中第一列时间为聚合计算发生的时间
    >select top 15 * from outputTable;
    
    time                    etf
    2019.06.04T16:40:18.476   14.749
    2019.06.04T16:40:19.476      14.749
    2019.06.04T16:40:20.477      14.749
    2019.06.04T16:40:21.477      22.059
    2019.06.04T16:40:22.477      22.059
    2019.06.04T16:40:23.477      34.049
    2019.06.04T16:40:24.477      34.049
    2019.06.04T16:40:25.477      284.214
    2019.06.04T16:40:26.477      284.214
    2019.06.04T16:40:27.477      285.68
    2019.06.04T16:40:28.477      285.68
    2019.06.04T16:40:29.478      285.51
    2019.06.04T16:40:30.478      285.51
    2019.06.04T16:40:31.478      285.51
    2019.06.04T16:40:32.478      285.51

    4. 性能测试

    我们在服务器上对DolphinDB的数据回放功能进行了性能测试。服务器配置如下:

    主机:DELL PowerEdge R730xd

    CPU:Intel Xeon(R) CPU E5-2650 v4(24核 48线程 2.20GHz)

    内存:512 GB (32GB × 16, 2666 MHz)

    硬盘:17T HDD (1.7T × 10, 读取速度222 MB/s,写入速度210 MB/s)

    网络:万兆以太网

    测试脚本如下:

    sch = select name,typeString as type from  quotes.schema().colDefs
    trs = cutPoints(09:30:00.001..18:00:00.001,60)
    rds = replayDS(