小 T 導讀:如果(guo)我們要做(zuo)行情(qing) tick 數(shu)據的(de)(de)存(cun)儲,怎(zen)樣的(de)(de)數(shu)據結構查找起(qi)來才(cai)會比較快?在加入 TDengine 之(zhi)前,本文(wen)作者丁博(bo)在弘(hong)源泰平量化投資做(zuo)量化工程(cheng)師,曾經遇(yu)到過這一類存(cun)儲行情(qing) tick 數(shu)據的(de)(de)問題,本文(wen)會就此問題進行詳細的(de)(de)技術解讀(du)。
本文將以(yi)標準 CTP 行(xing)情接口()為例,假設行(xing)情結構(gou)為 CThostFtdcDepthMarketDataField(),展開(kai)說(shuo)明。
內存存儲方案
如(ru)果你的需求僅僅是盤(pan)中實時分析,且監控的 Instrument(CTP 接(jie)口對現貨(huo)、期(qi)貨(huo)、期(qi)權等合(he)約的統稱(cheng)(cheng), 以(yi)下簡(jian)稱(cheng)(cheng)【合(he)約】) 總數不(bu)多,則可以(yi)直接(jie)使用內存(cun)存(cun)儲。通(tong)常只有超高頻交易(yi)系統才(cai)必須這么(me)做。內存(cun)存(cun)儲也有很(hen)多可選(xuan)方案,其中有兩大(da)方案較(jiao)為通(tong)用。
兩級 map 方案
第(di)一(yi)級 map 的(de)(de)(de)類型為 std::unordered_map,鍵為 InstrumentID, 值(zhi)為第(di)二級 map 的(de)(de)(de)指針(zhen)。第(di)二級 map 的(de)(de)(de)類型為 std::map,鍵為行情時(shi)間戳(chuo),值(zhi)為行情結構(gou)體(ti)。(注:行情時(shi)間戳(chuo)需要根據(ju) UpdateTime 和 UpdateMillisec 兩個字段構(gou)造一(yi)個類型為 long 的(de)(de)(de)毫秒值(zhi))。 std::unordered_map 底(di)(di)層(ceng)依賴的(de)(de)(de)數(shu)據(ju)結構(gou)是哈希表,按 key 索引(yin)速(su)(su)度是最快(kuai)的(de)(de)(de)。std::map 底(di)(di)層(ceng)的(de)(de)(de)數(shu)據(ju)結構(gou)是二叉樹搜索樹,可以嚴格按照 key 的(de)(de)(de)大小順(shun)序迭(die)代全部(bu)或(huo)某一(yi)段數(shu)據(ju)。 總體(ti)而言(yan)這個數(shu)據(ju)結構(gou)的(de)(de)(de)優勢是: 快(kuai)速(su)(su)查找(zhao)某個合約某個時(shi)間點或(huo)某個時(shi)間段返回(hui)的(de)(de)(de)行情。這是后續做交易(yi)信號計(ji)算的(de)(de)(de)基礎。
#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
由于每(mei)種合(he)約(yue)每(mei)天的(de)(de)標準(zhun)(zhun)行(xing)情(qing) tick 總數(shu)都是固(gu)定的(de)(de)(個別交易(yi)所除外),因(yin)此我們可以提前(qian)初始化好一個數(shu)組(zu)來存行(xing)情(qing)。按每(mei)秒(miao)(miao) 2 個 tick 算(suan)(500 毫秒(miao)(miao)一個點),標準(zhun)(zhun)行(xing)情(qing)的(de)(de)長度(du)可能是 28800。當收到(dao)行(xing)情(qing)通(tong)知時,行(xing)情(qing)時間距離(li)哪(na)個標準(zhun)(zhun) tick 點最近就(jiu)歸為哪(na)個 tick。比如(ru)行(xing)情(qing)時間是 9 點 50 分(fen) 20 秒(miao)(miao) 133 毫秒(miao)(miao),那么可以當作(zuo) 9 點 50 分(fen) 20 秒(miao)(miao) 0 毫秒(miao)(miao)的(de)(de)行(xing)情(qing)。如(ru)果出現前(qian)后兩個 tick 時間大(da)于 500 毫秒(miao)(miao)的(de)(de)情(qing)況(kuang),那就(jiu)還需要(yao)補(bu)全中間空缺的(de)(de)行(xing)情(qing),相當于邊收行(xing)情(qing)邊做標準(zhun)(zhun)化操作(zuo)。這樣(yang)做的(de)(de)優(you)勢是:
- 交易策略通常會依賴標準化的行情計算交易信號,收行情和標準化并作一步會更節省時間。
- 可以直接用數組下標索引對應時間的行情,查找的時間復雜度為 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)異軍(jun)突起(qi),使得盤中盤后使用(yong)一種存(cun)儲(chu)方(fang)(fang)案成為可(ke)能。特別是(shi)像 TDengine 這樣帶(dai)有緩存(cun)功能、消(xiao)息隊(dui)列功能和集群功能的時序數據(ju)庫,用(yong)來存(cun)行情是(shi)非(fei)常合適。下面(mian)我將以 TDengine Database 為例為大家(jia)介紹持久化存(cun)儲(chu)方(fang)(fang)案。
下載 TDengine Database Server
在下(xia)(xia)載階(jie)段,不同的(de)系統(tong)(tong)使(shi)用的(de)安(an)裝包也(ye)有所不同,Ubuntu 系統(tong)(tong)用 deb 包, CentOS 系統(tong)(tong)用 RPM 包。下(xia)(xia)載地址為: 。
安裝并啟動
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
安裝成(cheng)功后,如何啟動 TDengine Database 的提示信息(xi)就(jiu)會自動彈出,照著(zhu)操作就(jiu)可以。
建行情表
由于所有行情(qing)的(de)結構都是一(yi)樣的(de),因此只需(xu)要一(yi)張進行行情(qing)建(jian)表即可,其(qi)中每個(ge)合約(yue)對(dui)應一(yi)張子表,InstrumentID 作(zuo)為子表名,交易所代碼作(zuo)為一(yi)個(ge)行情(qing)標簽(qian)。為了方(fang)便(bian)演示(shi),下(xia)面的(de)示(shi)例只包含(han)了 4 個(ge)行情(qing)字段:
- 進入 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 對每(mei)個表的最新數(shu)據都有緩存功能,無(wu)需再(zai)讀(du)磁(ci)盤,使用 last 函(han)數(shu)就能快速(su)獲取。
#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);
}

