乐趣区

关于数据库:在进行行情-tick-数据存储时哪种数据结构查找起来更快

小 T 导读:如果咱们要做行情 tick 数据的存储,怎么的数据结构查找起来才会比拟快?在退出 TDengine 之前,本文作者丁博在弘源泰平量化投资做量化工程师,已经遇到过这一类存储行情 tick 数据的问题,本文会就此问题进行具体的技术解读。

本文将以规范 CTP 行情接口(http://www.sfit.com.cn/5_1_Do…)为例,假如行情构造为 CThostFtdcDepthMarketDataField(https://mckelv.in/python-ctp-…),开展阐明。

内存存储计划

如果你的需要仅仅是盘中实时剖析,且监控的 Instrument(CTP 接口对现货、期货、期权等合约的统称,以下简称【合约】)总数不多,则能够间接应用内存存储。通常只有超高频交易系统才必须这么做。内存存储也有很多可选计划,其中有两大计划较为通用。

两级 map 计划

第一级 map 的类型为 std::unordered_map,键为 InstrumentID,值为第二级 map 的指针。第二级 map 的类型为 std::map,键为行情工夫戳,值为行情构造体。(注:行情工夫戳须要依据 UpdateTime 和 UpdateMillisec 两个字段结构一个类型为 long 的毫秒值)。std::unordered_map 底层依赖的数据结构是哈希表,按 key 索引速度是最快的。std::map 底层的数据结构是二叉树搜寻树,能够严格依照 key 的大小程序迭代全副或某一段数据。总体而言这个数据结构的劣势是:疾速查找某个合约某个工夫点或某个时间段返回的行情。这是后续做交易信号计算的根底。

#include "ThostFtdcUserApiStruct.h"
#include "ThostFtdcUserApiDataType.h"
#include <map>
#include <unordered_map>
using namespace std;
int main()
{unordered_map<TThostFtdcInstrumentIDType, map<long, CThostFtdcDepthMarketDataField>*> tickData;}

map + array

因为每种合约每天的规范行情 tick 总数都是固定的(个别交易所除外),因而咱们能够提前初始化好一个数组来存行情。按每秒 2 个 tick 算(500 毫秒一个点),规范行情的长度可能是 28800。当收到行情告诉时,行情工夫间隔哪个规范 tick 点最近就归为哪个 tick。比方行情工夫是 9 点 50 分 20 秒 133 毫秒,那么能够当作 9 点 50 分 20 秒 0 毫秒的行情。如果呈现前后两个 tick 工夫大于 500 毫秒的状况,那就还须要补全两头空缺的行情,相当于边收行情边做标准化操作。这样做的劣势是:

  1. 交易策略通常会依赖标准化的行情计算交易信号,收行情和标准化并作一步会更节省时间。
  2. 能够间接用数组下标索引对应工夫的行情,查找的工夫复杂度为 O(1)。
#include "ThostFtdcUserApiStruct.h"
#include "ThostFtdcUserApiDataType.h"
#include <unordered_map>
#include <array>
using namespace std;
int main()
{unordered_map<TThostFtdcInstrumentIDType, array<CThostFtdcDepthMarketDataField, 28800>> tickData;}

长久化存储计划

无论是否做超高频交易,长久化存储行情都是有必要的。通常长久化存储为的是进行盘后复盘剖析, 因为在大数据量下,传统的存储计划(MongoDB、MySQL、间接存文件等等)很快就会遇到性能瓶颈(无论是读还是写),不适宜做盘中的计算。近年来,时序数据库(Time-Series Database)异军突起,使得盘中盘后应用一种存储计划成为可能。特地是像 TDengine 这样带有缓存性能、音讯队列性能和集群性能的时序数据库,用来存行情是十分适合。上面我将以 TDengine Database 为例为大家介绍长久化存储计划。

下载 TDengine Database Server

在下载阶段,不同的零碎应用的安装包也有所不同,Ubuntu 零碎用 deb 包,CentOS 零碎用 RPM 包。下载地址为:All Downloads – TDengine。

装置并启动

Ubuntu

sudo dpkg -i TDengine-server-2.4.0.7-Linux-x64.deb

CentOS

sudo rpm -ivh TDengine-server-2.4.0.7-Linux-x64.rpm

装置胜利后,如何启动 TDengine Database 的提示信息就会自动弹出,照着操作就能够。

建行情表

因为所有行情的构造都是一样的,因而只须要一张超级表进行行情建表即可,其中每个合约对应一张子表,InstrumentID 作为子表名,交易所代码作为一个行情标签。为了不便演示,上面的示例只蕴含了 4 个行情字段:

  • 进入 taos 命令行
bo@RDBB:~$ taos
Welcome to the TDengine shell from Linux, Client Version:2.4.0.12
Copyright (c) 2020 by TAOS Data, Inc. All rights reserved.
  • 执行上面的语句
create database marketdata;
use marketdata;
create stable tick(
        ts timestamp,
        updatetime binary(9),
        updatemillisec int,
        askprice1 double,
        bidprice1 double,
        askvolume1 int,
        bidvolume1 int
) tags (exchangeid binary(9));
  • 查看表构造
taos> desc tick;
             Field              |         Type         |   Length    |   Note   |
=================================================================================
 ts                             | TIMESTAMP            |           8 |          |
 updatetime                     | BINARY               |           9 |          |
 updatemillisec                 | INT                  |           4 |          |
 askprice1                      | DOUBLE               |           8 |          |
 bidprice1                      | DOUBLE               |           8 |          |
 askvolume1                     | INT                  |           4 |          |
 bidvolume1                     | INT                  |           4 |          |
 exchangeid                     | BINARY               |           9 | TAG      |
Query OK, 8 row(s) in set (0.000378s)

写入行情

#include "ThostFtdcUserApiStruct.h"
#include "ThostFtdcUserApiDataType.h"
#include "taos.h"
#include "taoserror.h"
#include <iostream>
#include <sstream>
using namespace std;
void insertTickData(TAOS* taos, CThostFtdcDepthMarketDataField &tick) {
        stringstream sql;
        // 会主动创立子表 tick.InstrumentID
        sql << "insert into" << tick.InstrumentID << "using tick tags("
                << tick.ExchangeID << ") values(now,'" << tick.UpdateTime << "',"
                << tick.UpdateMillisec << "," << tick.AskPrice1 << "," << tick.BidPrice1
                << "," << tick.AskVolume1 << "," << tick.BidVolume1 << ")";
        TAOS_RES *res = taos_query(taos, sql.str().c_str());
        if (res == nullptr || taos_errno(res) != 0) {cerr << "insertTitckData failed," << taos_errno(res) << "," << taos_errstr(res) << endl;
        }
}
int main()
{TAOS *taos = taos_connect("localhost", "root", "taosdata", "marketdata", 6030);
        // 结构测试数据
        CThostFtdcDepthMarketDataField tick;
        strcpy_s(tick.InstrumentID, "IH2209");
        strcpy_s(tick.UpdateTime, "14:10:32");
        strcpy_s(tick.ExchangeID, "DEC");
        tick.UpdateMillisec = 500;
        tick.AskPrice1 = 123.8;
        tick.BidPrice1 = 123.4;
        tick.AskVolume1 = 10;
        tick.BidVolume1 = 9;
        // 写入测试数据
        insertTickData(taos, tick);
        taos_close(taos);
}

查问最新的行情

TDengine 对每个表的最新数据都有缓存性能,无需再读磁盘,应用 last 函数就能疾速获取。

#include "ThostFtdcUserApiStruct.h"
#include "ThostFtdcUserApiDataType.h"
#include "taos.h"
#include "taoserror.h"
#include <string>
#include <iostream>
using namespace std;
CThostFtdcDepthMarketDataField* getLastTick(TAOS* taos, const char* instrumentID) {string sql("select last(*) from");
        sql += instrumentID;
        TAOS_RES* res = taos_query(taos, sql.c_str());
    if (res == nullptr || taos_errno(res) != 0) {cerr << "getLastTick failed," << taos_errno(res) << "," << taos_errstr(res) << endl;
                return nullptr;
        } 
        TAOS_ROW row = taos_fetch_row(res);        
        if (row == nullptr) {return nullptr;}
        CThostFtdcDepthMarketDataField* tick = new CThostFtdcDepthMarketDataField();
        //int64_t ts = *((int64_t*)row[0]);
        memcpy(tick->UpdateTime, row[1], 9);
        tick->UpdateMillisec = *(int*)row[2];
        tick->AskPrice1 = *((double *)row[3]);
        tick->BidPrice1 = *((double*)row[4]);
        taos_free_result(res);
        return tick;
}
int main() {TAOS* taos = taos_connect("localhost", "root", "taosdata", "marketdata", 6030);
        CThostFtdcDepthMarketDataField* tick = getLastTick(taos, "IH2209");
        cout << "askPrice1=" << tick->AskPrice1 << "bidPrice1=" << tick->BidPrice1 << endl;
        delete tick;
        taos_close(taos);
}

以上两个示例程序,展现了写入和查问的办法。联合 TDengine 内置的查问函数和按窗口聚合性能,可实现更多功能,比方:

  • 应用 MAX、FIRST、MIN、LAST 四个 SQL 函数计算 K 线上高、开、低、收四个价位。
  • 应用 INTERVAL 和 SLIDING 查问子句和 AVG 函数计算挪动均价。此处不再给出具体示例,可参考官网文档。

从理论业务登程的实践经验分享

除了上述内容外,TDengine Database 还有十分丰盛的剖析函数,如果你感兴趣的话倡议参考官网文档。此外,在 TDengine 的理论利用中,也有很多客户的实际是对于量化投资场景中的数据处理。

以同花顺为例,其每天都须要接管海量交易所行情数据,以确保行情数据的数据精确,但因为该局部数据过于宏大,而且应用场景颇多,因而每天会产生很多的加工数据,在组合治理 (PMS) 上还会应用到历史行情数据。之前他们采纳的是 Postgres+LevelDB 作为数据的存储计划,但仍旧痛点频发,随后通过对数据流、行情获取模块的剖析,发现目前次要存在以下两个亟需解决的问题:

依赖多,稳定性较差:PMS 作为多种类的投后剖析服务, 须要应用到各种日线数据、当天实时行情数据、当天分钟数据等,在数据获取方面须要依赖 Http 以及 Postgres、LevelDB 等数据库。过于多的数据获取链路会导致平台可靠性升高,同时依赖于其余各个服务,导致查问问题过于简单。
性能不能满足需要:PMS 作为多种类投后剖析,在算法剖析层面须要大量的行情获取,而且对行情获取的性能也有较大的要求,以后所有行情会占据大量剖析的性能。
从业务倒退的角度来讲,存储计划的革新火烧眉毛,之后同花顺开始对 ClickHouse、InfluxDB、TDengine 等数据存储计划进行调研。因为行情数据是绑定工夫戳的模式,所以显然时序数据库更实用于这个业务场景,在 InfluxDB 和 TDengine 之间,因为 TDengine 的写入速度远高于 InfluxDB,且集群版开源,同时还反对蕴含 C/C++、Java、Python、Go 和 RESTful 在内的多种数据接口,因而成为同花顺的最终选用计划。

革新之后的性能成果晋升还是非常明显的,下图是同花顺做的一张革新前后性能比照图,能够更为直观地感触到成果晋升:

同时革新后,稳定性也显著加强,革新前调用数据状况共 40W 次,共呈现 0.01% 的异样,革新后出现异常升高至 0.001%。

在 TDengine Database 官网的 Case 合集中,还有弘源泰平量化、同心源基金等几篇聚焦投资量化场景下数据处理难题的客户案例,因为篇幅所限,便不在此一一列举了,有须要的敌人能够去官网查找文章进行参考。如果还有投资量化场景下其余的数据处理难题,也欢送在文章下方进行留言,咱们后续能够加微信进行具体探讨和沟通。


想理解更多 TDengine Database 的具体细节,欢送大家在 GitHub 上查看相干源代码。

退出移动版