金融回测系统开发经验分享

1. 背景

平台对外提供了很多股票买卖策略,在策略上架之前,需要对策略进行回测以验证策略效果,传统的流程是产品同学提出策略算法,开发同学写回测代码,跑回测,产品同学再基于回测结果进行算法优化或者参数调优。整个流程如图1,通常耗时1~2周,投入2个人力。存在耗时长,迭代慢问题。除此之外,用户侧也希望平台能够提供回测功能,基于平台的数据,开发与测试用户自己的策略。因此业务侧打算开发一个回测系统,既能够内部使用,提升研发效率,也能够提供给用户使用,提升用户留存。开发时间从25年11月开始,历时1个月左右上线。
图1

2. 任务

实现回测系统,系统要求如下:

2.1. 低门槛

即便是小白用户,也能够随便创建策略,无需具备金融、编程等领域知识。

2.2. 高自由度

要求支持自定义策略,任意算法。

2.3. 高性能

回测系统要尽可能快速给出回测结果。

2.4 高准确性

回测结果要稳定可靠,不能多次运行结果不一致。

3. 难点与解决方案

3.1 难点

3.1.1 如何实现低门槛、高自由度、高准确性的回测系统?

3.1.2 回测如何实现高性能?

3.2 解决方案

在开发回测系统之前,我有一些回测程序开发经验,主要是对我们平台的策略进行回测,这些策略比较复杂,涉及十几个算法数百个指标。对代码实现准确性要求很高。另外回测程序往往需要反复的调算法、调参数、取数据、计算、分析,整个过程冗长、繁琐、重复,效率很低。如何实现一套高扩展性、高自由度、高准确性的回测系统确实是一个难题。没啥头绪。于是先去调研了一下业内常用的回测系统实现方案:

3.2.1 方案调研

3.2.1.1. 方案一 固定模板+动态参数

最早的方案就是这种,平台预先设置几种常用回测模板,用户可以调整回测参数,点击一键运行。

优点:

  1. 回测结果准确性强,算法经过迭代优化,不会出错。

缺点:

  1. 不灵活,自由度差。回测模板固定,无法满足用户自定义的策略。

  2. 门槛高,需要用户对回测算法、金融术语有理解。

3.2.1.2. 方案二 自定义脚本(GPL,General-Purpose Language,通用编程语言)

这种方案是用户可以自定义回测脚本,平台提供算力和数据。

优点:

  1. 最灵活,理论上支持任意策略。

缺点:

  1. 门槛高,需要用户具备编码、金融知识。

  2. 迭代慢,需要人工分析、进行参数调优。

3.2.1.3. 方案三 DSL (Domain-Specific Language,领域特定语言)

这种方案是在方案二的基础上进行了抽象,降低用户编码难度。比如下面这个例子,

在价格突破20日均线且RSI14低于30的时候买入。

如果使用方案二,需要写一个均线函数、RSI函数,然后写一个主函数在实现。

如果使用方案三,只需要 close > MA(20) and RSI(14) < 30,类似于伪代码。

方案二需要用户能够看懂代码,并进行debug,方案三则只需要用户看懂伪代码即可,至于伪代码的具体实现,则无需关心。显著降低用户编码门槛。

优点:

  1. 灵活度比较高,能够满足常见的策略实现,但对于一些特别复杂的策略,还是有些弱。

  2. 准确性高,算法都是预先优化调试好的,不会存在bug。

缺点:

  1. 实现复杂,DSL的运行需要实现一个编译器,具备词法分析、语法分析、生成AST(抽象语法树)、AST执行引擎,实现难度大。

  2. 门槛还是比较高,虽然用户无需理解代码,但还是需要掌握基本的金融术语,如RSI, MACD等等。

3.2.1.4. 方案四 LLM+GPL

方案四是在方案二的基础上进行了优化,使用LLM进行代码生成,用户只需自然语言描述即可,极大降低用户编码门槛。另外灵活性也比较高,理论上支持任意策略。但缺点也很明显,LLM生成质量不可控,对于复杂策略生成的代码质量可靠性不足。我们内部初期就曾尝试过使用LLM进行回测策略代码生成,结论是完全不可以生产使用。对于简单的策略,成功率能够达到50%,但是一旦涉及到复杂策略,生成的代码完全不可靠、结果不可控、不可调试。

优点:

  1. 低门槛,用户只需自然语言描述即可生成策略代码。

  2. 灵活度高,理论上LLM可以生成任何策略代码。

缺点:

  1. 质量不可控,LLM生成代码质量不可控,无法判断对错。另外,对于特别复杂的策略,往往涉及到数千行代码,这种场景验证与调试就是一个噩梦。

我们的设计目标是既要满足低门槛,又要满足高灵活度、确定性。典型的既要又要。在计算机领域,有一个经典论断, "没有银弹"(No Silver Bullet), 即不存在单一的技术,能够一劳永逸的解决所有问题。这并不意味着问题无法解决。当出现这种"既要又要"的时候,确实存在一个经典的解法,"混合策略"。

混合策略指结合多个方法的优点来解决同一个问题。比如在计算机领域,使用多级缓存(cpu片上缓存、内存、磁盘) 解决性能与成本问题,又比如Redis 混合AOF(既结合了RDB恢复快、占用空间小的优点,又结合了AOF实时性强的优点),其它还有很多,如feeds流系统设计中的推拉结合等等。结合我们的业务场景和业内实现,想到第五种方案:

3.2.1.5. 方案五 LLM+DSL混合方案

