1. 这次架构要解决什么问题
做量化系统的人大多有过这种经历:策略代码越写越长,指标函数散落在各个角落,今天在这个文件里加一个均线,明天在那个文件里补一个MACD,后天又为了某个回测临时写了个波动率指标,结果到上线的时候,整个代码库已经乱成一锅粥。我在实盘系统里也踩过同样的坑,而且踩得比别人更狠——最严重的时候,光指标相关的代码就积累了四千多行,其中三分之一是重复实现,还有一堆废弃函数根本没敢删,因为不确定哪个策略还在引用。
这套以AI量化为生的系统走到第17个版本,我决定停下来,认真解决指标代码的模块化问题,同时引入动态加载机制。这个决定不是头脑发热,而是连续三次事故逼出来的。第一次是一个指标改动影响了三个策略的回测结果,排查花了一整天;第二次是新增策略时发现指标函数命名冲突,旧的逻辑被静默覆盖;第三次更惨——盘中实盘运行时,因为指标模块加载了太多不必要的计算,导致信号延迟了整整两秒。这些问题的根源只有一个:指标和策略、指标和指标之间的耦合太深,系统根本不知道哪些指标是真正用到的,也更谈不上按需加载。
这次重构的核心目标有三个:第一,把指标从策略代码中彻底剥离,做成独立模块;第二,让系统只在真正需要某个指标时才加载对应的计算逻辑,不用的指标不占内存也不算时间;第三,让新增指标变成纯粹的"填表"工作,不需要动原有代码。如果你也在写量化系统,不管是用Python、C++还是别的语言,这篇文章里讲的这套思路应该能给你一些直接可用的参考。尤其是在实盘环境下,指标模块化带来的性能收益,绝对比你想象的要大。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 指标模块化的整体设计思路
2.1 先想清楚:指标到底是什么
在动手重构之前,我花了不少时间想一个看起来很简单的问题:指标到底是什么?一个均线是指标,一个MACD是指标,一个自定义的复合因子也是指标。从计算逻辑上看,它们都是"输入一段行情数据,输出一条新的序列"。但真正上手以后你会发现,这个定义太粗糙了。
指标之间是有依赖关系的。最简单的例子:EMA是MACD的基础,MACD又衍生出DEA和MACD柱,DEA还可以进一步用于信号判定。如果你把每个指标都做成完全独立的模块,就会出现大量重复计算——每个指标都重新算一遍EMA,效率低到让人无法接受。所以模块化的第一步,不是把指标统统拆开,而是把指标按照计算依赖整理成一个有向无环图,让每个基础指标只算一次,上层指标基于下层指标的结果继续计算。
我最终采用的是三层模型:基础计算层、指标定义层、策略调用层。基础计算层提供最原始的序列运算能力,比如求均值、求标准差、求斜率,这些是纯函数,不感知业务;指标定义层负责把基础计算组合成业务指标,例如RSI先算涨跌幅均值再归一化,这一层也是动态加载的主要对象;策略调用层则是策略代码通过统一接口去获取指标数据,完全不关心指标内部是怎么算出来的。
这三层分离之后,最明显的变化是:改动一个指标的内部实现,不再需要翻策略代码;新增一个指标,不需要改动任何调用方;系统启动时只加载策略真正引用的指标模块。理论上说,指标模块扩展从"改代码"变成了"加文件",我一直想要的就是这种感觉。
2.2 接口设计:一句话说清楚每个指标
接口设计是整个模块化方案的地基。如果接口定得不好,后面全白搭。我最早用字典传参,什么都能传,结果是什么都可能缺;后来改成每个指标一个函数,参数五花八门,调用方根本记不住。最终我采用的是约定式结构,每个指标模块只做四件事:定义名称、声明依赖、声明参数、实现计算函数。
一个标准指标模块长这样:
python复制# indicators/ma.py
class MAIndicator:
name = "MA" # 指标唯一标识,注册和查找用
dependencies = ["CLOSE"] # 依赖的基础数据或其他指标
params_schema = {
"period": {"type": "int", "default": 20, "min": 1, "max": 250}
}
def compute(self, context, params):
closes = context.get_series("CLOSE")
period = params["period"]
retu
