从单机策略脚本一路做到多策略并行、多周期共存的量化系统,你会发现一个绕不开的阶段:系统开始"能跑"了,但"改不起"了。一个均线周期改一下,要把回测、实盘、参数寻优几个模块挨个翻一遍;一个新指标上线,要动策略基类,还得担心影响已有策略。我在自己的AI量化系统里被这些问题反复按在地上摩擦之后,才下定决心做了一次系统架构优化,核心就两件事:指标模块化和动态加载。
这篇文章不聊选股公式怎么写,也不讨论深度学习模型调参,就纯讲系统架构层面怎么把"指标"从一个散落在各处的函数,升级成可注册、可动态加载、支持热替换的模块体系。适合那些手头量化系统已经写了半年以上、开始觉得拓展一个指标像动一次大手术的人。如果你还在单文件堆代码阶段,可能暂时感受不到痛点;但只要你打算把系统往前推,这篇文章里提到的设计思路和踩坑记录,迟早用得上。
1. 系统从"能跑"到"难改":指标腐化是怎么发生的
1.1 从单文件脚本到策略工厂:系统经历的三次变形
我最早写量化系统的时候,心里只有一件事:跑通。一个Python文件,里面从数据拉取到策略逻辑到下单接口全部堆在一起。均线金叉策略?写一个 cross_over 函数,放到 utils.py 里,然后在 main.py 里循环调用。当时觉得挺清晰,因为总共就三四个策略。
等策略数量上了两位数,我开始做第一次结构化:把所有策略抽象成 Strategy 基类,每个策略一个文件,通过统一的 run() 接口执行。这是第一次"模块化",但模块化的粒度是"整个策略",指标还是策略内部的私有函数。金叉在A策略里写一份,B策略里又复制了一份,改一个参数要全局搜索替换。
第二次变形是引入参数寻优工具。为了能让网格搜索自动跑,我必须把策略里的参数全部抽出来,通过配置文件传进去。这时候指标函数开始暴露新的问题:很多指标写死在了策略内部,你想扫描 ma_window 参数,但 ma 函数在策略文件里根本没法独立调用。于是我把指标挪出来,单独建了一个 indicators.py,然后把所有策略 import 它——这就是典型的"共享模块"阶段。
1.2 指标耦合进业务逻辑后,改一个公式有多痛
共享模块阶段看起来比之前好了,但真正让我痛到决定重构的是一次上线新因子的过程。
当时要给一个趋势策略增加"ADX过滤"逻辑。ADX是方向性运动指数,计算中需要先算 +DI 和 -DI。这些基础指标 indicators.py 里没有,于是我在文件末尾添加了三个函数。然后发现回测引擎里有一份指标缓存机制,但我改的是 indicators.py,缓存key没有覆盖到新指标,导致相同时间窗口的历史数据命中旧缓存,回测结果莫名其妙。接着又发现实盘runner里没有重新加载模块,跑的还是进程内存里的旧函数,ADX过滤完全没生效。
那天晚上我统计了一下,为了加一个指标,我改了:indicators.py(新增函数)、backtest/cache.py(加缓存key)、live/runner.py(重启和加载逻辑)、config/schema.py(加参数声明)、三个策略文件(各自import)。一共7个文件。最恐怖的是,这中间的任何一步漏掉了,系统都不会报错,只会悄悄给你一个错误的结果——这在量化系统里是最危险的。
这次经历给了我一个核心判断:指标不能是"共享函数库",而必须是"独立插件"。每个指标应该有自己清晰的边界:能算哪些参数、依赖哪些其他指标、输出结果怎么存储、版本是什么。使用方不关心指标内部怎么实现,只通过统一接口对话。
1.3 模块化不是"把函数拆文件",是"插件化"思考
很多人一听模块化,第一反应是"多建几个文件夹、把函数拆开放"。但真正的模块化,尤其是针对量化系统这种需要长期演进的项目,本质是插件化。
插件化的关键区别有三个:
- 独立性:一个指标模块的开发、调试、测试可以完全不依赖其他指标。它声明自己的输入输出,像一个黑盒。
- 可发现性:系统能自动发现有哪些指标可用,而不是靠手动 import 或者每次改注册表。
- 生命周期管理:指标可以被加载、刷新、卸载,而不影响系统的其他部分。
这三个特性,恰好就是动态加载要解决的事情。指标模块化解决的是"怎么组织代码",动态加载解决的是"怎么让系统知道指标并随时切换"。两者配合,才让"加一个指标不动旧代码"成为可能。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 指标模块化的核心设计:描述、计算、注册三分离
2.1 先把"指标是什么"定义清楚:元信息与计算逻辑分离
在动手拆模块之前,我花了比较多时间想一个问题:一个指标,本质上是什么?
后来我给自己总结了一个答案:指标 = 元信息(meta)+ 计算函数(compute)。元信息描述这个指标叫什么、有哪些参数、依赖什么数据、输出什么形状;计算函数负责拿到数据,按参数计算,返回结果。
这个"描述分离"的设计非常关键。为什么?因为系统里有很多地方,只需要指标的元信息就能工作,根本不需要真正计算。比如:
- 前端画指标配置表单,需要知道指标有哪些参数、参数类型是什么。
- 参数寻优工具,需要知道参数的取值范围和步长。
- 回测引擎在安排计算顺序时,需要知道指标的依赖关系。
- 结果存储模块,需要知道输出值的类型(是标量、Series还是DataFrame)。
如果这些信息散落在函数实现里,系统就只能靠黑盒测试或硬编码去猜;但如果把它们前置到元信息里,一切都可以驱动起来。
下面是我在实际系统中定义的指标基类,用Python dataclass来组织:
python复制from typing import Any, Dict, List, Optional, Type
import pandas as pd
from pydantic import BaseModel
class IndicatorParam(BaseModel):
"""指标参数描述,参数寻优和前端表单都靠这个"""
name: str
type: str # int / float / str / bool
default: Any = None
min_value: Optional[float] = None
max_value: Optional[float] = None
description: str = ""
class IndicatorMeta(BaseModel):
"""指标元信息:系统调度和UI展示的依据"""
name: str
display_name: str
description: str = ""
params: List[IndicatorParam] = []
dependencies: List[str] = [] # 依赖的其他指标名
required_columns: List[str] = [] # 需要数据中哪些字段,如 close/high/low/volume
output_type: str = "series" # series/dataframe/scalar
version: str = "1.0.0"
class BaseIndicator:
meta: IndicatorMeta = None # 子类必须声明
