但是一旦转入实战,你就会碰着各类障碍:回测中须要大量的数据,如果这些数据都从远程实时获取的话,会严重降落回测的速率;其次,一些数据源可能对你可用的数据量进行限定,这将导致回测中断乃至作废。
很显然,必须有一个高速确当地数据源,才能稳定地支撑我们的量化研究和交易。这个本地数据源缓存从远程得到的统统数据;如果我们须要最新数据,它也能自动去上游做事器获取。这样一来,无论是进行回测,还是实时交易就都能得到有力的支撑。
这一章,我们以大财主(Zillionare)的子项目Omega为例,来先容如何构建这样一个本地化的行情数据做事,并对干系技能栈的选择进行一些谈论。
供应数据本地化的开源项目不但Omega,比如QuantAxis,在github上得到了4k stars。但Omega的定位是,知足AI量化交易对数据存储的高速相应的需求。
Omega的定位是要对险些任何数据要求都能供应秒级以下的相应,比如取全市场1个月的分钟线数据,这样加上打算韶光,能够对分钟级别的旗子暗记搪塞宽裕。但纳秒级的高频交易被打消在外。这种级别的高频交易,其策略都是套利模型,拼的紧张是系统接入速率和性能,并不须要AI量化能力。而全体Zillionare的定位,是AI量化交易框架。
数据存储问题与其它框架不同,Omega把行情数据全部存入了Redis缓存,除此之外,没有其它落地的办法。
这样做的好处是显而易见的。在追求高速的路上,我们选择了最大略粗暴的办法。但第一个问题便是,这得须要多大的内存?
根据测试,将A股的全部股票(目前约4000支,我们在测试中仿照了5000支)的4年的日线数据存入Redis,也只须要750MB旁边的内存空间。如果精心设计存储构造和启用压缩,数据可能还有30%旁边的压缩空间。
当然,如果你要存储4年的分钟线的话,这将须要近180G的存储空间;如果要存储Tick级的数据则会更多。但是对AI量化交易而言,跨度为4年的分钟数据并不比跨度1个月的分钟数据包含更多的信息–至少从K线图上来看,它们的pattern都是自重复的。以是我疑惑存储1个月以上的分钟数据这种需求是否真的存在。
眼见为实。下面,我们来进行一个测试,看看存储4年的A股全市场日线数据,须要多永劫光和多大内存:
测试结果显示,假设我们以hashmap的办法来存储5000支个股4年旁边的日线数据,只须要花大约580MB的内存空间;而存储这么多数据,也只须要240秒,也便是4分钟旁边的韶光。测试机器利用了Surface Pro 4(2016年产),在wsl内安装的redis。如果是台式机或者做事器,这个韶光将会更快。
与这里的测试代码中,我们给每根k线多存了一个韶光帧字段,以是同样的数据量,大概须要750MB的内存空间。在韶光上,这个方案的紧张韶光开销花在对K线数据的串行化上(json.dumps),理论上可以利用orjson来进一步加速。
大财主利用的是异步多进程读写方案,以是实际上读写这么多数据,花的韶光还要比4分钟短很多,取决于你利用多少个进程。
这个方案与其它竞品比较,在韶光和内存占用上都显示出较大的上风。 如果须要Tick级的数据,须要利用列存储格式加上内存映射文件,再通过专门的数据做事器来供应。这是一个企业级的需求。
利用Redis来存放数据并非只有好处。上述存储方案也导致无法直接进行"本日收盘价小于3元的个股"这样的筛选。在AI量化交易中,这样的查询实行次数并不会频繁。我们写几个server script就好了。
财务数据的查询频率会比行情数据低很多,但须要支持更繁芜的查询,因此我们将其存放在数据库中。大财主选择的数据库是Postgres,它的性能是开源产品中压倒一切的。
数据的内存表示模型当行情数据加载到Python进程时,其它框架一样平常选择利用Pandas DataFrame。这对有速率哀求的量化工程来说是个灾害。
大财主选择Numpy数组作为险些所有数据的内存表示模型。Numpy数组比DataFrame占用内存更小,在AI量化领域常用的打算领域,Numpy的打算性能要快10到数十倍,如果须要利用第三方库进行技能剖析,这些第三方库多数也哀求利用Numpy数组来通报参数(比如ta-lib)。我们来看下面的例子:
上面的测试结果显示,在slicing这个操作上,Numpy要快近30倍,在均值、最大值这些数值打算上,Numpy要快近10倍。
从内存上看,彷佛Pandas只多用了128字节,但实际上并不是。Pandas在构建DataFrame时,每每须要多拷贝一两次Numpy数组。这种内存占用,从Pandas的角度来看,它已经开释了,但垃圾回收被推迟进行,在操作系统层面仍旧能看到内存占用。这种占用,会在物理内存紧张时引起大量的页交流,从而显著降落程序性能。
此外,在量化交易领域,Numpy比Pandas DataFrame还有一些不易察觉的易用性上风。比如,DataFrame不支持下面的利用方法:
但设想df是行情数据,我们常常会须要取行情数据的末了几条记录。这在Numpy表示中很方便;但在DataFrame表示中,你须要先把末了数据的索引算出来,然后才能取这个数据。
当然,DataFrame的替代都并不仅仅只有Numpy,Vaex和xarray,乃至dask对付Python程序员来说,都是常见的选择。在大财主里我们选择了Numpy,由于我们只须要Numpy来暂存少量数据(几k到几十兆),进行一些大略而高效的打算和数据交流。从这一点来说,前面提到的这些工具都是牛刀杀鸡了,反而会产生各种适配问题。
异步多进程分布式利用Redis和Numpy当然不能办理一个高性能的数据做事框架须要办理的全部问题。作为供应者,数据做事器必须具有高并发、低延迟相应的能力;同样,如果它不具有高并发向外要求(要求上游行情做事器)的能力,也就无法快速供应实时数据。
Omega采取了异步、多进程和分布式方案来办理知足高并发、低延迟相应的需求。下面的图展示了它的支配视图:
Omgea在行情数据获取上采取的是多进程分布式架构。Omega并不限定你利用某种特定的数据源,相反,只要有adaptor支持,任何上游数据源都可以利用。Omega的框架许可你利用多个异种数据源、或者同一数据源多个session并发。这样一来,限定获取实时行情速率的,就完备取决于你利用了多少session,或者上游做事器的能力如何了。
从上图可以看出,数据做事器(Omega)由一组Sanic做事器构成,通过前置的Nginx向数据消费者(比如策略、管理界面)供应数据。Sanic做事器是目前Python领域内最快的Web Framework之一(今年被Vibora夺走第一)。但从人气上看,目前仍胜Vibora一筹。
在Omega内部,数据读写事情被独立成Omicron库;无论是Omega吸收到数据之后的保存,还是消费者要求数据,都经由Omicron完成。这样,一些数据编码、解码的事情就都转移给Omicron,如果消费者须要的数据不在缓存中,也由Omicron来向Omega实时要求,消费者不须要关注这些细节。
当Omega通过Sanic返回消费者要求的数据时,利用二进制协议,返回通过pickle串行化的二进制数据。只管Sanic足够快,但HTTP作为一种面向运用层的协议,仍旧比基于TCP的rpc调用要慢。在企业版,我们将供应基于rpc的方案。
由于每次读写Redis的数据量可能少到几十个字节,也可能多达上兆字节(比如,取全市场一年的日线),因此我们利用了aioredis来读写缓存,以避免某次大数据量的读写壅塞其它事务的实行。
同样地,在读写Postgres数据库时,我们也利用了异步库asyncpg,以及在之上供应异步ORM的gino。在高性能运用中利用ORM显然是不明智的,把稳gino的ORM并不是传统的ORM,它只是在SQLAlchemy core(即供应SQL语句构建功能)之上的一个支持异步的封装。当SQLAlchemy 1.4发布之后,我们就可以直接在asyncpg中利用SQLAlchemy core,那时候可能也不再须要gino。
在Omega中,分布式紧张表示在行情数据同步上。如果设定了对全市场所有证券的行情数据进行同步,假设共有5000支,那么Omega会将它分解成5000个子任务,放入一个在Redis中暂存的行列步队,并且关照事情者进程开始同步。这些事情者可以分布在不同的机器上。我曾经希望能有一个框架来完成任务分解和分布式实行。但是celery并不支持异步;Dask对纯打算进程友好,对涉及IO的进程彷佛并不随意马虎编程(或者只是我还不熟习而已)。以是终极采纳了这种自酿的办法。
此外,消费者进程通过Omicron向Omega要求数据时,这里并不是分布式打算,而是负载均衡技能。负载均衡通过Nginx来配置。对初学者或者不追求高性能的运用来说,这个配置可以省略。
全体Omega的技能栈如下:
关于Zillionare和Omega
Zillionare是由一系列子项目组成AI量化框架。Omega是个中的数据做事部分。项目目前托管在Github上。