☰
ChatGPT量化因子挖掘:高频选股因子生成与回测实战
2026/10/2 4:45:00 网站建设 项目流程

简介:这份国金证券金融工程专题报告聚焦ChatGPT在量化选股中的实战应用,面向量化研究员、因子挖掘从业者及对AI选股感兴趣的进阶学习者。报告系统梳理了ChatGPT的模型原理与演进逻辑,重点讲解提示工程的使用方法,包括角色、任务、指令三要素提示公式及思维链提示等高级技巧,并给出因子挖掘的完整测试流程。资源为1个PDF文件,压缩包约2.16MB,内容涵盖周频价量变异系数因子构建、高频买卖盘力量因子测试,以及基于卖盘力量因子的中证1000指数增强策略回测结果,同时附有ChatGPT代码输出能力的实测评估与风险提示。目前已有247人学习。读者可借此掌握将大语言模型引入因子研究的思路框架,理解提示设计对因子有效性的影响,并参考报告中的IC表现、年化超额收益与信息比率等指标,评估策略的可行性与局限。

1. 从一份券商研报说起:ChatGPT 到底能不能挖出高频选股因子

2023 年 4 月,国金证券发了一篇叫《Alpha 掘金系列之五:如何利用 ChatGPT 挖掘高频选股因子?》的研报,在量化圈子里传得挺开。它做的事情说白了就一句话:把高频量价数据丢给 ChatGPT,让它自己写代码、自己算因子、自己筛因子,最后拼出一个能用的选股信号。这件事放到今天看依然有意思,因为它绕开了传统因子挖掘里最耗人的环节——人工试错。

传统做法是研究员先想一个逻辑,比如“开盘半小时成交量突然放大可能预示日内反转”,然后手写公式、回测、调参,一轮下来两三天。ChatGPT 的路子是反过来:你把数据字段和任务目标用 Prompt 描述清楚,让它批量生成候选因子表达式,再用程序自动回测筛选。人从“写因子”变成“审因子”,效率差一个量级。

这篇东西适合两类人看。一类是做量化研究但被因子枯竭卡住的,想知道 ChatGPT 在因子挖掘上到底能帮到什么程度、边界在哪;另一类是有 Python 基础、想把这套流程跑通但不知道从哪下手的。下面我按自己复现这套方案时踩过的路,把 Prompt 设计、数据准备、因子生成、回测筛选、避坑点一层层拆开讲。

2. 高频因子挖掘的完整链路:从 Prompt 到因子表达式

2.1 为什么选高频量价数据而不是财报数据

高频因子和基本面因子最大的区别在数据频率和信噪比。财报数据一年四个点,因子逻辑清晰但拥挤严重;高频量价数据是分钟级甚至 tick 级,信息量大但噪声也大,人工挖因子的试错成本极高。这恰恰是 ChatGPT 能发挥的地方——它不怕试错,你让它生成 200 个候选表达式,程序跑一遍回测,留下 IC 显著的那几个就行。

常见的高频数据字段包括:开盘价、最高价、最低价、收盘价、成交量、成交额、成交笔数、买卖盘口数据。国金那篇研报主要用的是分钟频的量价数据,因为这类数据获取门槛相对低,A 股 Level-1 行情就能覆盖大部分字段。

我一般会把数据整理成一张宽表,每行是一个时间戳,每列是一个字段,股票代码单独一列。这样后面让 ChatGPT 生成因子表达式时,它只需要引用列名就行,不用关心数据怎么来的。

import pandas as pd import numpy as np # 假设原始数据是分钟频,字段:datetime, stock_code, open, high, low, close, volume, amount df = pd.read_parquet("minute_bar.parquet") # 按股票分组,计算日内衍生字段 df = df.sort_values(["stock_code", "datetime"]) df["ret"] = df.groupby("stock_code")["close"].pct_change() df["vwap"] = df["amount"] / df["volume"].replace(0, np.nan) df["vol_ma5"] = df.groupby("stock_code")["volume"].transform( lambda x: x.rolling(5, min_periods=1).mean() ) df["amplitude"] = (df["high"] - df["low"]) / df["close"].shift(1) # 只保留需要的列,方便后续 Prompt 引用 cols = ["datetime", "stock_code", "open", "high", "low", "close", "volume", "amount", "ret", "vwap", "vol_ma5", "amplitude"] df = df[cols].dropna(subset=["ret"]) print(df.shape) print(df.columns.tolist())

