
如何开通基于 AI 打造的 A 股量化策略上三层策略骨架与代码落地“AI量化”这四个字最近几乎快被说烂了。但真有人问“那我到底该怎么从零开始搭一套能跑的AI量化策略”绝大多数人给出的答案要么是让你去看机器学习课程要么是丢给你一个封装好的黑盒框架跑完回测数据很好看真到实盘却完全不是那么回事。这篇文章不讲虚的我直接分享我拉通“AI A股量化”时踩出来的那条完整链路。今天先解决最核心的一件事先把三层策略骨架搭起来——信号层负责“买什么、卖什么”风控层负责“买多少、多少止损”执行层负责“成交清不清楚、滑点控制没控制”最后附上能直接改的 Python 代码。这篇主要面向两类人一类是有一定 Python 基础、想正经入行量化但不知道第一行代码写哪的另一类是已经手写过简单均线策略、但发现“加了一个AI模型之后就不知道怎么和原来那套交易逻辑拼起来”的朋友。看完你至少能搭出一个从“行情数据进”到“委托单出”的完整策略雏形跑通一次模拟盘闭环。1. 三层策略骨架先想清楚系统怎么解耦1.1 为什么我们最先搭骨架而不是先找模型很多人一上来就抱着“我要用深度学习预测股价”的心态开始写代码结果通常都在数据清洗阶段就放弃了。我自己的教训是A股量化、特别是偏中低频的策略真正的挑战从来不是“模型不够聪明”而是“当一个信号产生时整个链路能不能干净、及时、可控地把它变成一笔交易”。最早我写策略所有逻辑堆在一个文件里拉数据、算因子、出信号、算仓位、模拟下单全揉在一起。初看挺爽改个参数重跑一遍也就几分钟。但问题很快暴露回测和模拟盘的逻辑混在一起模型一旦要换特征或者想对比两套止损规则就得复制粘贴一大堆代码改到后面自己都分不清哪段逻辑在生效。后来我把系统重新按三层拆开整个思路瞬间就清晰了信号层根据数据输入输出一个“看好/看淡/中性”的判断以及对应的置信度。风控层拿到信号和当前持仓输出“今天实际要交易多少仓位、触发什么条件必须撤”。执行层根据风控层的指令结合盘口和费用约束生成实际委托单。三层之间要数据流向下传递不反向依赖。因为只有把这三层彻底解耦你才能做到“换一个模型不换风控、换一套风控不动信号、改一个执行参数不影响策略逻辑”。1.2 信号层解决“买什么、卖什么”信号层是整个策略的“大脑”它的输入是行情、基本面、舆情、乃至盘口数据输出是方向判断和强度打分。注意这里的输出不直接等同于买入或卖出它更像一个“分析师”只表达观点不负责动手。为什么要把“观点”和“决策”分开因为在实操中你会发现AI模型给出的原始输出往往是很原始的数值比如某个分类概率、回归预测的期望收益率。如果直接把这种数值拿去下单经常会遇到“模型说涨2%结果仓位算出来占用了90%资金”的荒谬情况。所以信号层一般还要做一道“语义化”加工把模型的数值输出映射成1看好、-1看淡、0观望三档同时附带一个0到1的置信度供风控层使用。我第一次搭信号层时犯过一个典型错误直接把 XGBoost 的分类概率当信号概率大于0.5就买入概率小于0.5就卖出。看上去很合理但实际回测下来换手率极高手续费把收益全吃光了。后来我在信号到交易的中间加了一个“死区”dead zone概率在0.55到0.6之间统统观望才把换手压下来。这就是信号层存在的意义——它天然承担了“过滤噪音”的责任。1.3 风控层解决“买多少、什么时候撤离”风控层是很多散户写策略时最不重视、但对长期收益影响最大的一层。它要回答三个问题第一给定信号的置信度当前应该下多少仓位第二持仓万一走反什么价位必须止损第三整个账户净值回撤到什么程度策略必须降杠杆甚至停摆。这一层设计得好不好直接决定策略的“存活率”。我见过不少刚入行的朋友信号模型做得非常花哨LSTM加注意力机制都用上了但仓位永远是单票满仓止损从来不止结果一次极端行情就回到解放前。其实A股本身有涨跌停、T1、部分股票流动性一般等约束风控层如果不能主动去适应这些规则再聪明的信号也救不了账户。风控层的一个核心思想是所有风控规则必须独立于当前信号。你不能因为“这个信号特别强”就临时跳过止损线那是情绪化交易的根源。我在代码里实现风控层的习惯是先让所有规则“无脑执行”哪怕空仓也要跑一遍风控检查逻辑——这样能保证规则的完备性不会在极端行情下被漏掉。1.4 执行层解决“以什么成本买到”执行层是最容易被新手忽略、但同样是最能体现系统是否“开通”了的一环。信号层说“可以买”、风控层说“买3成仓位”执行层要负责把它变成“明天开盘后以不超过XX价格买入XX股”这样一条可落地的委托。A股的执行约束非常多最小交易单位是100股科创板是200股起、之后按1股递增、买入申报价不能超过涨停价、T1规则下当天买进的股票当天不能卖。这些细节如果不在执行层处理好回测里利润丰厚实盘一跑就各种废单。所以完整的执行层应当至少包含根据可用资金和最小交易单位计算真实买入数量根据当前价和可容忍滑点计算限价根据T1规则判断某只票是否真的“可卖”必要时把大单拆成小单避免对流动性不足的标的造成冲击。2. 数据准备与AI信号生成让模型先能“开口说话”2.1 数据源的接入与清洗三层骨架里信号层的第一步是拿到干净的数据。对A股日线级策略来说最基础的数据就是“OHLCV”开盘、最高、最低、收盘、成交量。国内有很多开源数据接口数据质量参差不齐我建议优先选择提供“前复权”数据的接口。原因很简单如果股票发生过除权除息不复权的K线会出现向下的跳空缺口直接拿去算收益率特征会得到大量虚假的“暴跌”信号。用前复权数据处理历史回测再用“不复权价格 除权除息日修正”来撮合模拟盘是业界基本打法。拿到数据后我一般还会做三层清洗剔除上市不足半年的次新股这些股上市初期波动异常很多因子会失真。剔除ST和长期停牌股它们流动性差、涨跌停规则特殊初期接入容易带偏整体评估。对齐交易自然日把行情数据按交易日对齐非交易日的数据如果接口返回了直接丢弃。第一步的数据加载不用写太多花活核心是把“日期索引 前复权价格 成交量”整理好供因子计算使用。import pandas as pd def load_daily_data(code, start2018-01-01, end2024-12-31): # 这里是一个典型的开源数据接口封装示意实际使用时换成你的数据源即可 df get_daily_price(code, start_datestart, end_dateend, adjustqfq) df df[[date, open, high, low, close, volume]].copy() df[date] pd.to_datetime(df[date]) df df.set_index(date).sort_index() # 剔除停牌日成交量或成交额为0的行 df df[(df[volume] 0) (df[close] 0)] return df2.2 特征工程与标签构造有了K线数据下一步是构造特征。特征不用贪多我一开始只用了三组动量类过去5、10、20个交易日的累计收益率衡量趋势强度。波动类近10、20日收益率标准差衡量不确定性。量价配合类近5日量能变化率和价格变化率的比值衡量放量上涨还是缩量下跌。这三组特征加起来不到10个足够让模型学出一些简单的非线性关系。真正重要的是标签怎么构造。我的经验是用“未来5个交易日收益率为正与否”作为二分类标签比“未来1日收益率方向”稳定得多。5日窗口能过滤掉大量日内噪音也更贴近A股投资者真实持有周期。标签构造有一个最关键的细节防止未来函数泄漏。当你用第T日的数据做特征去预测第T1到T5日的收益方向时标签里绝对不能包含第T日当天收盘前无法获得的信息。很多初学者在同一个DataFrame里既算特征又算标签忘记用shift把标签整体往后挪一天导致回测时模型“偷看”了未来数据。这类泄漏在实盘里根本无法复制属于量化新手最容易踩的暗坑。def build_dataset(df): x df.copy() x[ret_1] x[close].pct_change() x[ret_5] x[close].pct_change(5) x[ret_10] x[close].pct_change(10) x[ret_20] x[close].pct_change(20) x[vol_10] x[ret_1].rolling(10).std() x[vol_20] x[ret_1].rolling(20).std() x[vol_ratio] x[volume] / x[volume].rolling(5).mean() x[price_vol_ratio] x[ret_5] / (x[vol_10] 1e-8) # 标签未来5日收益率是否为正。shift(-5) 表示取未来第5天的收益 x[future_ret] x[close].shift(-5) / x[close] - 1 x[label] (x[future_ret] 0).astype(int) # 特征必须在T日收盘时可得所以特征本身不需要shift # 但建模时要让模型用T日特征预测T1T5的标签这里通过时序切分控制 return x.dropna()2.3 用XGBoost生成方向信号特征和标签准备好之后模型选型我推荐先用 XGBoost。很多人觉得AI量化必须上深度学习其实中低频表格数据场景树模型往往更快、更稳、更好解释。我拿LSTM和XGBoost在同样特征集上比过XGBoost的 AUC 普遍高一些而且调参成本低得多。训练时我用了一个非常重要的切分方式时间序列按顺序切而不是随机切。因为股价数据天然是时序的如果随机把未来某一年数据混进训练集相当于模型提前见到了“答案”。我的习惯是前70%做训练、中间15%做验证、最后15%做测试验证集用来调超参数和确定买入阈值测试集只在最后跑一次避免反复试出来的“看上去很美”的结果。from xgboost import XGBClassifier features [ret_1, ret_5, ret_10, ret_20, vol_10, vol_20, vol_ratio, price_vol_ratio] full build_dataset(load_daily_data(000001)) train_end int(len(full) * 0.7) valid_end int(len(full) * 0.85) x_train, y_train full.iloc[:train_end][features], full.iloc[:train_end][label] x_valid, y_valid full.iloc[train_end:valid_end][features], full.iloc[train_end:valid_end][label] x_test, y_test full.iloc[valid_end:][features], full.iloc[valid_end:][label] model XGBClassifier(max_depth3, n_estimators100, learning_rate0.05, subsample0.8, random_state42) model.fit(x_train, y_train)这里我特意把random_state固定。为什么A股行情是确定的但模型训练过程中存在随机性如果你不固定随机种子今天跑出一个结果、明天跑出另一个结果后面所有风控参数调整都会变得毫无意义。固定随机种子是最基本、也最容易被忽略的实验纪律。2.4 从模型概率到交易信号模型输出的是“未来5日上涨”的概率不能直接拿来下单。我的做法是把它转换成一个带死区的离散信号。具体来说在验证集上跑出所有样本的概率分布然后挑选两个阈值当概率大于等于0.62时给1看多概率小于等于0.42时给-1看空中间一律给0观望。这两个阈值不是拍脑袋定的而是让验证集的F1分数尽量高、同时换手率尽量低。这一步本质上是压缩模型的自由度模型不必每一次都给出明确观点一个“犹豫”的0信号能够大幅减少无效交易带来的摩擦成本。prob model.predict_proba(x_valid)[:, 1] k_threshold_high 0.62 # 得在验证集上反复调 k_threshold_low 0.42 signal pd.Series(0, indexx_valid.index) signal[prob k_threshold_high] 1 signal[prob k_threshold_low] -1这里也顺带说明一个我常被问到的问题为什么阈值设得这么不对称因为A股有T1约束和做空限制没有办法直接做空所以“看空信号”更适合被解释为“空仓等待”而不是“卖出做空”。这样一来看空阈值可以设得保守一些只有模型非常有把握时才应该清仓而看多阈值对应的是可以入场的较强预期需要兼顾置信度和交易次数。3. 风控与仓位管理保证策略的“活下去”3.1 仓位计算公式信号层输出之后风控层要接住。我最常用的仓位模型是基于“置信度缩放”的固定比例仓位先定一个单票最大仓位上限比如10%然后乘以信号置信度得到实际仓位。置信度我用的是模型概率与阈值之间的“距离”距离越大说明模型越有把握持仓可以略微加重距离小则只给底仓。举个例子看多阈值为0.62当前概率0.8距离是0.18那么仓位调整系数可以是1 0.18 * 2 1.36再乘上最大仓位10%得到约13.6%的仓位如果概率只有0.63距离很小那仓位就只有10%附近。这么做的好处是模型大部分时间不会满配一旦行情超出预期账户还有加仓空间行情不及预期亏损头寸也相对可控。def calc_position(max_weight, prob, threshold_high): distance max(prob - threshold_high, 0) scale 1 distance * 2.0 weight max_weight * min(scale, 1.5) return round(weight, 4)这里需要提醒一句任何单票仓位上限都不能超过账户可承受的单笔亏损阈值。如果你单票最多只能亏总资金的1%且止损位距离入场价5%那最大仓位就不能超过20%。仓位和止损是两个天然绑在一起的旋钮单独拧一个都会出事。3.2 ATR止损与移动止损止损规则我在A股中低频策略里最常用的是ATR止损。ATR平均真实波幅衡量的是个股近期的日内真实波动范围比固定百分比止损更能适应不同波动率的股票。比如说你定了2倍ATR止损意思是从买入价向下2倍的日波幅才止损股票波动大就容忍多一点波动小就严格一点。实际计算ATR需要用到最高价、最低价和前一收盘价。计算方式不复杂关键是回测和实盘都要用同一套算法不能回测时用日线ATR、实盘时用5分钟ATR逻辑全错位。def calc_atr(df, period14): prev_close df[close].shift(1) tr np.maximum(df[high] - df[low], np.maximum(abs(df[high] - prev_close), abs(df[low] - prev_close))) return tr.rolling(period).mean()有了ATR止损价就是入场价 - 止损倍数 * ATR。如果信号还没失效但价格一路上涨我建议把止损失水平移到“入场后最高价 - 2倍ATR”这样能锁定利润同时给趋势足够空间。我见过很多人设了止盈线一涨到目标价就卖飞结果放着放着立刻翻倍了。移动止损比固定止盈更符合趋势交易的逻辑。3.3 组合级回撤熔断除了单票止损组合层面还得有一个总体的“熔断机制”。我的规则很简单如果账户净值距离最近高点的回撤超过15%次日开始强制空仓5个交易日等情绪冷却之后再重新开放交易。回撤一旦接近20%我再强制空仓10个交易日。空仓期间信号继续记录但不产生委托相当于给策略一次“重新冷静”的机会。这套规则的设计逻辑是当策略连续亏损到一定幅度大概率是市场风格发生了剧烈切换此时模型产出的信号可信度大幅下降。继续硬扛只会让亏损继续放大不如干脆停下来观察。风控层里面回撤熔断是最能避免“深度学习模型在某一年翻了车、却把前三年利润全部回吐”悲剧的防线。def check_drawdown(net_value, max_dd0.15): peak np.maximum.accumulate(net_value) dd net_value / peak - 1 if dd.iloc[-1] -max_dd: return {action: cut_all, reason: portfolio_drawdown} return {action: normal, reason: }回撤熔断不是越多越好。我见过有人把阈值设成5%结果一年内大部分时间都在空仓彻底错过了所有反弹。阈值本身要和策略的波动特征匹配验证集上最大回撤如果常年是12%那阈值就别放到10%如果策略最大回撤经常18%那15%才是合理位置。4. 执行层与回测验证让策略从纸面走向“模拟盘”4.1 从信号到委托单信号和仓位都算好之后执行层要把“该买某只股票10%仓位”翻译成“以什么价格、买多少股、哪一天能生效”。我处理A股模拟盘的经验是信号在第T日收盘后产生那么委托单最早只能挂第T1日。也就是说策略要有一个强制性的“次日生效”逻辑。千万别在回测里用当天的收盘价直接成交那是典型的未来函数。代码里最简单的处理是把信号整体shift(1)让第T日算出来的目标仓位第T1日才参与撮合。股数计算也要考虑A股的“一手100股”。先根据目标市值算出理论股数再向下取整到100的倍数。资金不足时剩余资金宁可留着当现金也不能为了满仓去凑零股。def calc_share_count(target_value, current_price, lot_size100): raw_shares int(target_value / current_price / lot_size) * lot_size return max(raw_shares, 0)除此之外盘口约束必须硬性编码。比如买入价不能高于涨停价卖出价不能低于跌停价如果当前价已经涨停意味着大概率买不进去这时候委托单要直接作废不能假装成交。4.2 回测引擎的搭建在回测层面我建议先把“向量化回测”跑通再做事件驱动回测。向量化回测适合快速验证策略逻辑对不对它假设所有信号在同一时间点批量作用在所有股票上省去逐笔撮合的复杂度。比如你有20只股票每只股票每天都有仓位目标直接按持仓市值变化计算组合净值即可。向量化回测最大的优点是快、清晰适合每天磨信号和仓位参数。但它的先天缺陷是忽略了冲击成本和个股流动性差异所以回测净值只能当参考。等策略逻辑基本定型我会再写一个按日循环的模拟盘脚本逐日根据信号下单成交价采用“次日开盘价 滑点估计”来模拟这才是真正接近实盘环境的验证。position target_weight.shift(1) # T日信号T1日生效 ret daily_stock_ret 1 portfolio_ret (position * ret).sum(axis1) (1 - position.sum(axis1)) # 剩余现金部分无收益 net_value (1 portfolio_ret).cumprod()4.3 交易成本与滑点模拟盘里交易成本必须包含佣金、印花税和滑点。佣金按成交金额的万二到万三估算买卖双向都收印花税只在卖出时收目前是千一滑点按买入价上浮0.1%、卖出价下浮0.1%估算。别小看这三个0.1%级别的数字中低频策略一年的换手如果超过10倍成本合计就是总资金的3到5个点。很多回测里年化20%的策略扣完成本只剩10%这非常现实。def estimate_trade_cost(buy_amount, sell_amount, commission_rate0.0003, stamp_tax0.001, slippage0.001): buy_cost buy_amount * (commission_rate slippage) sell_cost sell_amount * (commission_rate stamp_tax slippage) return buy_cost sell_cost4.4 评估指标怎么读最后回测结果不能只看“最终收益”。我最关注的指标有三个年化收益、最大回撤、夏普比率。其中最大回撤决定了你的心理承受能力和加杠杆空间如果最大回撤30%说明策略和你的风险偏好根本不匹配再高的年化也别碰。顺便说一句胜率这个指标很容易误导人。我的经验是如果平均盈亏比能到1.5以上胜率只有40%的短线策略依然能赚钱如果胜率70%但盈亏比只有0.5长期下来大概率亏。所以回测报告里我会把“平均盈利/平均亏损”和“胜率”并列看缺一不可。def evaluate(net_value): annual_return net_value.iloc[-1] ** (252 / len(net_value)) - 1 daily_ret net_value.pct_change().dropna() sharpe daily_ret.mean() / daily_ret.std() * np.sqrt(252) max_dd (net_value / np.maximum.accumulate(net_value) - 1).min() return {annual_return: annual_return, sharpe: sharpe, max_drawdown: max_dd}5. 常见问题与排查技巧实录5.1 未来函数的三个典型雷区这是量化新手最容易犯也最隐蔽的错误。我复盘自己踩过的雷总结出三个高频雷区第一个标签泄漏。前面提过构造未来收益标签时忘记整体后移导致模型提前学到了“未来信息”。判断方法很简单用训练好的模型在测试集上算AUC如果高到离谱比如超过0.75大概率有泄漏。第二个成交价用未来价。很多人回测时直接拿“当日收盘价”买入但信号是收盘后才产生的实盘根本买不到当天的收盘价。正确做法是信号T日出买入价至少是T1日的开盘价。第三个幸存者偏差。选股票池时如果直接用“当前还在上市的股票”就相当于剔除了过去几年退市的股票。回测结果看起来很好实际是漏掉了大量失败案例。正确做法是用历史某一天的成分股名单去回测那一天而不是用今天的名单覆盖全部历史。5.2 过拟合的识别AI量化里过拟合的典型表现是“训练集收益一片大好、验证集收益断崖下跌”。我的排查办法是看验证集和训练集的年化收益差如果差距超过50%或者直接由正转负先别急着怪市场大概率是特征或者模型参数过拟合了。常用的缓解手段包括增加样本内时间跨度、减少特征数量、限制数模型深度XGBoost的max_depth别超过4、增大subsample随机采样比例、把连续型收益率标签改为排序型标签等。但最粗暴也有效的一招是验证集指标不达标绝对不回测模型换参数。我之前就是因为太着急反复在测试集上调参最后测出来的结果完全失真。5.3 数据对齐与停牌处理A股停牌非常常见尤其重组停牌动辄一两个月。如果回测时没有对停牌做任何处理停牌期间持仓的每日收益可能是0看上去“回避了下跌”也可能“错过了上涨”结果严重失真。我的处理方法是把停牌日的收益标记为0同时在执行层把停牌股票的委托单自动挂起直到复牌才继续处理。数据对齐还有一个隐蔽问题不同数据源的交易日历不一样。有的接口会把节假日非交易日也保留有的会剔除。我统一的做法是把所有股票数据对齐到同一个交易日历上以全市场成交量最大的那些股票的交易日作为基准。我把这套三层骨架在模拟盘上完整跑通之后最大的体会是AI量化真正难的不是模型而是把模型输出放到一个受控的交易链路里。信号层负责“聪明”风控层负责“活着”执行层负责“落地”只要这三层界限清晰后面模型哪怕换成图神经网络或者说加一个舆情因子都只是替换其中一块积木而已。这篇是系列的上篇先把骨架和数据到信号这一段走通。后面有空我会单独写风控参数那部分——尤其是ATR止损倍数怎么根据你交易频率去调那又是一个能讲一整篇的话题。