乐趣区

关于量化: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…


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

退出移动版