ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

用大语言模型与提示工程挖掘中证1000选股因子

用大语言模型与提示工程挖掘中证1000选股因子

简介:《Alpha掘金系列之五:如何利用ChatGPT挖掘高频选股因子?》是一份面向量化研究者、金融工程从业者及对大语言模型选股感兴趣的进阶读者的专题报告,聚焦ChatGPT在因子挖掘与策略构建中的实际用法。资源包仅含1个PDF文件,约2.21MB,便于集中阅读与检索。报告先解析GPT与Transformer架构、相较LSTM的权重分配改进,以及Scaling Law和RLHF三步训练流程;随后介绍提示工程的角色—任务—指令公式、思维链提示等技巧。实战部分围绕周频价量变异系数因子、高频买卖盘力量因子展开,并给出中证1000指数增强策略回测、代码输出测试与风险提示,可帮助读者理解模型能力边界、复现研究框架并沉淀可验证的因子挖掘思路。已有1359人学习。

1. 需求:新因子的起点到底在哪

做高频因子挖掘的人大概都经历过这种时刻:论文里的公式抄完了,回测跑出来 IC 只有零点零几,换个窗口参数就翻车,而且你根本分不清是数据口径出了问题还是因子逻辑本身站不住。这份方案要解决的正是「新因子从哪来」这个起点问题——把 ChatGPT 这类大语言模型当成因子生成器,用提示工程把研究流程固定下来,最终落到一个能在中证 1000 成分股上跑通的选股因子和指数增强组合。它适合两类人:正在做量化方向毕设、课程设计,需要一个完整因子研究闭环的学生;以及要在团队里交付一份因子研究报告的从业者。整套流程分四段推进:模型为什么能给得出像样的答案、提示怎么写才收敛、周频与高频两条因子线怎么构建回测、最后是踩过的坑和验收口径。

2. 模型底座:Transformer 的权重分配与 RLHF 的三步训练

2.1 从 LSTM 的两个缺陷说起

要让模型给出可用因子,先得知道它的能力边界在哪。传统 LSTM 处理文本预测这类任务时有两个硬伤:一是对句子里不同词无法区分重要程度,所有输入被一视同仁地压进隐状态;二是长距离信息提取能力弱,句子一长,前面的信息就丢得差不多了。2017 年那篇提出 Transformer 的论文把这两个问题一起解决了,靠的是自注意力机制——让序列里每个位置与其他位置两两计算相关性,再按相关性给不同部分分配不同权重,上下文语义因此能被完整建模,长序列也不再是负担。

结构上,Transformer 保留了编码器和解码器的主体框架,编码器负责把输入压成带权重的表示,解码器负责逐词生成。这条路线带来的直接后果是模型可以吃下更大的数据集、堆更多的参数,语言模型的成长空间被彻底打开。参数规模的演进很能说明问题:初代 GPT 大约 1.17 亿参数,第二代到了 15 亿,第三代直接冲到 1750 亿,第四代又把输入从纯文本扩展到图像,复杂问题上的准确性和洞察力明显上了一个台阶。理解这条演进线对做因子研究有实际意义——参数量越大,模型对金融文本里那些隐含逻辑的捕捉越细,但它同时意味着输出的随机性和不可解释性也在增加,这一点后面会反复提到。

2.2 Scaling Law 与 RLHF:智能水平的两个来源

参数堆上去只是必要条件,真正让模型表现「像人」的是两件事叠加。第一件是涌现现象,也就是 Scaling Law:参数数量和训练数据量跨过某个门槛之后,模型会突然表现出小模型完全不具备的能力,比如举一反三、按示例推理。第二件是 RLHF,即人类反馈的强化学习,它把人类的偏好判断注入模型参数里,让输出更贴近人真正想要的答案,而不是统计上最可能的答案。

RLHF 的训练分三步,这三步值得拆开看,因为它直接解释了为什么模型有时候会给出一本正经的错误回答。第一步,从包含人类真实意图的指令集合里采样,由标注团队(原文披露是 40 名标注员)对这些指令做标注,然后在对话模型上做微调。第二步,再次从指令集采样输入,让模型对同一个问题生成多个不同回答,由标注员对这些回答的好坏排序,用排序结果训练一个打分模型。第三步,采样新的指令作为输入,用打分模型反过来优化模型的输出,让回答朝着人类评分高的方向偏移。三步走完,才有我们手里这个能对话、能写代码、能讲逻辑的模型。

