1. 从一笔模拟盘的爆仓说起:为什么我要重做这套架构
去年年底,我在一个模拟盘上跑了一套自认为还不错的期货日内策略。特征工程做了三十多个因子,回测夏普比率1.8,最大回撤控制在12%以内,看起来一切都很美好。结果实盘跑了两周,净值曲线直接给我画了一个滑梯——不是策略逻辑出了问题,而是整套架构在实盘环境下暴露了三个致命短板:特征计算延迟导致信号滞后、风控模块和下单模块各自为政、模型在行情切换时没有任何自适应能力。
那次之后我把整套系统推倒重来,从特征工程到风控重新设计了一遍架构。这篇文章就是把这次重构的完整思路和实操细节拆开来讲——AI交易期货的完整架构到底该怎么搭,特征工程怎么做才不掉坑,风控模块怎么设计才能真正兜住底。适合有一定编程基础、想认真做量化交易系统的朋友参考,不管你是刚入门还是已经跑过实盘,应该都能从里面找到一些能直接抄作业的东西。
先说清楚一个前提:这篇文章讲的是架构层面的设计思路和工程实现,不涉及任何具体的交易建议或收益承诺。期货交易本身风险极高,任何系统都不能保证盈利,架构做得再好也只是让你少犯低级错误。
2. 整体架构设计:为什么我选择“三层解耦”方案
2.1 从单体到解耦:一次痛苦的架构演进
最开始我的系统是一个大Python脚本,特征计算、模型推理、信号生成、下单执行、风控检查全塞在一个循环里。这种写法在回测阶段跑得挺欢,因为回测是逐K线推进的,每一步都是确定性的。但一到实盘就完蛋了——实盘的数据流是异步的,行情推送、特征计算、模型推理、订单回报各自有不同的时间尺度,你没法用一个同步循环把它们串起来。
我试过用多线程硬扛,结果就是GIL锁把CPU吃满,特征计算延迟从回测时的几毫秒飙到实盘的几百毫秒。对于日内策略来说,几百毫秒的延迟意味着你的信号价格和实际成交价格可能差出好几个跳。
后来我改成了三层解耦架构:
- 数据层:负责行情接入、数据清洗、特征计算,输出标准化的特征向量
- 决策层:负责模型推理、信号生成、仓位计算,输出目标仓位
- 执行层:负责风控检查、订单拆分、下单执行、持仓管理
三层之间通过消息队列通信,各自独立进程运行。这样做的好处是:任何一层出问题不会直接拖垮其他层,而且每层可以独立扩容和优化。比如特征计算慢了,我只需要加一个计算节点,不用动决策和执行层。
2.2 为什么选消息队列而不是共享内存
有人可能会问,三层之间用消息队列通信,不是增加了延迟吗?共享内存不是更快?
我实测下来的结论是:对于期货日内策略,消息队列增加的延迟(通常在几十微秒到几毫秒)完全可以接受,但它带来的解耦收益远大于这点延迟。共享内存的问题在于,它要求所有模块对数据结构的理解完全一致,一旦特征格式变了,所有模块都得跟着改,维护成本极高。而且共享内存的调试极其痛苦,出了bug你根本不知道是哪个模块写坏了数据。
我选的是ZeroMQ作为消息中间件,原因是它足够轻量,不需要额外部署broker,而且支持多种通信模式(PUB/SUB、PUSH/PULL、REQ/REP)。数据层用PUB模式广播特征向量,决策层用SUB模式订阅,执行层用PULL模式接收目标仓位。整个通信拓扑非常清晰。
注意:消息队列的序列化格式建议用MessagePack或者FlatBuffers,不要用JSON。JSON的序列化开销在高频场景下是不可忽视的,我实测MessagePack比JSON快3到5倍。
2.3 架构的容错设计
三层解耦之后,容错就变得很自然了。数据层如果挂了,决策层收不到新特征,会自动进入“无信号”状态,不会乱下单。决策层如果挂了,执行层收不到新目标仓位,会维持当前仓位不动。执行层如果挂了,风控模块会检测到心跳超时,自动触发平仓。
这里的关键是每一层都要有独立的心跳检测和降级逻辑。我的做法是每层每隔100毫秒发一次心跳到Redis,其他层订阅心跳。如果连续3次收不到某层的心跳,就认为该层已经失效,触发相应的降级策略。
3. 特征工程:从三十个因子砍到十二个的思考过程
3.1 特征不是越多越好:过拟合的教训
我最初那套系统有三十多个特征,涵盖了价格、成交量、持仓量、技术指标、统计特征等各个维度。回测的时候IC值(信息系数)都挺好看,但实盘一跑就发现,很多特征在样本外完全失效。
后来我复盘了一下,发现问题的根源在于:很多特征之间存在高度共线性,而且部分特征在特定行情下才会生效,行情切换后就变成了噪声。比如我用了RSI、KDJ、CCI三个动量指标,它们本质上都在描述同一件事——价格变化的速率,三个一起用不仅没有增加信息量,反而放大了过拟合风险。
砍特征的过程很痛苦,因为每个特征在回测里都有贡献,你舍不得删。但我的做法是:用滚动窗口做特征重要性分析,只保留在过去N个窗口中都稳定排在前列的因子。具体来说,我用LightGBM的feature importance,每500根K线重新计算一次,统计每个因子在最近20个窗口中的平均排名和排名标准差。平均排名靠前且标准差小的因子保留,其他的砍掉。
最终从三十多个砍到了十二个,样本外表现反而更稳定了。
3.2 十二个核心特征的分类与计算逻辑
保留的十二个特征分为四类:
第一类:价格动量特征(3个)
- 多周期收益率:分别计算1分钟、5分钟、15分钟的对数收益率。为什么要多周期?因为单一周期的收益率只能捕捉一个时间尺度上的动量,多周期可以捕捉动量的持续性。如果1分钟和15分钟收益率同向,说明短期和中期趋势一致,信号更强。
- 收益率偏度:过去20根K线的收益率偏度。偏度为正说明上涨时的波动大于下跌时的波动,市场情绪偏多。
- 收益率峰度:过去20根K线的收益率峰度。峰度高说明极端行情出现的概率大,需要警惕。
第二类:成交量特征(3个)
- 量比:当前K线成交量与过去20根K线平均成交量的比值。量比大于1.5说明当前成交活跃,信号可信度更高。
- 成交量加权收益率:用成交量加权的收益率,比简单收益率更能反映真实的价格变化。
- 主动买卖比:用Tick数据计算的主动买入量和主动卖出量的比值。这个特征需要Level-2行情支持,但效果非常好。
第三类:持仓量特征(2个)
- 持仓量变化率:当前持仓量与过去5根K线平均持仓量的比值。持仓量增加说明资金在流入,趋势可能延续。
- 持仓量加权价格:用持仓量加权的平均价格,反映市场的平均持仓成本。
第四类:市场微观结构特征(4个)
- 买卖价差:买一价和卖一价的差值,反映市场的流动性。
- 订单簿不平衡:买一至买五的总量和卖一至卖五的总量的比值。
- 大单净流入:过去1分钟内大单(超过一定金额的订单)的净流入量。
- 波动率:过去20根K线的已实现波动率。
这十二个特征的计算逻辑我都封装成了独立的函数,每个函数接收原始行情数据,输出一个标量。这样做的好处是每个特征可以独立测试和优化,不会互相干扰。
3.3 特征计算的性能优化
特征计算是整套系统里最耗CPU的部分。我最初用Pandas做特征计算,结果发现Pandas的滚动窗口计算在实盘环境下太慢了——每来一根新K线,Pandas都要重新计算整个窗口,时间复杂度是O(n)。
后来我改成了增量计算的方式。以20根K线的滚动均值来说,我不需要每次都重新算20个数的和,只需要维护一个滑动窗口的和,新K线进来时加上新值、减去最老的值,时间复杂度降到O(1)。这个优化让特征计算延迟从几十毫秒降到了几毫秒。
具体实现上,我用的是NumPy的环形缓冲区(ring buffer)。每个特征维护一个固定长度的数组,用一个指针记录当前写入位置,新数据覆盖最老的数据。计算均值、方差、偏度等统计量时,直接用NumPy的向量化操作在缓冲区上计算。
import numpy as np class RollingWindow: def __init__(self, size): self.size = size self.buffer = np.zeros(size) self.idx = 0 self.count = 0 def update(self, value): self.buffer[self.idx] = value self.idx = (self.idx + 1) % self.size self.count = min(self.count + 1, self.size) def mean(self): return np.mean(self.buffer[:self.count]) def std(self): return np.std(self.buffer[:self.count])这个类虽然简单,但在实盘环境下比Pandas的rolling快了一个数量级。
实操心得:特征计算一定要做增量更新,不要每次全量重算。我见过太多人用Pandas的rolling在实盘里跑,延迟高得离谱,还以为是网络问题。
4. 模型推理与信号生成:LightGBM为什么比深度学习更适合
4.1 模型选型的权衡
在期货日内交易这个场景下,我最终选择了LightGBM而不是深度学习模型。原因有三:
第一,样本量有限。期货日内策略的样本量通常不大,尤其是高频策略,一天可能只有几百个样本。深度学习模型需要大量数据才能训练好,样本量不够很容易过拟合。LightGBM在中小样本上的表现通常更稳定。
第二,推理速度要求高。日内策略对推理延迟非常敏感,LightGBM的推理速度在毫秒级别,而深度学习模型即使经过优化,推理延迟也通常在几十毫秒以上。
第三,可解释性。LightGBM可以输出特征重要性,方便我分析哪些特征在起作用。深度学习模型是个黑盒,出了问题很难排查。
当然,这不是说深度学习不能用。如果你的策略是低频的(比如日线级别),样本量足够大,深度学习模型可能会有更好的表现。但在日内高频场景下,LightGBM是更务实的选择。
4.2 标签设计与样本权重
模型训练的关键在于标签设计。我用的是三重障碍法(Triple Barrier Method):
- 设置一个止盈障碍(比如+0.5%)
- 设置一个止损障碍(比如-0.3%)
- 设置一个时间障碍(比如持有20根K线后强制平仓)
标签的取值是:先触及止盈障碍为1,先触及止损障碍为-1,先触及时间障碍为0。
这种标签设计的好处是它同时考虑了收益和风险,而且和实际交易逻辑一致。传统的固定时间窗口收益率标签的问题是,它假设你会在固定时间后平仓,但实际交易中你可能会提前止盈或止损。
样本权重方面,我给近期样本更高的权重,因为市场结构会变化,近期样本更能反映当前的市场状态。具体来说,我用指数衰减权重,半衰期设为500根K线。
4.3 信号生成与仓位映射
模型输出的是一个概率值,表示在当前特征下,价格先触及止盈障碍的概率。我设定一个阈值(比如0.6),当概率超过阈值时生成做多信号,低于0.4时生成做空信号,中间区域不操作。
仓位大小用凯利公式的分数版本计算:
f = (p * b - q) / b * k其中p是胜率,q是败率,b是盈亏比,k是一个缩放系数(我通常设为0.25)。用分数凯利而不是全凯利,是为了避免过度下注。
实际仓位 = 账户权益 * f / 合约价值
这个仓位计算方式比固定手数更合理,因为它会根据模型的置信度和当前的盈亏比动态调整仓位。
注意:凯利公式对参数非常敏感,p和b的估计误差会导致仓位计算偏差很大。我建议用滚动窗口估计p和b,而且一定要用分数凯利,不要用全凯利。
5. 风控模块:整套系统里最不能省的部分
5.1 三层风控体系的设计
风控是我这次重构中花时间最多的部分。我设计了三层风控体系:
第一层:事前风控(Pre-trade Risk Check)
在订单发出之前进行检查,包括:
- 单笔订单最大手数限制
- 单品种最大持仓限制
- 账户总仓位限制
- 单日最大亏损限制
- 订单价格偏离限制(防止乌龙指)
任何一项检查不通过,订单直接被拒绝,不会发到交易所。
第二层:事中风控(In-trade Risk Monitor)
在持仓过程中持续监控:
- 实时盈亏监控
- 保证金占用率监控
- 持仓集中度监控
- 市场波动率监控
如果触发预警线,会发出警报;如果触发强平线,会自动平仓。
第三层:事后风控(Post-trade Risk Analysis)
在交易结束后进行分析:
- 当日交易复盘
- 滑点分析
- 策略表现归因
- 风控规则有效性评估
这一层主要是为了持续优化风控参数。
5.2 关键风控参数的计算与设置
风控参数不能拍脑袋定,要有计算依据。我举几个例子:
单笔订单最大手数:根据账户权益和品种波动率计算。
max_lots = account_equity * risk_per_trade / (atr * contract_multiplier)其中risk_per_trade是单笔交易愿意承担的风险比例(我设为0.5%),atr是品种的ATR值,contract_multiplier是合约乘数。
单日最大亏损:设为账户权益的2%。这个数字是根据历史最大回撤和恢复时间确定的。如果单日亏损超过2%,说明要么策略失效了,要么市场出现了极端行情,应该停下来检查。
保证金占用率上限:设为70%。期货是保证金交易,保证金占用率过高会导致可用资金不足,一旦出现不利行情容易被强平。留30%的缓冲是为了应对突发情况。
持仓集中度上限:单品种持仓不超过总仓位的30%,单板块持仓不超过50%。这是为了分散风险,避免某个品种或板块的黑天鹅事件导致整体账户大幅回撤。
5.3 风控模块的工程实现
风控模块我写成了一个独立的Python进程,通过消息队列接收订单和持仓信息,通过Redis存储风控状态。关键的风控检查逻辑如下:
class RiskManager: def __init__(self, config): self.max_lots = config['max_lots'] self.max_position = config['max_position'] self.max_daily_loss = config['max_daily_loss'] self.margin_limit = config['margin_limit'] self.daily_pnl = 0 self.positions = {} def check_order(self, order): # 检查单笔手数 if order['lots'] > self.max_lots: return False, '单笔手数超限' # 检查品种持仓 current_pos = self.positions.get(order['symbol'], 0) new_pos = current_pos + order['lots'] if order['direction'] == 'buy' else current_pos - order['lots'] if abs(new_pos) > self.max_position: return False, '品种持仓超限' # 检查单日亏损 if self.daily_pnl < -self.max_daily_loss: return False, '单日亏损超限' return True, '通过'这个类虽然简单,但涵盖了最核心的风控逻辑。实际使用中还需要加上保证金计算、持仓集中度检查等。
实操心得:风控模块一定要独立于策略模块,不能把风控逻辑写在策略代码里。我见过太多人把风控和策略混在一起,结果策略一改风控就失效了。风控应该是最后一道防线,独立运行,独立部署。
6. 实盘部署与运维:那些文档里不会写的事
6.1 部署架构与硬件选型
实盘部署我用的是一台云服务器,配置是8核16G,放在交易所机房附近(延迟更低)。操作系统是Ubuntu 20.04,Python版本是3.9。
为什么选云服务器而不是本地机器?因为云服务器的网络稳定性更好,而且可以随时扩容。本地机器虽然延迟可能更低,但一旦断电或断网,损失可能远超省下的那点延迟。
硬件方面,CPU比GPU重要。LightGBM的推理不需要GPU,但特征计算是CPU密集型的。我选的是高频CPU(主频3.5GHz以上),而不是多核低频CPU。
6.2 监控与告警
实盘系统必须要有完善的监控和告警。我用的方案是Prometheus + Grafana + Alertmanager:
- Prometheus采集各项指标(延迟、吞吐量、盈亏、持仓等)
- Grafana做可视化展示
- Alertmanager负责告警推送(邮件、短信、即时通讯工具)
关键监控指标包括:
| 指标 | 正常范围 | 告警阈值 |
|---|---|---|
| 特征计算延迟 | <5ms | >20ms |
| 模型推理延迟 | <10ms | >50ms |
| 订单成交延迟 | <100ms | >500ms |
| 消息队列积压 | <10条 | >100条 |
| 保证金占用率 | <50% | >70% |
| 单日亏损 | >-1% | <-2% |
这些阈值是根据我的实盘经验设定的,不同策略可能需要调整。
6.3 常见故障与排查
实盘运行中遇到过不少故障,我挑几个典型的说说:
故障一:特征计算延迟突然飙升
排查发现是某个特征的计算逻辑里有一个O(n)的操作,当窗口内数据量增大时延迟线性增长。解决方法是改成增量计算。
故障二:订单被交易所拒绝
排查发现是订单价格偏离太大,触发了交易所的废单规则。解决方法是在下单前加上价格偏离检查,超过一定阈值就拒绝下单。
故障三:消息队列积压
排查发现是决策层的模型推理变慢了,导致消费速度跟不上生产速度。解决方法是优化模型推理,同时增加决策层的消费者数量。
故障四:风控模块误触发
排查发现是风控参数设置过严,正常波动也会触发。解决方法是根据历史数据重新校准风控参数。
注意:实盘系统一定要有完善的日志记录,每个关键步骤都要打日志。出了问题第一时间看日志,比瞎猜高效得多。
7. 回测与实盘的差异:为什么回测赚钱实盘亏
7.1 回测中的常见陷阱
回测和实盘的差异是量化交易里最坑人的地方。我踩过的坑包括:
未来函数:用了未来数据做特征。比如用当根K线的收盘价计算特征,然后假设能在当根K线开盘时下单。这是最隐蔽的坑,因为回测里看不出来,实盘里直接失效。
滑点低估:回测里假设按信号价格成交,但实盘里你的订单可能排在队列后面,成交价格差好几个跳。
流动性假设:回测里假设你的订单能全部成交,但实盘里如果订单量太大,可能只能部分成交。
手续费低估:期货的手续费包括交易所手续费和经纪商佣金,不同品种差异很大,回测里一定要算准。
7.2 如何让回测更接近实盘
我的做法是:
- 用Tick数据回测,而不是K线数据。Tick数据能更真实地模拟成交过程。
- 加入滑点模型,根据订单量和市场流动性动态计算滑点。
- 加入延迟模型,模拟从信号生成到订单成交的延迟。
- 用Walk-forward回测,而不是简单的样本内回测。Walk-forward能更好地评估策略的样本外表现。
Walk-forward的具体做法是:把数据分成多个时间段,用前一段训练模型,后一段测试,然后滚动前进。这样能模拟实盘中模型不断更新的过程。
7.3 实盘表现监控与策略迭代
实盘上线后,我会持续监控策略的表现。关键指标包括:
- 滚动夏普比率:过去20个交易日的夏普比率
- 滚动最大回撤:过去20个交易日的最大回撤
- 胜率和盈亏比:实时统计
- 信号衰减:模型输出的概率分布是否发生变化
如果滚动夏普比率持续低于阈值,或者滚动最大回撤超过阈值,我会暂停策略,重新检查模型和特征。
策略迭代的节奏通常是:每周做一次小复盘,每月做一次大复盘。小复盘看当周的交易明细,大复盘看整体的策略表现和风控有效性。
8. 一些零散但重要的经验
8.1 关于数据质量
数据质量是量化交易的生命线。我遇到过好几次因为数据问题导致的亏损:
- 行情数据缺失,导致特征计算错误
- 数据时间戳错乱,导致信号时序混乱
- 数据源切换,导致价格不连续
解决方法是:建立数据质量监控体系,对每一批数据进行校验,包括完整性检查、时序检查、异常值检查。发现问题数据立即告警,不要等到策略出问题才发现。
8.2 关于模型更新频率
模型更新频率是个需要权衡的问题。更新太频繁,模型不稳定;更新太慢,模型跟不上市场变化。
我的做法是:每周更新一次模型,但每天监控模型表现。如果模型表现突然恶化,立即触发紧急更新。更新模型时,用最近半年的数据训练,用最近一个月的数据验证。
8.3 关于资金管理
资金管理比策略本身更重要。我见过太多人策略不错但资金管理一塌糊涂,最后亏得血本无归。
我的资金管理原则是:
- 单笔交易风险不超过账户权益的0.5%
- 单日最大亏损不超过账户权益的2%
- 单周最大亏损不超过账户权益的5%
- 单月最大亏损不超过账户权益的10%
任何一项触发,立即停止交易,检查原因。
8.4 关于心理因素
量化交易虽然是用程序做决策,但背后还是人在操作。心理因素对交易结果的影响非常大。
我的经验是:不要盯盘。程序跑得好好的,你盯盘只会手痒想干预。我见过太多人因为盯盘时忍不住手动干预,结果把程序化的优势全毁了。
另外,不要因为短期亏损就改策略。任何策略都有回撤期,短期亏损不代表策略失效。我的做法是设定一个观察期(比如20个交易日),观察期内不做任何策略调整,观察期结束后再评估。
9. 后续可以扩展的方向
这套架构目前跑得还算稳定,但还有很多可以优化的地方。我列几个后续想尝试的方向:
第一,引入在线学习。目前的模型是每周离线更新的,如果能改成在线学习,模型可以实时适应市场变化。不过在线学习的工程复杂度很高,需要解决样本权重、灾难性遗忘等问题。
第二,多品种扩展。目前只跑了一个品种,后续想扩展到多品种。多品种的好处是分散风险,但挑战是不同品种的特征和模型可能需要分别优化。
第三,引入强化学习。目前的信号生成是监督学习,标签是人工设计的。强化学习可以直接优化交易目标(比如夏普比率),不需要人工设计标签。但强化学习的样本效率低,训练不稳定,需要大量实验。
第四,优化执行算法。目前的执行是简单的限价单,后续想尝试TWAP、VWAP等执行算法,降低冲击成本。
这些方向我还在探索中,有进展了再分享。
最后说一句:AI交易期货不是印钞机,架构做得再好也不能保证盈利。市场永远在变化,今天有效的策略明天可能就失效了。保持敬畏,持续学习,控制风险,才是长期生存的关键。我在实盘里踩过的坑远不止文章里写的这些,有些坑甚至让我一度想放弃。但每次复盘之后,总能找到改进的方向。这套架构也不是终点,只是一个当前阶段相对稳定的版本,后续还会继续迭代。