从一句话到一笔下单,我拆开了 Vibe-Trading 的风控决策回路

发布于 2026年07月02日 01:39 #Agent 框架#Github 解读 原文链接

从一句话到一笔下单,我拆开了 Vibe-Trading 的风控决策回路 封面图
  • Vibe-Trading 可通过自然语言指令直接生成专业回测报告,如帮我回测 BTC-USDT 20/50 均线 2024 全年
  • 决策回路拆解:自然语言到策略解析,再到数据获取、回测计算、报告导出,全程无需手动写代码
  • 风控是核心设计:每笔下单前自动经过仓位控制、止损设置、最大回撤校验三层风控,避免情绪化交易
  • 产品形态创新:把交易系统从专业软件变成对话即交易,降低量化门槛,普通用户也能在手机上完成策略回测

凌晨一点,朋友甩给我一条消息,「帮我回测一下 BTC-USDT 的 20/50 均线策略,2024 全年,算完导出报告」。

他说这话的时候在地铁上,没开电脑,没装 Python,更没碰过 tushare 或 ccxt。但他手机里跑着一个叫 Vibe-Trading 的东西,一行命令就能把这句话变成一份带回测曲线、最大回撤、基准对比的报告。

这事儿让我有点好奇。一个自然语言指令,到底是怎么变成一笔下单的?中间的意图解析、风控、执行,每一步都在干什么?哪些是真本事,哪些是话术?

我花了一个下午扒源码。HKUDS/Vibe-Trading,16380 star,2792 fork,MIT 协议,Python 写的,最近一次 push 就在昨天。拆开看挺有意思的。

一句话怎么变成一次回测

先说最外层。Vibe-Trading 的核心是一个 ReAct 循环,住在 agent/src/agent/loop.py 里,类名就叫 AgentLoop。你输入那句话之后,它不是直接去调 API,而是先跑一轮思考-行动-观察的循环。

说真的,ReAct 循环本身不算稀奇,LangChain 那套大家都会用。但 Vibe-Trading 在上下文管理上下了点功夫。它搞了五层压缩,从轻到重分别是 _microcompact(清旧工具结果)、context_collapse(折叠长文本块,零成本不走 LLM)、auto_compact(LLM 结构化摘要)、compact 工具(模型主动触发压缩)、以及迭代更新(第 N 次压缩更新上一次的摘要而不是从头来)。

阈值设计得有点意思。MICROCOMPACT_THRESHOLD 设成 TOKEN_THRESHOLD 的一半,默认 40000 token 的 50% 就是 20000。注释里写得很直白,说这个阈值不能太低,否则短跑也会清工具历史,把模型可能还要引用的指标结果给删了。

你想想看,一个回测跑下来,工具调用的结果可能有十几个,K 线数据、指标值、回测报告,全是长 JSON。如果每次都无脑清,模型到后面就忘了前面算过什么。这个分层压缩的设计,其实就是为了在「记得住」和「不爆 token」之间找平衡。

一层一层拆,意图解析这层没有太花哨的东西。它靠的是系统提示词加工具注册表,让模型自己决定调哪个工具。68 个工具挂在注册表里,从 get_market_datarun_backtestrun_swarm,模型按 ReAct 节奏挑着调。

真正值得说的是它怎么从「回测」走到「实盘」。

Mandate,一份冻结的授权契约

老实讲,我一开始看到「autonomous trading」这个词是有点警惕的。让一个 LLM 自己下单,这事儿的风险不用我多讲。但翻到 agent/src/live/mandate/model.py 之后,我发现它的设计比我想的保守得多。

核心是一个叫 Mandate 的 frozen dataclass。注意,是 frozen,不是普通 dataclass,也不是 Pydantic 模型。注释里解释了为什么,说 mandate 在启动时读一次就再不变,用 frozen dataclass 能给出最强的不可变保证,而且零验证面,Agent 没有任何写路径能改它。

这份契约分三层。

第一层 HardCaps,纯量化天花板。max_order_notional_usd 管单笔订单金额,max_total_exposure_usd 管总持仓市值,max_leverage 管杠杆倍数(1.0 就是纯现金),max_trades_per_day 管每日下单次数。还有一个 account_funding_usd,注释特意标注这是 BROKER-SIDE 执行的,Vibe-Trading 这边只是镜像一份做本地预算,真正的硬墙在券商那边。

第二层 UniverseConstraint,限定 Agent 能碰什么标的。但这里有个设计取舍挺值得说的,它不是 ticker 白名单。注释原话写的是,用白名单会杀掉 Agent 的发现能力。它给的是资产类别范围(美股、港股、A 股、加密货币)、市值下限、流动性下限、以及一个排除黑名单。Agent 在这个范围内自己挑标的,但每个标的都要过结构化检查。

第三层 ConsentMeta,授权溯源。有 created_atconsent_token_sha256(用户同意凭证的哈希)、brokeraccount_ref,还有一个 expires_at。重点在这个过期时间,默认 30 天。注释写了一句很硬的话,一份活的 mandate 不能永远活着。授权不是永久的,过期了就 fail-closed,必须重新授权。