在我们这个场景,既然要同时实现高灵活度、低门槛、高确定性。那就可以将LLM、DSL结合起来,LLM提供高灵活性、低门槛,DSL提供确定性。

这种方案的优点是及利用了LLM生成的灵活性,低门槛,又利用了DSL确定性。从而能够满足业务要求,缺点是实现复杂,需要实现一个DSL编译器,如果在没有智能编码助手的时代,开发一个编译器难度很大,起码要耗时2个月起步,但有了Cursor或者Claude之后,仅仅需要几天时间就能够搞定。

这里简单说一下DSL的设计,我们预先实现了150+种算法, 如RSI, MACD, MA等等,DSL里直接调用即可,无需LLM从零开始生成,极大降低LLM出错的概率,同时确保了算法的准确性。另外对于系统不支持的算法,DSL也提供了基本的运算符,包括加减乘除、逻辑运算符等等,即便用户自创了一个新的算法,这套系统也能够利用基本的运算符表达式来生成对应的函数,就像搭积木一样。

关于DSL编译器,这里使用Cursor 来生成,功能包括词法分析、语法分析、AST生成、AST执行引擎等基本功能,AST执行引擎的实现原理是基础的树遍历解释器方案。

就以之前的例子(在价格突破20日均线且RSI14低于30的时候买入。)来说,LLM生成如下配置

bash 复制代码
Strategy:
    final_result:
        close > MA(20) and RSI(14) < 30

AST经过词法、语法分析之后,生成的AST如下
图2

AST执行引擎会自底向上递归执行表达式,最终返回一个true or false, 即买卖信号。

通过LLM+DSL混合方式实现了低门槛、高灵活、高准确性的业务要求。

3.2.2 实现高性能

下一个问题是如何实现高性能。

回测程序执行时间由以下几个部分决定

Ticker总数、回测时间段、回测K线粒度、算法复杂度。

对于单个ticker的回测执行过程如下

取Ticker行情数据 -> 计算 -> 分析

Ticker越多,回测周期越长、K线粒度越细,所需要的计算、存储资源越多。这里的时间主要由两部分组成,数据读取+数据计算。

如果要对标普500成分股回测一年min1(1分钟)的K线:

K线总数=500*250*390=48.75M

单根K线(OHLCV,ID,Timestamp,Ticker), 大小约为 20*5 + 8*2 + 16 = 132Byte

本次回测需要读取的总数据量再 48.75M*132Byte = 6G

仅单次任务就需要从DB读取48.75M条记录,占用内存空间6G,耗费资源过大。因为我们使用MySQL存储K线数据,面对如此海量的数据读取,直接读取MySQL肯定是不行的。另外,我们单个节点的内存上限是16G,即便全部运行回测,也无法同时执行多个回测任务。因此需要对数据读取和计算进行大量优化。

3.2.2.1 数据读取优化

针对数据读取,采用的优化方案包括,多级缓存+数据压缩。这里的多级缓存包含文件缓存+内存缓存。缓存的内容是使用zstd压缩后的行情数据。缓存服务启动后,会从数据库中读取原始数据,经过pb编码之后,再经过zstd压缩写入到本地文件中。当接口请求的时候,先读取内存换成,缓存不命中再读取文件缓存。如果文件缓存也没有,则通过singleflight防击穿机制从数据库中加载。之后再异步pb编码和zstd压缩,更新缓存。

以AAPL为例,不压缩的情况下,5年日线占用的空间为

5*250*132Byte = 165K, 经过zstd压缩编码后只需要38.2K左右, 压缩率为(38.2*100/165)=23.2%。

美股5年日线原始大小为10K*5*250*132Byte=18.8M, 这个数据全量缓存到内存里完全无压力。美股1年日内min1原始大小为10K*250*390*132Byte=1.44G。日内数据量要远远超过日间数据。业务侧需要缓存min1, min5, min15, min30, 1h, 2h, 4h, 1d, 1wk. 如果不做任何优化处理,单独缓存这些数据就需要一个ec2 节点,成本比较高。结合实际的使用场景,我们仅针对热门股票进行缓存,比如标普500, 纳斯达克 100,以及市值前1000的股票。

总的缓存标的不超过2000个,整个文件缓存占用空间大小约100MB。

3.2.2.2 计算优化

接下来说一说如何提升计算速度,最简单的方法就是并行计算,创建多个协程并行执行,比如同时回测1000个Ticker, 开N路协程提升N倍,这里为了避免单个用户占用过多资源,会限制单个任务的并发数。除此之外,回测引擎也针对计算做了优化,很多算法使用到相同的指标,比如MA, RSI, KDJ等等。这里会缓存指标结果,如果多个算法使用到相同的指标,则可以直接复用。还有一点,业务层面做了几点保护措施,一个是限制了单次回测Ticker总数,另一个是针对不同时间粒度所能够获取的时间段也做了限制,如日线可以获取5年,min1 最多提供半年。目标是减少回测的计算量。

整体设计流程图如下

图3

4. 结果

  1. 缓存命中率93%,基本上不对数据库造成任何压力。

  2. 回测任务平均执行时长27s,小型回测任务5秒内返回结果。

上线后效果还不错,无论是执行效率、执行时间、执行结果准确性、策略自由度各方面都满足预期。

5. 收获

5.1 多用AI编码工具,要是没有Cursor或者Claude,人工手搓一个编译器不知道要耗时多久。

5.2 面对既要又要的场景,考虑使用混合策略。

相关推荐
东东不邪2 年前
如何从零开始设计一套⌈中性策略回测系统⌋?
量化交易·回测系统