☰
回测曲线再漂亮,也得先过Fail-Closed这道关:AI量化风控实战
2026/10/20 12:25:22 网站建设 项目流程

1. 为什么回测曲线越漂亮,我越不敢点上线按钮

先讲个真实经历。前阵子我跑出一版AI量化策略,样本外测试结果相当不错,年化收益、夏普比率、最大回撤都挺能唬人。团队里刚来的同事兴奋地问我什么时候上实盘,我说再等等。他很不理解,觉得回测都这么好了,为什么还不赶紧推上线。

这个问题其实被我很多同行问过。AI量化听起来光鲜,但真正敢把模型接到实盘账户上的人并不多。不是大家不想,而是回测与实盘之间,隔着三道很难绕过去的坎:过拟合、数据漂移、交易成本。这三样东西,任何一个没处理好,都会让那条漂亮的回测曲线变成一张废纸。

1.1 过拟合:调到样本内"完美",恰恰是对样本外的背叛

做特征工程、调模型超参的时候,很容易陷入一个陷阱——为了让历史数据上的表现更好看,不断调整参数、增加特征、缩小样本区间。最后模型在训练集上几乎完美复刻了每一次上涨下跌,但放到没见过的数据上,表现立刻走样。

这里有个特别容易被新手忽略的点:AI量化模型的过拟合,不像学校里考试作弊那样明显。你用的每个特征、每个超参数组合,在回测框架里看起来都"合理"。但问题在于,金融市场本身信噪比极低,模型很容易把噪声当成规律来学。当模型能把一段历史行情里的每根K线都解释得头头是道,我反而更警惕——它学的可能根本不是市场规律,只是那段行情的"密码"。

1.2 市场状态漂移:昨天学到的东西,今天可能已经过期

金融市场不是一个静态环境。波动率会变,流动性会变,不同资产之间的相关性也会变。今天我们训练模型时用到的数据分布,可能和明天的实盘数据分布产生系统性偏差。

举一个很常见的例子:某个策略在低波动行情下表现很好,因为它捕捉的是趋势启动前的"平静期"特征。结果市场突然进入高波动状态,这类特征的整体分布发生漂移,模型输入不再符合训练时的分布假设,输出的信号自然失去参考价值。我在实际工程里见过不少策略,并不是突然"变差",而是"输入分布悄悄变了,模型还在用旧眼光看新市场"。

1.3 交易成本这只"隐形的手":回测利润的偷走者

回测里成交价永远是那一刻的盘口价,滑点、手续费、冲击成本三者叠加,在流动性好的品种上可能不明显,一旦品种冷门或者订单量大,利润就会被侵蚀得很厉害。很多策略毛收益率看着不错,扣完成本后直接变成负收益。

更麻烦的是,AI量化模型通常交易频率偏高,这意味着每一笔交易的成本都会被反复叠加。哪怕单次成本只有万分之几,一天几十次交易下来就是一笔大数目。回测框架里如果不把成交延时、对手盘深度、撮合延迟都算进去,你看到的收益率就是"理想世界"的收益率,和实盘相去甚远。

所以,"不敢上线"不是一句自嘲,而是对这三道坎的敬畏。正因为如此,我给自己定了一条原则:在把AI信号接到实盘之前,必须先过一道"Fail-Closed"的关卡——系统一旦发现任何异常迹象,宁愿不做任何交易,也不要硬着头皮下单。

2. Fail-Closed的核心:先说"做不了什么",再说"做什么"

Fail-Closed这个概念最早是从网络安全和工业控制领域来的。安全防火墙在不确定某个请求是否安全的时候,默认选择拒绝;核电站在控制信号异常时,默认触发急停。它的核心精神是:当系统无法确认自身处于安全状态时,宁可停止服务,也不要带着不确定性继续运行。

在AI量化交易里,这个思想的价值被很多人低估了。

2.1 止损管的是"亏多少",Fail-Closed管的是"能不能开新仓"

很多人会把Fail-Closed和止损混淆。止损解决的是"这笔仓位已经亏了,我最多容忍它亏多少"的问题,它管的是存量风险。Fail-Closed解决的是"这笔单子到底还能不能下"的问题,它管的是增量风险。

两者区别很关键。一个亏损中的策略,如果系统状态本身是健康的,止损触发后平仓离场是正确的。但如果模型输入已经出现严重异常、数据管道延迟、甚至特征出现空值,这时候即使策略看起来还在盈利,也应该停止开新仓。因为此时你根本不知道下一个信号是不是一堆错误数据算出来的。

所以我设计的模板里,止损逻辑和Fail-Closed逻辑是分开的两层。止损层负责管理已有仓位的最大亏损,Fail-Closed层负责在系统异常时关闭"开仓"这个动作本身。

