精选文章

把交易直觉变成可计算的因子:5 层框架与 ATR 案例

从自然语言交易直觉出发,用五层框架把想法变成可计算、可验证的量化因子。

本文目录
  1. 1. 一、因子是什么:从一句直觉到两段代码
    1. 1.1. 同一个直觉,两种因子写法
    2. 1.2. 好因子的四个特征
  2. 2. 二、5 层因子框架:从入场到出场
  3. 3. 三、8 因子全景字典:每层留一个”代表队员”
  4. 4. 四、AI 帮你写因子:从一句话到一个 Python 函数
    1. 4.1. 取数:一句话拉 2 年日 K
    2. 4.2. 写因子:自然语言 → 可执行函数
    3. 4.3. 双版本并行的价值
  5. 5. 五、为什么”双版本”是这套方法的关键
  6. 6. 六、把交易直觉翻译成可执行因子
  7. 7. 参考资料

看到 MACD 金叉就想买,看到放量就觉得”要起飞”,看到 ATR 突然放大就慌——这些直觉到底有没有数据支撑?

我做过一次量化实验,目标只有一个:把”我以为有用”的指标,全部翻译成一段不会说谎的代码,再用两年的数据问它一句话——你到底准不准。

第一步,是把脑子里那种”金叉就该买”的模糊感觉,变成一个可以跑、可以验、可以被数据打脸的因子。


一、因子是什么:从一句直觉到两段代码

因子说穿了,就是把你嘴里那句”今天波动有点大”翻译成一条等长的数值序列。每一行对应一个交易日,每一格是一个数。

听起来很朴素,但绝大多数散户停在了”嘴里说”这一步。因子的存在就是逼你把直觉量化,量化之后才有验证,验证之后才知道你信的那套靠不靠谱。

我用 ATR 来举这个例子,原因很简单——它是少数几个能用一句话说清楚、又能被市场反复打脸的指标。

ATR(Average True Range)衡量的是每天价格运动的幅度,公式不复杂:

TrueRange = max(H - L, |H - PC|, |L - PC|)
ATR14    = Wilder(TrueRange, 14)   # TA-Lib 默认,近似 EMA

PC 是前一日收盘。说人话:今天的最高最低之间走了多远,再把”昨天收盘到今天”的跳空也算进去,三者取最大,最近 14 天做一次 Wilder 平滑(TA-Lib 的 ta.ATR 默认就是这个口径,比简单移动平均更接近原始 ATR 定义;代码里直接调它就行)。

ATR 是一个吵闹的孩子。它一安静,可能是蓄势,也可能只是死水,得看后面有没有催化;它一爆响,往往意味着事件驱动已经发生、市场切到了新的能量状态。AI 概念股(NVDA、AMD、TSLA、MSFT 这一类)尤其如此——财报、发布会、监管口风,每一次波动率台阶都对应着一桩事。

同一个直觉,两种因子写法

我第一次跑 ATR 时差点把它算反了。直觉是对的:”今天波动比前几天大很多”,但写代码时可以写成两种完全不同的形态。

连续版本(continuous)——保留幅度信息:

atr_ratio = ATR14[t] / SMA(ATR14, 3)[t-1]
# 输出:1.12, 0.87, 1.31 ... 一串浮点数

它告诉你”今天 ATR 是过去 3 天均值的几倍”。1.12 是温和扩张,1.31 就是踩了油门。

离散版本(discrete)——只关心事件触发:

atr_break = (ATR14[t] > SMA(ATR14, 3)[t-1]).astype(int)
# 输出:0 或 1

它只问一句:”今天的 ATR 是不是高于过去 3 天均值?”是就 1,否就 0。

同一个直觉,派生出两个版本,是因为它们后面要走完全不同的验证路径。连续版会被丢进 IC、ADF 那一套来看”幅度是否单调预测收益”;离散版会被丢进 PRF(Precision-Recall)和事件研究那一套来看”信号触发后到底涨没涨”。

在这个数据集上,连续版本的 IC 拉胯,离散版本却跑出了 Lift +5.4 个百分点的成绩。这种”幅度不灵但事件灵”的分歧,正是因子工程里最值得玩味的地方——也是为什么要双版本并行写。

好因子的四个特征

