简介:TradingAgents是一个基于多Agent LLM协作的金融交易框架,面向量化研究、金融科技开发者及大模型应用实践者,可用于股票数据分析、交易决策推演和多智能体模拟讨论等场景。资源包共918个文件,以532个py源码文件为核心,覆盖框架主逻辑、工具链与配置脚本;271个pyc预编译文件便于直接加载运行,21个md文档辅助阅读代码,还包含示例CSV行情数据(如贵州茅台历史数据)、PNG可视化图表及虚拟环境配置等,整体仅11.45MB,轻量且结构完整。已有621人学习使用。通过这份资源,读者可快速搭建多Agent交易实验环境,观察LLM智能体在数据获取、观点交换、投资决策中的协作机制,并借助现成脚本与示例数据验证或改造自身策略,是理解金融大模型应用的实用参考。
1. 多Agent不是噱头:TradingAgents到底解决了什么问题
TradingAgents 是一个把多个 LLM Agent 组成「虚拟交易团队」的开源框架。它不像 ChatGPT 那样问一句「要不要买」,而是让基础研究员、新闻分析师、技术分析师、交易员、风险管理师各自独立分析同一只股票,再通过多轮辩论形成交易决策。拆完代码后我发现,Agent 之间通过结构化辩论交换观点、互相质疑,最终输出带置信度的买入/卖出/持有信号。
这个项目解决的核心问题是「单一大模型对单只股票的分析太浅」。单个 LLM 容易在观点上摇摆,或者被新闻摘要带偏;多 Agent 系统逼着不同角色从多个维度独立思考,再在辩论阶段收敛。如果你想在个人量化研究里引入 LLM 决策,又不想从零搭多角色编排框架,TradingAgents 是一个可以直接改的起点,适合有 Python 基础、熟悉 OpenAI 类 API 的读者。
2. TradingAgents的架构:一场由LLM驱动的交易圆桌会议
2.1 六个角色的职责边界:从研究员到风控的完整链路
TradingAgents 的角色设计几乎是对冲基金投研流程的镜像。最底层是基础研究员和新闻分析师,一个负责财务报表与估值指标,一个负责新闻、公告和舆情;往上一层是技术分析师,只看K线和量价指标;再往上是交易员,关注流动性和订单簿;最后是风险管理师和投资组合经理。六个角色各拿各的数据、各说各的结论,最后由组合经理统一下判断。
为什么要把任务拆这么细,而不是让一个大模型直接分析完?核心原因是上下文隔离。所有角色如果共享同一份数据,LLM 会不自觉地把注意力放到最抢眼的新闻上,而忽略基本面。研究员看不到K线,就不会被前一天的涨幅带偏;新闻分析师拿不到财务数据,就只能在舆情层面发言。角色分离本质上是把「数据隔离」变成系统级的强制约束,而不是靠提示词去劝模型。
各角色使用的数据源在项目里是分开配置的,常见配置如下:
| 角色 | 输入数据 | 分析目标 | 典型输出 |
|---|---|---|---|
| 基础研究员 | 财务报表、估值历史 | 内在价值 | 目标价区间 |
| 新闻分析师 | 新闻标题、公告 | 事件冲击 | 利好/利空/中性 |
| 技术分析师 | K线、成交量、技术指标 | 趋势与点位 | 支撑/压力位 |
| 交易员 | 流动性、买卖价差 | 执行可行性 | 建议仓位 |
| 风险管理师 | 波动率、回撤 | 风险预算 | 最大损失上限 |
| 投资组合经理 | 以上全部 | 综合决策 | 买入/卖出/持有+置信度 |
这张表在实战中最大的价值是帮你定位改造成本。我自己改这个框架时,最先做的就是替换「输入数据」这一列。比如新闻分析师默认吃 Yahoo 新闻,我换成了自己的RSS聚合源,其余角色的数据流完全不用动。组合经理的角色在系统里很特殊——它不做具体分析,只做综合判断,它的提示词会被要求「如果角色间分歧过大,必须明确指出分歧点,而不是强行抹平」。这让最终决策具备可追溯性,你可以反查任何一个角色投了什么票、理由是什么。
2.2 多轮辩论机制:信号从分歧走向收敛的过程
辩论是 TradingAgents 的核心价值,不是把几个角色的输出简单拼在一起。第一轮是独立分析,每个角色基于自己的数据子集给出结论;第二轮开始,每个角色能看到前一轮其他人的发言摘要,然后决定坚持还是修正自己的观点;最后一轮由组合经理做最终裁定。
这个机制在代码层面看起来是这样的:
async def generate_signal( ticker: str, agents: dict, rounds: int = 3, decision_role: str = "portfolio_manager", ): transcript = [] for i in range(rounds): for role, agent in agents.items(): prompt = build_debate_prompt( ticker=ticker, role=role, prev_transcript=transcript[-2:], # 只取最近两轮,控制token data=load_market_data(ticker, role), # 按角色取不同数据 ) resp = await agent.arun(prompt) transcript.append({"role": role, "round": i, "content": resp}) final_prompt = aggregate_prompt(transcript, decision_role) decision = await agents[decision_role].arun(final_prompt) return parse_signal(decision)代码里最需要留意的两个点是prev_transcript=transcript[-2:]和data参数。前者不是把全部历史发言塞给每个 Agent,而是只喂最近两轮——多 Agent 系统最怕上下文爆炸,这个切片是控制 token 成本的关键。后者保证每个角色只看到分配给它自己的数据子集,研究员不会再被技术指标干扰。
rounds=3是官方示例的默认值,但我建议第一次跑通时先用 2。原因很直接:每增加一轮,所有 Agent 的调用次数就翻一倍。两轮已经足够完成「提出观点 → 回应质疑」的最小闭环,三轮的价值主要体现在真实交易场景里的论据挖掘。
辩论机制的心理依据也很有意思:LLM 在辩论中给出的观点质量,明显高于单次检索后直接回答。当角色 A 说「我认为该买入,因为营收同比增长 20%」,角色 B 会回应「但同行业平均增长是 35%,20% 实际上是落后」。这种对抗性追问,是单 Agent 场景里很难天然出现的,也正是 TradingAgents 值得拆开看的原因。
2.3 为什么多 Agent 结构不能退化成单一大模型
如果把 TradingAgents 换成单个大模型,把所有角色提示词塞进一个 system prompt,看起来省了 token,但会立刻丢掉两样东西:上下文隔离和对抗性检查。上下文隔离在上一节讲过,对抗性检查是更微妙的损失。
单一大模型内部的「自我批判」往往流于形式。它会说「我注意到风险」,但不会真正推翻自己刚给出的买入结论。多 Agent 则不同——每个角色的上下文是隔离的,批判对方时携带的信息差是真实的,不是同一个上下文里的自问自答。比如技术分析师说「突破 50 日均线,看多」,新闻分析师如果看到了财报电话会议里的负面指引,它会直接质疑「突破缺少成交量配合」。这种跨数据源的交叉验证,在统一上下文的模型里几乎无法稳定复现。
当然,多 Agent 也带来硬成本:一次完整分析至少要 6 角色 × 2 轮 = 12 次 API 调用。如果做全市场扫描,缓存和并发控制就是必选项,具体见后文 3.2 节和 5.5 节。另外有个工程细节值得注意:TradingAgents 的 Agent 工厂在初始化时会读取每个角色的 model 字段,也就是说可以给不同角色配不同的模型。像研究员、新闻分析师这类数据密集型角色,用 gpt-4o-mini 级别的模型就够;组合经理这种需要综合判断的角色再用更强的模型。这种混合模型策略能把单次分析的 token 成本压到全量高配的 40% 左右,决策质量下降有限。如果你想做大股票池扫描,这几乎是一开始就要定好的事。
3. 把TradingAgents跑起来:环境配置与首次模拟交易
3.1 环境准备:Python版本、依赖安装与LLM API配置
TradingAgents 官方要求 Python 3.10+,我在 3.11 环境下跑通,没有遇到版本兼容问题。依赖以 langchain、pandas、yfinance、pydantic 为主。先准备虚拟环境并安装依赖:
python3.11 -m venv venv source venv/bin/activate pip install -r requirements.txt # 在环境变量中配置 LLM Key export OPENAI_API_KEY="sk-xxxxxxxx"如果你使用的是其他兼容 OpenAI API 的服务商,常见做法是在项目配置里改 base_url 和 model 名,或者增加两个环境变量OPENAI_API_BASE和LLM_MODEL,指向你自己的服务地址和模型 ID。
注意:项目源码默认会加载
.env文件。建议把 API Key 写进.env而不是直接 export,避免在 shell history 里留下明文。
装完依赖后别急着跑,先验证一下数据链路通不通。这一步能省掉后面大量的无意义排错时间。
3.2 数据源配置:Yahoo Finance与缓存机制
TradingAgents 默认用 yfinance 拉取行情、基本面和新闻。网络不稳定时 yfinance 经常超时,所以我一般会打开本地缓存,让同一只股票的行情数据在 24 小时内不重复拉取:
# config.yaml data: source: yfinance cache_dir: ./data/cache cache_ttl_hours: 24 lookback_days: 365cache_ttl_hours=24意味着你在一天内重复分析同一只股票不会产生新的 HTTP 请求。调试阶段强烈建议打开,我当时跑一次 20 只股票的回测,没有缓存时有一半时间耗在重复下载数据上。lookback_days控制拉取多长的历史数据,默认 365 天;如果你要做长周期回测,改这里就行,不用改代码。
提示:项目依赖 langchain、pandas、yfinance。如果你的网络环境拉取 Yahoo 行情超时,先确认能否正常访问行情接口,再检查请求超时配置,别第一时间怀疑代码。
3.3 首次模拟交易:跑一个真实的信号生成
确认数据源正常后,开始第一次信号生成。常见做法是从项目入口文件执行:
python examples/run_single_ticker.py --ticker AAPL运行时会看到终端按顺序打印每个角色的发言。先是研究员输出估值分析,接着新闻分析师给出舆情判断,然后技术分析师、交易员、风险管理师逐个发言。最后一条是组合经理的决策,格式类似 JSON,包含决策方向、置信度和理由。
第一次跑的目标不是「得到正确答案」,而是「观察全套流程是否跑通」。建议把 rounds 参数调成 2,重点看 transcript 里每个角色的发言是否合理。我第一次跑时,新闻分析师把一条几个月前的旧闻当成最新消息分析,原因是数据源没做时间过滤——这个现象后面在避坑章会细讲。
跑通之后建议立刻做两件事:把 transcript 保存为 JSON 文件;再把组合经理输出的 JSON 和各角色原始发言做对照。这能帮你熟悉多 Agent 的决策链路,后续调参时你能准确知道是哪个角色的哪句话影响了最终决策。
3.4 自定义一个属于自己的 Agent 角色
TradingAgents 的 Agent 基类被设计成可扩展的。假设你想加一个「行业对比分析师」,对比目标公司与同行业三家公司估值差异,可以继承基类重写分析方法:
from tradingagents.core import BaseAgent class IndustryComparer(BaseAgent): role_name = "industry_comparer" def analyze(self, market_data: dict) -> dict: peers = market_data.get("peers", []) target_pe = self._calc_pe(market_data["target"]) peer_pe_avg = self._calc_pe(peers) # 计算同业平均PE return { "target_pe": target_pe, "peer_pe_avg": peer_pe_avg, "gap_ratio": round(target_pe / peer_pe_avg - 1, 4), }角色的注册流程一般是在配置文件的agents列表里加上industry_comparer,再在系统提示词目录里添加一个对应的 prompt 文件。新角色之后会自动参与辩论循环,不需要改任何调度代码。
我实际用这个方式加过一个「宏观情绪分析师」,专门判断美元指数和美债收益率对目标股票的间接影响。改动只涉及 prompt 文件和一个注册项,数据管道和辩论逻辑完全没碰。但这里有一个容易翻车的细节:如果你加了新角色却忘了在配置里注册,系统会静默降级成旧角色集合,不会报错。你以为是新角色在辩论,实际上它压根没进场。每次改完配置后,我都会在日志里核对一遍「本场参与辩论的角色列表」,确认注册项生效。
4. 信号质量与风控参数:从「能跑」到「可用」
4.1 关键参数解析:temperature、top_p 与辩论轮数
流程跑通之后,接下来就是调参,这也是决定这个系统「可用」还是「玩具」的关键一步。影响最大的是四个参数:temperature、top_p、max_tokens 和 rounds。
先看 temperature。研究员、新闻分析师这类「输入事实、输出判断」的角色,适合 0.2~0.3 的低温度,避免模型过度发散;辩论环节的交易员和组合经理,可以用 0.5~0.7,让模型更敢提出不同的切入角度。如果你把所有角色都调到 0.7,第二轮辩论时观点会发散到收不住,三个角色各说各话,最终决策像抛硬币。
top_p 和 temperature 是互补关系。我的经验是固定其中一个,用另一个来控制输出多样性。组合temperature=0.3+top_p=0.9对分析型角色很稳定;两只都拉高的话,采样随机性叠加,输出质量会明显下滑。
max_tokens 需要按角色单独设。研究员的估值分析要展开计算过程,给 1024;新闻分析师只需要结论加一句话理由,512 足够。所有角色共用一个 max_tokens 值看似省事,实际上既浪费 token,又让短输出角色产生无意义的废话填充。
rounds 是最贵的参数。每多一轮,token 消耗近似翻倍。实盘决策 3 轮就够了;做研究实验 2 轮够用。不建议 4 轮以上——第四轮开始各角色发言内容重复明显,边际信息量接近零。我常用的初始参数组合如下:
| 参数 | 分析类角色 | 决策类角色 | 影响 |
|---|---|---|---|
| temperature | 0.2~0.3 | 0.5~0.7 | 越高观点越多样 |
| top_p | 0.9 | 0.95 | 采样范围 |
| max_tokens | 512~1024 | 1024 | 输出长度 |
| rounds | 2~3 | — | token 成本近似倍增 |
4.2 长窗口记忆与上下文管理:让 Agent 记住「上一轮说了什么」
多 Agent 系统的一个隐藏坑:每个 Agent 都是无状态的。每一轮调用都是独立的 API 请求,Agent 不会天然记住自己上一轮说过什么。TradingAgents 的做法是把历史发言写进下一轮的 prompt,但只保留最近两轮——这是 2.2 代码里那个切片做的事。
这个设计带来一个实际问题:如果某个角色在第一轮提出了一个关键数据点,到第三轮时它已经被挤出上下文窗口,角色会在后续辩论里遗忘甚至重新提问。常见做法是引入一个 summary buffer,把每一轮每个角色的核心观点压缩成一句话摘要,替代原始全文进入后续上下文:
def summarize_round(transcript: list[dict]) -> str: """把一轮发言压缩为每位角色的结论摘要,用于长期记忆""" summary = [] for record in transcript: text = record["content"] summary.append(f"{record['role']}: {extract_core_claim(text)}") return "; ".join(summary)extract_core_claim的核心思路是只保留带数字或方向的句子。比如「业绩超预期 15%,维持看多」会被保留,「我认为该股长期走势取决于宏观环境」会被过滤。实际测试下来,加了这个摘要层之后,token 消耗减少了约 40%,而且 Agent 在第三轮仍然能引用第一轮的结论,而不是靠遗忘重新编一个。代价是压缩粒度需要自己调——摘取太短会丢语义,摘太长成本又上去了。没有标准答案,按你的模型能力和预算试。
4.3 回测验证:如何量化多 Agent 系统的交易质量
参数调完,必须回答那个终极问题:这套系统到底能不能赚钱?不跑回测的自我评估都是自我安慰。TradingAgents 本身偏向单 ticker 分析,要量化评估,需要把它放进回测循环里逐日调用。
回测里最大的坑是未来函数。当 Agent 在分析 2024-01-05 的行情时,它不能看到 2024-01-06 之后的数据。但因为数据源是一次性拉取整段历史,稍不注意就会让模型「作弊」。常见做法是给数据加载包一层截止日期:
def load_data_up_to(ticker: str, cutoff: date) -> pd.DataFrame: all_data = yf.download(ticker, start="2023-01-01") return all_data[all_data.index <= cutoff]注意 cutoff 必须统一用 UTC 时区。美股数据用本地时间比较时,开盘当天很容易出现日期偏移,导致截止日多了一天或少了一天,回测结果差很多。
回测流程一般是:按天切分历史区间,每天调用一次信号生成,把决策与当天收盘后的实际走势对比,最后统计胜率、平均收益率和最大回撤。胜率的统计口径也有讲究——信号为 buy 且未来 5 日收益为正才算成功;信号为 sell 且未来 5 日收益为负算成功;hold 不纳入统计。否则牛市里随便发个 buy 信号都能算对,胜率虚高得离谱。
注意:一次完整回测调用包含 6 个 Agent × 2 轮 × 3 次重复,也就是 36 次 LLM 调用。遇到 API 限流的概率很大,建议在代码里加指数退避重试,而不是手动重跑。
5. 避坑指南:多Agent LLM交易系统的隐蔽工程问题
5.1 幻觉行情:Agent 引用了不存在的数据
现象:研究员在输出里写「该公司连续 12 个季度营收增长」,但拉取回来的财报只显示了 4 个季度。更麻烦的是,第二轮辩论里的其他 Agent 会引用这个幻觉数据作为论据,错误在辩论链条里被传播放大。
原因:LLM 在信息不完整时会主动补全,尤其是基本面字段比较稀疏的时候。模型无法区分「看到了数据」和「推理出了数据」,会把推断包装成事实输出。
解决:在系统提示词里强制要求每个角色只引用数据源中实际存在的字段,并注明数字来源。我在研究员 prompt 里加了一句「如果你的数据源里没有某项指标,请直接说没有,不要估算」。同时在辩论 prompt 里限制「只能引用前一轮发言中标注了来源的数据」。加完后幻觉出现频率明显下降。这个改动成本极低,收益非常高,建议第一个做。
5.2 复读机式共识:多轮辩论变成互相附和
现象:第一轮六个角色观点各不相同,第二轮开始集体转向同一个方向,到第三轮基本全员「同意」,辩论机制形同虚设。
原因:LLM 本身就有社交一致性倾向。当角色 A 看到一个强势的看多观点后,会倾向于附和,尤其是自己的原始分析不够扎实时。低 temperature 也会让模型变保守,不敢反驳。
解决:两个手段配合用。一是辩论类角色的 temperature 调到 0.6 以上;二是在提示词里写明「如果你认为对方论据有漏洞,必须明确指出,不能含糊地说同意」。后者性价比极高。另外可以把首轮发言设为不可修改,后续轮次只能围绕首轮观点展开,能有效抑制角色倒戈。
5.3 上下文爆炸:API 费用在长股票池上失控
现象:跑 20 只股票的回测,月底账单比预期高出三倍。
原因:多轮辩论的 token 消耗远超单次调用。一只股票一次完整分析是 36 次调用,每次调用的 prompt 里还带着前面几轮的发言,token 数随轮次线性上涨。20 只股票乘上回测天数,最终消耗轻松到百万 token 级别。
解决:把辩论轮数降到 2;对中间轮次发言做截断,只保留每个角色最后 200 字;打开 3.2 节的缓存,让数据拉取不重复。如果还超预算,可以把组合经理换成更便宜的模型——最终决策对推理能力的要求反而低于前期的拆分分析。
5.4 回测里的未来函数:策略在实盘里完全失效
现象:回测胜率 78%,实盘模拟两周胜率掉到 40% 以下。这是量化项目最经典的翻车方式。
原因:Agent 在分析某一天时「意外」读到了当天之后的数据。yfinance 一次性拉取整个时间区间,回测循环里如果没做截止控制,模型很容易从本地文件里读到未来数据而不自知。
解决:严格使用 4.3 节的load_data_up_to包装,切割数据时按行索引截止,不要按日期字符串过滤。另一个容易踩的点是时区——把 cutoff 统一成 UTC 再比较,别让本地时间和交易所时间混在一起比较。
5.5 API 限流:并发触发 429,整个流程中断
现象:generate_signal 跑到一半,多个 Agent 并发调用直接报 429,整个分析中断,已消耗的 token 全部浪费。
原因:六个 Agent 每一轮都在同时对同一个 API 发起请求,瞬间并发数超出账号限制。GPT-4 级别模型的每分钟请求上限更严格,问题特别明显。
解决:在 Agent 调用外层统一加信号量限流,限制全局并发数,并对限流异常做指数退避重试:
import asyncio semaphore = asyncio.Semaphore(3) async def limited_arun(agent, prompt): async with semaphore: for attempt in range(3): try: return await agent.arun(prompt) except RateLimitError: await asyncio.sleep(2 ** attempt) raise RuntimeError(f"Agent {agent.role_name} failed after 3 retries")RateLimitError在不同 API 客户端里命名不同,OpenAI SDK 里是openai.RateLimitError,langchain 里可能是别的名字,需要看你项目里实际用的客户端。信号量值设为 3 是我试过比较稳的配置,设为 5 时在账号并发上限较低时还是会触发限流。
6. 进阶技巧:用 LLM-as-Judge 给每个 Agent 的发言质量打分
「能跑」和「可用」之间的差距,在 LLM Agent 系统里有效的度量方式只有一个:独立的旁路评审。我在项目里加了一个 judge 单元,它不参与交易决策,只在每轮分析完成后,对每个 Agent 的发言从逻辑一致性、数据支撑度、风险意识三个维度打分,并输出简短点评。有了这个评分,我不用等实盘就能预估一个配置的好坏——好配置的评分曲线是稳定向上的,坏配置在第一轮之后就会掉头。
实现起来并不复杂,在 generate_signal 返回后把完整 transcript 交给一个独立的评审模型:
async def judge_transcript(transcript: list[dict]) -> dict: judge_prompt = f"""你是资深量化研究员。请对以下多Agent辩论记录打分: - 逻辑一致性 (0-10):观点前后是否矛盾 - 数据支撑度 (0-10):是否引用具体数字而非空谈 - 风险意识 (0-10):是否主动识别潜在风险 辩论记录: {format_transcript(transcript)} 输出JSON: {{"logic_score": 0, "data_score": 0, "risk_score": 0, "comment": ""}}""" resp = await judge_agent.arun(judge_prompt) return json.loads(resp)评审模型用 GPT-4o、Claude 这类指令遵循能力强的模型都可以。有一个细节很重要:同一个辩论记录要让 judge 评审三次取中位数,因为单次评分方差很大。均值容易掉进极值,中位数稳定得多。
这个技巧最大的价值体现在调参对比上。当我想判断「3 轮辩论」比「2 轮辩论」值不值得多付一倍 token 成本时,只看 judge 分数的变化就够了——如果 3 轮只把总分提高不到 5%,果断换回 2 轮。从那以后,我每次调整 temperature、换模型、改提示词,都会强制走一遍 judge 流程,把三次评分的中位数存成基线。没有这个基线,多 Agent 系统就是黑匣子,你永远不知道是哪个改动让效果变差。希望这个办法能帮你在 TradingAgents 的深水区少走弯路。
本文还有配套的精品资源,点击获取