关于量化:WonderTrader架构详解之二从数据说起

前言

  《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最终也是采纳的文件存储的形式,尽管两头兜了一个大圈才绕回来。

  • 关系数据库

    关系数据库,作为数据存储的传统主力,始终占用一席之地。关系数据库,如MYSQLMSSQLOracle等,对于结构化的数据十分敌对,尽管读写效率不如文件存储,因为要建设索引,还要引入额定的空间占用,然而对于大部分低频数据的存储还是可能满足的。数据库存储有一个最好的益处就是个别数据库都有可视化管理工具,十分不便对数据进行治理。如果要搭建一个投研平台,要不便团队外部成员查看数据的话,那么关系数据库会是一个不错的抉择。WonderTrader的历史数据也反对MYSQL存储的形式。

  • 分布式数据库

    分布式数据库是时下大数据浪潮下的宠儿。笔者之前在一家量化私募工作的时候,专门调研过过后比拟支流的一些分布式数据库,包含HadoopCassandraMysql集群等。笔者认为,分布式数据库存储的实现和关系型数据库没有太大的差异,而分布式数据库的外围在于数据的安全性(有备份)和事务处理的并发性。对于量化平台来说,数据存储不会有特地简单的业务逻辑,所以不必放心分布式事务这方面的问题,外围关注的点还是在于查问数据能够在多个节点并发执行,从而提高效率。笔者认为,分布式数据库是比拟适宜较大的量化团队或者钻研团队应用的,因为团队成员多,各类数据的总量也会十分微小,分布式数据库可能轻松胜任这样对的利用场景。

  此外,目前还风行一种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数据结构。
    交易日开始的时候,依据预设的容量,如1024tick数据,计算一个初步的文件大小,而后将文件用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数据V2
    typedef 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线数据V2
    typedef 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,orderid
    32921200,20210108,1610088820000,DCE.y.2105,开多,1,8160,      211111,      544405
    45861050,20210111,1610347619000,SHFE.ru.2105,平今多,1,14040,      220809,      521384
    45861053,20210111,1610347619000,SHFE.ru.2105,开空,2,14040,      220810,      521385
    45861055,20210111,1610347619000,SHFE.hc.2105,平今多,6,4484,      220812,      521387
    45861054,20210111,1610347619000,CZCE.OI.2105,平多,2,10103,      220811,      521386
    45861056,20210111,1610347619000,SHFE.sp.2103,平今多,2,5962,      220813,      521388
    45861058,20210111,1610347619000,CZCE.TA.2105,开空,2,3912,      220815,      521390
    45861057,20210111,1610347619000,DCE.y.2105,平多,4,8082,      220814,      521389
    45861061,20210111,1610347619000,DCE.c.2105,平多,13,2838,      220816,      521391
    45861059,20210111,1610347619000,CZCE.FG.2105,平空,5,1794,      220817,      521392
    45861060,20210111,1610347619000,CZCE.MA.2105,平空,4,2331,      220818,      521393
    45861062,20210111,1610347619000,CZCE.RM.2105,平多,1,2988,      220819,      521394

    委托明细:

    localid,date,inserttime,code,action,volumn,traded,price,orderid,canceled,remark
    32921200,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,fee
    CFFEX.IF.HOT,201909100940,SHORT,OPEN,3959.6,1,entershort,27.32
    CFFEX.IF.HOT,201909191450,SHORT,CLOSE,3917,1,exitshort,27.03
    CFFEX.IF.HOT,201909201345,LONG,OPEN,3935,1,enterlong,27.15
    CFFEX.IF.HOT,201909201355,LONG,CLOSE,3929.6,1,exitlong,271.14
    CFFEX.IF.HOT,201909201400,SHORT,OPEN,3925,1,entershort,27.08
    CFFEX.IF.HOT,201909231435,SHORT,CLOSE,3878.6,1,exitshort,26.76

    策略平仓:

    code,direct,opentime,openprice,closetime,closeprice,qty,profit,totalprofit,entertag,exittag
    CFFEX.IF.HOT,SHORT,201909100940,3959.6,201909191450,3917,1,12780,12780,entershort,exitshort
    CFFEX.IF.HOT,LONG,201909201345,3935,201909201355,3929.6,1,-1620,11160,enterlong,exitlong
    CFFEX.IF.HOT,SHORT,201909201400,3925,201909231435,3878.6,1,13920,25080,entershort,exitshort
    CFFEX.IF.HOT,SHORT,201909241420,3904,201909251440,3888.2,1,4740,29820,entershort,exitshort
    CFFEX.IF.HOT,SHORT,201909251500,3875.6,201909271400,3858.8,1,5040,34860,entershort,exitshort
    CFFEX.IF.HOT,SHORT,201909300940,3850.4,201909300945,3858.2,1,-2340,32520,entershort,exitshort
    CFFEX.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_baron_schedule有什么区别?为什么会有这样的问题呢?笔者对市面上很多量化平台都做了一个简略的调研,发现大多数平台只有on_bar,所以一些敌人对WonderTraderon_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旨在给各位量化从业人员提供更好的轮子,将技术相干的货色都封装在平台中,力求给策略研发带来更好的策略开发体验。

WonderTradergithub地址:https://github.com/wondertrad…

WonderTrader官网地址:https://wondertrader.github.io

wtpygithub地址:https://github.com/wondertrad…


市场有危险,投资需谨慎。以上陈说仅作为对于历史事件的回顾,不代表对将来的观点,同时不作为任何投资倡议。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理