☰
AI量化策略三层骨架:从数据特征到模型信号再到组合执行
2026/10/21 7:40:19 网站建设 项目流程

做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[f'ret_{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_estimators=200, max_depth=3, learning_rate=0.05, subsample=0.8, random_state=42 ) 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时刻已知信息,标签是t+N时刻才揭晓的结果,两者绝不能在同一行里同时用到未来数据。这是模型层最容易出未来函数的地方。

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_cash=1000000): 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的进阶玩法,包括更贴近实盘的撮合引擎和动态仓位策略,感兴趣的可以先自己动手把这篇的代码跑通。

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

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

立即咨询