2.2 可观测、可断言、可熔断:三条设计原则

我在搭自己的模板时,给Fail-Closed抽象了三条设计原则:

可观测:系统每个关键环节的状态必须能量化。模型推理用了多少毫秒?特征管道的数据延迟是多久?今天和过去30天的特征分布差异有多大?这些指标不能等到出了问题再去看,而要在系统运行中实时暴露出来。

可断言:每个状态都要有一个明确的阈值,超过阈值就是异常。不能只记录"模型推理时间变长了",要写清楚"推理时间超过100毫秒就触发警告,超过200毫秒就直接关闭开仓权限"。没有明确断言的监控只是日志,不是风控。

可熔断:一旦断言失败,系统能自动切断后续操作链路。这个切断不是提示一个人来看,而是自动执行。为了防止频繁开关导致"狼来了"效应,熔断后还要有一段时间的观察期,观察期过后系统状态恢复稳定,才允许重新打开。

2.3 一个类比:电梯故障时的"急停优先"

我会跟团队里新来的同事打一个比方:一部电梯在运行中如果检测到门锁信号异常,它的默认动作是停在当前楼层、停止运行,而不是继续往上一层走。因为继续运行可能把人困在半空,甚至引发事故。表面上看,电梯停运造成了不方便,但和"带着故障继续运行"相比,停运的风险小得多。

AI量化系统也一样。当特征数据异常、模型延迟过高、或者实盘行情与训练分布明显背离时,继续交易就像电梯带着门锁故障继续跑。暂时停止开仓虽然可能错过一些机会,但至少不会让你在系统失控的状态下盲目下注。这就是我理解的Fail-Closed在量化交易中的价值:它不是用来帮你赚钱的,而是用来保证你在出错时不会亏大钱的。

3. 模板三件套:状态观测、分级闸门、出口熔断

明确了Fail-Closed的理念之后,我开始把它落成一套可以复制到不同策略里的模板。整体结构可以拆成三块:状态观测层、分级闸门层、出口熔断层。

3.1 状态观测层:给模型装一块"血压计"

状态观测层负责采集系统运行时的关键健康指标。我在模板里至少观测以下四组数据:

  • 特征分布漂移:拿当天实时的特征分布和训练期的基线分布做对比,常用的指标是KS检验或PSI(群体稳定性指数)。如果PSI超过0.2,就说明特征分布发生明显变化。
  • 推理与数据延迟:从数据落地到特征生成、模型推理结束,每个环节都打上时间戳,统计耗时。量化交易对时效性极其敏感,模型和行情之间的时间差拖得越久,信号有效度越低。
  • 异常值数量:实时统计特征里空值、Inf值、超出正常范围的异常值数量。
  • 当日盈亏与回撤状态:虽然这东西和Fail-Closed没有直接关系,但它决定了系统当前的风险承受底线,可以作为辅助熔断条件。

观测层本身不做什么决策,它只负责把状态暴露出来,写入环形日志,并触发下一层的判定。

3.2 信号校验层:置信度再高,也要过一致性检查

信号校验层做的是"模型输出信号到了我手上之后,我凭什么信它"的检查。很多AI量化方案只盯着模型准确率,忽略了信号一致性校验。

我常用的校验有这么几项:

  • 输入分布校验:模型推理时用的特征向量,和训练集上的特征分布是否一致?如果不一致,哪怕模型给出99%的置信度,我也直接丢弃这个信号。
  • 信号稳定性校验:同一时刻反复推理多次,结果是否一致?如果模型对微小扰动极其敏感,说明它在这个输入区域附近是不稳定的,这类信号拿来实盘等于掷骰子。
  • 置信度阈值校验:模型的输出概率低于某个阈值(我自己常用0.65)时,不开仓。不是说0.6到0.65之间的信号一定错,而是低置信度区间内信号的期望胜率往往不足以覆盖交易成本,不开仓更划算。

信号校验层的设计原则是"多疑"。宁可错杀一个正确信号,也不放过一个错误信号。

3.3 执行闸门层:下单前的最后一道关卡

执行闸门层是Fail-Closed的落地点。前面所有观测和校验,最终都要汇聚到这里,决定一件事:这笔订单到底要不要送到交易所。

我把它设计成一个独立的策略包装层,任何AI模型的输出信号都要先经过一次"所有健康指标为True"的检查,才能进入执行模块。执行模块内部还会再次检查:

  • 当前策略是否处于熔断状态?
  • 当前账户权益是否低于预设底线?
  • 距离收盘还剩多少时间?流动性是否足够支持这笔订单?

这三关里任何一关不过,订单都会被拦截。

3.4 伪代码骨架:把三件套装进一个策略类

