AI量化策略三层骨架:从数据特征到模型信号再到组合执行
做AI量化的人很多但能把“AI”两个字真正落到A股策略代码里的其实没那么多。市面上的教程要么只讲模型原理要么只给一堆策略代码中间那条“从想法到可回测、可实盘”的链路反而很少有人完整讲清楚。我写这篇的初衷就是想把这条链路拆开揉碎用“上篇”先解决骨架问题你拿到一堆行情数据和AI工具之后到底按什么结构把策略搭起来才不会越写越乱也敢于拿真钱去验证。适合正在学量化的开发者、刚入行想转量化研究的分析师以及被各种“AI选股”课程忽悠过但始终没跑通完整流程的朋友。这篇要讲的三层骨架是我自己折腾了大半年、推翻重来三次之后沉淀下来的结构。它把策略拆成“数据与特征层、模型与信号层、组合与执行层”三个独立模块每一层只干一件事层与层之间用标准接口对接。这么做最大的好处是模型不行就换模型数据源不行就换数据源交易逻辑要改也不用把全盘代码推倒重来。下面我会结合代码一步步说明每一层怎么搭、为什么这么搭以及我踩过的那些坑。1. 别急着写代码先想清楚三层骨架是什么我第一次做AI量化策略的时候就是典型的反面教材把数据下载、特征计算、模型训练、买卖逻辑全写在一个脚本里跑通一次要十几分钟稍微改一个参数就要全部重跑而且根本无法定位到底是数据问题还是模型问题还是交易逻辑问题。后来我意识到量化策略本质上是一条流水线流水线必须要分段每一段有清晰的输入和输出才能独立调试、独立优化。1.1 三层各自的目标原料、大脑、手脚我的划分方式很简单用“人做投资决策”来类比最直观第一层是数据与特征层它解决“看到什么”的问题。对应到人就是你每天看盘、读财报、盯资金流入这些信息收集动作。在代码里这一层负责把原始行情数据变成一张特征宽表行是时间列是各种因子和标签。它不关心用哪种模型只关心数据干不干净、特征有没有代表性。第二层是模型与信号层它解决“怎么想”的问题。对应到人就是你根据看到的信息做判断这只股票趋势走坏了那只股票放量突破值得关注。在代码里这一层是任何AI算法的核心输入是特征层给的宽表输出是一个“预测值”或者“方向信号”比如未来5天上涨概率、预期收益排序或者干脆是一个多空分类标签。第三层是组合与执行层它解决“怎么做”的问题。对应到人就是你决定仓位多少、什么价位买、分几笔买、止损怎么设。在代码里这一层把模型的信号翻译成具体的持仓指令再结合手续费、滑点等约束输出最终的资金曲线。这三层的关系可以用一句话概括第一层负责把市场翻译成AI能懂的表格第二层负责把表格解读成观点第三层负责把观点变成行动。任何一层做得不好整个策略都会崩但崩的根源通常能在某一层里单独揪出来。1.2 为什么这种分层能救命分层设计最大的价值不是看起来“架构优雅”而是排错效率高。我举一个真实经历有次策略回测曲线非常漂亮年化收益高得离谱但我一看交易记录就发现很多买入点出现在当天最高价附近卖出点出现在当天最低价附近——这几乎必然是“未来函数”。如果是单脚本结构你得在几百行代码里慢慢翻但分层之后问题直接锁定在组合层的“撮合假设”部分因为只有那一层会用到当日最高最低价来模拟成交。我花了不到十分钟就修复了。另外分层之后每一层都可以独立做单元测试。数据层可以检查有没有空值、跳空、复权错误模型层可以检查训练集和测试集的分布是否一致组合层可以检查换手率、持仓集中度是否合理。这些测试单独跑都很快定位问题比整体调试快得多。2. 第一层数据与特征骨架让AI先吃饱饭这一层是整个策略的地基也是我见过翻车率最高的一层。很多人觉得数据就是下载个CSV、读进DataFrame就行但实际上A股数据的坑比模型本身多得多。2.1 行情数据选哪套先想清楚三个问题数据选型上三个问题必须先回答要不要复权、用什么频率、怎么处理停牌和缺失。关于复权我的建议是做特征用前复权做交易撮合计算用真实价格两者必须分开存。前复权价格能真实反映收益率避免除权除息造成的价格断崖但实盘下单用的是不复权的真实价格。很多策略表现稳定却实盘对不上就是因为在复权价格上模拟成交。我常用的方案是同时拉取“前复权行情”和“原始行情”两张表特征计算全部用前复权组合层撮合时再对齐到真实价格。关于频率日线是第一选择原因很简单噪声小、数据稳定、容量足够A股全市场计算。分钟级别数据我不会在策略初期碰一来数据量百倍增长二来分钟数据里的微观结构噪声会让AI模型学到大量“假规律”。等日线策略能稳定跑赢基准再考虑把周期降下来不迟。关于停牌处理核心原则是“宁可缺失不要填充”。停牌日的行情直接置为NaN让模型自己在样本里学习如何处理这种缺失比用前值填充或者插值更接近真实交易状态。另外ST股、上市不足一年的次新股在特征层就应加标记或直接剔除避免它们成为模型里的特殊性样本。2.2 标签工程AI到底在预测什么特征层里最容易被忽略的是把“预测目标”定义清楚。做多策略最常见的标签是未来N日收益率但我建议不要直接用原始收益率而是用“相对基准的超额收益”或者直接对收益做排名转化。举个例子你预测某只股票未来5天涨5%看起来很高但如果同期大盘涨了8%这只股票实际上是跑输大盘的做多它不如买指数。所以我的标签通常是这样构造的未来5日收益率减去同期大盘比如沪深300指数收益率再对这个超额收益做标准化或者分位数转化。这样模型学到的不是“涨跌”而是“相对强弱”。标签的计算窗口也要跟着策略持仓周期走。你想持有5天就用5日标签想持有20天就用20日标签。我早期犯过的一个错误是用5日标签训练但回测时让模型信号驱动20日调仓结果策略逻辑完全错位表现自然一塌糊涂。标签周期和调仓周期必须严格一致。2.3 特征列表与代码骨架不用多先用起来初学者容易犯“特征越多越好”的毛病我见过有人一上来就造200多个因子模型训完自己都不知道哪些在起作用。我的建议是最小可用特征集起步先跑通全流程。下面这套特征组合是我反复验证过、性价比很高的基础集滚动窗口的涨跌幅过去5日、10日、20日累计涨跌幅反映短期动量。波动率特征过去20日收益率的标准差反映风险水平。量价配合特征过去5日平均成交额相对20日平均成交额的比值反映资金活跃度。距离均线的位置收盘价相对20日均线的偏离度反映趋势状态。相对强弱特征个股过去10日涨跌幅减去大盘指数过去10日涨跌幅。代码上我习惯用Python结合pandas以向量化方式统一计算核心结构是这样import pandas as pd import numpy as np def calculate_features(df, index_returns): df df.copy() # 价格动量特征 for window in [5, 10, 20]: df[fret_{window}] df[close].pct_change(window) # 波动率特征对数收益率的标准差 df[log_ret] np.log(df[close] / df[close].shift(1)) df[vol_20] df[log_ret].rolling(20).std() # 量价配合特征 df[amount_ma5] df[amount].rolling(5).mean() df[amount_ma20] df[amount].rolling(20).mean() df[amount_ratio] df[amount_ma5] / df[amount_ma20] # 均线偏离度 df[ma20] df[close].rolling(20).mean() df[close_ma20] df[close] / df[ma20] - 1 # 相对强弱特征 df[excess_ret_10] df[ret_10] - index_returns.shift(0) # 删除未计算的空值行 df df.replace([np.inf, -np.inf], np.nan).dropna() return df这段代码生成的每一列都是模型可以直接吃的数值特征。实际生产中你会在全市场股票上循环调用这个函数然后合并成一张大宽表。需要特别注意pct_change和rolling都会在序列开头产生NaN最终一定要dropna否则模型会把NaN当缺失值处理影响训练结果。3. 第二层模型与信号骨架把预测变成能用的方向数据准备好了接下来就是整条流水线的核心模型层。这一层的任务是把特征宽表变成可交易的信号操作上分三步选模型、做训练验证、把预测值翻译成信号。3.1 模型选型别被“深度”两个字骗了A股日线级别的AI量化我的经验是“轻量模型优先”。树模型和线性模型在中小样本上往往比深度学习更可靠原因很简单日线数据的样本量以“股票数乘天数”计看似很多但独立有效样本其实有限深度学习动辄几万参数的模型很容易过拟合。我常用的是梯度提升树比如LightGBM它在表格类特征上有几个天然优势能处理特征间的非线性关系、对缺失值有内建处理、训练速度快、调参空间相对可控。Transformer这类模型不是不能用但需要非常大的数据量和非常精细的正则化设计不适合作为第一个跑通的方案。选模型还有个容易忽略的原则和你标签构造方式匹配。如果你的标签是“未来5日超额收益是否为正”这种二分类就选分类模型输出概率如果你的标签是连续超额收益就选回归模型输出数值。我自己更倾向用回归模型预测连续的超额收益然后排序选票因为分类会丢掉“涨多涨少”的信息量。3.2 训练验证老老实实按时间切别搞随机切分量化模型的验证和普通机器学习最大的区别在于数据有强时间相关性。如果你用随机打乱的方式切训练集和测试集模型会“偷看未来”测试集表现会虚高。正确做法是严格按时间顺序切分比如2018-2021年做训练2022年做验证2023年做最终测试。但只切一次也不够因为市场风格会漂移。我常用的方案是滚动训练滚动验证每6个月重新训练一次模型训练窗口48个月验证窗口6个月然后记录每次验证窗口的表现。这么做成本高一点但能比较真实地反映策略在风格变换下的稳定性。代码上核心训练流程非常短关键在数据切分方式from sklearn.ensemble import GradientBoostingRegressor from sklearn.metrics import mean_squared_error # train_df和valid_df是已经按时间排序好的特征宽表 feature_cols [ret_5, ret_10, ret_20, vol_20, amount_ratio, close_ma20, excess_ret_10] model GradientBoostingRegressor( n_estimators200, max_depth3, learning_rate0.05, subsample0.8, random_state42 ) model.fit(train_df[feature_cols], train_df[label]) valid_df[pred] model.predict(valid_df[feature_cols]) mse mean_squared_error(valid_df[label], valid_df[pred])这里一定要强调特征列和标签列必须来自同一个“时点”的截面数据也就是说特征用的是t时刻已知信息标签是tN时刻才揭晓的结果两者绝不能在同一行里同时用到未来数据。这是模型层最容易出未来函数的地方。3.3 从预测值到信号把模型输出变成仓位指令模型输出一个数字还不是信号。你的策略是做多还是多空都做是全市场打分选头部还是只对特定股票池操作这些需要在模型层末尾统一成标准信号格式。我的处理方式是每天对全市场股票做预测按预测值从高到低排序选前20%作为候选买入池后20%作为候选卖出池如果做对冲。然后用一个简单的置信度过滤预测值低于历史预测分布某个分位数比如30%分位的信号直接丢弃宁可不交易也不要乱交易。因为模型预测的误差在低置信区间非常大强行交易只会增加手续费损耗。这一步的输出格式我定义为标准的“信号表”每一行有三个字段——股票代码、信号方向1或-1或0、预测强度原始预测值。组合层只认这张表完全不关心模型内部长什么样。层与层的解耦就是从这种标准接口约定开始的。4. 第三层组合与执行骨架从信号到真金白银之间还有一段路模型给了信号别急着兴奋。从“信号”到“收益”之间隔着手续费、滑点、涨跌停买不进、仓位分配这些现实约束。这一层负责把这些全考虑进去输出一份落到资金上的答案。4.1 仓位管理信号不是让你满仓的理由很多第一次跑通模型的人都会问模型说某只股票要涨难道不是全仓买入吗我的回答是如果你只交易一只股票那本质上是在赌模型的单一预测胜率再高也顶不住一次黑天鹅。我的仓位分配习惯分两步首先按“信号方向”确定交易名单然后按“预测强度排名”来分配仓位权重。具体做法是等权重起始再结合波动率做调整——波动率高的股票给低权重波动率低的给高权重这就是最简单的风险平价思想。公式可以写成目标权重 基准权重 / 个股波动率然后再归一化。还要设定单票仓位上限我一般不超过总资金的10%。A股单日涨跌停限制使得任何一只票都可能出现连续跌停无法卖出的情况如果不做仓位上限一只票就能把整个组合拖垮。4.2 交易成本与撮合假设回测里最骗人的地方这一小节是整个策略从“纸上富贵”走向“真实可交易”的关键。我的经验很直接成本假设越保守回测越接近实盘。A股的成本至少包含三个部分佣金双边约万1到万3最低5元、印花税卖出单边千一近年的政策变化可能影响具体数值但计算框架必须保留这一项、滑点我通常按成交价偏移0.1%到0.2%估算。代码里我撮合时统一按“信号当日收盘价 滑点比例”成交而不是用次日开盘价或者盘中最高最低价。用收盘价的好处是简单可复现滑点比例用来补偿模型信号发布与实际成交之间的时间差。买卖都按这个逻辑绝不手软。给大家一个直观感受一个年化换手50倍的策略如果完全不考虑成本可能年化收益30%把双边万分之三佣金加上千一印花税算进去直接掉到18%再把滑点加上可能只剩12%。所以成本模型怎么设直接决定策略是真能赚钱还是幻觉。4.3 回测框架结构一张图说不清但代码能说清组合层的核心是一个事件循环每天拿到模型信号表根据当前持仓、可用资金、交易成本计算出今天要下的买单和卖单然后更新持仓和资金曲线。下面是我常用的回测主循环伪代码def run_backtest(signal_dict, price_dict, init_cash1000000): cash init_cash positions {} # {股票代码: 持仓数量} equity_curve [] for trade_date in sorted(signal_dict.keys()): # 1. 先处理卖出释放资金 for stock in list(positions.keys()): if stock not in signal_dict[trade_date] or signal_dict[trade_date][stock] 0: sell_price price_dict[stock][trade_date] * (1 - slippage) cash positions[stock] * sell_price * (1 - commission_rate) del positions[stock] # 2. 处理买入 buy_list signal_dict[trade_date] for stock, strength in buy_list.items(): # 根据策略计算目标仓位权重 target_value portfolio_value * weight_for(strength) available_value min(cash, target_value) buy_price price_dict[stock][trade_date] * (1 slippage) shares int(available_value / (buy_price * 100)) * 100 # A股按手买入 if shares 0: cost shares * buy_price * (1 commission_rate) if cost cash: cash - cost positions[stock] positions.get(stock, 0) shares # 3. 更新净值 portfolio_value cash for stock, shares in positions.items(): portfolio_value shares * price_dict[stock][trade_date] equity_curve.append((trade_date, portfolio_value)) return equity_curve真实的回测框架会比这段复杂得多比如要处理涨跌停无法成交、停牌无法卖出、分红送股对持仓的影响。但核心逻辑就是上面这三步卖、买、记账。回测完整跑完你会得到一条资金曲线然后要算年化收益、最大回撤、夏普比率、胜率、盈亏比这些指标。我特别关注最大回撤因为回撤过大即使最终收益不错你也很难在实盘中坚持下来。5. 常见问题与避坑实录我掉过的坑不希望你掉第二次做AI量化这大半年我踩过的坑写出来能凑一篇万字长文。这里挑几个最具代表性的按出现频率排序每一个都对应真实翻车案例希望能帮后来者省掉几周甚至几个月的弯路。5.1 未来函数回测曲线越漂亮越要警惕未来函数是量化回测里最隐蔽、最致命的坑。它的本质是你在t时刻的决策用到了t时刻之后才产生的信息。最常见的三种形式一是特征计算时用了未来数据比如用当天的收盘价决定当天的买入二是标签和特征没有在时间轴上对齐三是撮合时用了信号产生时刻之后的最优价格。我的检查方法是“错位一日”测试把信号整体往后平移一天再回测如果策略收益出现断崖式下跌说明原始回测很可能依赖了未来信息。这个测试简单但极其有效我在给策略做正式评估之前必跑一遍。注意未来函数导致的收益不是“运气好”而是程序逻辑错误。这种情况下实盘表现绝对不可能复现回测曲线。5.2 过拟合与幸存者偏差一套参数换一个时段就失效过拟合在AI量化里是常态不是意外。特征几十个、模型超参数一堆你以为自己在找“最优参数”其实很多时候只是在找一个恰好拟合历史噪声的组合。我的应对措施是三层防守第一限制模型复杂度LightGBM的max_depth控制在4以内树数量不超过300第二做参数敏感性测试把关键参数上下浮动20%如果策略表现剧烈波动说明它正在过拟合的边缘反复横跳第三做时段外验证训练集只用2018-2021年数据如果2022年之后的测试集表现和训练期差距巨大立刻回到模型层去减特征或者加正则化而不是去“优化参数以适配新数据”。幸存者偏差则是数据层面的坑回测时如果只用当前还在上市的股票那么退市股全被剔除了策略的真实收益会被系统性高估。现在很多数据源提供历史股票列表回测时必须使用每个时间点的存量股票池而不是今天的股票池。5.3 实盘前的最后一关小资金、慢动作、低预期就算回测曲线再漂亮我也建议先用极小资金跑一个月实盘同时记录每一笔交易的实际成交价和模型预期价之间的偏差。这个偏差数据能反馈滑点假设是否合理、信号延迟是否超出预期、实盘接口的推送是否稳定。我见过最典型的落差是回测里年化40%实盘因为滑点和频繁交易手续费实际连10%都不到。原因就是回测里用了理想化的撮合假设。所以我会在实盘初期故意把成本假设调高一倍来跑模拟盘如果高成本假设下策略还能正收益再考虑上实盘。这个方法救过我两次否则真金白银交的学费会贵得多。最后说点真正的心里话折腾AI量化大半年最大的体会不是哪个模型更准、哪个因子更有效而是“把流程拆干净”这件事本身的威力远超想象。三层骨架不是一个理论框架它是排错效率、迭代效率、心态稳定性的决定性因素。当你面对一条连续亏损的资金曲线能快速定位是数据问题、模型问题还是交易问题你才有机会稳住阵脚继续优化下去。这套方法里我还有很多细节没展开特征筛选的递归消除、模型概率校准、组合层更细致的风险控制这些我计划放在中篇继续写。下篇会更侧重Layer2和Layer3的进阶玩法包括更贴近实盘的撮合引擎和动态仓位策略感兴趣的可以先自己动手把这篇的代码跑通。