这段代码做了三件事:按股票分组算收益率、算 VWAP(成交量加权均价)、算 5 期成交量均线和振幅。参数上注意min_periods=1是为了避免开盘前几分钟数据被全部丢掉,replace(0, np.nan)是防止成交量为零时除零报错。跑完之后你会得到一张干净的宽表,列名就是后面 Prompt 里要引用的字段名。

2.2 Prompt 怎么写才能让 ChatGPT 吐出可用的因子表达式

这是整套流程里最关键的环节。Prompt 写不好,ChatGPT 给你的东西要么是废话(“可以计算动量因子”),要么是没法执行的伪代码。我的经验是把 Prompt 拆成四个部分:角色设定、数据字段说明、输出格式约束、示例。

角色设定让它进入量化研究员的状态;数据字段说明告诉它有哪些列可以用;输出格式约束是最重要的——必须要求它输出 pandas 可执行的表达式,而不是自然语言描述;示例给一两个,让它知道你要的粒度。

PROMPT_TEMPLATE = """ 你是一名量化研究员,擅长从高频量价数据中挖掘选股因子。 我有如下字段的分钟频数据: - open, high, low, close: 开盘价、最高价、最低价、收盘价 - volume: 成交量 - amount: 成交额 - ret: 分钟收益率 - vwap: 成交量加权均价 - vol_ma5: 5期成交量均线 - amplitude: 振幅 请生成 {n} 个高频选股因子表达式,要求: 1. 每个因子用一行 pandas 表达式表示,输入是 DataFrame df,输出是 Series 2. 因子逻辑要有金融含义,不要随机组合 3. 表达式里只能使用上述字段和 numpy 函数 4. 输出格式为 JSON 列表,每个元素包含 name 和 expression 两个字段 示例: {{"name": "volume_ratio", "expression": "df['volume'] / df['vol_ma5']"}} {{"name": "price_position", "expression": "(df['close'] - df['low']) / (df['high'] - df['low'] + 1e-8)"}} """

参数说明:{n}控制生成数量,我一般一次让它生成 20 到 30 个,太多了质量会下降。1e-8是防止分母为零的常见写法。输出 JSON 是为了后面程序解析方便,不用去正则匹配自然语言。

拿到 ChatGPT 的返回后,先做一轮语法检查,把不能执行的表达式过滤掉。这一步能筛掉大概 10% 到 20% 的废品。

import json def parse_factors(response_text): """解析 ChatGPT 返回的因子列表,过滤掉语法错误的""" factors = json.loads(response_text) valid = [] for f in factors: try: # 用一小段数据试跑,看表达式能不能执行 test_df = df.head(100).copy() result = eval(f["expression"], {"df": test_df, "np": np}) if isinstance(result, pd.Series) and result.notna().sum() > 50: valid.append(f) except Exception as e: print(f"因子 {f['name']} 执行失败: {e}") return valid

这段代码的核心是eval试跑。注意eval有安全风险,生产环境里应该用ast模块做白名单校验,或者用numexpr替代。我这里为了演示方便直接用eval,实际跑的时候建议加一层沙箱。

2.3 因子回测框架:IC 计算和分层测试

因子生成出来只是半成品,必须回测才知道有没有用。高频因子的评估和日频因子类似,核心指标是 IC(信息系数)、ICIR(IC 的均值除以标准差)、换手率、分层收益单调性。

IC 的计算方式是每个时间截面上,因子值和下一期收益率的相关系数。高频数据里“下一期”通常指下一个调仓周期,比如 30 分钟或 1 小时。我一般用 Rank IC,也就是先对因子值和收益率做排序再算相关系数,这样对异常值更稳健。

