LLM交易Agent的'问答强≠交易强'陷阱:用StockBench方法论科学评测AI选股
一个刺骨结论:金融问答SOTA,放到实盘却亏钱
语言流畅度 ≠ 真实的概率推理能力。模型可以说出非常”合理”的分析,但这不代表它真正理解了交易。
2026年,AI Agent做量化交易已经从”概念演示”进入了”实盘部署”阶段。TradingAgents、AlphaCrafter等开源框架在GitHub上疯狂刷星,知乎”AI交易Agent”话题阅读量破千万。看起来,让大模型读财报、分析新闻、生成交易信号,已经是触手可及的未来。
但一个令人不安的问题始终悬在空中:那些漂亮的回测曲线,真的来自AI的”选股能力”吗?还是因为模型在训练时已经”见过”了未来的数据,所谓alpha不过是数据泄露的幻觉?
2026年,一篇名为StockBench的论文(arXiv:2510.02209)给出了令人震惊的答案:在真实的多月份股票交易环境中,LLM Agent的累计收益、最大回撤、Sortino比率等关键指标,与金融问答任务的SOTA表现几乎没有相关性。
换句话说,一个能在金融问答测试中拿满分的模型,放到实盘可能亏得一塌糊涂。
这不是危言耸听。我自己运行的中证800信号追踪系统,2861条实盘信号的整体胜率只有37.9%——这意味着盲跟信号买入,10次里亏6次。而PolyBench(arXiv:2604.14199)让7个顶级大模型预测Polymarket事件,仅2个获得正收益,其余5个全部亏损——尽管它们的”陈述置信度”都很高。
这篇文章就来做一次彻底的”拷问”:为什么”问答强”无法转化为”交易强”?StockBench的评测设计给了我们什么启示?如何自己搭建一个科学的评测台?
为什么”问答强”无法转化为”交易强”——三个断裂点
StockBench论文的核心发现可以概括为三个”断裂点”,它们解释了为什么LLM在静态金融知识任务上的优秀表现,无法迁移到动态交易决策中。
断裂点1:信号 → 决策的鸿沟
金融问答任务通常是静态的:给你一个财报、一条新闻、一组指标,让你判断”这家公司未来会涨还是跌”。这是一个分类问题,模型只需要输出一个标签。
但真实交易是序列决策问题:Agent需要每天(甚至每分钟)做出buy/sell/hold的选择,每个选择都会影响后续的状态空间。今天的”卖出”决策可能意味着明天失去了继续观察的机会,而今天的”持有”可能意味着错过了止盈窗口。
问答任务测的是”预测能力”,交易任务测的是”决策序列的累积收益”。 两者之间隔着巨大的鸿沟。
断裂点2:答案正确 ≠ 交易盈利
即使模型”预测对了”方向,也不一定能赚钱。考虑以下场景:
- 模型预测”明天涨”,但涨幅只有0.1%,扣除交易成本后反而亏钱
- 模型预测”明天跌”,但下跌发生在盘中而非收盘,无法执行
- 模型预测”涨”,但波动率过高导致止损被触发
交易盈利需要同时满足三个条件:方向正确 + 幅度足够 + 时机恰当。 问答任务只测第一个条件,而交易需要三个同时满足。
断裂点3:预训练记忆 vs 实时推理
这是最致命的一点。大语言模型在预训练时读过了互联网上所有的财报、新闻、股价讨论。当你让它分析”贵州茅台2024年走势”时,它可能直接调用了训练数据中的记忆,而不是在”推理”。
这就是CSI300记忆基准揭露的核心问题:对LLM Agent来说,数据泄露不只是你输入了什么,还包括它训练时见过什么。
在L4级别(严格隔离,时间与名称均脱敏)下,LLM Agent的累计收益几乎完全由被动市场暴露(beta)解释,缺乏持续的选股alpha。换句话说,L1级别下那15%的”超额收益”,很大程度是因为模型在预训练中已经”见过”了行情走势——它不是在预测未来,而是在回忆过去。
StockBench评测设计拆解:contamination-free基准
StockBench论文的设计相当精巧,它解决了传统LLM交易评测中的几个核心问题。
核心设计原则
| 维度 | 传统评测 | StockBench |
|---|---|---|
| 数据时效性 | 静态数据集(可能已泄露) | 2025年3月~7月实时数据,持续更新 |
| 评估方式 | 单次预测准确率 | 多月份连续交易收益 |
| 泄露控制 | 无 | contamination-free(防污染)设计 |
| 决策粒度 | 二分类(涨/跌) | buy/sell/hold序列决策 |
| 评估指标 | 准确率、AUC | 累计收益、最大回撤、Sortino比率 |
评测流程
StockBench的评测流程如下:
每日流程(每个交易日):
1. 输入:当日市场信号(价格、基本面、新闻)
2. Agent决策:输出buy/sell/hold + 仓位建议
3. 执行:按次日开盘价执行交易
4. 记录:累计收益、回撤、换手率等指标
5. 更新:进入下一交易日
关键创新:数据切片防泄露。 StockBench使用2025年3月~7月的数据,并且会持续更新,确保评测数据不会出现在未来LLM的预训练语料中。这是第一个真正”contamination-free”的交易基准。
评估指标
StockBench采用五个核心指标:
累计收益 (Cumulative Return) → 总赚钱能力
最大回撤 (Max Drawdown) → 最惨时亏多少
Sortino比率 → 下行风险调整后的收益
换手率 (Turnover) → 交易频率(影响成本)
胜率 (Win Rate) → 盈利交易日占比
注意:单一指标不可靠。 一个高收益的策略可能伴随极高的回撤,一个高胜率的策略可能因为一次大亏就抹去所有利润。必须多维度综合评估。
自己搭一个迷你评测台:数据切片、收益归因、风险调整后收益
理解了StockBench的设计思想,我们可以用Python搭建一个简化的评测台,用于评估自己的LLM交易Agent。
Step 1: 数据准备与防泄露切片
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
class DataSlicer:
"""
防泄露数据切片器。
核心思想:训练数据、验证数据、评测数据严格分离,
且评测数据的时间窗口在模型预训练截止之后。
"""
def __init__(self, db_path, pretrain_cutoff="2025-01-01"):
"""
pretrain_cutoff: 模型预训练数据截止时间
评测数据必须全部在这个日期之后
"""
self.pretrain_cutoff = pd.to_datetime(pretrain_cutoff)
self.db_path = db_path
def load_evaluation_data(self, codes, start_date, end_date):
"""
加载评测期数据(严格在pretrain_cutoff之后)
"""
import duckdb
start = pd.to_datetime(start_date)
end = pd.to_datetime(end_date)
# 检查评测数据是否在预训练截止之后
if start < self.pretrain_cutoff:
raise ValueError(
f"评测数据起始日期 {start_date} 早于预训练截止 {pretrain_cutoff},"
"可能存在数据泄露!"
)
con = duckdb.connect(self.db_path, read_only=True)
codes_str = "', '".join(codes)
df = con.execute(f"""
SELECT code, date, open, high, low, close, vol as volume, amount
FROM stock_daily
WHERE code IN ('{codes_str}')
AND date >= '{start_date}'
AND date <= '{end_date}'
ORDER BY code, date
""").fetchdf()
con.close()
return df
def create_train_test_split(self, df, train_end_date):
"""
Walk-forward式分割:训练集在train_end_date之前,测试集在之后
"""
train = df[df['date'] < pd.to_datetime(train_end_date)]
test = df[df['date'] >= pd.to_datetime(train_end_date)]
return train, test
# 使用示例
slicer = DataSlicer(
db_path='/mnt/c/Users/Administrator/clawd/data/quant/quant_v2.duckdb',
pretrain_cutoff="2025-01-01"
)
# 评测数据:2025年3月~7月(StockBench同款窗口)
eval_data = slicer.load_evaluation_data(
codes=['000001.SZ', '000002.SZ', '600000.SH'], # 示例股票
start_date='2025-03-01',
end_date='2025-07-31'
)
Step 2: 收益归因分析
class ReturnAttribution:
"""
收益归因分析器。
将总收益分解为:市场beta + 选股alpha + 交易成本
"""
def __init__(self, positions_df, benchmark_returns):
"""
positions_df: 包含 columns ['date', 'code', 'action', 'price', 'shares']
benchmark_returns: 基准指数日收益率序列
"""
self.positions = positions_df
self.benchmark = benchmark_returns
def calculate_portfolio_returns(self):
"""
计算投资组合日收益率
"""
# 简化版:假设每日调仓,按次日开盘价执行
trades = self.positions.copy()
trades['next_day_return'] = trades.groupby('code')['price'].shift(-1).pct_change()
# 过滤掉hold操作
executed = trades[trades['action'].isin(['buy', 'sell'])]
executed['trade_return'] = executed['next_day_return'] * executed['action'].map({'buy': 1, 'sell': -1})
daily_returns = executed.groupby('date')['trade_return'].sum()
return daily_returns
def decompose_returns(self):
"""
收益分解:总收益 = beta + alpha - 成本
"""
portfolio_returns = self.calculate_portfolio_returns()
# 市场beta:用基准指数收益率加权
beta_returns = self.benchmark.reindex(portfolio_returns.index, fill_value=0)
# Alpha = 组合收益 - beta收益
alpha_returns = portfolio_returns - beta_returns
# 累计收益
cumulative_portfolio = (1 + portfolio_returns).cumprod() - 1
cumulative_beta = (1 + beta_returns).cumprod() - 1
cumulative_alpha = (1 + alpha_returns).cumprod() - 1
return {
'cumulative_portfolio': cumulative_portfolio.iloc[-1],
'cumulative_beta': cumulative_beta.iloc[-1],
'cumulative_alpha': cumulative_alpha.iloc[-1],
'daily_portfolio': portfolio_returns,
'daily_alpha': alpha_returns
}
def risk_metrics(self, returns):
"""
风险指标计算
"""
# 最大回撤
cumulative = (1 + returns).cumprod()
running_max = cumulative.cummax()
drawdown = (cumulative - running_max) / running_max
max_drawdown = drawdown.min()
# Sortino比率(下行风险)
downside_returns = returns[returns < 0]
downside_std = downside_returns.std() * np.sqrt(252)
annual_return = returns.mean() * 252
sortino = annual_return / downside_std if downside_std > 0 else np.nan
# Sharpe比率
sharpe = annual_return / returns.std() * np.sqrt(252) if returns.std() > 0 else np.nan
return {
'max_drawdown': max_drawdown,
'sortino_ratio': sortino,
'sharpe_ratio': sharpe,
'annual_return': annual_return,
'win_rate': (returns > 0).mean()
}
# 使用示例
attribution = ReturnAttribution(positions_df, benchmark_returns)
decomposition = attribution.decompose_returns()
risk_metrics = attribution.risk_metrics(decomposition['daily_portfolio'])
print(f"累计收益: {decomposition['cumulative_portfolio']:.2%}")
print(f"市场Beta: {decomposition['cumulative_beta']:.2%}")
print(f"选股Alpha: {decomposition['cumulative_alpha']:.2%}")
print(f"最大回撤: {risk_metrics['max_drawdown']:.2%}")
print(f"Sortino比率: {risk_metrics['sortino_ratio']:.2f}")
print(f"胜率: {risk_metrics['win_rate']:.2%}")
Step 3: 评测台主框架
class StockBenchLite:
"""
简化版StockBench评测台。
用于评估LLM交易Agent在多月份交易中的真实表现。
"""
def __init__(self, llm_client, data_slicer, attribution):
self.llm = llm_client
self.slicer = data_slicer
self.attribution = attribution
self.positions = []
self.daily_decisions = []
def run_evaluation(self, codes, start_date, end_date):
"""
运行完整评测
"""
# 1. 加载数据
data = self.slicer.load_evaluation_data(codes, start_date, end_date)
# 2. 每日决策循环
dates = sorted(data['date'].unique())
for i, date in enumerate(dates[:-1]): # 最后一天无法决策(无次日数据)
day_data = data[data['date'] == date]
next_day_data = data[data['date'] == dates[i + 1]]
# 3. 调用LLM生成决策
for _, row in day_data.iterrows():
prompt = self._build_prompt(row, next_day_data)
decision = self.llm.generate(prompt)
# 4. 记录决策
self.daily_decisions.append({
'date': date,
'code': row['code'],
'action': decision['action'], # buy/sell/hold
'price': row['close'],
'shares': decision.get('shares', 100)
})
# 5. 执行交易并计算收益
positions_df = pd.DataFrame(self.daily_decisions)
results = self.attribution.decompose_returns()
risk = self.attribution.risk_metrics(results['daily_portfolio'])
return {
'decisions': self.daily_decisions,
'returns': results,
'risk_metrics': risk
}
def _build_prompt(self, row, next_day_data):
"""
构建每日决策Prompt
"""
return f"""
你是一个量化交易Agent。基于以下数据做出明日交易决策:
股票: {row['code']}
今日收盘价: {row['close']:.2f}
今日成交量: {row['volume']:,.0f}
今日成交额: {row['amount']:,.0f}
请输出JSON格式决策:
{{
"action": "buy" | "sell" | "hold",
"shares": 交易数量,
"reason": "简短理由(不超过50字)"
}}
"""
# 使用示例
# bench = StockBenchLite(llm_client, slicer, attribution)
# results = bench.run_evaluation(codes=['600036.SH'], start_date='2025-03-01', end_date='2025-07-31')
QTMRL启示:用多指标强化学习让Agent学会”何时不动”
StockBench揭示了问题,但如何解决问题?另一篇论文QTMRL(arXiv:2508.20467)给出了一个方向:多指标强化学习。
核心思想
QTMRL将交易决策建模为多臂老虎机问题(Multi-Armed Bandit),Agent需要在多个指标之间动态分配注意力:
状态空间:
- 技术指标(MACD、RSI、布林带等)
- 基本面指标(PE、PB、ROE等)
- 市场情绪(新闻情感、资金流向等)
动作空间:
- buy(买入)
- sell(卖出)
- hold(持有)
- skip(跳过,不操作)← 关键创新
奖励函数:
- 正向:风险调整后的收益(Sharpe/Sortino)
- 负向:交易成本 + 回撤惩罚
“何时不动”的价值
QTMRL的核心洞见是:在交易决策中,“不操作”本身就是一个有价值的动作。
传统RL Agent倾向于频繁交易(因为每次都有动作奖励),但真实市场中大部分时间应该”观望”。QTMRL通过引入”skip”动作和交易成本惩罚,让Agent学会在信号不明确时保持耐心。
class QTMRLLikeAgent:
"""
简化版QTMRL风格Agent。
关键:引入skip动作 + 交易成本惩罚。
"""
def __init__(self, action_space=['buy', 'sell', 'hold', 'skip']):
self.action_space = action_space
self.transaction_cost = 0.001 # 0.1%交易成本
def decide(self, state):
"""
基于多指标状态做出决策
"""
# 简化:用规则引擎模拟RL策略
signals = self._extract_signals(state)
# 信号强度阈值:低于阈值则skip
if signals['strength'] < 0.3:
return 'skip'
# 信号方向
if signals['direction'] > 0.5:
return 'buy'
elif signals['direction'] < -0.5:
return 'sell'
else:
return 'hold'
def _extract_signals(self, state):
"""
从状态中提取多指标信号
"""
# 简化版:实际应使用RL模型
return {
'direction': state.get('momentum_score', 0),
'strength': state.get('confidence_score', 0)
}
推理漂移诊断:Risk-Feedback Alignment给的”可审计轨迹”方法
2026年另一篇重要论文Representation Signatures & Risk-Feedback Alignment(arXiv:2605.28850)提出了一个关键问题:LLM Agent在长期交易中会发生”推理漂移”。
什么是推理漂移?
推理漂移是指:Agent在初始阶段可能做出合理的交易决策,但随着时间推移,它的决策逻辑逐渐偏离最初的设计,导致收益下降。
典型表现:
- 初期:严格遵循风控规则,止损及时
- 中期:开始”扛单”,亏损时不愿止损
- 后期:频繁交易,追涨杀跌
可审计轨迹方法
Risk-Feedback Alignment提出用表示签名(Representation Signatures)来诊断推理漂移:
class DriftDetector:
"""
推理漂移检测器。
通过比较Agent在不同时间段的"决策表示"来检测漂移。
"""
def __init__(self, llm_client, window_size=30):
self.llm = llm_client
self.window_size = window_size
self.decision_signatures = []
def extract_signature(self, decision_context):
"""
提取决策的"表示签名"
"""
# 简化版:用决策理由的embedding表示
prompt = f"""
分析以下交易决策的核心逻辑,用3个关键词概括:
决策: {decision_context['action']}
股票: {decision_context['code']}
理由: {decision_context['reason']}
输出格式:关键词1, 关键词2, 关键词3
"""
response = self.llm.generate(prompt)
return response.strip().split(',')
def detect_drift(self, recent_decisions):
"""
检测近期决策是否与历史模式偏离
"""
# 计算近期签名的统计特征
recent_signatures = [self.extract_signature(d) for d in recent_decisions[-self.window_size:]]
# 与历史基线比较
baseline_signatures = self.decision_signatures[:len(recent_signatures)]
# 计算签名相似度
similarities = []
for r_sig, b_sig in zip(recent_signatures, baseline_signatures):
overlap = len(set(r_sig) & set(b_sig)) / len(set(r_sig) | set(b_sig))
similarities.append(overlap)
avg_similarity = np.mean(similarities)
# 如果相似度低于阈值,判定为漂移
if avg_similarity < 0.6:
return {
'drift_detected': True,
'similarity': avg_similarity,
'action': "建议重新校准Agent或检查输入数据"
}
return {'drift_detected': False, 'similarity': avg_similarity}
落地清单:评测你的LLM交易Agent前必须回答的6个问题
在把你的LLM交易Agent投入实盘之前,请逐一回答以下问题:
问题1:评测数据是否”contamination-free”?
- 评测数据的时间窗口在模型预训练截止之后
- 数据切片采用Walk-forward方式,无未来信息泄露
- 股票名称、日期等标识符已脱敏(参考CSI300记忆基准的L4级别)
问题2:评测指标是否多维?
- 不仅看累计收益,还要看最大回撤、Sortino比率
- 区分Alpha和Beta收益
- 考虑交易成本(佣金、滑点)
问题3:决策序列是否完整?
- 评测包含完整的buy/sell/hold序列,而非单次预测
- 每个决策都有明确的执行价格和时间
- 持仓状态在决策间正确传递
问题4:Agent是否学会”何时不动”?
- 动作空间包含”skip”或”hold”选项
- 交易成本在奖励函数中体现
- 在低置信度场景下Agent倾向于观望
问题5:推理是否稳定?
- 用表示签名检测推理漂移
- 定期对比Agent决策逻辑的一致性
- 设置”决策异常告警”机制
问题6:是否经过实盘信号追踪?
- 至少3个月的实盘信号追踪数据
- 信号胜率、均收等指标已验证
- 与回测结果对比,差异在合理范围内
FAQ:常见问题解答
Q1: StockBench和PolyBench有什么区别?
StockBench测的是LLM在多月份连续股票交易中的表现,关注累计收益、回撤等交易指标;PolyBench测的是LLM在二元预测市场(Polymarket)中的预测能力,关注准确率。两者互补:PolyBench揭示”问答强≠推理强”,StockBench揭示”问答强≠交易强”。
Q2: 我的LLM Agent在回测中收益很好,为什么实盘亏钱?
最可能的原因是数据泄露。检查以下几点:
- 训练数据是否包含测试期的信息(时间泄露)
- 特征计算是否用了未来数据(特征泄露)
- LLM是否在”回忆”而非”推理”(预训练泄露)
参考LLM交易数据泄露检验一文中的检测方法。
Q3: 用哪个LLM做交易Agent性价比最高?
根据LLM vs 传统因子对比的实测,DeepSeek V3在因子生成任务上IC最高(0.041),且成本仅为GPT-4o的1/5。Claude Sonnet和GPT-4o虽然更贵,但在交易任务上并没有显著优势。
Q4: 评测台需要多少数据量?
StockBench使用**5个月(约100个交易日)**的数据。少于这个窗口,结果可能受短期市场波动影响;多于这个窗口,评测成本过高。建议至少3个月(60个交易日)作为最低要求。
风险提示
- 评测结果不代表未来收益。任何基准测试都只能反映历史表现,市场环境和模型行为都在变化。
- LLM交易Agent仍处于早期阶段。本文讨论的评测方法旨在帮助识别”伪Alpha”,而非保证盈利。
- 实盘前务必小资金测试。先用极小仓位验证Agent的真实表现,确认无系统性缺陷后再逐步加仓。
- 风控优先于收益。无论评测结果如何,都必须设置止损、仓位上限等风控措施。
相关文章推荐
- AI选股Agent全军覆没?LLM交易数据泄露检验——7个SOTA模型仅2个正收益的真相
- AI选股Agent对决:LLM vs 传统因子——CogAlpha框架五维评估体系详解
- AI Agent量化交易自动化实战——20任务架构与评测台定位
- 回测引擎V2架构设计——评测台与回测引擎的边界
- Cron调度指南——自动化评测的调度方案
本文数据来源:StockBench (arXiv:2510.02209)、QTMRL (arXiv:2508.20467)、Risk-Feedback Alignment (arXiv:2605.28850)、PolyBench (arXiv:2604.14199)、CSI300记忆基准 (arXiv:2605.28359)、自有CSI800信号追踪系统(cron 21ca2f58b097,2861条已验证信号)。评测台代码为简化教学版,实际部署需补充异常处理、日志记录、性能优化等环节。