大模型+量化投资:从Agent选股到夏普比率组合优化实战 这套系统我断断续续写了大概三周核心就一件事把大模型的“阅读理解能力”和量化投资的“数学优化”接在一起做出一条从选股到组合权重分配的可执行链路。老实说一开始我也怀疑ChatGPT这类大模型选股是不是玄学真跑起来才发现关键不在于它“说哪只股票好”而在于你怎么设计它的工作方式、怎么把它的输出变成量化模型能吃进去的结构化数据。这篇文章把我踩过的坑、拆过的模块、最后跑通的代码逻辑全部整理出来包含agent设计、数据获取、夏普比率优化的完整实现以及我遇到的一堆报错和排查过程。对量化入门者、想玩大模型落地场景的朋友都算是一份能直接“抄作业”的参考。1. 项目整体设计与思路拆解1.1 解决什么问题做投资组合的人通常卡在两个地方一是选股也就是从几千只标的里挑出候选池二是配权也就是给挑出来的股票分配资金比例。传统量化思路里选股这步经常依赖财务因子、量价因子这类结构化数据但你有没有发现很多决定性信息是非结构化的——比如公司公告里的措辞、研究员报告里的逻辑推演、新闻里对行业政策的描述。这些信息用代码直接提特征很费劲恰恰是大模型的强项。所以我这个项目的定位是用大模型扮演“基本面研究员”的角色读新闻、读财报摘要、读管理层讨论给出一个带有推理逻辑的评分再用夏普比率作为优化目标把这些评分和行情数据结合起来算出各股票的最优配置比例。这样既不做纯拍脑袋的人工选股也不做纯冷因子的黑箱模型而是一个“语义分析数学优化”的混合体。1.2 为什么选夏普比率作为优化目标配权这一步有好几种常见思路等权重、最小方差、最大收益、风险平价但夏普比率是个很“中庸”的选择。它的公式是 (组合预期收益 - 无风险利率) / 组合波动率说白了就是衡量“每一份波动能换来多少超额收益”。只追求最大收益的缺点是容易全仓压到波动最大的标的上只追求最小方差的缺点是收益可能太保守夏普比率则把收益和风险放在同一个公式里权衡符合大多数人的真实诉求——我多承担风险得有个说法波动太大但收益平庸的组合在夏普比率眼里就是垃圾。我在设计里选定组合优化目标为最大化夏普比率还有一个实际原因它写起来简单scipy的minimize函数几十行就能搞定而且对初学者很友好权重结果也容易解释。相比之下风险平价要解非线性方程组最大收益则暴露在极端权重下调试成本高。第一次做这个方向夏普比率是最好的“起步目标函数”。1.3 整体架构与模块划分我把整个系统拆成四个模块层层递进数据层负责拉取行情、新闻、财报摘要做清洗对齐。行情数据用来算收益率、协方差矩阵文本数据喂给大模型做评分。分析层也就是agent层。我设计了几个不同角色的agent比如数据采集agent、基本面研究agent、风险提示agent它们共享一个大模型底座但system prompt完全不同。优化层把agent输出的评分、置信度、风险提示综合成预期收益向量再把行情数据算出协方差矩阵最后用scipy最大化夏普比率求解最优权重。输出层生成一份组合报告包含权重分配、预期夏普比率、各标的的风险贡献以及agent给出的配置理由摘要。这层架构其实有点像经典的“数据-决策”分离数据层不关心你怎么分析分析层不关心你怎么优化每一层都只对上层暴露结构化接口。这样做的最大好处是后续想换掉某个环节很方便比如把ChatGPT换成本地部署的Qwen模型只动分析层后面的优化代码一行都不用改。2. 大模型选股Agent设计与数据链路2.1 Agent架构设计我在项目里定义了两个核心agent一个叫基本面研究员一个叫市场情绪分析师。基本面研究员的任务是输入股票代码、公司简介、最近财报摘要、重大公告输出一个从0到100的基本面评分外加三到五条理由。市场情绪分析师负责读当天新闻、社交媒体舆情摘要、行业板块异动数据输出一个情绪分数取值范围在-100到100之间。为什么要拆两个角色而不是让一个agent全干我实测下来的原因是任务混在一起时大模型容易“和稀泥”理由写得很全面但评分没有区分度。拆开之后每个agent的职责边界清晰输出风格稳定后续就算要替换模型prompt改起来也方便。你甚至可以给每个agent用不同的模型——基本面研究用参数更大的模型保证推理质量情绪分析用轻量模型降成本。agent之间我还加了一个简单的协调逻辑两个agent先并行跑各自输出结构化评分主控agent拿到两边的结果后进行综合。综合这一步不是简单加权平均而是让主控agent参考两个子agent的评分和理由给出一个最终推荐列表推荐列表里包含“优先级”和“配置建议描述”——比如“适度超配”还是“低配”。这些描述会映射成优化器里的预期收益区间。2.2 数据获取与清洗谈到数据必须说一个最烦人的问题行情数据和文本数据的日期对齐。股票收益率是按交易日算的但新闻发布时间不一定是交易日而且新闻对应的“主体”经常不直接用股票代码而是用公司简称。我做了三步清洗统一时间戳把所有数据都转成无时区的北京时间再按交易日切割。规则很简单非交易日发布的消息归到下一个交易日的开盘前信息池。实体对齐用一份“股票代码-公司全称-常用简称”的映射表在文本里做匹配。注意要处理大小写和全半角问题否则“某某银行”和“某某银行股份有限公司”会被当成两个实体。行情数据处理日收益率计算时要剔除停牌日不然协方差矩阵会失真。停牌对协方差贡献的其实是“假零收益”会把相关性和波动率都拉低。行情我用的公开数据接口拉日线文本数据则来自公开新闻聚合把这些原始文本交给agent之前我会先做截断单条新闻只保留前500字符财报摘要保留前2000字符超出部分直接丢弃。大模型的上下文窗口虽然大但塞太多无关内容反而会让评分不稳实测保持输入精炼准确输出质量最高。2.3 让大模型输出结构化结果让大模型输出结构化的JSON是关键中的关键。我一开始用自然语言让模型“给出评分”结果它回了一段散文解析起来极其痛苦。后来改用严格JSON约束并在prompt里给出一个样例格式。设计JSON结构时我的建议是字段名用英文枚举值用全小写字母数值范围在prompt里写死。比如{ symbol: 000001.SZ, fundamental_score: 72, sentiment_score: -15, confidence: 0.65, reason: 主营收入稳健但行业政策存在不确定性 }“confidence”字段是给优化层的风险调整信号。我最后计算预期收益时不是直接用评分而是用“评分乘以置信度”低置信度的评分会被压缩防止模型在它不懂的标的上瞎给高分。这里有个必须重视的坑大模型输出的JSON偶尔会非法会有多余的逗号、缺引号、甚至夹杂markdown代码块标记。解决这类问题就是在解析前先做两层处理第一层用正则把代码块标记剥离第二层用json.loads尝试解析失败则把文本交给一个“修正agent”重新整理格式。虽然听起来绕但在整个流程里反而能保证稳定性。3. 夏普比率组合优化的核心实现3.1 夏普比率原理与计算细节夏普比率不只是公式而已计算里藏着很多容易忽略的点。首先预期收益率向量怎么来我采用的是大模型评分映射法先把基本面评分和情绪评分压缩到合理区间再用它们对历史年化收益率做调整。单纯用历史收益均值有严重的幸存者偏差单纯用模型评分又在金融学上站不住脚所以两者要结合。设第 i 只股票的大模型综合分为 S_i范围0~100历史日均收益率为 m_i那么我的预期收益率定义为expected_return_i (1 - alpha) * m_i alpha * (S_i - 50) / 50 * target_spread这里的 target_spread 是一个年化调整系数比如8%代表模型评分在满分为100分时能带来的最大超额收益偏移alpha是模型权重我实测在0.4到0.6之间比较稳太小模型评分没影响太大历史信息被冲掉。这个公式一点也不玄妙它的作用只是把模型的“观点”翻译成优化器能用的数学语言而且翻译规则透明可调。协方差矩阵的计算也要注意用历史日收益率估算但时间窗口要避开“单边暴涨暴跌期”否则波动率被高估或低估。我一般用滚动240个交易日同时给协方差矩阵加一个很小的对角正则项比如 1e-4保证矩阵正定否则scipy的优化器容易数值爆炸。矩阵正定这个细节比很多人想象中的更重要。3.2 权重优化实现最大化夏普比率本质上是个带约束的凸优化问题。约束就两条权重和为1权重非负。不设做空上限是因为个人投资者日常根本不加杠杆做空加了空头权重结果没法落地。我还加了单标的权重上限默认30%防止优化器为了冲高夏普比率把鸡蛋全放一个篮子。直接看代码import numpy as np from scipy.optimize import minimize def negative_sharpe(weights, mu, cov, rf): port_return np.dot(weights, mu) port_vol np.sqrt(np.dot(weights.T, np.dot(cov, weights))) return -(port_return - rf) / port_vol def optimize_portfolio(mu, cov, rf0.02, max_weight0.3): n len(mu) constraints ( {type: eq, fun: lambda x: np.sum(x) - 1}, ) bounds tuple((0, max_weight) for _ in range(n)) init_weights np.array([1.0 / n] * n) result minimize( negative_sharpe, init_weights, args(mu, cov, rf), methodSLSQP, boundsbounds, constraintsconstraints, options{maxiter: 200, ftol: 1e-12} ) if not result.success: raise RuntimeError(foptimization failed: {result.message}) return result.x几个点你得注意初值不要给全零或全在边界上的值我第一版从“全部权重集中在第一只股票”开始迭代结果直接收敛到了局部最优改成等权重初值后稳定性明显提升。另外ftol不能太宽松否则迭代次数没跑满就停了权重分布会带毛刺。最后一定检查result.success失败就抛异常别静默返回垃圾权重。3.3 回测与结果评估优化器不是算完权重就收工得用历史数据做回测验证。我做了两套回测第一套叫“静态回测”拿调仓日的快照数据计算权重然后持有到下一个调仓日期间不调整按月度或双周调仓。这样做的好处是贴近真实操作不会因为频繁交易把钱全交手续费。第二套叫“滚动窗口敏感性测试”每换一段历史窗口重新算一遍协方差矩阵和模型评分看看最优权重是不是突变。如果权重变化非常大说明优化结果对输入很敏感组合稳定性差我做了一个“平滑机制”新权重与旧权重做加权混合旧权重占40%新权重占60%避免每次调仓都大幅换仓。评估指标除了最终的夏普比率我还看组合的最大回撤和换手率。很多人被夏普比率迷惑觉得高就好但如果你为了0.05的夏普提升多付了2%的交易成本那就得不偿失。我的经验是调仓频率控制在两周一次交易成本影响最小回测结果最优。4. 实操过程与核心环节实现4.1 环境准备与依赖安装我建议直接用Python 3.10以上版本创建虚拟环境。依赖比较简洁pandas、numpy用于数据处理scipy用于优化openai或你用的模型SDK用于调用大模型akshare或yfinance用于行情数据。不建议一次装一堆深度学习框架这个项目本质上不需要训练模型大模型是“调用现成接口”本地只做数据处理和数值计算。安装命令pip install pandas numpy scipy openai akshare如果想把大模型接成本地部署推荐用Ollama或vLLM启动本地模型再通过OpenAI兼容接口调用。这样做的好处是API成本可控、数据不出本机缺点是本地模型能力上限摆在那看你的需求取舍。4.2 大模型分析模块的实现我写了一个通用的agent调用函数每个agent只传不同的prompt模板。下面这段是我项目里的核心代码之一from openai import OpenAI client OpenAI( api_key你的密钥或本地服务密钥, base_url你的接口地址或本地服务地址/v1 ) def invoke_agent(client, model_name: str, system_prompt: str, user_content: str) - str: resp client.chat.completions.create( modelmodel_name, messages[ {role: system, content: system_prompt}, {role: user, content: user_content} ], temperature0.2, response_format{type: json_object} # 要求JSON输出 ) return resp.choices[0].message.contenttemperature设成0.2是经验值太高评分没稳定性太低又有点呆。如果要兼顾多样性0.2到0.4之间最合适。我给基本面研究员的system prompt大致是这样写的“你是一名专业的证券分析师擅长从公开信息中判断企业基本面。请基于用户提供的公司财务摘要和新闻对该公司给出0到100的基本面评分。评分越高代表基本面越强。必须使用JSON格式回答字段包括symbol、fundamental_score、confidence、reason。”给情绪分析师的prompt则重点强调“不要过度解读单个新闻区分短期波动与结构性变化”否则模型容易看到一条负面新闻就把情绪分数打到-80太敏感。4.3 完整主流程代码主流程就是串起整条链路。我用一个pipeline函数输入是候选股票代码列表输出是组合权重和报告def run_pipeline(symbols): # 1. 数据层获取行情和文本数据 price_data fetch_prices(symbols) news_data fetch_news(symbols) # 2. agent层并行分析 fundamental_results [] sentiment_results [] for sym in symbols: f_result invoke_agent(client, model, FUND_PROMPT, build_fund_input(sym)) s_result invoke_agent(client, model, SENTI_PROMPT, build_senti_input(sym, news_data[sym])) fundamental_results.append(parse_json(f_result)) sentiment_results.append(parse_json(s_result)) # 3. 综合打分置信度加权 mu build_expected_return_vector(fundamental_results, sentiment_results, price_data) # 4. 协方差矩阵 cov compute_covariance(price_data, window240) # 5. 优化 weights optimize_portfolio(mu, cov, rf0.02, max_weight0.3) # 6. 报告 return generate_report(symbols, weights, mu, cov)这个流程最大的特点就是一切都有实际数据支撑权重不是模型拍脑袋给的是优化器在“预期收益”和“风险”之间权衡算出来的而预期收益的估计又是大模型评分结合历史行情共同给出的。每一层都能单独验证出了问题能快速定位。4.4 运行演示与结果解读我实际跑过一次只有四只股票的演示。候选池是A股里流动性较好的四家公司。大模型给出的综合评分分别是82、74、61、55。基于评分映射后的预期收益排序也是如此但权重解出来并不是顺序排列评分最高的那只权重是27%左右评分第二的权重反而是30%达到上限原因在协方差矩阵——第一只股票波动率偏高且与其他股票相关性较强拉高组合整体风险第二只股票虽然评分略低但波动贡献小与第三只股票的负相关性起到了对冲作用。这个结果特别能说明问题只靠大模型打分是不够的必须交给优化器做全局权衡这正是这个项目的价值所在。单独选股再看不出差别但放到组合层面风险和相关性就成了约束条件最优解往往和直觉不一样。5. 常见问题与排查技巧实录5.1 模型接口与配置报错最典型的一类问题出在模型调用上。比如提示“model not supported when using codex with a chatgpt account”这个错误我排查了很久才发现是客户端配置的模型名和服务端部署的模型名不一致。你自己用OpenAI兼容接口时也会碰到类似的客户端写的模型名是“gpt-…”但本地服务里启动的模型注册名是“qwen…”两边对不上接口直接拒绝。解决办法是去服务端的模型列表接口查一下实际注册名再在客户端保持一致。还有运行开源agent项目时常见的“config.toml无法加载”报错。很多人以为这跟代码有关系其实九成是配置文件路径写错了或者TOML语法有问题最常见的语法坑是字符串值忘记了引号、section缩进不规范。排查方法很简单用一个TOML解析器单独解析这个文件如果解析通过那就是程序读取路径的问题检查当前工作目录是否为项目根目录。这类配置报错和模型能力没关系纯粹是工程细节但能卡你一晚上。5.2 数据质量问题与处理技巧行情数据接口偶尔会返回空值或停牌导致的缺失pandas里一算协方差矩阵就全是NaN。我踩过的具体坑是某个交易日某只股票因为停牌没有数据但另一只股票有收益数据结构对齐后出现错位。处理方法是先做索引对齐保留所有交易日时间戳缺少的数据用前向填充同时对停牌收益直接填0然后从协方差计算的样本里剔除当天交易量低于某阈值的“准无效日”。文本数据更大的坑是乱码和重复。新闻抓取下来要统一用UTF-8处理去掉控制字符。重复新闻会对情绪评分产生放大效应——同一条新闻被多个网站转载if你喂给模型时不去重模型会把这条新闻当成多次确认情绪分数虚高。我后来对文本内容做了MD5去重加上标题相似度去重效果立竿见影。5.3 优化结果的过拟合与稳定性问题夏普比率优化有个天然毛病用历史协方差和预期收益去求解经常得到“在历史上看起来很完美、实盘中一塌糊涂”的组合。我记得第一次跑回测的时候优化出的夏普比率高达2.3换了个时间窗口再试直接变成0.6。问题就出在协方差矩阵对小样本非常敏感个别极端涨跌会把相关性估计带偏。我的解决办法是三步第一协方差矩阵采用“指数加权移动平均”而不是简单移动平均近期的数据权重更高平稳得多第二权重中加入“上限约束”其实已经提到过了单标的30%上限能有效防止优化器在单边行情里走极端第三引入“缩水因子”——夏普比率计算时的收益预期打八折风险惩罚上调20%这是交易员的保守估计逻辑。虽然这样做会让最终夏普数值略降但回测稳定性提高了不少。注意这个系统只用于技术研究和个人学习不是自动投资建议。大模型的评分包含主观推断历史收益不预示未来表现所有输出都只是概率性参考。实操里务必结合自己的风险承受能力量力而行。6. 一些写在最后的实操心得项目做到后面我最大的体会是大模型选股不是给你一个“必胜代码”而是帮你在海量非结构化信息里提取一致性的维度。ChatGPT这类模型真正的价值是降低信息处理成本而不是预测股市。一个冷静的工程化做法是把模型当成人来管理——明确给任务、要求输出格式、限定边界让它只做自己擅长的事。在我这套系统里模型只负责“读懂并评分”不负责“决定交易”交易决策永远交给数学优化器加上风险约束这样整个系统才有解释性。还有一点想分享的是agent设计从一开始就不要铺太大。我最初设计了六个agent包括政策解读agent、行业轮动agent、技术面agent等等结果真正稳定输出有价值的只有两个。agent不是越多越好每个多出来的角色都会带来一次大模型调用的延迟和一次输出解析的失败概率。先跑通最小闭环再加角色才是正道。如果你也想复现这套链路建议从三四只股票的小池子开始先跑通“数据-评分-优化-回测”这条线再逐步扩大候选池。数据源、模型、评分函数都可以替换但只要架构搭整齐了以后换什么上游都省心。我后来想继续扩展的方向是把调仓频率改成动态触发式当组合偏离目标权重超过阈值才调仓这样能进一步压低交易成本。这个就留给你自己去试了。