系统设计:股票数据管理系统
介绍
对于量化系统来说,高效管理股票数据是非常重要的。本文设计了一个股票数据管理系统。数据频率是日频数据。
设计功能:
- 完整保存历年股票数据
- 保证数据完整性,做到不重、不漏
- 高效、高性能
- 低内存占用,能在低端笔记本上运行
该系统主要用于历史数据分析、模拟交易,因此首先考虑后复权数据的稳妥保存。
看盘属于次要需求,看盘数据不进行严谨的数据保存,随看随取。
数据存储方式
数据量预估
假设股票有 4000 支,每支股票每日行情用一行描述,每交易日数据量是 4k 行。
10 交易日是 4w 行。假设一年 200 交易日,一年数据量是 80w 行。十年数据量是 800w 行。20 年是 1.6kw 行。
以数据库单表维度来看,这数据量还是不小的。
存储方式选型
按照格式可以分为:文件存储和数据库存储。
由于股票数据是时间序列,不存在其它关系,因此用文件存储比较合适。
按照是否本地又分为远程保存和本地保存。由于会涉及到大量的回测,因此采用本地存储,将性能最大化。
本地存储方式
数据存储的问题还是比较复杂的。即使确定了使用本地文件存储,还有一系列问题:
- 使用 csv 还是 HDF5?还是 SQLite?
- 单文件保存全量数据还是分开存
- 分开怎么存?
- 衍生的指标、信号怎么缓存?
由此可见,面对的问题大着呢。
日期格式
有一个小细节,关于日期字符串格式,一开始我使用 "%Y-%m-%d",因为看着比较舒服。
后来我发现 "%Y%m%d" 更好,能够方便地转换成整数比大小。
数据获取
数据保存的问题暂时一放,接下来思考数据如何获取?
常见的股票数据获取有两种方式:
- 个股维度:针对一只股票,拉取其一段时间的数据
- 时间切片维度:针对某一天,拉取所有股票当日数据
按照时间切片维度拉取数据,是效率最高的方式。但是这个维度有点反常识,因为常识中还是习惯个股时间序列的维度。
时间切片数据