这套训练模式还带来了三种使用范式:零样本(没有任何训练示例,靠已有知识和推理完成新任务)、单样本(给一组示例就能学会)、少样本(少量语料即可理解和推理类似语料)。放到因子挖掘场景里,我一般走少样本路线——在提示里塞两三个合格因子和两三个不合格因子的样例,模型输出的收敛速度明显快于纯零样本。代价是提示变长、每次都得多花一点上下文预算,但比起反复重问,这笔账是划算的。

3. 提示工程:三段式公式、提示分类与思维链

3.1 角色、任务、指令:提示公式怎么写才收敛

提示工程的核心是设计起始短语,指导模型朝你要的方向生成。标准的提示公式由三个元素组成:角色,即模型在这次生成任务里扮演谁;任务,对需要生成的内容做清晰简短的叙述;指令,生成文本时必须遵守的规则。一般形式是「作为一个【角色】,按照这些指示生成【任务】:【指令】」。比如让模型做客服答疑,可以写成「作为一名客服人员,请生成客户询问的回复:这些回复应当专业、准确、简洁」。

限制提示的作用不只是让回答更好看,它实际上是在向模型灌输一个角色,起到约束行为的效果。角色越具体,回答越有针对性。同理,指令部分越详尽,回答范围被压得越窄,拿到想要结果的概率越高。最开始的提示可能是「作为一名资深的量化研究员,请给出一个你认为行之有效的选股因子」,模型会给你一个听起来合理但完全不能用的东西。接下来得加约束:「该因子必须具有原创性、有较强的逻辑支撑、且应当有出色的业绩表现」。再往下,还得限定因子类别、构建方式和使用数据范围——这一步做完,模型才开始正确使用逐笔成交数据里的字段,并且按提示要求用求均值的方式完成降频。

把上面这套东西落成可复用的模板,比每次手写提示靠谱得多:

# 提示模板:把角色、任务、约束拆成独立变量,便于批量生成与版本管理 PROMPT_TMPL = """作为一名{role},请{task}。 约束条件: 1. {c1} 2. {c2} 3. {c3} 输出格式:先给出因子定义与计算公式,再说明经济逻辑,最后列出所需数据字段。""" def build_prompt(role, task, constraints): # constraints 传长度为 3 的列表,避免逗号堆叠导致模型漏读某一条 return PROMPT_TMPL.format( role=role, task=task, c1=constraints[0], c2=constraints[1], c3=constraints[2], ) prompt = build_prompt( role="资深高频量化研究员", task="设计一个仅使用委托价和委托量数据的选股因子", constraints=[ "因子必须原创,不能是已知的动量或反转类因子", "构建过程必须给出明确的数学表达式,不得使用模糊描述", "输出需说明该因子为何在中证 1000 成分股上具备横截面区分度", ], )

这段代码没有玄学,关键在三点:约束用编号列表而不是一句话连写,模型漏读的概率会低很多;角色和任务分开传参,方便同一套模板换不同角色做对比实验;输出格式固定成「公式—逻辑—字段」三段,后面人工复核时按段检查,不会漏掉字段确认这一步。实际使用时通常还会追加一轮对话,把上一轮输出里含糊的地方单独拎出来追问,比如「第三条约束里提到的标准化,请给出具体公式和分母口径」。

3.2 提示分类与思维链:把复杂问题拆成可校验的步骤

除了标准公式,还有几类针对不同任务的提示方式,可以在研究流程的不同环节各取所需。知识生成提示是把新信息与特定主题的已有知识结合,生成新知识,适合让模型在既有文献基础上做变形组合;情感分析提示用于判断一段文本的情感倾向,做新闻情绪因子时用得上;强化学习提示通过给模型一组输入和奖励,让它根据奖励调整行为;对抗性提示则反过来,让模型生成难以被分类的文本,适合做因子的压力测试场景。这四类里,知识生成和对抗性提示在因子挖掘中用得最多,前者的产出直接就是候选因子,后者可以用来检验因子是不是靠某个特定样本区间撑起来的。

情感分析这类任务我实测过,结论要写清楚:模型对含有明确情感词的文本判断稳定,但输入文本没有明确情感词时,输出的不确定性会明显上升,因为神经网络本身带随机性。整体准确率仍然可观,但你不能把它当成确定性的分类器用,必须留人工复核的环节。

