简介:这份资源是一份以量化交易为核心的 PDF 文档,面向对 Alpha 因子挖掘、策略建模与投资组合风险管理感兴趣的金融从业者、量化初学者及相关专业学生。内容系统讲解量化交易的定义与特点,梳理其从 20 世纪 60 年代至今的发展脉络,并重点覆盖 Alpha 的定义、统计/机器学习/数据挖掘三类挖掘方法、回测验证与优化思路,以及市场风险、模型风险等风险类型与管控方式。资源为单个 PDF 文件,压缩包共 1 个文件,大小约 285KB,便于直接阅读与收藏。内容层次清晰,结合均值回归、深度学习等实际案例说明策略构建逻辑,已有 1114 人下载学习。阅读后可建立从数据驱动决策、模型化执行到风险控制框架的完整认知,适合用于快速入门整体量化体系或作为策略研究的概念参考。
1. 量化交易策略的完整链路:Alpha发现只是前半场
量化交易策略从想法到实盘,最容易被低估的不是找信号,而是把信号变成一条可信的净值曲线。标题里"从Alpha发现到风险管理"这句话,本质上是把一套策略切成两半:前半段解决"凭什么赚钱",后半段解决"怎么保证亏的时候死不了"。很多刚接触量化的开发者,把全部精力压在挖因子上,觉得只要找到一个有效的Alpha,策略就成了。实际做下来你会发现,真正让策略长期活下来的,往往是回测有没有埋雷、参数是不是过拟合、仓位怎么分配、回撤能不能扛住。这篇文章就按这条链路往下拆:先讲Alpha怎么从想法变成可检验的信号,再讲回测怎么做才不骗人,然后讲风险管理怎么落地,最后把真实数据上最容易翻车的几个坑挨个过一遍。
2. 因子挖掘与Alpha发现:把灵感变成可检验的信号
Alpha在日频多因子框架下,通常表现为一个可以给标排序的因子。不管你是看动量、低波动、毛利率,还是某个另类数据指标,它要成为Alpha,必须能反复通过统计检验。靠感觉画出来的一条曲线不叫Alpha,叫过拟合的巧合。
2.1 因子不是拍脑袋:先过这三道检验
一个候选因子至少要回答三个问题。第一,它在历史上是否稳定地预测了未来收益?第二,这种预测能力是不是少数几天、少数几只股票撑起来的?第三,扣掉交易成本和滑点之后还剩下多少收益?
第一个问题的常用量化指标是IC,也就是因子值和未来N日收益的秩相关。我一般按交易日做截面相关,再取时间序列均值。这里用秩相关而不是普通皮尔逊相关,是因为因子值常有极端值,皮尔逊相关会被拉偏,而投资组合真正用的本来就是排序,秩相关和实际逻辑更吻合。第二个问题看IC的标准差,IC均值高但波动大,说明赚钱靠运气;用IC把两者结合,IC均值除以IC标准差,得到的ICIR才是衡量因子稳定性的关键。第三个问题看分层回测和扣费后的收益:把标的分成十组,看多空组合收益是不是单调递增,以及换手率对应的成本是否吃掉超额收益。
2.2 计算因子IC:一段可以直接复用的Python代码
因子检验第一步就是把IC算出来。我习惯把不同标的历史因子值整理成一张表,行是日期,列是标的代码,未来收益也整理成同样结构,直接逐日计算相关:
import pandas as pd def calc_ic(factor_df, ret_df, min_sample=30, method='spearman'): # factor_df: 每个交易日一个截面,index为日期,columns为标的代码 # ret_df: 未来N日收益率,结构与factor_df对齐 # min_sample: 当日有效样本数低于该值则跳过 ic_list, date_list = [], [] for dt in factor_df.index: f = factor_df.loc[dt] r = ret_df.loc[dt] valid = f.notna() & r.notna() if valid.sum() < min_sample: continue ic_list.append(f[valid].corr(r[valid], method=method)) date_list.append(dt) ic = pd.Series(ic_list, index=date_list) return ic, ic.mean(), ic.std(), ic.mean() / ic.std()这段代码把每个交易日的截面因子值和截面未来收益做相关,得到一条IC的时间序列。逻辑上避免了把所有时间混在一起算相关——那会把不同市场环境下的人群差异吃掉。返回的四个值按顺序是IC序列、IC均值、IC标准差和ICIR。method参数默认spearman,稳健优先;如果你确信因子值已经做了严格的去极值和标准化,可以切换method='pearson',但实战里我很少这么干。min_sample这个参数很关键,样本太少算出来的相关没有任何意义,日频股票截面我一般设30起步,你要是做期货CTA,一篮子标的本身就只有几十个,可以放宽到10到15。
2.3 因子预处理:去极值、标准化、中性化
原始因子值不能直接进模型。市值因子天然偏态,一个超大盘股就能主导排序;另类数据里也常有异常值,比如某天成交量的突刺。我常用的预处理工序有三步:MAD去极值、z-score标准化、行业和市值中性化。
import numpy as np def mad_winsorize(factor, n=5): # factor: pd.Series,某个截面上的因子值 # n: MAD倍数,超过该范围的极端值被截断到边界 med = factor.median() mad = (factor - med).abs().median() if mad == 0: return factor lower = med - n * 1.4826 * mad upper = med + n * 1.4826 * mad return factor.clip(lower, upper)MAD是绝对中位差,比标准差稳健,它本身不受极端值影响,因此用MAD去圈定极端值不会出现"极端值把标准差拉大,反而没被截掉"的尴尬。1.4826是把MAD换算成标准差的常数,n=5对应大约5个标准差。n越大越宽松,越小越严格,我用n=5处理财务类因子,用n=3处理量价类因子,因为量价因子噪声更大。要注意的是mad==0时需要直接跳过,不然会除零,这种情况常见于停牌导致大量标的因子值相同。
标准化在去极值之后做,把截面因子减均值除以标准差,转成z-score,让不同量纲的因子可以比较。中性化则是把因子值对行业哑变量和log市值做线性回归,取残差作为新的因子值,目的是剔除风格和行业暴露,防止你以为发现了Alpha,其实只是押中了某个行业。这段回归逻辑用最小二乘就能跑:
def neutralize(factor, industry_dummies, market_cap): # industry_dummies: 行业哑变量矩阵,行与factor对齐 # market_cap: 市值列,通常取log后放入回归 X = pd.concat([np.log(market_cap), industry_dummies], axis=1) X.insert(0, 'intercept', 1.0) beta = np.linalg.lstsq(X.values, factor.values, rcond=None)[0] resid = factor.values - X.values @ beta return pd.Series(resid, index=factor.index)lstsq是稳定的最小二乘实现,比直接求逆更不容易被共线性搞爆。残差的含义是"剔除了行业和市值影响后,因子剩下的那部分预测能力"。做完中性化,因子IC通常会有明显变化——有时变好,有时变差,如果变差很厉害,你要警惕这个因子本质上是某个风格的伪装。
2.4 多因子合成:用ICIR当权重,别凭感觉拍
单因子覆盖的信息有限,实战里都是多因子合成。最简单的做法是等权合成,把几个z-score因子平均。等权的好处是稳健、不容易过拟合,坏处是让质量差的因子拉低整体。我一般用ICIR加权,让预测更稳定的因子拿更高权重:
def icir_weight(icir_dict): # icir_dict: {因子名: ICIR值} w = pd.Series(icir_dict) w = w[w > 0] # ICIR为正才保留 return w / w.sum() def combine_factors(factor_zscores, weights): # factor_zscores: {因子名: 标准化后的因子DataFrame} # weights: 由icir_weight得到的权重Series combined = 0 for name, w in weights.items(): combined = combined + w * factor_zscores[name] return combined注意权重的计算窗口不能和样本内检验用同一段数据,否则又是前视偏差。我一般用过去24个月滚动算ICIR,再用最近一个月的ICIR值去合成下个月的因子。另一个细节是因子之间的相关性要定期看,两个ICIR都很高的因子如果相关性到0.8以上,本质上是同一个Alpha,合进去只会放大风险,不会增加信息。
3. 回测与参数设定:先让历史数据不撒谎
信号合成之后,下一步是回测。回测的目的不是让你看到一条漂亮的净值曲线,而是尽可能真实地模拟"如果当时按这个规则交易,结果会怎样"。很多策略死在回测这一步,因为回测里埋了未来函数和成本低估。
3.1 向量化回测与事件驱动回测:起步选哪个
常见做法有两种。向量化回测把整个时间序列一次性算完,所有信号、持仓、收益都是数组运算,速度快、代码短,适合日频、上百只标的的策略验证。事件驱动回测按交易日逐个处理信号,能精细模拟停牌、涨跌停、撮合排队这些撮合细节,更贴近实盘,但代码复杂度高一个量级。
我一般这样选:策略刚成型时用向量化,先把逻辑跑通、参数调顺;等到准备小资金实盘前,再用事件驱动回测做一次"实盘校验"。不要一上来就搭事件驱动框架,你很可能在框架上花两周,最后发现策略本身是废的。
| 维度 | 向量化回测 | 事件驱动回测 |
|---|---|---|
| 速度 | 快,秒级出结果 | 慢,逐日逐事件处理 |
| 代码量 | 很少 | 多,需要维护撮合和订单状态 |
| 停牌/涨跌停处理 | 粗糙,需手动绕开 | 天然支持 |
| 适用场景 | 因子研究、参数初筛 | 实盘前验证、高频策略 |
3.2 最小可跑的回测代码:信号转持仓再转净值
我最常用的向量化回测,核心就一个函数。它的输入是目标仓位信号和收盘价,输出是策略净值。关键点在于信号必须shift(1),这是为了防止前视偏差——当天收盘才生成的信号,最早只能次日开盘执行:
import pandas as pd import numpy as np def make_signal(close, fast=5, slow=20): ma_fast = close.rolling(fast).mean() ma_slow = close.rolling(slow).mean() signal = (ma_fast > ma_slow).astype(float) # 多头为1,空仓为0 return signal def backtest_daily(signal, close, cost=0.002): # signal: 每日收盘时的目标仓位,范围[0,1]或[-1,1] # close: 每日收盘价序列 # cost: 单边换仓成本,包含手续费和滑点 pos = signal.shift(1).fillna(0) # 次日生效 ret = close.pct_change().fillna(0) trade = pos.diff().abs() # 换仓量 port_ret = pos * ret - trade * cost # 收益减换仓成本 nav = (1 + port_ret).cumprod() return nav逻辑上,pos乘以当日收益得到持仓盈亏,trade乘以cost得到换仓损耗。pos.diff().abs()同时涵盖加仓和减仓,因为减仓也要付手续费和滑点。cost参数是单边成本,包含佣金、印花税和滑点加起来的一个综合值。这个模型用收盘价近似成交价,会略微乐观——但作为起步阶段的筛选工具完全够用。真要精细,就改成"次日开盘成交",把pos的shift和开盘价对齐。
3.3 三个必调参数:成本、滑点、涨跌停处理
回测里最影响可信度的三个参数,我按重要性排:换仓成本、滑点、涨跌停处理。
换仓成本是最容易让回测失真的地方。A股做股票,双边佣金加印花税加滑点,我压测时至少按单边千分之二到千分之三算。很多新手按交易所佣金费率万几去设,回测里交易又多,出来的净值高得吓人,实盘一跑就原形毕露。一个习惯做法是:把成本参数调高一倍,如果策略收益还在,说明它能扛摩擦;如果收益直接变负,那实盘大概率也活不了。
滑点要分场景设。日内高换手策略,比如每天换仓30%以上,滑点至少要按成交价的千分之一到千分之二算;日频低换手策略,千分之零点五也够了。另类数据类因子要特别小心,信号拥挤的标的滑点远远大于普通标的。
涨跌停处理不能省略。股票涨停时你买不进去,跌停时你也卖不掉,回测如果假设所有信号都能成交,极端行情里会高估收益。向量化阶段我一般做一层简单过滤:信号目标持仓的股票当天涨停,就把仓位顺延到下一个交易日;跌停反之。事件驱动框架里这是天然支持的,这也是为什么实盘前要做一次事件驱动复核。
4. 风险管理落地:仓位、熔断与监控指标
信号负责赚钱,风控负责活着。很多人把风控简单理解成"止损",其实完整的风险管理至少包括三件事:仓位怎么定、回撤怎么熔断、每天该盯什么指标。把这三件事写成函数,放进策略流程里,才算把标题里的"风险管理"真正落地。
4.1 仓位管理:目标波动率加风险平价
先给结论:固定比例仓位是最差的做法之一,因为它完全忽略了市场状态。行情好的时候你只敢开三成仓,波动大的时候你又满仓硬扛,收益和风险全凭感觉。我常用的替代方案是目标波动率仓位:先估算策略当前的风险水平,然后把仓位调整到让组合波动率逼近某个目标值。
target_vol = 0.15 # 目标年化波动率,按你的风险承受力定 # 仓位 = 目标波动率 / 滚动估计的策略波动率 # 滚动窗口用20个交易日,年化系数252 vol = daily_ret.rolling(20).std() * np.sqrt(252) pos_vol = (target_vol / vol).clip(0, 1)这套逻辑背后的道理是:风险低的时段多下注,风险高的时段少下注,长期下来单位风险获得的收益更稳定。参数target_vol承担了调仓的"野心",15%年化波动对应稳健型,25%对应激进型。要注意clip(0,1)把杠杆限制在0到1之间,如果目标波动率选得比策略实际波动率还高,pos_vol整天是1,风控等于没起作用。
如果是多策略组合,我会在子策略之间用风险平价摊仓位。子策略目标仓位与其波动率成反比,让每个策略对组合的风险贡献大致相等。这一步的作用是防止某个高波动策略悄悄占掉整个组合的波动预算。
4.2 回撤熔断:给策略加一道半自动保险
仓位管理解决的是"平时该下多大注",回撤熔断解决的是"出问题时怎么活下来"。我习惯写一个统一的风控函数,同时叠加波动率仓位和回撤熔断:
def risk_control(nav, daily_ret, target_vol=0.15, max_dd=0.20): # nav: 策略原始净值序列 # daily_ret: 策略日收益序列 # max_dd: 允许的最大回撤,超过则仓位逐步归零 dd = nav / nav.cummax() - 1 # 当前回撤,负值 vol = daily_ret.rolling(20).std() * np.sqrt(252) pos_vol = (target_vol / vol).clip(0, 1) pos_dd = (1 + dd / max_dd).clip(0, 1) # 回撤越深仓位越小 pos = (pos_vol * pos_dd).fillna(0) return pospos_dd的曲线是这样的:回撤为零时它等于1,回撤达到max_dd时它等于0,中间线性下降。这么做比"回撤超过5%直接清仓"更柔和——直接清仓容易在回撤底部砍仓,等反弹时又重新追进去,来回打脸。线性降仓至少给了策略一个恢复期。
把它接进回测和实盘流程里,只需要在生成目标仓位后乘上这个风控系数。注意fillna(0)处理了数据前20天波动率算不出来的情况,宁可空仓也不要在没有风险估计时满仓。
4.3 风险指标盯哪些:从夏普到卡玛的取舍
每天复盘看什么,决定了你第二天怎么改策略。我自己的盯盘清单就四个数:最大回撤、夏普比率、卡玛比率、换手率。夏普比率衡量的是单位波动率的超额收益,卡玛比率用的分母是最大回撤,更贴合"我最多会亏多少"这个真实恐惧。
| 指标 | 计算方式 | 我的参考阈值 | 说明 |
|---|---|---|---|
| 最大回撤 | max(1 - nav/cummax) | 小于策略目标风险预算 | 直接决定心理承受力 |
| 夏普比率 | 年化收益/年化波动 | 日频大于1.5算可用 | 太高要警惕过拟合 |
| 卡玛比率 | 年化收益/最大回撤 | 大于1.0才谈得上优秀 | 比夏普更贴合实际亏损 |
| 换手率 | 每日换仓量均值 | 结合成本一起看 | 换手高成本高,收益必须覆盖 |
夏普比率不是越高越好。日频策略夏普做到4以上,要么是因子真的强到碾压市场,要么是回测里埋了未来函数或成本低估。卡玛比率大于1意味着赚钱速度比亏钱速度更快,这个指标在回测和实盘之间更一致。换手率本身不直接说明好坏,但它是交易成本的放大器:一个年化换手50倍的策略,哪怕单边成本只有千分之二,一年下来成本就是20%的净值损耗,Alpha必须远高于这个数才值得做。
5. 避坑排查:五个让策略从回测翻车到实盘失灵的问题
下面这五个问题,是我见过的量化新手里重复率最高的踩坑记录。每条我都按现象、原因、解决的顺序写,方便你对照排查自己的策略流程。
5.1 回测曲线完美,实盘就垮
现象:策略在历史数据上跑出漂亮净值,年化收益很高、回撤很小,但一上实盘,前两周就开始亏。原因:十有八九是未来函数。最常见的是当天收盘价计算信号,又在同一根K线上按收盘价成交,等于开卷考试。解决:统一规则——收盘生成的信号最早次日开盘执行。代码里就是signal.shift(1),我见过有人图省事不shift,回测收益直接翻倍,这不是策略强,是时间旅行。
5.2 股票池里全是"活下来"的股票
现象:回测用今天的股票池回测十年前,收益高得离谱。原因:幸存者偏差。你用的成分股清单是现在还在上市的标的,那些已经退市、被ST、暴跌过的标的根本没进历史回测,等于剔除掉了最差的一半样本。解决:回测必须用当时时点的股票池。常见做法是准备一个"全市场历史股票列表",包含已退市标的,并在回测每个交易日按当时的上市状态和停牌状态动态筛选池子。
5.3 参数试一圈总能找到好看的
现象:把均线周期、持仓天数、止损线挨个试,总能找到一组参数让回测夏普特别高。原因:这就是过拟合。参数越多,你越是在用历史数据"填空",找到的不是规律,是噪声的模式。解决:把样本切成训练段和验证段,参数只在训练段上调,验证段只看一次,看结果决定是否保留。更严格的用滚动前推测试,让每次调参都只基于历史数据,这个做法放到最后一章讲。
5.4 夏普2.0,实盘只有0.5
现象:回测夏普2以上,实盘跑三个月,收益剩一半不到。原因:交易成本设太低。回测里佣金按万几算,滑点设成0,实际成交时滑点加上市场冲击,成本可能高出一个数量级。解决:先按双边千分之三到千分之四压测一遍,如果策略还能正收益,再谈实盘。记住一个经验:回测里省的成本,实盘都会加倍还回来。
5.5 因子IC很高,组合却不赚钱
现象:单因子IC有0.05,ICIR也过了1.5,但合成到组合里,扣费后收益接近零。原因:因子预测方向有效,但换手率太高,交易成本把Alpha吃光了;或者因子之间存在高度相关,合成后重复押注同一个风险。解决:在因子筛选阶段就看它的换手率,优先保留ICIR相当但换手更低的因子;同时定期计算因子相关矩阵,相关性超过0.7时考虑只保留ICIR更高的那个,或者做一个去相关处理再合成。
6. 进阶技巧:用滚动前推测试验证策略稳健性
如果你已经跑通了前面所有流程,下一步就是把静态回测升级成滚动前推测试。静态回测的问题在于:你所有的调参决策都看过了整段历史,样本内再严谨,结果也是"考过的题再考一遍"。滚动前推的做法是只让策略站在当时的窗口上看过去,然后用未来的数据检验它真实的预测能力。
def walk_forward(panel, train_len=504, test_len=126): # panel: 因子和价格数据的完整面板 # train_len: 训练窗口长度,504个交易日约等于两年 # test_len: 测试窗口长度,126个交易日约等于半年 results = [] start = 0 while start + train_len + test_len <= len(panel): train = panel.iloc[start:start + train_len] test = panel.iloc[start + train_len:start + train_len + test_len] # 只在train段上做因子筛选和参数寻优 # 然后把最优参数固定,在test段上做样本外回测 sample_out_nav = run_strategy(train, test) results.append(evaluate(sample_out_nav)) start += test_len return results这段代码的核心思想是:训练窗口和测试窗口严格不重叠,每次只在训练段上选因子、调参数,然后把模型固定下来,滚到下一段历史上去跑。常见的窗口配置是训练两年、测试半年,然后每次向前滚动半年。这样你会得到一串样本外净值,把它们拼接起来看整体表现,这个结果比任何一次静态回测都可信。
窗口长短影响不小。训练窗口太短,参数估计噪声大;太长,市场风格已经漂移,模型跟不上。我一般先试504天训练加126天测试,再对比672天加126天,选样本外表现更稳定的一组。还有两个细节要注意:一是每次滚动都要重新做因子筛选,不能第一轮选完后面就固定,否则还是变相用了未来信息;二是滚动结果要按测试段拼接后的整体净值去评估,单独看某一段的夏普意义不大。
我早年做策略时也是只看一次性回测,后来被一条漂亮的样本内曲线骗过一次大回撤,才被迫改成滚动前推。现在每产出一个新策略,我会先跑一遍滚动前推,如果样本外表现和样本内差太多,这个策略直接放弃。这个习惯帮我挡掉了至少一半看着诱人但实际不成立的方案。做量化的时间越长越会意识到,策略本身可以朴素,验证流程必须严格。希望这章的内容能帮你少走一段弯路。
本文还有配套的精品资源,点击获取