以(yi)上兩(liang)個示例程序(xu),展示了寫入(ru)和查詢的方法(fa)。結(jie)合 TDengine 內置的和功能,可(ke)實現更多功能,比如:
- 使用 MAX、 FIRST、 MIN、 LAST 四個 SQL 函數計算 K 線上高、開、低、收四個價位。
- 使用 INTERVAL 和 SLIDING 查詢子句和 AVG 函數計算移動均價。此處不再給出具體示例,可參考。?
從實際業務出發的實踐經驗分享
除了上述內容外(wai),TDengine Database 還有非(fei)常豐(feng)富(fu)的分(fen)析(xi)函數,如果你感興趣的話(hua)建議參考官方文檔(dang)。此外(wai),在 TDengine 的實(shi)際應用中(zhong)(zhong),也有很多(duo)客戶的實(shi)踐是關于量化投資(zi)場景(jing)中(zhong)(zhong)的數據處理。
以(yi)同花順為(wei)例(li),其每天(tian)都需(xu)要接收(shou)海量交易所行(xing)情(qing)(qing)數(shu)(shu)據(ju)(ju),以(yi)確保行(xing)情(qing)(qing)數(shu)(shu)據(ju)(ju)的(de)(de)數(shu)(shu)據(ju)(ju)準確,但(dan)由于該部(bu)分(fen)數(shu)(shu)據(ju)(ju)過于龐(pang)大,而且(qie)使用(yong)(yong)場景頗(po)多(duo),因(yin)此每天(tian)會產生很(hen)多(duo)的(de)(de)加工數(shu)(shu)據(ju)(ju),在(zai)組合管(guan)理(PMS)上還(huan)會使用(yong)(yong)到歷史(shi)行(xing)情(qing)(qing)數(shu)(shu)據(ju)(ju)。之前他們采用(yong)(yong)的(de)(de)是 Postgres+LevelDB 作為(wei)數(shu)(shu)據(ju)(ju)的(de)(de)存儲方案(an),但(dan)仍(reng)舊痛點頻發,隨后通(tong)過對數(shu)(shu)據(ju)(ju)流(liu)、行(xing)情(qing)(qing)獲取模塊(kuai)的(de)(de)分(fen)析(xi),發現目前主要存在(zai)以(yi)下兩個亟需(xu)解決的(de)(de)問題:
- 依賴多,穩定性較差:PMS作為多品種的投后分析服務, 需要使用到各種日線數據、當天實時行情數據、當天分鐘數據等,在數據獲取方面需要依賴Http以及Postgres、LevelDB等數據庫。過于多的數據獲取鏈路會導致平臺可靠性降低,同時依賴于其他各個服務,導致查詢問題過于復雜。
- 性能不能滿足需求: PMS作為多品種投后分析,在算法分析層面需要大量的行情獲取,而且對行情獲取的性能也有較大的要求,當前所有行情會占據大量分析的性能。
從(cong)業務發展的角度來講,存儲方案的改(gai)造迫在眉睫,之后同花順開始對 ClickHouse、InfluxDB、TDengine 等數據存儲方案進行調研。由(you)于(yu)(yu)行情(qing)數據是綁定(ding)時間戳(chuo)的形式,所(suo)以(yi)顯然時序數據庫更適用(yong)于(yu)(yu)這個業務場景,在 InfluxDB 和(he)(he) TDengine 之間,由(you)于(yu)(yu) TDengine 的寫入速(su)度遠高(gao)于(yu)(yu) InfluxDB,且集群版(ban)開源(yuan),同時還支持包含 C/C++、Java、Python、Go 和(he)(he) RESTful 在內的多種數據接口,因此成為同花順的最終選用(yong)方案。
改造之后的(de)性(xing)能(neng)效果提(ti)(ti)升還是非常明顯的(de),下(xia)圖(tu)(tu)是同花順(shun)做的(de)一張改造前(qian)后性(xing)能(neng)對比圖(tu)(tu),可以更為直觀地感受到(dao)效果提(ti)(ti)升:

同時改造后,穩定性也顯著增強(qiang),改造前調用數據情況(kuang)共(gong) 40W 次,共(gong)出現 0.01% 的異(yi)(yi)常(chang),改造后出現異(yi)(yi)常(chang)降低至 0.001%。
在 TDengine Database 官(guan)網的(de) Case 合集中,還(huan)(huan)有(you)弘源泰平量化、同(tong)心源基金等(deng)幾(ji)篇聚(ju)焦投(tou)資量化場(chang)景下(xia)數(shu)據處(chu)(chu)理(li)難(nan)題的(de)客戶(hu)案例(li),由(you)于篇幅所限,便(bian)不(bu)在此一(yi)一(yi)列舉了,有(you)需(xu)要的(de)朋友可以(yi)去官(guan)網查(cha)找文(wen)章進行(xing)參考。如果還(huan)(huan)有(you)投(tou)資量化場(chang)景下(xia)其(qi)他的(de)數(shu)據處(chu)(chu)理(li)難(nan)題,也歡迎在文(wen)章下(xia)方進行(xing)留言(yan),我們(men)后續可以(yi)加微信進行(xing)詳細討(tao)論和溝通。


