这三层叠在一起,其实定义了一个模式。我愿意叫它「有界自治契约」。不是给 Agent 全权委托,也不是手动按每一下单,而是画一个圈,圈内它自己玩,圈外一步都迈不出去。

六步 fail-closed,一笔订单要过的关

授权契约画好了,但契约本身不拦人,得有执行层去查。这层住在 agent/src/live/order_guard.py,类名 LiveOrderGuardTool

这个类继承自 MCPRemoteTool,但它只包那些下单类工具。读类工具(查持仓、查行情)走原路,不经过它。区分靠的是一套三级分类器,在 agent/src/live/classification.pyclassify_tool() 里。

分类器的逻辑值得细说。Tier 1 看 MCP 工具自带的 readOnlyHint 注解,Tier 2 是维护者手工维护的 per-broker 映射表,Tier 3 是 default-deny,不认识的统统当 WRITE 处理。关键是 Tier 2 优先级高于 Tier 1。注释里引了 MCP 规范自己的原话,客户端永远不应该基于不可信服务器返回的 ToolAnnotations 做工具使用决策。一个券商如果把 place_order 标成 readOnlyHint=True,骗不过 curated map。

过了分类,进入 LiveOrderGuardTool.execute(),六步检查,全部 fail-closed,任何一步不过就 DENY,不碰券商 API。

第一步 load_mandate,没有有效 mandate 或 schema 版本不对,DENY。第二步查过期,过了 expires_at,DENY,路由去重新授权。第三步 halt_flag_set,kill switch 触发了,DENY,连远程调用都不发。第四步 extract_order_intent,订单参数解析不出来,DENY。第五步读持仓和余额,用券商自己的 READ 工具。第六步 check_mandate,把订单意图和 mandate 比对,通过就 ALLOW 转发,否则 DENY 或 PAUSE。

check_mandateagent/src/live/enforcement.py 里,检查顺序是固定的,排除名单、工具类型、资产类别、单笔金额、总敞口、杠杆、每日次数、资金。第一个挂掉的检查出结果,返回一个 BreachEvent。全过返回 None,就是 ALLOW。

这里有个细节我觉得特别重要。数量型违规和结构型违规处理方式不同。结构型(比如标的在黑名单里、工具类型不允许)直接 DENY,因为改 mandate 才能放行,而 Agent 永远不能改 mandate。数量型(比如超了单笔金额上限)是 PAUSE_FOR_REAUTH,暂停等重新授权。这个区分很关键,它把「违规」分成了「你改不了的事」和「可以商量的事」。

还有一点,repeatable = False。注释原话,一笔 live 订单永远不能被静默重发。下单工具不可重试。一个网络抖动导致的失败,宁可让模型重新决策,也不能自动重发一笔可能已经成交的订单。

Kill switch,独立于 LLM 的最后防线

上面说的六步检查,都建立在 Agent 配合的前提下。但如果模型本身在死循环呢?如果 SSE 总线挂了呢?

Vibe-Trading 的答案是一个文件系统层的 kill switch,住在 agent/src/live/halt.py

它就是一个 sentinel 文件,路径 <runtime_root>/live/HALT。文件存在就是停,不存在就是没停。halt_flag_set() 在每次下单工具调用前查这个文件,在任何券商调用之前。

关键在于,这个停机不依赖 LLM 配合。注释写得很清楚,哪怕 Agent loop 卡在迭代中间,哪怕模型在死转,哪怕事件总线挂了,只要这个文件在,下一笔订单就发不出去。

这事儿很重要。

还有一个 per-broker 的版本,<runtime_root>/live/<broker>/HALT,可以单独停一个券商通道而不影响别的。全局 flag 永远优先。

sentinel 文件里写了个小 JSON,记录是谁触发的、什么时候、什么原因。但注释特意强调,文件内容不可读或格式坏了,照样当触发处理。fail-closed 到这种程度,文件的「存在」就是停机,JSON 只是溯源用的元数据。trip_halt() 写文件用的是同目录临时文件加 os.replace,原子操作,不会留下半个坏文件。

坦白讲,这套风控设计的认真程度超出了我对一个开源项目的预期。但认真归认真,它有没有不诚实的地方?这得往回测那头看。

回测漂亮,实盘是另一回事

这是我最想聊的部分,也是这类项目最容易注水的地方。

先说回测引擎。agent/backtest/engines/base.py 的注释写得很清楚,所有市场引擎继承 BaseEngine,跑的是 bar-by-bar execution loop,数据加载、信号生成、目标权重预算、逐 bar 执行、市场规则强制、指标计算、产物输出。跑完会出 run_card.json,还会跑 Monte Carlo、Bootstrap CI、Walk-Forward 验证,加上分层归因(交易级赢家输家、beta 回归、市场状态分析)。

这套东西看着很专业。但你想想看,回测和实盘之间有几道天堑?

