LLM交易AgentStockBenchAI选股评测交易Agent benchmark强化学习量化数据泄露检验

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个交易日)作为最低要求。


风险提示

  1. 评测结果不代表未来收益。任何基准测试都只能反映历史表现,市场环境和模型行为都在变化。
  2. LLM交易Agent仍处于早期阶段。本文讨论的评测方法旨在帮助识别”伪Alpha”,而非保证盈利。
  3. 实盘前务必小资金测试。先用极小仓位验证Agent的真实表现,确认无系统性缺陷后再逐步加仓。
  4. 风控优先于收益。无论评测结果如何,都必须设置止损、仓位上限等风控措施。

相关文章推荐


本文数据来源: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条已验证信号)。评测台代码为简化教学版,实际部署需补充异常处理、日志记录、性能优化等环节。

💬 评论