真正让复杂任务可用的是思维链提示,也就是引导模型分步骤思考、把中间推导过程展现出来。举个例子:IC 作为因子评价指标是有漏洞的。两个因子可以有几乎相同的 IC 值,但因子值与收益率的相关性在不同分组区间差异巨大——A 因子 IC 是 0.22,多头组表现很好;B 因子 IC 是 0.25,多头组反而最差。直接按 IC 排序选因子,实盘效果可能和指标完全相反。解决思路是按分组给 IC 赋不同权重,让多头组权重更高,再求加权 IC。

直接问模型「怎么改进 IC 指标」,它给不出这个角度。改成思维链:先描述两个因子 IC 相近但多头表现相反的现象,问它问题出在哪;模型会回答「每组内分别计算 IC,再把组内相关系数求均值得到横截面 IC」——方向对了但逻辑有漏洞。接着在下一轮直接指出漏洞,模型才会给出加权平均 IC 的方案。再补一句「A 股存在做空限制」,它才会修正成给做多组更高权重。整个过程五轮左右,每一轮都在纠偏。这条经验很值钱:模型不是不能想深,是你得把台阶一级一级给它铺好,而且每一级都要人来校验。

4. 因子落地:周频变异系数与高频买卖盘力量的两条线

4.1 变异系数因子:5 日滚动与四个变体

第一条线走中低频。让模型基于量价数据给出独特因子,它给出的方案是在收盘价和成交量两个维度分别计算变异系数再相乘,衡量股票的波动性和流动性。这个思路可以进一步变形,把两个变异系数相除,得到新的组合因子。用日频量价数据、取过去 5 天的价格和成交量做滚动计算,公式是这样:

import numpy as np import pandas as pd def cv_factor(price_df, vol_df, window=5): """price_df / vol_df: index 为交易日, columns 为股票代码, 值已做前复权处理 返回调仓日截面上的四个因子值, 每行一只股票""" mu_p, sd_p = price_df.rolling(window).mean(), price_df.rolling(window).std() mu_v, sd_v = vol_df.rolling(window).mean(), vol_df.rolling(window).std() li = sd_p / mu_p # 价格变异系数:波动与价格水平的比值 vli = sd_v / mu_v # 成交量变异系数:量的稳定性 latest = -1 # 取窗口末行,对应周频调仓日 return pd.DataFrame({ "LI": li.iloc[latest], "VLI": vli.iloc[latest], "LIxVLI": (li * vli).iloc[latest], "LI2VLI": (li / vli).iloc[latest], })

参数上有几个点要交代清楚。window 取 5 是原文口径,对应一周的交易日数,取值变大因子会更平滑但衰减更慢,取值变小噪声会吃掉信号;分母用均值而不是标准差,是为了把价格水平和波动幅度拆开,否则高价股的因子值会被系统性放大;最后取窗口末行,是为了对齐「每周初开盘买入、每周最后一个交易日收盘卖出」的调仓节奏,中间的行只在滚动计算时用,不参与截面排序。这四个变体的差别在于对量价关系的假设不同:相乘是认为波动和流动性同时恶化才有效,相除是认为两者背离时信息量更大。

4.2 回测口径与多头超额不足的归因

回测范围是 2016 年到 2022 年 8 月的中证 1000 成分股,周度调仓,以每周初开盘价买入、每周最后一个交易日收盘价卖出。四个因子的表现放在一张表里看更直观:

因子IC 均值IC 的 T 值多空年化多空夏普多头年化超额
LI-5.36%-8.078.35%0.51-1.09%
VLI-2.79%-7.538.40%0.862.75%
LIxVLI-5.29%-10.748.89%0.710.74%
LI2VLI-3.11%-4.692.06%0.130.74%

IC 均值为负不是坏事,它说明因子值越低、未来收益越高,与低波动异象一致。四个因子的 IC 的 T 值绝对值全部大于 2,统计上站得住,多空净值曲线除 LI2VLI 外走势也相对平稳。但问题出在最后一列:多头组合年化超额收益最高只有 2.75%,LI 甚至是负的。这就是典型的「指标漂亮、策略做不出来」——IC 是个对称指标,多空两头都算贡献,而 A 股实际只能做多,多头组不赚钱,这个因子就没法直接变成产品。这条线最终没有继续往下做,原因就在这里,不是因子无效,是收益来源和交易约束对不上。

4.3 高频因子:3 秒快照的十档委托量价聚合