class FailClosedStrategy: def __init__(self, cfg): self.feature_baseline = cfg["feature_baseline"] self.psi_threshold = cfg.get("psi_threshold", 0.2) self.max_latency_ms = cfg.get("max_latency_ms", 200) self.min_confidence = cfg.get("min_confidence", 0.65) self.circuit_open = False self.open_time = None def check_state(self, features, latency_ms): # 状态观测层:计算实时指标 psi = compute_psi(features, self.feature_baseline) anomaly_ratio = compute_anomaly_ratio(features) if psi > self.psi_threshold: raise StateError("feature_distribution_drift") if latency_ms > self.max_latency_ms: raise StateError("inference_latency_high") if anomaly_ratio > 0.05: raise StateError("anomaly_ratio_over_threshold") def generate_signal(self, features): # 信号校验层 if self.circuit_open: return None try: self.check_state(features, measure_latency()) except StateError as e: self.open_circuit() return None prob = self.model.predict_proba(features.reshape(1, -1))[0, 1] if prob < self.min_confidence: return None return {"direction": 1 if prob > 0.5 else -1} def execute(self, signal, account_state): # 执行闸门层 if not signal: return "NO_TRADE" if account_state.equity < self.min_equity: return "REJECT" if not has_enough_liquidity(signal): return "REJECT" return self.broker.place_order(signal) def open_circuit(self): self.circuit_open = True self.open_time = datetime.now()

这段代码不是完整实盘实现,而是展示Fail-Closed的控制流骨架。核心就一句话:信号不是直接从模型流向交易所,而是流经一道又一道检查,检查不通过就断电。

4. 实战拦截案例:一个用旧数据训练的"漂亮信号"是怎么被识破的

理论讲再多,不如一个真实案例有说服力。说一次我实际遇到的拦截事件。

4.1 事故背景:一个结构异常的特征值

之前跑一个基于量价因子的AI策略,训练集用的是过去一年的日线数据。模型上线到模拟环境之后,前两周表现正常。到第三周的某个交易日,策略产生了一个方向性信号,模型置信度给到了0.92,在当时的阈值得分里算是相当高的信号。

但观测层突然报警:实时特征里的某个量价因子,和训练集基线的分布差异飙到了历史极值。查看日志发现,当天数据源里某几个交易品种的成交量数据出现了明显的跳变,导致这个因子的数值形态和过去一年完全不同。信号本身是在"异常数据"基础上计算出来的,模型的0.92置信度针对的是"它在训练分布上见过的模式",而不是"它在这个异常输入上的把握"。

4.2 排查链路:从特征分布漂移到数据管道

当时排查的过程是这样的:

第一步,确认特征值本身没有算错。重新拉了原始行情数据,发现当天某些品种的Volume字段在午盘后出现连续大量重复值——这是下游推送端在做数据对齐时出了问题。

第二步,确认模型确实接收到了异常特征。调出推理日志,发现该特征列在午盘后持续处于"偏离基线两个标准差以上"的状态。

第三步,回算"如果当时没有Fail-Closed拦截,这笔单子会怎么样"。用模拟撮合环境回放了一下,发现那个信号如果真被下单,会在下午一段窄幅震荡行情里连续开多,最终在一根快速下杀K线中被扫止损。

这次拦截让我更加确信一件事:**高置信度信号不等于高确定性信号。**置信度描述的是模型对自身输出的把握,和"这个输入数据是否正常"完全是两码事。

4.3 拦截决策:为什么宁可不交易,也不能带着不确定性下单

很多人在实盘中会贪心——"这个信号都0.92置信度了,不买不是浪费吗?"但Fail-Closed的逻辑不是这样的。

当特征分布已经出现系统性漂移时,你面对的是一个"推理条件失效"的问题。此时下单的期望收益是一个未知数,更准确地说,是一个不可估计的未知数。既然不可估计,就要默认拒绝。

那时我给自己定了一条规则,沿用至今:**只要特征漂移检测告警没有解除,哪怕模型连续给出10个高置信度信号,也一律不交易。**市场每天都在,机会永远是下一班车,但本金亏穿了,就真的什么都没了。

5. 被低估的系统性风险:是人写代码还是AI写代码,都会犯的那类错

最近观察到一个有意思的现象:随着AI辅助编程工具普及,写一个AI量化策略的代码门槛变得非常低。让大模型帮你把因子计算、模型推理、信号生成一条龙写出来,确实快。但这也带来一个隐患——很多生成出来的策略代码,从功能层面看是完整的,从风控层面看是裸奔的。

5.1 AI辅助编码:代码完整不等于风控完整

人写策略的时候,多多少少会想到"万一行情数据断流怎么办""万一模型推理超时怎么办",这些问题会在代码里留下处理逻辑。但AI辅助生成的代码,更倾向于围绕"正常情况"来写。你让它生成一个信号模型,它就老老实实给你一套特征处理加模型推理加信号输出的流程。你如果不主动提醒,它不会替你想好"数据异常时该不该停止交易"这种边角问题。