第一,滑点。回测引擎用的是历史 OHLCV,成交假设是「这个价格能买到」。BaseEngine 走的是标准 bar 级模拟,我在源码里没有看到对滑点的显式建模。实盘呢?一个市价单打下去,流动性不够的时候滑点能把回测里的利润吃掉一大块。这意味着回测报告里的收益率,到实盘大概率要打折。

第二,延迟。回测是「这根 K 线收盘,下根开盘买入」,零延迟。实盘从模型决策到 API 调用到券商成交,中间有 LLM 推理延迟、网络延迟、券商排队延迟。一个 5 分钟级别的信号,延迟 30 秒可能就换了个价格。Vibe-Trading 的 live gate 在下单前会实时读报价,order_guard.py 里的 _normalize_intent_notional 先查券商 quote 再查 data loader,取 explicit notional 和 quantity 乘价格里更大的那个来卡上限。但读到的价格和最终成交价格之间,还是有 gap。

第三,数据源不一致。回测用 tushare、akshare 这些历史数据,实盘下单用券商自己的行情。agent/backtest/loaders/ 目录下我数了一下,将近 20 个 loader,每个对复权方式、分红处理不完全一样。回测和实盘用不同数据源,策略表现可能有偏差。

这些不是 Vibe-Trading 独有的问题,是整个量化回测领域的通病。但项目把这些风险讲清楚了吗?

讲了一部分,但不够透。README 写了「Experimental, use at your own risk」,也说「No custody, no venue, the broker holds funds and executes, we only relay intent」。这些是对的,Vibe-Trading 确实不碰钱,只传意图。但「experimental」这个词太轻了。一个能让 LLM 自动下单的系统,风险提示应该比这重得多。你在源码里能看到 AdvisoryOrchestrator 这个咨询层,设计成 fail-open 且纯观察性,永远不会 block 或 alter 一笔订单。也就是说,即便接了外部风控服务,它的意见也只是参考,真正拦人的还是 mandate gate 那六步。这个设计是诚实的,但 README 没把这层关系讲明白,用户可能以为咨询层能拦单,其实不能。

那些「不能下单」的券商

扒源码的时候我发现一个有意思的细节,不是所有接进来的券商都能下单。

agent/src/live/registry.py 里挂了 11 个券商的分类映射表,从 Robinhood 到 IBKR 到 Tiger 到 Alpaca。但 Longbridge、Trading 212、Dhan、Shoonya 这几个,place_ordercancel_order 在第一行就硬拒绝。

原因写在更新日志里。这些券商的 API 没有运行时的 paper/live 区分器,你没法在代码层面判断当前连的是模拟环境还是真实环境。所以安全做法是只允许 paper 和只读。更新日志原话,一个没有结构性 paper/live 守卫的券商,上限就是 paper 加只读。

这其实是一个很诚实的设计决策。与其假装支持实盘然后在某个边界情况下漏过去,不如一开始就硬拒绝。但这个诚实在 README 的 feature 表里是看不出来的,你得翻更新日志和源码才知道哪些券商真的能下单,哪些只是挂着好看。

目前真正支持有界实盘下单的是 Tiger、Alpaca、OKX、Binance、Futu 这五个,加上 Robinhood 走 MCP。而且每一个都要先 commit 一份 mandate,过 OAuth,挂 kill switch,审计全程留痕。agent/src/live/audit.pywrite_live_action() 每次决策都写一条审计事件,返回的 tool_result 里冻一个 live_action key,api_server 的 SSE 中继可以拿这个发 live.action 事件,全程不碰 Agent loop。

留给你的判断

拆完这一圈,我想提炼一个东西。

Vibe-Trading 真正值得学的,不是那 68 个工具,也不是 456 个 alpha 因子,甚至不是五层上下文压缩。是它那套有界自治的工程范式。

我管它叫 Mandate-Gate 模式。核心就三句话,把授权冻结成不可变契约,把检查做成 fail-closed 的固定顺序闸门,把最后一道防线放在 Agent 够不到的文件系统层。这三层叠起来,一个会自己思考的 Agent 才敢让它碰真钱。

这个模式不止适用于交易。任何需要给 LLM 一定自主权、但又不能完全信任的场景,都套得上。内容发布前的审核闸门,运维操作前的权限校验,数据写入前的完整性检查,本质上都是「画个圈让它圈内自由」的问题。关键是那三件事要同时做到,契约不可变、检查不可绕过、底线不可达。

但你也得清醒。Mandate-Gate 挡得住结构性违规,挡不住市场风险。回测再漂亮,滑点和延迟照样吃利润。kill switch 能瞬间停机,但停不掉已经成交的订单。审计日志能溯源,但溯不回已经亏掉的钱。advisory 层能给意见,但 fail-open 意味着它挂了订单照发不误。

工具是好工具,框架是好框架。但拿它碰真金白银之前,先跑 paper,跑足够久,然后问问自己,这个 mandate 的圈,你画得够紧吗。

评论互动

© 2026 王若风的技术博客 · Powered by Astro