前言
《WonderTrader架构详解》系列文章,上周介绍了一下WonderTrader的整体架构。本文是该系列文章的第二篇,次要介绍WonderTrader数据处理的机制。
往期文章列表:
- WonderTrader架构详解之一——整体架构
数据的分类
量化平台对于数据的依赖无需多言,量化平台除了作为数据的消费者以外,同时又是数据的生产者。对于一个量化平台,须要跟不同的数据打交道。
量化平台解决不同类型的数据的时候,为了达到更好的解决效率,会采纳不同的解决形式。那么数据又怎么分类呢?笔者按照本人多年的教训,大略将数据分为以下几类:
1、从工夫角度辨别
从工夫角度辨别,咱们能够把数据分为实时数据和历史数据。
实时数据,次要针对行情数据,包含实时的tick数据、分钟线数据,以及股票
level2
数据。实时数据对于策略的重要性自不用多说,信号触发、止盈止损、危险管制都离不开实时数据,这就要求实时数据的解决必须满足两个根本条件:疾速和稳固。疾速指的是处理速度要快,不能因为解决数据而减少太多的延时;稳固指的是数据不能失落,要疾速长久化。除此之外,实时数据还有一些别的特点:个别状况下数据量绝对历史数据较少、数据拜访频率低等等。
历史数据,也是次要针对行情数据,包含历史分钟线、历史高频数据如tick和股票level2数据。也包含基本面数据、异类数据等,因为这类数据更新频率个别较低,基本上也能够归为历史数据。
和实时数据不同,历史数据在一个交易日的范畴内能够看作是静态数据。历史数据的特点是:数据量大、拜访频率低、拜访个别依照工夫范畴筛选数据。这样的特点,就要求历史数据在存储的时候必须要思考检索的便利性、存储的老本、治理的便利性等问题。此外,还须要思考一些特地的场景下的需要,诸如:是否要给投研人员间接查阅、是否要对外提供数据服务等。
2、从频率角度辨别
从频率角度辨别,咱们能够把数据分为高频数据和低频数据。
高频数据,咱们个别指的是以秒甚至毫秒为单位更新的数据,如前文提到的tick数据和股票的
level2
数据。高频数据个别数据量都十分大,以A股
level2
数据为例,以csv
形式存储的话,一天的数据量几十个G是没有问题的。这样大的数据量,如果放到关系型数据库中,那基本上就是劫难了。另外,实时的高频数据对提早也十分敏感,如国内期货的tick
数据是500ms
一笔,如果解决时速度慢,下一个时刻的tick
进来的时候,还有很多数据没有解决完,就会造成数据提早太大,缓冲队列长度一直减少,对于策略来说也是一种劫难。低频数据,咱们个别指的是分钟线以上周期的K线数据、财务数据,以及其余更新工夫距离在1分钟以上的数据。
低频数据绝对高频数据来说,数据量就小了很多。以A股数据为例,
tick
数据3秒一笔,1分钟线的数据量是tick
数据的1/20
,而5分钟线的数据量是tick
数据的1/100
。因为低频数据的数据量不大,咱们在存储低频数据的时候,也有了更多的抉择余地。关系数据库、文件系统、NoSQL
数据库等等,都能够依据不同的应用场景列入抉择的范畴。
3、从起源角度辨别
从起源角度辨别,咱们能够把数据分为行情数据、交易数据和平台数据。
行情数据,咱们解决数据的外围还是围绕行情数据开展的。
行情数据,次要指的就是从行情接口接入的实时行情数据,以及通过数据伺服对实时行情数据做再加工而失去的各种二次数据,策略的计算都只针对行情数据进行的。行情数据处理得好,能够节约读取的工夫,晋升策略的处理速度。行情数据再加工的数据品质,也是影响策略体现的一个因素。因为行情数据的特殊性,所以行情接口都只会推送最新的快照信息过去,这就要求如果须要应用更早的行情数据,就必须本人解决实时行情数据落地的工作,还得向应用模块提供数据拜访的接口。
交易数据,次要指的是从交易接口获取到的交易相干的数据,诸如资金数据、持仓数据、订单数据等。
交易数据和行情数据不同,因为交易数据会在交易柜台保护一个残缺的数据,所以咱们能够通过交易接口拿到最新的交易数据,而不须要本人去做落地和保护。另一方面,思考到多点登录等状况,咱们也不能齐全信赖本地缓存的交易数据,而须要实时同步柜台的交易数据。这其实对于平台来说,是升高了保护的难度,只须要每次登录交易通道的时候做一次同步,前面就能够依据回报自动更新了。
平台数据,次要指的是平台作为数据的生产者生产的数据,如策略输入的信号、策略的实践部位和实践资金等数据。
平台生成的数据,是对策略的绩效进行剖析来说十分重要的数据,这外面包含逐笔的成交数据、每一个进出场的残缺的回合数据(平仓明细),以及每天开盘后的资金数据。这样的数据一般来说都不会太多,毕竟个别状况下,交易信号的数量是远小于行情数据的数量的。同理,这样的数据更新频率也不会太高,所以对于存储的要求也没有那么高。
数据的存储
后面大抵介绍了一下对数据的分类。不同类的数据如何存储呢?如何兼顾读写的效率和便捷性呢?不同的应用场景又如何抉择存储形式呢?数据的安全性又如何保障呢?
1、根本准则
WonderTrader在存储数据的时候,遵循以下根本准则:
效率优先
效率优先,次要指的是数据读写的效率要尽量高,一方面是实时数据的接管处理速度快,而策略在应用最新数据的时候也要尽可能的高效拜访,另一方面是历史数据的读取速度快。
便于管理
便于管理,也分两个方面:一个方面是数据的可维护性要强,因为数据出错的状况总是难以避免的,而数据的存储,不能影响数据的可维护性。另一方面是要便于迁徙,因为在部署新的策略执行节点的时候,总是须要一些历史数据的,如果不便于迁徙,那么新部署策略执行节点就会十分麻烦。
2、存储形式比照
在WonderTrader迭代的过程中,笔者也已经用过不少存储形式。大抵上分为文件存储、关系数据库、分布式数据库三种形式。
文件存储
文件存储,最简略也最难,对开发也有肯定要求。简略在于不依赖任何服务,轻易什么语言都能很容易的对文件进行读写。难点在于,如何设计一个正当的文件数据格式,才可能满足业务场景的须要,让读写更高效,这对架构和开发的要求还是比拟高的。文件读写的速度个别都很快,因为没有冗余,都是程序间接拜访。一些处理速度要求高的场景,利用
mmap
把文件映射到内存中,处理速度会更快,而且还不会失落数据(除非硬盘坏掉)。笔者刚开始入行的时候,是一家股票数据供应商,就是用的这种形式存储数据。而WonderTrader最终也是采纳的文件存储的形式,尽管两头兜了一个大圈才绕回来。关系数据库
关系数据库,作为数据存储的传统主力,始终占用一席之地。关系数据库,如
MYSQL
、MSSQL
和Oracle
等,对于结构化的数据十分敌对,尽管读写效率不如文件存储,因为要建设索引,还要引入额定的空间占用,然而对于大部分低频数据的存储还是可能满足的。数据库存储有一个最好的益处就是个别数据库都有可视化管理工具,十分不便对数据进行治理。如果要搭建一个投研平台,要不便团队外部成员查看数据的话,那么关系数据库会是一个不错的抉择。WonderTrader的历史数据也反对MYSQL存储的形式。分布式数据库
分布式数据库是时下大数据浪潮下的宠儿。笔者之前在一家量化私募工作的时候,专门调研过过后比拟支流的一些分布式数据库,包含
Hadoop
、Cassandra
、Mysql
集群等。笔者认为,分布式数据库存储的实现和关系型数据库没有太大的差异,而分布式数据库的外围在于数据的安全性(有备份)和事务处理的并发性。对于量化平台来说,数据存储不会有特地简单的业务逻辑,所以不必放心分布式事务这方面的问题,外围关注的点还是在于查问数据能够在多个节点并发执行,从而提高效率。笔者认为,分布式数据库是比拟适宜较大的量化团队或者钻研团队应用的,因为团队成员多,各类数据的总量也会十分微小,分布式数据库可能轻松胜任这样对的利用场景。
此外,目前还风行一种NoSQL
数据库类型,这类数据库,能够是分布式的,也能够是独立的,相同点在于不应用SQL
语句,而是用别的接口进行拜访。笔者已经应用过leveldb
进行数据存储,读写速度可能满足绝大多数场景,这也是一种NoSQL
数据库。然而leveldb
的致命缺点是独占式治理,也就是说没有方法基于leveldb
构建读写拆散的机制。最初一次重构WonderTrader,笔者也彻底摈弃了leveldb
,又回到文件存储的思路。
3、WonderTrader存储机制
鉴于以上存储形式各自不同的特点,WonderTrader联合了局部需要,设计了本人的数据存储机制。
实时行情数据全副采纳文件存储原始数据构造,并应用
mmap
的机制映射到内存中,便于读写typedef struct _BlockHeader{ char _blk_flag[FLAG_SIZE]; //文件头的非凡编码,用于辨认是否为自定义文件 uint16_t _type; //数据类型标记 uint16_t _version; //文件版本,不压缩为1,压缩寄存为2} BlockHeader;typedef struct _RTBlockHeader : BlockHeader{ uint32_t _size; //数据条数 uint32_t _capacity; //数据容量 uint32_t _date; //交易日} RTBlockHeader;//tick数据数据块typedef struct _RTTickBlock : RTDayBlockHeader{ WTSTickStruct _ticks[0]; //tick序列} RTTickBlock;
下面的代码展现了实时
tick
数据存储模块的数据结构,文件头外面记录了以后数据的条数和以后映射的文件的数据容量,以及以后的交易日,文件后则跟随着间断的tick
数据结构。
交易日开始的时候,依据预设的容量,如1024
条tick
数据,计算一个初步的文件大小,而后将文件用mmap
映射到内存地址中,针对映射的内存地址做一个类型转换,就能够间接像拜访内存对象一样对文件进行对读写了。如果在运行的过程中,数据超过容量下限,则从新扩大文件大小,扩大的策略根本如std::vector
,每次成倍扩大,而后再从新映射即可。
其余如实时的1分钟线
、5分钟线
的文件构造,也是如上的数据结构,只有具体的数据的构造不同。历史高频行情数据采纳文件存储压缩后的二进制数据
typedef struct _BlockHeaderV2{ char _blk_flag[FLAG_SIZE]; //文件头的非凡编码,用于辨认是否为自定义文件 uint16_t _type; //数据类型标记 uint16_t _version; //文件版本,不压缩为1,压缩寄存为2 uint64_t _size; //压缩后的数据大小} BlockHeaderV2;//历史Tick数据V2typedef struct _HisTickBlockV2 : BlockHeaderV2{ char _data[0];} HisTickBlockV2;
下面的代码展现了历史
tick
数据存储的文件构造。除了和实时数据一样的头部以外,历史高频数据还有一个size
用于标记前面压缩当前的数据的大小,并在读取的时候进行数据大小的校验。校验通过当前,对数据进行解压,解压实现当前,依据type
指定的数据类型,将解压的数据做一个类型转换,就能失去一个间断区块的历史高频数据。历史低频行情数据反对文件压缩存储和
MYSQL
存储typedef struct _BlockHeaderV2{ char _blk_flag[FLAG_SIZE]; //文件头的非凡编码,用于辨认是否为自定义文件 uint16_t _type; //数据类型标记 uint16_t _version; //文件版本,不压缩为1,压缩寄存为2 uint64_t _size; //压缩后的数据大小} BlockHeaderV2;//历史K线数据V2typedef struct _HisKlineBlockV2 : BlockHeaderV2{ char _data[0];} HisKlineBlockV2;
如下面的代码所示,历史低频行情数据如K线数据,用文件存储的时候,文件构造的设计和历史高频数据是一样的。
然而低频数据也能够利用MYSQL
存储,存储的数据表格创立代码如下:CREATE TABLE `tb_kline_min5` ( `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, `exchange` VARCHAR(20) NOT NULL DEFAULT '' COLLATE 'utf8_general_ci', `code` VARCHAR(30) NOT NULL DEFAULT '' COLLATE 'utf8_general_ci', `date` INT(10) UNSIGNED NOT NULL DEFAULT '0', `time` INT(10) UNSIGNED NOT NULL DEFAULT '0', `open` DOUBLE(22,4) NOT NULL DEFAULT '0.0000', `high` DOUBLE(22,4) NOT NULL DEFAULT '0.0000', `low` DOUBLE(22,4) NOT NULL DEFAULT '0.0000', `close` DOUBLE(22,4) NOT NULL DEFAULT '0.0000', `volume` DOUBLE(22,6) UNSIGNED NOT NULL DEFAULT '0.000000', `turnover` DOUBLE(22,4) NOT NULL DEFAULT '0.0000', `interest` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0', `diff_interest` BIGINT(20) NOT NULL DEFAULT '0', `createtime` DATETIME NOT NULL DEFAULT current_timestamp(), `updatetime` DATETIME NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `exchange_code_date_time` (`exchange`, `code`, `date`, `time`) USING BTREE, INDEX `exchange_code` (`exchange`, `code`) USING BTREE)COMMENT='5分钟线表'COLLATE='utf8_general_ci'ENGINE=InnoDB;
交易数据全副存储在内存中,每次登录胜利当前,通过接口查问并重构内存中的数据,再定期做一个落地即可
{ "positions": [ { "code": "DCE.v.2105", "long": { "newvol": 0.0, "newavail": 0.0, "prevol": 2.0, "preavail": 2.0 }, "short": { "newvol": 0.0, "newavail": 0.0, "prevol": 0.0, "preavail": 0.0 } }, { "code": "SHFE.cu.2105", "long": { "newvol": 0.0, "newavail": 0.0, "prevol": 0.0, "preavail": 0.0 }, "short": { "newvol": 0.0, "newavail": 0.0, "prevol": 1.0, "preavail": 1.0 } } ], "funds": { "CNY": { "prebalance": 14530204.42, "balance": 14530204.42, "closeprofit": 0.0, "margin": 547022.7, "fee": 0.0, "available": 13983181.72, "deposit": 0.0, "withdraw": 0.0 } }}
下面展现了一个
simnow
账号接口拉取到的持仓和资金数据。此外还有成交数据和委托数据,根本格局如下:
成交明细:localid,date,time,code,action,volumn,price,tradeid,orderid32921200,20210108,1610088820000,DCE.y.2105,开多,1,8160, 211111, 54440545861050,20210111,1610347619000,SHFE.ru.2105,平今多,1,14040, 220809, 52138445861053,20210111,1610347619000,SHFE.ru.2105,开空,2,14040, 220810, 52138545861055,20210111,1610347619000,SHFE.hc.2105,平今多,6,4484, 220812, 52138745861054,20210111,1610347619000,CZCE.OI.2105,平多,2,10103, 220811, 52138645861056,20210111,1610347619000,SHFE.sp.2103,平今多,2,5962, 220813, 52138845861058,20210111,1610347619000,CZCE.TA.2105,开空,2,3912, 220815, 52139045861057,20210111,1610347619000,DCE.y.2105,平多,4,8082, 220814, 52138945861061,20210111,1610347619000,DCE.c.2105,平多,13,2838, 220816, 52139145861059,20210111,1610347619000,CZCE.FG.2105,平空,5,1794, 220817, 52139245861060,20210111,1610347619000,CZCE.MA.2105,平空,4,2331, 220818, 52139345861062,20210111,1610347619000,CZCE.RM.2105,平多,1,2988, 220819, 521394
委托明细:
localid,date,inserttime,code,action,volumn,traded,price,orderid,canceled,remark32921200,20210108,1610088820000,DCE.y.2105,开多,1,1,8162, 544405,FALSE,全副成交报单已提交45861050,20210111,1610347619000,SHFE.ru.2105,平多,1,1,14040, 521384,FALSE,全副成交报单已提交45861053,20210111,1610347619000,SHFE.ru.2105,开空,2,2,14040, 521385,FALSE,全副成交报单已提交45861055,20210111,1610347619000,SHFE.hc.2105,平多,6,6,4483, 521387,FALSE,全副成交报单已提交45861054,20210111,1610347619000,CZCE.OI.2105,平多,2,2,10103, 521386,FALSE,全副成交报单已提交45861056,20210111,1610347619000,SHFE.sp.2103,平多,2,2,5960, 521388,FALSE,全副成交报单已提交45861058,20210111,1610347619000,CZCE.TA.2105,开空,2,2,3910, 521390,FALSE,全副成交报单已提交
回测环境下,产生的平台数据都存在内存中,回测完结当前,实时输入到文件中
策略成交:code,time,direct,action,price,qty,tag,feeCFFEX.IF.HOT,201909100940,SHORT,OPEN,3959.6,1,entershort,27.32CFFEX.IF.HOT,201909191450,SHORT,CLOSE,3917,1,exitshort,27.03CFFEX.IF.HOT,201909201345,LONG,OPEN,3935,1,enterlong,27.15CFFEX.IF.HOT,201909201355,LONG,CLOSE,3929.6,1,exitlong,271.14CFFEX.IF.HOT,201909201400,SHORT,OPEN,3925,1,entershort,27.08CFFEX.IF.HOT,201909231435,SHORT,CLOSE,3878.6,1,exitshort,26.76
策略平仓:
code,direct,opentime,openprice,closetime,closeprice,qty,profit,totalprofit,entertag,exittagCFFEX.IF.HOT,SHORT,201909100940,3959.6,201909191450,3917,1,12780,12780,entershort,exitshortCFFEX.IF.HOT,LONG,201909201345,3935,201909201355,3929.6,1,-1620,11160,enterlong,exitlongCFFEX.IF.HOT,SHORT,201909201400,3925,201909231435,3878.6,1,13920,25080,entershort,exitshortCFFEX.IF.HOT,SHORT,201909241420,3904,201909251440,3888.2,1,4740,29820,entershort,exitshortCFFEX.IF.HOT,SHORT,201909251500,3875.6,201909271400,3858.8,1,5040,34860,entershort,exitshortCFFEX.IF.HOT,SHORT,201909300940,3850.4,201909300945,3858.2,1,-2340,32520,entershort,exitshortCFFEX.IF.HOT,SHORT,201909301000,3852.6,201910111130,3883.6,1,-9300,23220,entershort,exitshort
实盘环境下,产生的平台数据除了在内存中要保留以外,还要及时输入到文件中,并在下一次重启的时候,从新从文件中加载到内存中
{ "positions": [ { "code": "CFFEX.IF.HOT", "volumn": -5.0, "closeprofit": -929100.0000000036, "dynprofit": 89400.00000000056, "details": [ { "long": false, "price": 5806.6, "volumn": 5.0, "opentime": 202102181116, "opentdate": 20210218, "profit": 89400.00000000056, "maxprofit": 128400.00000000056, "maxloss": -3599.9999999994543, "opentag": "Q3_" } ] } ], "fund": { "total_profit": -929100.0000000036, "total_dynprofit": 89400.00000000056, "total_fees": 33604.200000000004, "tdate": 20210218 }, "signals": {}, "conditions": { "settime": 0, "items": {} }}
下面展现了策略实盘中的缓存数据,其中包含持仓数据、资金数据、信号列表以及条件单列表。
实盘数据处理框架
后面介绍了WonderTrader存储数据的机制,那么各种数据在WonderTrader中又是怎么样解决的呢?
1、行情数据读写拆散
行情数据的读写拆散指的是行情落地是一个过程,而行情应用又是另外一个过程。这是一个十分重要的机制,对于整个平台的架构都是一个要害的机制。
- 首先,读写拆散的机制,能够设计成1+N的数据提供机制,很不便就能向多个框架运行的实例提供数据反对。
- 其次,策略在运行过程中,很难防止人工干预程序的运行,不论是出于风控的须要,还是调仓的须要。如果行情数据读写在一个过程中,那么在人工干预的时候,很容易就会造成数据失落。而读写拆散的机制,能够防止对数据落地过程的操作,而减小数据失落的危险。
2、平台中的行情数据同步
笔者在跟一些敌人交换的过程中,常常会遇到同一个问题:on_bar
和on_schedule
有什么区别?为什么会有这样的问题呢?笔者对市面上很多量化平台都做了一个简略的调研,发现大多数平台只有on_bar
,所以一些敌人对WonderTrader的on_schedule
会体现出不了解的状况。
那么为什么WonderTrader会设计一个on_schedule
呢?
对于一个策略,可能会用到单个标的的多种周期的K线,也可能会用到多个标的雷同周期的K线。然而最新的行情数据达到的工夫有先有,有些不沉闷的标的,甚至很长时间都没有最新的快照过去。如果策略以on_bar
作为重算的触发事件,可能就会遇到某些K线还没有真正的完结,等策略响应完了,这些标的的最初一笔tick
才进来。回测的时候,策略必然用的是失常完结的K线,而实盘时,这样的景象是十分不敌对的,有时候甚至会造成十分坏的影响。
为了解决这样的问题,WonderTrader设计了一个机制,能够尽量保障在on_schedule
调用的时候,所有该当闭合的K线全副都曾经触发了on_bar
,从很大水平上防止了后面提到的状况产生。(极其状况下,因为延时抖动等各种起因,也无奈完全避免K线最初一笔tick
在策略重算过当前才收到的状况)这个机制的外围,还是依赖于tick
数据的工夫戳,对K线进行同步回调。
3、平台数据管理
后面也提到,平台数据次要指的是策略的数据,包含持仓数据、成交数据、信号数据、回合数据以及每日绩效数据。这些数据的治理,也遵循读写拆散的准则。
平台在运行的过程中一直的生成数据,并定时将数据写入到文件中。而wtpy提供的监控服务,作为平台数据的消费者,读取平台生产的数据,并向web
终端提供数据。整个过程,采纳非入侵式的形式获取数据,而不对平台减少额定的工作,不影响平台的执行效率。
结束语
本文对WonderTrader的数据处理机制的介绍就到此结束了,心愿可能对各位朋友有所启发。笔者程度无限,不免有错漏之处,还请各位朋友多多包涵斧正。下一篇,笔者将围绕信号执行拆散,来介绍WonderTrader的下单机制,望各位读者届时多多捧场。
最初再安利一下WonderTrader
WonderTrader旨在给各位量化从业人员提供更好的轮子,将技术相干的货色都封装在平台中,力求给策略研发带来更好的策略开发体验。
WonderTrader的github
地址:https://github.com/wondertrad...
WonderTrader官网地址:https://wondertrader.github.io
wtpy的github
地址:https://github.com/wondertrad...
市场有危险,投资需谨慎。以上陈说仅作为对于历史事件的回顾,不代表对将来的观点,同时不作为任何投资倡议。