我审阅自己的因子时,会拿这四条尺子量一遍:

  1. 有经济逻辑——你能用一句话解释清楚为什么它该有效
  2. 有区分度——IC 或 Lift 大于 0,至少能跑赢瞎猜
  3. 跨期稳定——在不同市场状态、不同时间段都成立,不是单一时段拟合出来的偶然
  4. 与已有因子低冗余——|ρ| < 0.7,否则就是同一个信息的两次计数

只满足前两条的因子很多,能扛过后两条的不多。ATR 正好暴露了一个经典分歧——同一个直觉,连续版的区分度被 IC 一票否决,离散版却跑出非常清晰的事件信号。

把因子写成代码这件事,看起来是技术活,骨子里是认知活。你嘴里说”放量突破”,可以在十几个不同的语境下意思都不一样:是相对昨天放量,还是相对均线放量?倍数是 1.2 还是 1.5?要不要要求收盘价同时突破?写代码逼着你把每一个模糊词钉死成一个数字,直觉的颗粒度被强制拉到了”可执行”层级。

你会发现一件有意思的事:很多脑子里听起来无懈可击的策略,一旦写出代码,自己就会发现哪里不对。比如”金叉买入”这条规则,写到最后你必须回答——是日线金叉还是周线?金叉之后立刻买还是次日开盘?买完之后什么时候卖?只要这三个问题里有一个你之前没想过,你就知道你刚才信的不是策略,是一句口号。


二、5 层因子框架:从入场到出场

凭一个因子下单这事我干过,结果就是踩到大行情之外的”假突破”。教训是因子要分层,每一层只解决一个独立维度的问题,叠加时才不至于把同一件事数三遍。

我把入场到出场的因子分成 5 层,顺序固定:

5 层因子框架

第一层 趋势确认。它是入场的”准入门槛”,回答”这股票现在到底在走多还是走空”。代表因子是 MA 多头排列、MA50 站稳一周、年线站稳一周、MACD 金叉(业内常把金叉归到动量/转折类,这里我把它当成趋势的辅助过滤用——同一个指标在不同框架里的位置本来就不唯一,要紧的是别在两层重复使用)。趋势这层不通过,后面的全部不算数。

第二层 动量触发。趋势是个慢变量,动量给你”这一刻有没有催化剂”的快变量。代表因子 high_new_3m:今天的 high 是不是过去 3 个月的最高。突破 3 月新高这件事本身,就是动量爆发的硬证据。

第三层 量能辅助。价格动了,量没跟上,是孤掌难鸣。volume_up_vs_prev 看环比放量,volume_gt_mavol_k 看是不是高于 20 日均量的 1.2 倍。我的经验是:量能层不是”必要条件”,而是”过滤器”——它能把很多无量假突破挡在门外。

第四层 波动率择时——ATR 突破因子所在的一层。前三层告诉你”该不该买”,这一层告诉你”现在是不是出手的时刻”。atr_break_mean_short(ATR14 突破短期均值)就属于这一层。波动率突然加速,意味着市场进入了一个新的能量状态——可能是事件驱动,可能是流动性变化。这里要点明一下:扣扳机赌的不是”突破当天本身”,而是”切换之后的延续”——离散版后面验证的也是触发后 T+N 天的累计涨幅,事件已经发生、但趋势惯性还在。

第五层 出场管理。出场常常是新手的盲区。我自己的处理方式是让出场和入场用同一个变量——既然进场看的是 ATR 放大,那么出场就看 ATR 缩小(缩量止盈)或者前低减一个 ATR(动态止损)。同源逻辑的好处是:你不会用一套理论开仓,又用另一套理论平仓,结果两套打架。

这 5 层有两条铁律:同层只选一个代表因子,层与层之间各管一段(不是数学意义上的统计独立,而是说每层回答一个独立维度的问题,避免把同一件事在两层重复计数)。哪些因子只是同一信号的两种说法,需要交给 Spearman 冗余矩阵检验。


三、8 因子全景字典:每层留一个”代表队员”

把 5 层框架落地,就是一个 8 因子的字典。下面这张全景图是我的入场因子集合,每个因子配一行明确的代码逻辑——这一点很重要,说不清代码逻辑的因子,本质上就不是因子,只是一种感觉。

入场因子全景字典

出场层另有 atr_shrink_take_profit(ATR 缩量止盈)和 dynamic_stop_prior_low_minus_atr(前低减 ATR 动态止损),不在入场字典里。