def calc_ic(df, factor_col, ret_col, period=30): """ 计算 Rank IC df: 包含 datetime, stock_code, factor_col, ret_col 的 DataFrame period: 未来收益的期数(分钟数) """ df = df.sort_values(["stock_code", "datetime"]).copy() # 计算未来 period 期的累计收益 df["future_ret"] = df.groupby("stock_code")[ret_col].transform( lambda x: x.rolling(period).sum().shift(-period) ) df = df.dropna(subset=[factor_col, "future_ret"]) # 每个时间截面算一次 Rank IC ic_series = df.groupby("datetime").apply( lambda g: g[factor_col].corr(g["future_ret"], method="spearman") ) return ic_series def factor_report(df, factor_col, ret_col="ret", period=30): ic = calc_ic(df, factor_col, ret_col, period) ic_mean = ic.mean() ic_std = ic.std() icir = ic_mean / ic_std if ic_std > 0 else 0 positive_rate = (ic > 0).mean() return { "IC_mean": round(ic_mean, 4), "IC_std": round(ic_std, 4), "ICIR": round(icir, 4), "IC>0占比": round(positive_rate, 4), "样本数": len(ic) }

参数说明:period=30表示看未来 30 分钟的收益,这个参数要根据你的调仓频率来定。如果你的策略是每天调仓一次,那 period 应该设成一天的总分钟数。method="spearman"就是 Rank IC,换成"pearson"就是普通 IC。ICIR 大于 0.3 通常认为因子有一定预测能力,大于 0.5 算不错。

分层测试是把股票按因子值分成 5 组或 10 组,看每组未来收益是否单调。单调性好的因子,说明因子值和收益之间有稳定的线性关系,更适合直接用来选股。

3. 把 ChatGPT 接入自动化流水线:批量生成与筛选

3.1 用循环批量调用 API 生成因子池

手动一个个问 ChatGPT 效率太低,实际做的时候要把它接进自动化流程。核心思路是写一个循环,每次换一个 Prompt 角度(比如动量类、反转类、波动率类、量价背离类),让 ChatGPT 生成一批因子,解析后存进因子池。

import openai import time # 注意:API key 从环境变量读取,不要硬编码 client = openai.OpenAI(api_key=os.environ["OPENAI_API_KEY"]) PROMPT_ANGLES = [ "动量类因子,捕捉价格趋势的持续性", "反转类因子,捕捉短期超买超卖", "波动率类因子,衡量价格波动的不对称性", "量价背离类因子,捕捉成交量与价格的不同步", "盘口结构类因子,利用高低开和振幅信息", ] def generate_factor_pool(angles, n_per_angle=20): pool = [] for angle in angles: prompt = PROMPT_TEMPLATE.format(n=n_per_angle) + f"\n本次请专注于:{angle}" try: resp = client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": prompt}], temperature=0.7, ) factors = parse_factors(resp.choices[0].message.content) pool.extend(factors) print(f"{angle}: 生成 {len(factors)} 个有效因子") except Exception as e: print(f"{angle} 调用失败: {e}") time.sleep(1) # 避免触发限流 return pool factor_pool = generate_factor_pool(PROMPT_ANGLES) print(f"因子池总数: {len(factor_pool)}")

参数说明:temperature=0.7是让输出有一定多样性,设太低每次生成的因子都差不多,设太高会出现大量无意义组合。time.sleep(1)是防止触发 API 限流,如果你的账号额度高可以缩短。model参数根据你实际能用的模型来填,不同模型生成质量差异明显,建议用能力强的模型做因子生成。

3.2 因子去重和相关性过滤

ChatGPT 生成的因子池里一定有大量重复或高度相关的。比如“收盘价除以开盘价”和“收盘价减开盘价再除以开盘价”,本质是同一个东西。不去重的话,后面回测出来的结果会严重高估因子的独立性。

去重分两步:第一步按表达式字符串去重,简单直接;第二步按因子值的相关性去重,把相关系数大于 0.8 的因子对里保留 IC 更高的那个。

def dedup_by_correlation(df, factors, ic_dict, threshold=0.8): """ 按因子值相关性去重,保留 IC 更高的因子 factors: [{"name": ..., "expression": ...}, ...] ic_dict: {factor_name: ic_mean} """ # 先算出每个因子的值 factor_values = {} for f in factors: try: factor_values[f["name"]] = eval(f["expression"], {"df": df, "np": np}) except Exception: continue names = list(factor_values.keys()) # 按 IC 从高到低排序,优先保留 IC 高的 names.sort(key=lambda x: abs(ic_dict.get(x, 0)), reverse=True) keep = [] for name in names: redundant = False for kept in keep: corr = factor_values[name].corr(factor_values[kept]) if abs(corr) > threshold: redundant = True break if not redundant: keep.append(name) return keep

