大模型赋能基本面量化:从财报文本到可回测因子的完整实操链路
发布时间:2026/10/11 13:46:05 锦皓数字建站

先声明一下这篇不是那种“AI能预测股价”的标题党内容。我实际做下来最深的感受是大模型在基本面分析里真正能打的点不在于“预测”而在于“把财报里那些机器难读、人读又费劲的文本信息变成一个可量化、可复现、可回测的因子”。这篇文章会把我的完整实操链路——从财务指标打分、Llama模型调用、评分融合到回测验证——全部拆开讲清楚代码和思路都会给到方便你直接在自己的数据上复现。1. 为什么要在基本面分析里引入大模型传统的基本面量化大家普遍会做一件事把收入报表、资产负债表、现金流量表里的关键财务指标抓出来按一定的规则打分最后合成一个总分以此判断一家公司的基本面强弱。这套方法本身没什么问题但它有个很明显的天花板结构化数据只能告诉你“发生了什么”很难告诉你“为什么会发生”。比如某家公司的营收同比增长了20%这个数字本身是确定的但它背后的原因是什么——是新产品放量、是并表、是一次性收益、还是会计调节——指望从几个财务比率里直接看出来几乎不可能。这时候就需要非结构化信息来补充。过去这种事情主要靠研究员去读财报原文、读管理层讨论、读业绩说明会纪要然后形成主观判断。但主观判断有个问题不可复现、难以回测、不同人读出来的结论还不一样而且效率太低几百家公司的财报如果都靠人工读一个季度下来要花掉的精力是惊人的。大模型在这一环的价值就体现出来了。它能把“读财报原文并提炼判断”这件事自动化而且判断标准是稳定的——同一个提示词、同一个模型参数对不同公司、不同时期的文本输出风格和逻辑是可对齐的。这就有资格成为一个因子进入模型和回测系统里。还有一个很实际的原因**很多中小市值的公司卖方覆盖不够研报很少市场关注度低。**这种情况下财报文本几乎是唯一的高质量信息源传统因子在这些标的上往往数据稀疏而文本分析恰恰能填补这个空白。所以这个项目的核心思路不是用大模型替代传统的财务指标评分而是把它俩接起来结构化指标负责衡量“现状质量”大模型文本分析负责衡量“叙述质量和管理层的信心”两者融合之后再放到历史数据里去验证看这个融合后的评分到底能不能带来超额收益。2. 整体框架设计从财报数据到交易信号的完整链路整个系统我从上到下分成五层每一层之间数据流是单向的方便单独替换或者调试。第一层数据层。需要的是两类数据。一类是结构化财务数据主要是利润表、资产负债表、现金流量表的关键科目按季度对齐另一类是非结构化文本数据也就是财报里“管理层讨论与分析”那一部分。这两种数据的获取渠道不同、更新频率不同、清洗方式也不同我建议分开存储、分开处理最后再通过公司代码和报告期对上。第二层特征层。结构化数据这一路筛选出十几个核心财务指标并且统一成可比口径。文本数据这一路会用滑窗方式截取与当期相关的文本片段因为Llama这类模型的输入长度有限制整个报告期几十页文本不可能全放进去必须做切片和筛选。第三层评分层。这是核心。结构化指标走一套传统的分位数打分机制每个指标映射到0到100的分数文本数据走大模型链路让模型针对营收驱动因素、毛利率趋势、风险因素这十几个维度分别输出结构化判断再转成分数。最后用一套权重规则把两部分合成一个基本面总分。第四层信号层。总分算出来之后不直接无脑买入。我给信号加了三个约束条件分数绝对值排名、较上期的分数变化幅度、以及估值分位的过滤。换句话说不仅要基本面好还得基本面在变好同时估值不能太离谱。第五层验证层。用历史数据做回测。回测这里有个关键点必须做严格的点时间切片。也就是说你在回测中某一天决策时使用到的所有数据都必须是那一天之前已经公开的任何未来数据都不能混进去包括财务数据本身——因为财报发布日和报告期结束日之间有滞后期这个必须处理干净。整体架构确认之后下面每层的关键实现我逐个展开。3. 收入报表的多指标评分体系搭建财务指标打分这部分虽然是传统技术但它是整个融合评分系统的基本盘不能糊弄。指标选得好不好、口径统不统一直接决定后面大模型输出能不能有意义地跟它叠加。3.1 核心指标怎么选我最终选了四组、共十一个核心指标覆盖成长、盈利、质量和估值四个维度成长性营业收入同比增长率归母净利润同比增长率营收环比增速的边际变化盈利性毛利率净利率ROE净资产收益率质量与安全经营现金流与净利润的匹配度也就是净利润现金含量资产负债率应收账款与营收的占比变化估值PE分位当前市盈率在历史分布中的位置PB分位这里面有两个容易踩坑的地方。第一个是“营收环比增速的边际变化”很多人只看同比但同比天然滞后因为它是跟前年同期比反应比较慢。我加的环比边际变化本质上是在捕捉增长的加速度尤其是单季度营收增速环比还在抬升的标的其基本面趋势往往更强。第二个是“经营现金流与净利润的匹配度”。这个指标的重要性远超一般人的认知净利润是权责发生制下的结果应收账款虚增、存货积压都会导致账面利润好看但真金白银没进来。如果净利润高增长但经营性现金流迟迟跟不上这种增长质量是要打折扣的。3.2 打分规则怎么定我没有用固定阈值而是用“全市场分位数映射”原因很简单不同行业的盈利能力天然不同用同一个绝对标准去卡等于是在给特定行业开后门。分位数映射的做法是在某一个时间截面上把全市场所有可比公司的某个指标做排序用百分位排名作为这个维度的得分。具体实现大概长这样import pandas as pd import numpy as np def rank_score(series, direction1): 将序列转换为0~100的分位数得分 direction1 表示指标越大越好 direction-1 表示指标越小越好 valid series.dropna() if len(valid) 0: return series * 0 50 # 无数据时给中性分 ranks valid.rank(pctTrue) scores ranks * 100 if direction -1: scores 100 - scores return scores.reindex(series.index, fill_value50) def compute_fundamental_score(df): df: 每个公司-报告期一行的宽表包含上述指标列 df df.copy() df[score_rev_yoy] rank_score(df[营收同比增长率]) df[score_profit_yoy] rank_score(df[归母净利润同比增长率]) df[score_rev_acc] rank_score(df[营收环比增速边际变化]) df[score_gross_margin] rank_score(df[毛利率]) df[score_net_margin] rank_score(df[净利率]) df[score_roe] rank_score(df[ROE]) df[score_cash_quality] rank_score(df[净利润现金含量]) # 资产负债率是越小越好direction-1 df[score_debt] rank_score(df[资产负债率], direction-1) # 应收账款占比变化越大越差 df[score_receivable] rank_score(df[应收账款占比变化], direction-1) # 估值分位越低越好 df[score_pe] rank_score(df[PE分位], direction-1) df[score_pb] rank_score(df[PB分位], direction-1) # 权重配置成长和盈利的权重略高 weights { score_rev_yoy: 1.2, score_profit_yoy: 1.2, score_rev_acc: 1.0, score_gross_margin: 1.0, score_net_margin: 1.0, score_roe: 1.2, score_cash_quality: 0.8, score_debt: 0.6, score_receivable: 0.6, score_pe: 1.0, score_pb: 0.6 } score_cols list(weights.keys()) df[fundamental_score] ( df[score_cols].mul(weights).sum(axis1) / sum(weights.values()) ) return df这段代码的逻辑很直白但有三个细节值得注意一是权重不要给得太极端。单指标权重差距过大会导致分数被一两个维度绑架反而失去“多指标互相印证”的意义。1.2的上限和0.6的下限是我反复测下来比较稳的范围。二是缺失值给50分中性分。基本面打分最怕的就是数据缺失直接丢样本这样样本量会大幅缩水。给中性分意味着“没有信息也不扣分”让缺失的标的不至于因为数据不全就被系统性低估。三是动态计算PE分位和PB分位时要用滚动窗口。有的票看着PE不高是因为处在历史估值中枢整体抬升的环境中滚动5到8年的分位数能更好反映当前估值在长期区间里的位置。到这里结构化评分就有了它是0到100之间的一个连续值。接下来就是文本链路和大模型上场的时候了。4. Llama模型语义分析引擎的实现细节文本这块是整套系统里信息增益最大的部分但坑也最多。我前前后后调了不止一个星期才把提示词、上下文构造和输出解析这几件事理顺。4.1 模型选型与推理环境模型用的是开源Llama系列的7B和13B版本。选这个规模的考虑是更大的模型效果确实更好但在大批量处理全市场财报时推理耗时和显存开销会变成瓶颈。实测下来经过适当量化的13B模型在通用A100级别单卡上批量推理吞吐能够满足全市场季度财报的处理需求。我用的是4-bit量化版本在推理速度和输出质量之间比较平衡。如果机器配置一般7B量化版也完全可以用文本打分的输出质量差距没有想象中那么大毕竟这个场景侧重信息抽取和逻辑判断而不是开放式创作。4.2 提示词设计关键是输出schema约束从一开始我就给大模型加了硬性要求必须按JSON格式输出而且所有维度必须输出数值打分禁止模棱两可。大模型在自由生成模式下很容易输出“公司经营稳健未来发展可期”这种正确但无用的废话必须用输出格式把它逼到可量化的维度上来。我用的提示词大概结构是这样的prompt_template 你是一名资深财务分析师。请阅读以下上市公司财报的“管理层讨论与分析”摘录并从以下维度给出评分0-100和判断理由。 公司{company_name} 报告期{period} 财报摘录 {text_snippet} 要求评分维度 1. 营收驱动持续性该公司的收入增长来源是否可持续是否有一次性因素 2. 盈利能力趋势毛利率/净利率的变动趋势管理层对此的解释 3. 风险因素暴露文本中提及的经营风险、行业风险、财务风险程度 4. 管理层信心信号管理层对未来的表述是否积极是否有明确战略描述 5. 文本信息增量该文本是否提供了超出财务数字的额外信息 输出格式严格按以下JSON格式不要输出其他内容 {{ 营收驱动持续性: 78, 营收驱动理由: ……不超过50字……, 盈利能力趋势: 65, 盈利能力理由: ……, 风险因素暴露: 88, 风险因素理由: ……, 管理层信心信号: 70, 管理层信心理由: ……, 文本信息增量: 60, 文本信息理由: …… }} 几个细节解释一下“风险因素暴露”这个维度的分数含义跟其他维度不同它是越高代表风险越大所以在后续融合环节我会把它的方向反过来。这一点在提示词里其实应该写得非常明确否则模型可能会按“风险越低分越高”的直觉来打。理由限定50字以内。这是为了避免模型输出冗长的解释性内容既浪费token也影响后续解析。而且理由本身并不参与打分它更多是提供一个可追溯的解释方便出问题时排查。每个维度都是独立打分的不允许模型在输出里出现“综合来看”之类的话。一旦它自己开始做综合判断分数的可解释性和稳定性就会下降。4.3 财报文本怎么切大模型有输入长度限制所以文本切片策略很关键。财报里“管理层讨论与分析”部分动辄几十页直接全量塞进去不现实。我的做法是优先级截取而不是均匀截取。管理层讨论与分析的正文信息密度其实高度集中在几个固定的子段落里经营情况回顾、收入与成本分析、研发投入、风险提示、未来发展展望。我的切片逻辑是先尝试全文截取前3000个字符如果包含“未来发展展望”子标题则把该部分的前1500个字符追加进来如果包含“风险”相关段落则把相关风险描述截取进来。也就是说输入大模型的文本大约在3000到5000个字符之间覆盖的是“已发生的经营回顾 已明确的风险因素 对未来的表态”这条信息链。因为我要的不只是模型对基本面好坏做判断更希望它捕捉到“管理层怎么解释变化、怎么看待风险、对未来的语气如何”这三者合并起来才是比财报数字多出来的那部分信息增量。4.4 批量推理与结果校验批量调用的时候我加了一个重试机制模型偶尔会因为格式问题输出非JSON内容导致解析报错。处理办法是捕获解析异常后把错误信息拼到新的提示词里让模型修正输出格式最多重试两次如果还失败这笔记录就打上缺失标记后续按中性分处理避免因为一条脏数据让整个回测管道崩掉。另外一个非常重要的校验逻辑是方向一致性校验。比如模型给“营收驱动持续性”打了90分但在理由里写的却是“主要依赖一次性资产处置收益”这种就是明显的矛盾输出。我在后处理里加了一个简单的关键词校验如果理由里出现一次性、非经常性、大幅下滑、亏损扩大等负面关键词而对应分数高于80就把这条分数强制下调到60以下。这个规则简单粗暴但过滤掉了相当一部分模型“满嘴跑火车”的输出。4.5 把文本评分合成一个因子五个维度的输出我不会直接用简单平均而是按这样的逻辑合成营收驱动持续性、盈利能力趋势、管理层信心信号是正向因子风险因素暴露是负向因子文本信息增量是调节因子它的作用是如果文本信息增量本身很低说明这段文本基本没提供什么额外信息那么文本评分的置信度就要打折扣。def parse_llm_output(response_json): score_positive ( response_json[营收驱动持续性] * 0.3 response_json[盈利能力趋势] * 0.25 response_json[管理层信心信号] * 0.25 ) score_risk response_json[风险因素暴露] score_info response_json[文本信息增量] # 信息增量越低越压缩文本评分的权重 confidence score_info / 100.0 llm_score (score_positive - score_risk * 0.5) * confidence 50 * (1 - confidence) llm_score max(0, min(100, llm_score)) return llm_score这里的核心想法是大模型打分不靠谱的时候宁可让它回归中性分50也不要让它制造虚假信号。信息增量低就意味着模型只是把财报上的数字换了个说法又说了一遍这种输出本身就是噪声压缩它的影响是对的。5. 结构化分与文本分融合以及交易信号的生成两路分数都出来了剩下就是把它们合起来并转成可操作的交易信号。5.1 融合权重怎么定融合权重我经历了明显的版本迭代第一版是各50%平均结果回测效果一般问题出在文本分数噪声偏大第二版给结构化分70%权重文本分30%效果有明显提升第三版做成了动态权重以“文本信息增量”得分作为权重调节器——信息增量高时文本分也就是LLM评分的权重从30%上调到45%信息增量低时压到15%。def merge_scores(fundamental_score, llm_score, info_score): # info_score 来自 LLM 输出的文本信息增量 base_llm_weight 0.30 info_ratio info_score / 100.0 llm_weight 0.15 0.30 * info_ratio llm_weight min(llm_weight, 0.45) fund_weight 1.0 - llm_weight merged_score fund_weight * fundamental_score llm_weight * llm_score return merged_score这样做的好处是系统能够自动识别哪些公司的财报文本值得重视哪些公司的管理层层写了一大堆车轱辘话、信息含量很低。话越多不一定信息越多关键是话里的有效密度。5.2 信号生成规则融合理财之后的基本面分数是一个0到100的连续值但不能直接拿来买。我加了三个约束之后才生成信号绝对分位门槛当期基本面总分在全市场的百分位排名进入前20%才允许选入备选池边际改善确认当期分数相比上一个报告期必须有所提升而且提升幅度至少超过3分。这一条表面上牺牲了一些提前量但会显著降低“买进之后基本面恶化”的概率估值过滤PE分位超过90%的不选。这样的信号逻辑对应的是一个偏中低频、月度调仓的框架买入、卖出都以财报披露为节奏不做日内交易也不做短期反转博弈。def generate_signal(score_df, prev_scores, pe_percentile): 输入当期得分、上期得分、PE分位输出多头信号/无信号/做空信号 thres_rank score_df[merged_score].quantile(0.80) signal [] for code, row in score_df.iterrows(): if row[merged_score] thres_rank: signal.append(0) continue if row[merged_score] - prev_scores.get(code, 50) 3: signal.append(0) continue if pe_percentile.get(code, 50) 90: signal.append(0) continue signal.append(1) return signal到这一步整个从“财报文本 财务数字”到“信号”的链路就已经完整了。接下来还需要证明这套信号确实有参考价值不然分数算得再辛苦也只是自嗨。6. 回测验证怎么确认这套评分真的有用回测是整个系统里最能暴露问题的环节。很多人直接拿历史数据一跑看着收益曲线高兴半天结果没过多久就发现逻辑里有未来函数或者幸存者偏差曲线当场崩塌。我在这个环节踩过的坑值得单独说说。6.1 严格的时间点切割做基本面回测最核心的一个原则是决策日能看到的财报只能是决策日前已经发布的。财报的报告期是季度末但实际披露时间往往滞后一到四个月。比如一份三季报报告期截止9月30日但11月份才披露。如果直接在10月1日就使用这份财报的数据就引入了未来函数。我的处理方式是在数据表里同时保留两个日期字段report_date报告期和ann_date公告日期回测时全部用ann_date对齐# 错误写法用一个未公开的财报数据做决策 df[df[report_date] today] # 正确写法必须要求公告日期已经过去 df[df[ann_date] today]这个错误早期我也犯过直接用报告期做筛选回测收益夸张得不像话后来发现是未来函数在帮忙。改成公告日对齐之后曲线瞬间就真实了许多。6.2 回测框架选型基本面策略一个月调仓一次交易频率很低所以我选的是事件驱动型回测而不是瞬时撮合回测。后者对高频交易有意义对月度调仓反而失真。具体指标上我固定的评估项包括年化收益率最大回撤夏普比率超额收益相对基准指数月度胜率换手率其中换手率必须要看因为它直接影响交易成本。基本面因子最怕的就是信号来回翻转一个季度买这个、下个季度换那个手续费和滑点会吃掉大部分超额收益。如果信号产生的换手率很高那通常意味着评分规则里的边际改善门槛设得太敏感。6.3 基准对比与分组检验比收益曲线更重要的一件事是看分组单调性把全市场按基本面总分分成五组从高分到低分考察未来一个季度的平均收益。如果确实线性递减说明这个因子有区分度如果高分组和中间组没差别那这个评分系统大概率只是在捕捉市场贝塔。我自己的测试结果大致是这样的分布虚构示意分组平均季度收益%第一组最高分4.8第二组2.1第三组0.6第四组-0.9第五组最低分-2.3虽然具体数字会随样本区间变化但单调性基本是稳的。这个单调性实验是验证基本面评分价值的核心证据比单看一条净值曲线有意义得多。6.4 过拟合防护调参的时候任何一个参数组合都能在后视镜里找到一段漂亮的曲线这不算本事。我的做法是把样本区间切成三段前段做参数探索中段做参数验证后段做样本外测试只有三段行为模式一致、且逻辑上说得通的参数才保留下来禁止在样本外区间反复调整参数一旦在后段调参那段数据就变成了新的训练集失去验证意义。这套流程走下来之后我对最终融合评分的信心就不是拍脑袋了结构化分提供了相对稳定的基本盘LLM文本分在信息增量高的标的上提供了额外的区分度两者结合后的分组单调性更干净边际改善信号也明显过滤掉了一批基本面下行的标的。7. 实操中踩过的坑和避坑方法如果说前面的篇幅是“应该怎么做”这部分是“我实际遇到了什么坑”有些坑非常隐蔽不写出来估计有人会重复踩。7.1 财务指标的“跨期可比性”容易翻车有些公司的财报会进行追溯调整尤其是发生会计政策变更、合并范围变化时上年同期数字会被重述。如果你用的历史数据是调整前的而实际上一份已发布的报告里对标的是调整后的数字同比计算出错会很严重。解决办法是尽量使用经过供应商标准化处理的接口数据不要自己从原始财报里手工拼同比。7.2 大模型输出的格式化问题比想象中多不要以为提示词里写“严格按JSON格式”就一定得到JSON。实测下来批量跑几百条总有几条输出里带多余的解释性文字或者把JSON嵌在代码块里。可靠的方案是解析时用正则先把JSON块抽出来再解析而不是直接json.loadsimport re import json def extract_json(raw_text): # 兼容文本前后有多余文字的情况 matches re.findall(r\{[^{}]*\}, raw_text, re.DOTALL) if not matches: return None for m in matches: try: return json.loads(m) except json.JSONDecodeError: continue return None7.3 批次处理时的“语境漂移”如果一批数据里既有消费股又有半导体股模型在打分时可能会受到上下文干扰——前一个样本如果是一家高毛利公司模型在评价下一家低毛利公司时打分尺度可能不自觉被带偏。解决办法是按行业分批推理让模型在一批任务里保持一致的参照系。同行业公司的商业模式相近毛利率、负债率水平可比打分稳定性会明显更好。7.4 数据量不足时谨慎用边际改善过滤边际改善信号依赖两个时间点的对比如果数据历史只有两三年、没有经历完整的市场周期就容易误伤那些本身一直优秀、但分数遇到基准回落的公司。对小样本区间我建议把边际改善的3分阈值放宽到0分以上或者直接关闭这一层过滤。7.5 文本切片时注意防止“拼接上下文错位”我在短期切片的时候曾经直接把“经营回顾”和“未来展望”两段强制拼在一起结果模型把“未来计划投入X”误读成“已经完成投入X”导致营收驱动持续性打分失真。建议在文本拼接时加入明确的段落分隔标记并在提示词里说明“以下内容中【经营回顾】和【未来展望】是不同时间段的信息需区分判断”效果显著好转。我把常见的坑整理成一个速查表方便排查现象可能原因排查与解决回测收益异常高使用报告期而非公告日筛选存在未来函数改用公告日期对齐全部决策逻辑LLM输出无法解析模型输出了多余解释文字或Markdown代码块用正则提取JSON块失败时按缺失处理并重试文本分噪声大不同行业文本混在一起推理打分尺度漂移按行业分批调用模型控制参照系一致高分票却跑输边际改善过滤放行了短期跳升增加连续两期改善确认或提高分数跳升门槛换手率过高分数变化太敏感信号翻转频繁提高分数改善幅度阈值或加入交易成本惩罚项财报数字对不上追溯调整导致的跨期口径不一致统一使用标准化财务数据源的调整后数据8. 关于这套系统的适用范围和边界最后说几句个人的真实体会。这套“基本面多指标评分 Llama模型文本增强 回测验证”的方案目前最适合的场景是中低频、月度调仓、覆盖全市场的选股框架。它不是给短线交易设计的也不适合那种指望每周都能买到暴涨股票的思路。财报文本更新频率天然是季度级别的你不可能让一个季度更新的信息去支撑日内交易的决策。从纯技术层面看这套框架最大的优势是把以前不可回测的“文本判断”转换成了可回测、可复现、可追溯的因子。有了这个转换基本面分析就不再只是研究员报告里的“我们认为”而是变成了一组可以验证、可以比较、可以发现单调性关系的量化信号。这种结构化恰恰是大模型进入金融领域的正确姿势。从实际效果看LLM文本评分的边际贡献不在于把每只股票都判对而在于在结构化分本身就模糊的区域提供了额外的区分能力——那些财务指标看起来都差不多的公司文本里管理层怎么解释、怎么展望往往藏着真正让基本面产生分化的线索。而这种线索恰恰是传统多因子模型看不见的。这套方案当然还有改进空间比如引入更多非结构化数据源、尝试用更细粒度的提示词模板做行业定制、或者用多模型交叉验证来降低单模型幻觉的影响。但就当前阶段来说把这一整套从数据到评分到回测的链路跑通并且每一步的逻辑都对得上我个人认为已经算得上一个完整、可靠、可以持续迭代的基本面量化系统了。代码方面核心部分都已经贴在对应章节里了财务数据接口和模型推理部分根据自己的环境对接就行。建议第一次复现的时候先把结构化评分跑通、回测一遍确认结果稳定之后再接入大模型这样可以避免两路分数一起调的时候出了问题根本分不清是哪一层的原因。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。