不是说AI生成的代码有什么问题,而是写代码的人如果不具备Fail-Closed意识,AI大模型根本不会替你补上这块拼图。它擅长的是把"明确定义的需求"翻译成代码,而"需求"本身需要人来定义。

5.2 回测过拟合的"自动化"版本

另一个被放大的风险是,AI辅助编程让"快速试更多参数组合"变得太容易了。以前手动调参可能一天试几十组,现在已经可以自动写循环、自动跑网格搜索、自动筛选最优参数。

这种自动化调参的效率提升是好事,但也在客观上加速了过拟合的进程。你今天用历史数据搜索出来的那个"最优"超参组合,本质上是在无数随机游走里挑出的一条最像规律的一条。搜索空间越大,找到虚假规律的概率就越高。

所以我在自己的实验里,会刻意限制调参次数。一个策略在验证集上如果连续尝试了超过一定次数还没找到稳定的表现,我会直接放弃而不是继续加深搜索。

5.3 模板能兜住什么,兜不住什么

Fail-Closed模板能兜住的,是那些"系统运行过程中能够被观测到"的异常,比如数据延迟、特征分布漂移、信号置信度不足。这些在运行时都能被量化、被检测、被断言。

兜不住的是那些"模型根本没有能力意识到"的问题,比如策略本身就选错了特征、市场逻辑已经发生根本性变化但特征分布还没来得及体现、或者你在回测阶段就引入了错误的数据拼接方式。这类问题藏在源头,任何运行时的风控闸门都拦不住。

这也是为什么我坚持"模板是安全网,不是免死金牌"的观点。AI量化的工程化,一半靠系统设计,一半靠策略研究时的自律。后者没有任何模板能代劳。

6. 把Fail-Closed模板装进回测与实盘:我的落地清单

最后分享一份我自己在用的落地清单。它不算什么标准答案,但至少能让你在"要不要上线"这个问题上,有一个相对理性的判断流程。

6.1 回测期的改装清单

回测框架不能只算净值和最大回撤,还要把Fail-Closed的检查项加进去。

  • 滑点与手续费要往保守里设:我一般用上限估计,比实盘正常水平再多算30%-50%。
  • 加入数据异常模拟场景:比如随机去掉某一天的行情数据、给某些特征列塞入空值,看看策略在"输入被打断"时是否会自动停止。
  • 设置延迟模拟:行情到信号输出之间人为增加时间延迟,观察策略收益的衰减速度。
  • 分布漂移切片测试:把训练集之外的行情数据按波动率高低切块,分别跑测试,看策略在哪种市场状态下会触发漂移检测。如果它频繁触发,说明这个策略本质上只适合某一种市场环境。

6.2 上线前的验收测试

模拟盘跑满至少两周后,还要做一次静态检查:

  • 确认所有Fail-Closed检查项在模拟盘中都有相应日志输出。如果日志有缺失,说明有些路径没有被覆盖。
  • 手工触发一次熔断,检查订单是否真的被拦截。不要等到实盘再去验证。
  • 确认熔断恢复机制正常。观察期结束后,系统能自动恢复交易状态,不需要人工介入。

6.3 每天开市前要做的监控检查

  • 看一眼特征分布漂移指标,确认它没有在开盘前就已经告警。
  • 检查数据管道是否有延迟,尤其是多数据源的行情拼接是否对齐。
  • 查看模型上次重训练的时间。如果训练周期太久、数据分布早已漂移,策略应该被自动挂起,等到重训练完成后再恢复。

6.4 一些我踩过坑之后的额外建议

  • 熔断阈值不要一次设得太严或太松。太严会导致系统频繁停止交易,错过大量机会;太松则形同虚设。我自己的经验是从中等阈值起步,跑了几天之后根据实际触发的频率来调整。
  • 所有的Fail-Closed日志都要做结构化存储,方便事后复盘。不要只是打印到控制台就完事了。
  • 如果条件允许,给观测层单独跑一个独立进程,不要和策略进程放在一起。否则策略进程卡死的时候,观测层也没法正常报告状态,那风控就失灵了。

我在实际使用中发现,Fail-Closed模板最大的作用不是帮我多赚了多少钱,而是帮我过滤掉了大量自欺欺人的"好信号"。

每次系统拦截一个高置信度信号,我都会保存当时的特征快照,攒了一批之后回头复盘。看得多了之后你会发现,真正让你亏钱的,往往不是那些你"明显看不懂"的时刻,而是那些你"以为自己看懂了其实根本没有"的时刻。Fail-Closed这个模板,至少会在你自以为是的时候,礼貌地拍一下你的肩膀:先停一下,想清楚再动手。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询