参数说明:threshold=0.8是相关性阈值,设太低会误杀有差异的因子,设太高去重效果不明显。我一般先用 0.8 跑一遍,看看剩下多少,再根据因子池大小调整。abs(ic_dict.get(x, 0))用绝对值是因为有些因子是反向的,负 IC 同样有价值,只要取反就行。

3.3 样本外验证:别在训练集上自嗨

这一步是很多人翻车的地方。ChatGPT 生成的因子是在你给它的数据上“看起来有效”,但如果你用全样本回测然后直接上实盘,大概率会失望。正确做法是把数据切成训练集和验证集,训练集上筛因子,验证集上确认 IC 是否还显著。

def train_test_split_by_time(df, split_ratio=0.7): """按时间切分,前 70% 做训练,后 30% 做验证""" dates = sorted(df["datetime"].unique()) split_idx = int(len(dates) * split_ratio) train_dates = set(dates[:split_idx]) test_dates = set(dates[split_idx:]) train_df = df[df["datetime"].isin(train_dates)].copy() test_df = df[df["datetime"].isin(test_dates)].copy() return train_df, test_df train_df, test_df = train_test_split_by_time(df) # 训练集上算 IC,筛选 train_ic = {f["name"]: factor_report(train_df, f["name"])["IC_mean"] for f in factor_pool if f["name"] in train_df.columns} # 验证集上确认 test_ic = {f["name"]: factor_report(test_df, f["name"])["IC_mean"] for f in factor_pool if f["name"] in test_df.columns} # 找出训练和验证 IC 同号且都显著的因子 robust_factors = [ name for name in train_ic if abs(train_ic[name]) > 0.02 and abs(test_ic.get(name, 0)) > 0.02 and np.sign(train_ic[name]) == np.sign(test_ic.get(name, 0)) ] print(f"稳健因子数量: {len(robust_factors)}")

参数说明:split_ratio=0.7是训练验证比例,高频数据里我一般用 7:3 或 6:4。abs(...) > 0.02是 IC 显著性门槛,高频因子 IC 普遍偏低,0.02 已经算能用。np.sign同号判断是确保因子方向在样本外没有反转,方向反转的因子直接丢掉。

4. 避坑与排查:ChatGPT 挖因子最容易翻车的五个地方

4.1 因子表达式里有未来函数

现象:回测 IC 高得离谱,0.15 以上,但实盘一跑就亏。

原因:ChatGPT 生成的表达式里用了shift(-1)或者直接引用了未来数据。比如它可能写出df['close'].shift(-1) / df['close']这种表达式,回测时看起来完美,实盘根本拿不到未来价格。