第二条线换到高频。高频数据挖掘的拥挤度相对低,挖出来的因子与传统的量价因子独立性也更强。这次把提示的数据范围死死限定住:只能用委托价和委托量,且必须原创。经过几轮引导,模型给出买卖盘力量因子。落到实现上,处理方式是对每只股票每天的 3 秒快照数据,分别求出前十档的买入委托价、买入委托量、卖出委托价、卖出委托量,再在日内求均值,得到买盘力量、卖盘力量和买卖盘力量差异三个因子:

def force_factor(snapshot, base_price_col="vwap"): """snapshot: 单只股票单日 3 秒快照, 含 bid_p1..bid_p10 / bid_v1..bid_v10 以及 ask_p1..ask_p10 / ask_v1..ask_v10, base_price_col 为当日基准价""" bid_p = snapshot[[f"bid_p{i}" for i in range(1, 11)]].mean(axis=1) bid_v = snapshot[[f"bid_v{i}" for i in range(1, 11)]].sum(axis=1) ask_p = snapshot[[f"ask_p{i}" for i in range(1, 11)]].mean(axis=1) ask_v = snapshot[[f"ask_v{i}" for i in range(1, 11)]].sum(axis=1) base = snapshot[base_price_col] # 委托价相对基准价的偏离度乘以委托量, 量纲被压到价格比例上, 可横截面比较 b_force = bid_v * (1 - bid_p / base) s_force = ask_v * (ask_p / base - 1) return { "BForce": b_force.mean(), "SForce": s_force.mean(), "BSForce": ((b_force - s_force) / (b_force + s_force)).mean(), }

代码里有三个设计决策值得单独讲。第一,委托量用十档求和而不是取单档,是为了把靠近盘口的挂单力量整体纳入,单档容易被撤单行为干扰。第二,价格偏离度乘委托量,本质是把「挂得多」和「挂得远」合成一个量,挂得越远说明意愿越弱,乘上 (1 - bid/base) 之后自然被折价,量纲统一到价格比例上,横截面上不同价位的股票才可比。第三,买卖盘力量差异做了 (B-S)/(B+S) 的归一化,原始差值在不同股票之间没有可比性,归一化之后取值范围收敛到 -1 到 1 之间,逻辑上也更干净——它衡量的是买卖双方力量的失衡程度,而不是绝对强度。基准价通常取当日成交量加权均价,取收盘价也可以,但收盘价会被尾盘异动带偏,我一般优先用加权均价。

4.4 衰减、降频与指增组合

日频口径下的测试结果分化很明显:

因子IC 均值IC 的 T 值多空年化多空夏普多头年化超额
BForce-0.24%-0.6118.74%0.9611.17%
SForce-1.02%-2.874.06%0.226.57%
BSForce1.13%7.0037.00%4.5117.29%

买卖盘力量差异因子的表现最突出,多空夏普 4.51,最大回撤只有 4.88%,多头年化超额 17.29%。买盘力量因子 IC 不显著但多空区分度好,多头超额也有 11.17%。这说明日内 tick 里的委托价和委托量确实能预测次日横截面收益差异。但日频调仓的成本太高,实际投资里基本吃不到这个收益,所以必须降频。降到周频后,买卖盘力量差异因子衰减太快、第二天基本失效,只能保留买盘和卖盘两个单边因子,它们的多头年化超额分别是 9.77% 和 10.20%,多空单调性一般但多头组合还能用。

最终选相对更稳的卖盘力量因子构建中证 1000 指数增强策略,在单边千分之二的手续费率下,年化超额收益 7.17%,信息比率 0.57。这个数字不算惊艳,但它是扣过成本的、能落地的数字,比日频那个 37% 有意义得多。至于模型写代码的能力,实测结论是:常用的因子测试框架、组合优化函数它能给得比较准,但细节必须逐行看——字段名对不对、库版本兼不兼容、有没有用到未来数据,这三处最容易出问题。在模型给的代码上做微调,研究效率提升是实打实的。

5. 避坑排查:五个最容易翻车的环节

5.1 公式照抄,回测 IC 趋近于零甚至反向

现象是模型给的因子表达式看着没问题,自己实现出来跑回测,IC 在零附近晃,或者方向跟预期完全相反。原因八成在数据口径,不在因子逻辑:价格没做前复权,除权日附近的价格跳变会被变异系数当成真实波动;快照数据没去重,同一时间戳重复推送导致委托量被放大;停牌日的空值被 fillna(0) 填掉,成交量变异系数直接失真。解决顺序是先对齐字段字典再写代码,拿单只股票、单日数据手工算一遍因子值,跟代码输出对得上,再上全样本。这一步看着笨,但能省掉后面几天的排查时间。