你会注意到趋势层堆了 4 个因子——这是因为趋势的视角天然多元:年线管的是大背景、MA50 管的是中期、多头排列管的是均线结构、MACD 管的是转折点。它们都属于”确认大方向走多”,但回答的是趋势的不同切片。冗余检验时我们会发现 ma50 和 ma_bullish_alignment 的 Spearman ρ 高达 0.72——同层冗余的典型样本,不能在同一个组合里同时打分。

每层为什么不多放几个?因为同层冗余的因子叠加是数学上的虚荣指标——你看着分数涨了,其实只是把同一个信息源的权重悄悄加了倍。

你可能会问:那如果我手头有 30 个心仪的指标,怎么往这个 5 层框架里塞?我的处理方式是先按经济逻辑把它们分到 5 个桶里,再在每个桶内部跑一遍 Spearman 矩阵看相关性,最后只保留每个桶里最具代表性的一个或两个。这一步会砍掉至少一半的”我以为有用”的指标——这种砍掉过程不是损失,是必要的减法。

字典之外的因子怎么处理?两条原则。第一,没法塞进 5 层中任何一层的,先停下来想想——它要么是我没想清楚的复合因子,要么是它根本不属于”入场信号”的范畴(比如基本面因子、舆情因子,是另一套体系)。第二,能塞但和现有因子相关性高于 0.7 的,直接丢掉,不要纠结。我给自己定的规矩是:宁可少 5 个因子,也不要让冗余信号污染组合分。


四、AI 帮你写因子:从一句话到一个 Python 函数

数据和代码这两件事,是把交易直觉变成因子的体力活。我以前的循环大概长这样:

打开 IDE → 想一下变量名 → 查 API 文档 → 写一段 fetch_kline → 处理时区/前复权 → 检查是不是有空值 → 再写因子函数 → 跑一下发现哪里维度不对 → debug → 出图 → 发现今天就剩 2 小时了。

现在这套流程被压缩成了一句话。

取数:一句话拉 2 年日 K

我用的是 futu-api(一个公开的 Python SDK,类似 yfinance 但走专用网关 OpenD)。Claude Code 帮我把”自然语言指令”转成可执行脚本,整个过程不到 1 分钟。

输入是这么一句话:

获取 15 支美股 AI 概念股 2024-05 至 2026-05 的日 K,前复权,保存为一份 Parquet(多标的合并)。

产出是一张落盘表:

项目 值
标的数 15 只
每标的 K 线数 499 根
总行数 7,485
日期范围 2024-05-13 ~ 2026-05-08
复权方式 前复权(QFQ)

文件落盘成 panel_kline.parquet,索引是 datetime,列是 code, open, high, low, close, volume。这个 date × asset 的面板格式直接喂给后面的因子计算工具,列对齐、时间对齐,没有任何手工 reshape。

前复权这一步不能省。如果数据没做前复权,每次拆股、分红都会在原始价格序列里制造一个”假突破”——你写一个 ATR 突破因子,会被这些幽灵事件污染到面目全非。垃圾数据进,垃圾因子出,AI 帮你把好的是数据这一关。

写因子:自然语言 → 可执行函数

下一步更简单。我把交易直觉用一句话扔给 Claude:

基于 K 线数据,计算 atr_break_mean_short:当日 ATR14 大于前一日 3 日均值时触发,同时产出连续版 atr_ratio,存入面板 Parquet。

它产出的代码是这样的:

def compute_atr_factors(df, p=14, m=3):
    df['atr14']    = ta.ATR(df.high, df.low, df.close, timeperiod=p)
    df['atr_ma3']  = df['atr14'].shift(1).rolling(m).mean()
    df['atr_ratio']             = df['atr14'] / df['atr_ma3']
    df['atr_break_mean_short']  = (df['atr14'] > df['atr_ma3']).astype(int)
    return df

注意 shift(1) 那一句——前一日 3 日均值,避免用未来数据。这种容易出错的细节,AI 在生成代码时会主动加上 lookahead 防护。我自己手写的版本曾经漏掉过 shift,结果”完美预测”了 20% 的次日收益,开心了 5 分钟才反应过来在时间维度上偷跑了。

双版本并行的价值

这段代码一次性产出两个因子:连续版 atr_ratio 和离散版 atr_break_mean_short。一份代码,两条验证路径,两套报告。