解决:在parse_factors里加一道检查,扫描表达式里有没有shift(-或future字样,有就直接丢弃。另外回测框架里要严格用shift(-period)只出现在计算未来收益的那一步,因子表达式本身不能含未来信息。

4.2 生成的因子全是同一类

现象:让 ChatGPT 生成 50 个因子,去重后只剩 3 个,而且都是动量类。

原因:Prompt 里没有限制因子类别,ChatGPT 会倾向于生成它认为“最合理”的那一类,导致多样性不足。

解决:在 Prompt 里明确要求覆盖多个类别,或者用PROMPT_ANGLES那种方式分角度调用。我一般会强制要求“至少包含动量、反转、波动率、量价关系四个类别各 5 个”。

4.3 API 返回格式不稳定导致解析失败

现象:json.loads报错,ChatGPT 返回的内容里混了自然语言解释。

原因:Prompt 里虽然要求了 JSON 格式,但模型有时候会“好心”加一段说明文字,比如“以下是生成的因子:”然后再跟 JSON。

解决:解析前先用正则把 JSON 部分抠出来,或者用response_format={"type": "json_object"}参数强制 JSON 输出。另外在 Prompt 里加一句“只输出 JSON,不要任何额外文字”。

4.4 因子值分布极端导致 IC 计算失真

现象:某个因子 IC 算出来是 0.3,但分层测试完全没单调性。

原因:因子值里有极端异常值,比如除以一个接近零的数导致因子值变成 1e10,Rank IC 虽然对异常值稳健,但分层测试会受影响。

解决:算 IC 之前先做去极值处理,用中位数绝对偏差(MAD)法或者分位数截断。我一般用 1% 和 99% 分位数截断,简单有效。

def winsorize(series, lower=0.01, upper=0.99): lo = series.quantile(lower) hi = series.quantile(upper) return series.clip(lo, hi)

4.5 忽略交易成本和换手率

现象:因子 IC 不错,但实盘收益被交易成本吃光。

原因:高频因子换手率天然高,如果调仓频率是 30 分钟一次,一天换手好几次,手续费和冲击成本累积起来很吓人。

解决:回测时必须把交易成本算进去。A 股双边手续费加印花税大概千分之一点几,高频策略还要考虑冲击成本。我一般会在回测里设单边千分之二作为保守估计,如果因子在这个成本下还能盈利,才值得进一步研究。

5. 进阶技巧:用 ChatGPT 做因子迭代和组合优化

前面讲的都是单因子生成和筛选,但真正能上实盘的往往是因子组合。ChatGPT 在这个环节也能帮上忙,而且用法和单因子生成不太一样。

一个我常用的技巧是让 ChatGPT 做“因子迭代”。具体做法是:把上一轮筛选出来的有效因子和它们的 IC 表现一起丢给 ChatGPT,让它基于这些因子的逻辑生成变体。比如“volume_ratio”这个因子 IC 不错,就让它生成“volume_ratio 的 5 期均线”“volume_ratio 的截面排名”“volume_ratio 和振幅的交互项”等衍生版本。这样比从零生成命中率高得多。

ITER_PROMPT = """ 以下是我上一轮筛选出的有效因子及其 IC 表现: {factor_list} 请基于这些因子的逻辑,生成 10 个变体因子,要求: 1. 每个变体是对原因子的改进或组合,不是简单重复 2. 输出格式为 JSON 列表,包含 name 和 expression 3. 表达式只能使用以下字段:open, high, low, close, volume, amount, ret, vwap, vol_ma5, amplitude """ def iterate_factors(valid_factors, ic_dict, n=10): factor_list = "\n".join( f"- {f['name']}: {f['expression']} (IC={ic_dict.get(f['name'], 0):.4f})" for f in valid_factors[:10] ) prompt = ITER_PROMPT.format(factor_list=factor_list) resp = client.chat.completions.create( model="gpt-4", messages=[{"role": "user", "content": prompt}], temperature=0.6, ) return parse_factors(resp.choices[0].message.content)

参数说明:valid_factors[:10]只取前 10 个因子,太多会超出上下文限制。temperature=0.6比生成新因子时略低,因为迭代任务需要更聚焦。这个循环可以跑多轮,每轮把新筛出的因子加进去,几轮下来因子池会越来越精。

另一个技巧是让 ChatGPT 帮你做因子正交化。把相关性高的因子对丢给它,让它生成正交化后的表达式。比如两个因子高度相关,可以让它生成“因子 A 对因子 B 回归后的残差”作为新因子。这个操作在传统量化里要手写回归代码,ChatGPT 可以直接给你表达式。

最后说一个验证方法:把 ChatGPT 生成的因子和已知的经典因子(比如反转因子、换手率因子)做相关性对比。如果新因子和经典因子相关性低于 0.3,说明它提供了增量信息,值得保留;如果高于 0.7,那基本是经典因子的变体,意义不大。这个检查能帮你快速判断因子池里有多少是真正的新东西。

我自己跑这套流程最大的教训是:别指望 ChatGPT 一次给你一个能上实盘的因子。它的价值在于把因子挖掘的试错成本从“人工两天一个”降到“程序两小时一批”,你真正要做的是设计好筛选和验证的流水线,让机器去试错,人来把关逻辑。这套东西跑顺了,因子池的更新速度会比纯人工快一个数量级。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询