时间序列的示意图如右图所示。
时间切片数据有一个重要作用,就是作为当日行情的唯一事实。
比如说对于一个个股,当日没有数据,有可能是停盘了,也可能是没有拉取该日数据。如何辨别呢?只要看是否有当日的切片数据,如果有,说明是停盘了,如果没有,说明是没拉数据。(当然,不排除拉下来的数据是停盘的)。
后续很多设计,都基于时间切片的唯一事实。
切片数据的保存
对切片数据的保存还是比较简单直观的。
通常获取下来是 Pandas DataFrame 格式,直接按照日期保存 csv 文件即可。
切片数据重组
实际中使用的是基于个股的时间序列,这就涉及到对切片数据的重组问题。具体如下图所示:
这里存在一个数据量问题。时间切片包含 4000+ 股票,假设我要加载一支股票一年的数据,读取时间切片,实际上把 4000+ 支股票一年的数据都加载进了内存!
这个问题靠堆硬件是可以解决的:找一台内存大的电脑,索性把全量股票数据载入,然后这台机器持续运行着,以增量方式每日更新数据。这是一个解决问题的办法,而且是一个好办法。简单粗暴,KISS,便于维护。
可是我比较抠门,我希望这个系统能在我花 ¥2.5k 买的轻薄本上运行(8GB 内存,256GB 硬盘,实际可用内存是 7GB,集显还吃了我 1GB!)。
数据量问题
内存不足的情况下,如何解决大数据量问题呢?通俗的说法就是多倒腾。
这就跟家居收纳是一个道理。小房间里收纳的物品多了,在取用的时候,难免要像华容道那样,来回倒腾。
所谓倒腾,指要预先创建一些中间文件。比如,预先根据切片文件,生成个股维度时间序列数据,进行保存。这样未来取用的时候,直接加载该数据即可。
2022-03-30 更新:
- 首先要确定一个问题,使用 Pandas 加载全量股票数据,需要多大内存。
- 全量数据分为几个维度:
- 拉取历史数据是渐进的,不会立即拉完,可能长期内都不会有几十年的数据
- 新增数据也是渐进的,每天都会增长
vaex 进一步优化内存
如果使用 Pandas,内存还有进一步优化的空间。因为 Pandas 默认会把全量数据加载到内存中,内存开销还是比较大。
vaex 是一个类似 Pandas 的 Python 库,它的特点是数据是懒加载的。vaex 会把磁盘文件映射到内存中(并不实际加载),只在真正用到时惰性求知,因此内存占用量要更小。
假设说我使用 vaex 加载了全量数据,但是没有使用这些数据,那么没有一条行情被真正加载到内存当中。这就是 vaex 的魅力。
技术选型
在这一节中考虑最终的技术选型。
初期我还是会使用 Pandas,因为这是目前我掌握的技术,也是网上资料比较多的技术。并且采用全量加载方式,并记录实际的数据量。
随着后续数据量的加大,如果内存缺失不够用了,再考虑优化,比如使用 vaex。
这也是遵循 K.I.S.S,不要过早优化。
动态序列生成
HDF5 切片数据
每日的切片数据,基于 vaex,使用 HDF5 进行保存。需要注意的是,vaex 的 HDF5 格式跟 Pandas 保存的好像不兼容。
HDF5 相对于 CSV,由于是二进制,更加节省空间。不过对于我 256GB 硬盘来说,磁盘空间倒不成问题。
动态拼装
有了 vaex,就不需要像 Pandas 那样预生成个股时间序列保存中间结果了。而是可以直接读取切片后再拼装。
这里只有磁盘加载的开销。由于采用 SSD,这部分性能也不错。
这里有一个问题,要不要把切片预先合成一个整体,这样性能会更好一些(只需加载一次,而不用多次加载)。但带来的问题是,每次更新数据,都要读取历史数据然后合并一次,由于数据更新是日频操作,读写开销是比较大的。
需要考虑到这样一个事实:数据是有冷热之分的,几十年前的数据访问频率低,近期数据访问频率高。
分片数据的加载,也不是一股脑把几十年的都加载进来,而是采用按需加载,只会加载所需的时间片。
因此,实际上对于时间片的读取量并没有那么大,按照分片保存已经足够了,反倒是每天合并一次,由于没想清楚,搞得特别重。
交易日历索引
哪些时间片加载进来了,哪些没加载,需要有一个模块进行记录。
通俗来说,这就是一个日期的列表,加载上来的时间片打个勾。
它的作用在于,如果我要加载某支股票一段时间的数据,首先遍历该索引,确保这段时间里的时间片都加载进来了。
起到保障数据完整性的作用。
该类命名为 StockData,数据结构如下:
其中包含两个数据结构:
- 连续日期序列:一个日期的列表,日期需要是连续的
- 日期更新状态字典:改日期的数据,是否被加载到内存中
- DataFrame:没有画在图中,股票数据内容
从中可知:StockData 中的结构,与 Pandas DataFrame 是一一对应的关系
StockData 只负责数据序列和拉取状态维护,具体加载和请求操作由专门的类进行负责:
响应式惰性求值
这样,就行形成一条惰性求值的响应链:
指标
行情数据的获取问题解决了,但是问题并没有结束。
技术指标是研判行情的重要因素。而技术指标是基于行情序列生成的。
动态生成 vs 静态计算
技术指标有成百上千种,如果说 20 年的行情数据是 1.6kw 行,那么指标数据是上亿了。
上亿数据量也不是说不能存,还是要看是算好了存好呢,还是动态计算好。
其实这个问题跟要不要把切片预先合成一个整体有点类似,如果要把技术指标算好了预先存在本地,那么每次更新行情都要重新计算一遍指标。这个计算量还是比较大的,尤其是指标多了以后,每次拉全盘行情,运算量都不小。
同时累计数据写入量也很大,当然有优化的空间,但是实际上相当于对于行情数据的二级磁盘缓存,保持两者的一致性需要引入一定的复杂度。
最关键的问题是,很多股票、很多时候的指标,我压根就不会去看。为了根本不看的东西,给自己找这么多麻烦干嘛。
当然,在进行全盘策略回测的时候还是会用到的,真到那时候,用一个时间窗动态计算就好了,那点时间开销还是能接受的。
技术指标大多数计算公式都不复杂,我花 ¥2.5k 买的这台轻薄本,CPU 是 AMD Ryzen 3 5300U,性能我非常有信心,完全 Hold 得住。
复杂指标
动态计算也有例外,对于有些复杂指标,比如通过深度学习学出来的指标,还是得静态计算。
这种比较复杂的指标,本身非常重,只能静态计算,因此投入产出比合适的话,值得针对它引入这些复杂度。
这里还有一个问题,假设使用深度学习或者强化学习,我的小轻薄本是彻底吃不消了(没显卡),得转战我的 PC 机,这涉及到一个多机协作的问题。
其实这个问题也比较简单,这里简单记录一下想法,就不展开了:
- 时间切片数据:可以使用 rsync 同步到 NAS 上,然后 PC 机进行同步
- 两台机器的通信可以使用 RPC(Python 标准库里就带 RPC 库)
- 还是使用 NAS 中转数据,PC 机算好数据之后,存到 NAS 上,轻薄本再同步到本地
前向数据加载
有些指标需要基于更早的数据计算,比如 200 日均线,顾名思义,需要往前 200 个交易日的数据才能算出。
因此,在计算指标的时候,需要加上一个环节,前向数据加载,加载到足够的数据再进行计算。
整个性求值的响应链延长如下:
需要注意的是:
- 前向数据加载:也只是读取更多切片文件的索引,并不会带来实际大内存占用,多的是频繁的文件打开读取时间
- 序列1指标计算的过程,实际上向前延展了序列1(以 200 日均线为例),这也是惰性求值的,这部分开销是可接受的
信号
信号与指标不是有关联,但不是一类事务。
指标是连续的时间序列。信号是事件,比如常见的“金叉”“死叉”。
对于信号,也需要考虑动态生成 vs 静态计算的问题。这里延续的思路也是一样的,简单的就动态算,复杂的就静态生成。
信号比指标更加复杂,因为它即依赖于行情,又依赖与指标。
指标日历索引
序列依赖于分片,在根据时间范围生成序列的时候,根据日历索引来保障分片的完整性。
同理,信号依赖指标,根据时间范围生成信号的时候,也要建立一个指标索引,根据指标索引来保障指标的完整性。
前向指标加载
同理,信号可能会基于时间范围外的指标,这时候前面计算的指标不够用了,因此对于指标来说,也有一个前向指标加载的过程。
而这个过程,有可能会包含嵌套的前向数据加载过程。
整个过程如下:
省掉前向加载
两次前向加载,实际上是非常烦人的事情,对导致系统的复杂度大幅升高:
- 主要还是基于切片数量按需载入的原则
- 只有在切片数量不够用的时候,才加载新的切片
- 不过这里需要注意的是:使用 vaex 加载切片不会载入实际数据,内存开销很小
从这一角度来看,我有点让步,开始考虑在系统启动时加载全量切片,几千个文件!
一次性读取几千个文件还是挺重的一个操作,如果一个系统是 7*24 小时运行的,也就罢了,好几个月加载不了一会。
关键我每天带着轻薄本跑来跑去,有的时候在通勤路上,只能用 MicroPC 掌上电脑,需要频繁启动、关闭系统,每次都这么个读法,有点烦。当然,这也有一种解决方法,就是电脑不要关机,每次都休眠或者挂起,这倒也是个办法……办法总比问题多。
时间片合并与单片并存
我不太想用休眠的方法,不够省心。我想了一种将合并和分片相结合的方式:
- 可以有选择地,定期将时间片进行合并,形成大片
- 大片和单片并存
这样:
- 每天拉数据还是单片
- 过往数据已经形成了一大片
- 我只需要定期的执行一下片合并,形成新的一大片即可
限制:中间一个大片
理想情况下,大片跟小片可以任意穿插,这样的话维护起来又比较烦。
施加一个限制,系统能够大大简化:只允许中间存在一个大片,两头可以插单片。
这样,追加老数据的时候,可以沿着时间轴往前插单片,追加新数据的时候则往后插,中间如果有大片,不允许有空洞。
这样,大片总能够沿着中心位置,向两头增长。数据维护起来也简单。
整体过程如下:
总结
股票数据管理系统只是量化系统中基础的一个部分,量化系统整体还是比较复杂的。
这套管理系统相较于网上开源的一些系统来说,对数据的管理更加完善一些,对电脑的内存要求也低一些,普通的低端笔记本就能跑大规模策略。
这很大一部分功劳来自于 vaex 先进的内存映射和惰性求值系统。
不过话说回来,如果堆机器能搞定的事情,还是不要搞得太复杂。如果我有一个固定的工作环境,有 7×24 运行的高性能服务器,我会选择就用 Pandas,一次性把数据都怼进去完事。
这套系统最大的魅力,就是我再地铁或打车的时候,拿着 MicroPC,能够时刻对各种交易策略进行研究,对于社畜比较友好。