我以前会先写一个版本,跑完发现不对再去改另一种形态,来回折腾两小时。现在 AI 一次性把两种形态摆在桌面上,等于把”双盲实验”前置——下一步我直接拿数据投票,看哪一版能扛过统计检验,而不是先入为主押一边。

整个从交易直觉到 Parquet 落盘的过程,我大概只需要描述清楚两件事:因子的经济逻辑和输出口径。剩下的体力活都被压平了。

AI 因子流水线


五、为什么”双版本”是这套方法的关键

回到 ATR 这个案例。

连续版 atr_ratio 测出来的 IC_IR 只有 0.188,远低于 0.5 的工业级阈值。如果按传统量化教材的处方——“IC_IR 不达标就丢掉这个因子”——我应该把 ATR 直接扔进垃圾桶。

但同样的指标,切换成离散版本一跑,PRF 给出 Precision 61.86%、Lift +5.4 个百分点,事件研究的 t 统计量 13.1、p 值小于 10⁻³⁷。这些数字不是同一个量级的”勉强能用”,而是”非常清晰的有效信号”。

提一句口径:这些数字来自 15 只美股 AI 概念股、2024-2026 年偏牛市区间,未扣交易成本、也没跑过样本外复验。把它当成”双版本设计能救一个差点被误杀的因子”的证据够用,但别当成可以直接照搬的普适结论。

同一个直觉,在”幅度”维度上拉胯,在”事件”维度上封神——这种分裂背后的逻辑是:ATR 的预测力不来自它的具体数值大小,而来自”是否触发了一次突破”这个事件本身。波动率不是连续光滑地预测未来收益的,它是阶梯式地切换市场状态的。IC 在常用定义下是预测值和未来收益的秩相关,本质上衡量的是单调关系——你拿一把测单调关系的尺子去量一个阶梯函数,自然会得到”看着像噪声”的结果。

这件事教会我两件事:

第一,因子设计阶段不要押注单一形态。同一个直觉,连续 + 离散两种形态都试一遍,让数据告诉你哪种形态承载了真信号。

第二,统计指标失败不等于因子失败。IC_IR 不达标只能说明”单调幅度信息不可用”,不能说明”这个指标整体没有预测力”。换条路(离散化、阈值化、事件化)继续验,往往柳暗花明。

我后来做其他指标时养成了一个习惯:写每个因子都写两遍,连续一份、离散一份,双盲交给统计检验投票。这个习惯让我节省了至少一半”早早把好因子误杀”的成本。

这件事还有第三层启发,关于”指标失败”的归因。过去我习惯把一个因子跑出负 IC 解释成”市场没效率”或者”标的不对”,把锅甩给外部。这次实验让我意识到,更常见的真相是——我用了错误的统计语言去问一个对的问题。相关系数家族(无论 Pearson 还是 Spearman 秩相关)假设的都是连续单调关系,可现实中很多有效信号是阶梯式、阈值式、事件式的。换句话说,不是因子不灵,是测量工具用错了。

这个认知会迁移到很多其他场景:做 A/B 实验时如果只看均值不看分布、做用户分层时只看相关性不看断点、做趋势分析时只用线性回归——都可能踩到同样的坑。统计指标是望远镜,但每架望远镜只能看一种波段,选错了就只能看到一片黑。


六、把交易直觉翻译成可执行因子

因子设计归根到底只做一件事——把交易直觉编码成代码。代码这一步迈不过去,所有的”我感觉这指标好用”都只是迷信。

真正需要回答的问题是:

你现在用的指标,能写出明确的代码吗?写出来之后,是只有一个版本,还是连续版和离散版都有?

如果答不上来,从一个最简单的指标开始——MACD 金叉也好,MA 多头排列也好,把它写成一个返回 0/1 的 Python 函数。然后把过去 2 年的数据丢进去,看看它在你最熟悉的标的上到底每年触发多少次。这一步迈完,你跟”凭感觉交易”之间,就隔了一个数量级的差距。


参考资料

  • futu-api 官方文档(公开 Python SDK,类似 yfinance)
  • 《主动投资组合管理》Grinold & Kahn——IC、IR 等基础统计量定义
  • 《101 Formulaic Alphas》(WorldQuant)——因子工程的工业级范式
  • TA-Lib 文档:ta.ATR 实现细节与参数说明