5.2 同一个提示跑两次,因子完全不同

现象是今天让模型生成因子,明天用看起来一样的提示再问,给的公式结构就变了,实验没法复现。原因是大语言模型本身带随机性,采样温度、上下文长度、甚至前几轮对话的残留都会影响输出。解决办法是把提示模板版本化管理,每次调用的完整对话日志落盘存档,同一个研究结论至少跑三次独立采样,只保留重复出现的因子思路。那些只出现过一次、逻辑又特别绕的,大概率是模型的即兴发挥,直接丢掉。

5.3 IC 显著但多头组不赚钱

现象是 IC 的 T 值大于 2,多空净值曲线漂亮,可多头组合年化超额是负的或者接近于零。原因是 IC 是秩相关系数,对多空两侧一视同仁,而 A 股存在做空约束,收益只能从多头组来。解决办法是评价体系里强制加两项:一是分组单调性,看十分位组合的收益是不是逐组递增;二是多头超额,把它跟 IC 并列作为硬指标。更彻底的做法是按分组给 IC 加权,多头组权重更高,用加权 IC 替代原始 IC 做初筛,这条思路本身也是从模型的思维链对话里逼出来的。

5.4 日频夏普 4.51,实盘一上就亏

现象是日频回测的多空夏普高得离谱,换成实盘或者加上真实成本之后直接转负。原因有两个:一是换手率,日频调仓意味着每天全仓换一遍,单边千二的成本一天就是千四,一年两百多个交易日,成本能把超额全部吃掉;二是因子衰减,高频因子往往只对次日收益有预测力,第二天就失效,你根本没有持有到第三天的理由。解决办法是回测阶段就把成本参数写死,先按单边千二跑一遍看还剩多少,再测因子在不同持有期上的 IC 衰减曲线,衰减快的就老老实实降频,别幻想靠交易频率堆收益。

5.5 模型给的代码跑得通,结果是错的

现象是代码没报错,能出数,但因子值和手工计算对不上,或者回测收益明显偏高。原因通常是三类:字段名张冠李戴(把卖一价当成买一价用)、库函数的默认参数和预期不一致(某些 rolling 默认 min_periods 处理会引入空值)、以及最隐蔽的未来函数——用当日收盘数据算了因子,却在当日开盘就下单。解决办法是三层校验:单票单日手工对数值、随机抽十个交易日做逐行断点、以及把因子计算和平仓时点的时间戳打印出来看有没有重叠。代码是模型写的,责任是你的,这一条没有捷径。

6. 验收与答辩:三层校验和一条自证链路

验收这套东西,我一般分三层走,每层都有明确的通过标准。数据层看字段对齐和时点正确,随机抽十只股票、五个交易日,手工复算因子值,误差超过千分之一就退回重做;因子层看 IC、分组单调性和多头超额三项是否同时成立,只有 IC 显著而多头不行的,一律标记为「不可直接使用」;组合层看扣费后的年化超额和信息比率,卖盘力量因子那条线给出的是 7.17% 和 0.57,这两个数必须是在单边千二成本假设下跑出来的,换成本参数就要重跑。

层级校验项通过标准
数据层复权、快照去重、停牌处理抽样复算误差小于千分之一
因子层IC 的 T 值、分组单调性、多头超额T 值绝对值大于 2 且多头超预期为正
组合层扣费后年化超额、信息比率、最大回撤成本假设写死在配置里,可一键复跑

答辩时最容易被追问的一点是「这因子到底是模型想出来的还是你想出来的」。我的答法是把提示链路完整摆出来:第几轮问了什么、模型答了什么、哪一轮的回答有明显漏洞、我怎么纠偏的、最后落地的公式和模型最初给的版本差在哪。这份对话日志比任何解释都有说服力,它证明的是研究流程可控,而不是把功劳推给工具。第二个高频问题是样本外表现,回答时不要只报样本内的漂亮数字,把因子衰减曲线和降频后的表现一起给,主动说出「日频夏普 4.51 但扣费后不可用,所以降到周频」,比等着被问出来强得多。

从那以后我每次让模型出因子,都强制先把提示模板和历史对话存档,再动手写一行代码——不是为了留证据,是因为翻车的时候你只有这份日志能告诉你,问题出在提示上还是数据上。希望帮到你。

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

返回列表