到 6 月底,一致性语料库已从 167 个参照策略扩充到 252 个。它从未覆盖到的三个 bug,却在社区分享的脚本里暴露了出来:未建模的保证金强平、不认汇率的购买力检查,以及悄无声息、从未上膛的移动止损。
本文中的数据测于 2026年6月29日,现保留原样。最新结果请参阅我们如何衡量一致性。
5 月我们上一次撰文介绍语料库时,一致性语料库里有 167 个参照策略,其中 165 个与 TradingView 做到严格逐笔一致。此后语料库继续扩充:到 6 月底,它包含 252 个策略,其中 251 个达到「优秀」一致性(99.6%),1 个有文档记录的异常,0 个失败。
总数本身是最无聊的部分。有意思的是语料库没有覆盖到的东西:三个我们此前没有建模的 TradingView 经纪商行为。每一个都是因为某个社区分享的脚本走到了 252 个策略都没走过的代码路径,才被发现的。下面逐一拆解这三个行为。
截至 2026-06-29 的语料库状况
corpus/validation_report.md (2026-06-29)
Total probes: 252
Excellent: 251 (99.6%)
Anomaly: 1
Strong/Moderate/Weak/Minimal/Missing: 0
TV trades: 389,590
Engine trades: 389,688
Matched: 389,468
在整个语料库上,引擎比 TradingView 多产生了 98 笔交易,相对 389,590 笔参照交易,偏差为 0.025%。这是汇总口径;按策略拆开看,下文那一个异常只占其中一笔。其余的来自另外 23 个策略:那份报告的评级标准允许交易笔数有不到 1% 的差距,因此它们仍被评为「优秀」。这些策略大多只差一两笔,其中四个合计占了 98 笔中的 90 笔。
当时最新加入的六个探针属于 drawing 类别。我们上线了一个运行时,把 Pine 绘图对象(line.get_price、label/box 的几何信息)当作回测期间的数据来读取,而不只是图表标注,这批探针就是那时加入的。如果策略把画出来的线当作计算状态——比如把手动锚定的趋势线用作动态支撑位——引擎就必须像 TradingView 一样,对脚本已经删除的对象继续提供几何信息。六个探针全部被评为「优秀」。
那一个异常,如实交代
那个对不上的策略,我们没有把它过滤掉。anomaly-equity-mirror-strategy-equity-01 恰好卡在保证金准入的 1× 权益边界上。我们当时的分析(语料库备注将其置信度评为 95% 或以上)认为,TradingView 的经纪商模拟器恰恰在这条边界上是非单调的:它放行了一部分超出权益的入场,又拒绝了一部分未超出权益的入场,而且不同 K 线之间的判断前后不一。我们的引擎是确定性的:权益相同,准入判断每次都相同。按照 Pine 文档记载的保证金语义,这是正确的;而根据上述分析,这并不是 TradingView 经纪商当时在这一条边界上的实际行为。要与之对齐,我们就得自己引入不确定性,所以我们没有这么做。这个差距写进了文档,没有藏起来,而且当时在 252 个策略中只此一例。(此后,维护者在 2026-09-29 的基线中已将这个探针评为「优秀」,语料库 README 也注明上述诊断正在复核。)
Bug 一:TV 会强平的那个仓位
对于已经击穿保证金要求的仓位,TradingView 的经纪商不会等你的策略逻辑来决定何时离场——它会直接强平。如果一个杠杆仓位或空头仓位的权益跌到所需保证金超过账户权益的地步,TradingView 就会在发生的当根 K 线上强平:以这根 K 线上对该仓位最不利的价格成交,平掉恢复保证金所需数量的四倍(最多平掉整个仓位),不管 strategy.exit() 的条件满足与否。
引擎之前没有建模这一点。它会继续持仓,等策略自己的逻辑最终把仓位平掉——对于没有设置紧止损的策略,这可能意味着一个 TradingView 早已强平的仓位,在引擎里又多拿了几十根 K 线。有 24 个社区分享的脚本一路持有杠杆或空头仓位,直到触发保证金追缴;在这些脚本上,TradingView 的交易列表里都有「Margin call」出场,而引擎从未产生过这类出场。
修复方案是:当账户权益跌破保证金要求时触发强制平仓,按 TradingView 经纪商的同一方式计算强平价格,再把强平数量按该品种的最小交易单位(lot step)取整——TradingView 不会强平小于这个 lot step 的零碎仓位;只要回测时传入了该步长,我们现在也一样。
这对在永续合约上回测杠杆或做空策略的人影响最大。如果你的权益曲线在回撤期间看起来过于平滑,原因可能就在这里。
Bug 二:不认汇率的购买力检查
Pine 策略可以声明一种与品种计价货币不同的账户货币——比如用 currency.INR 去交易一个 USDT 交易对。在检查你是否买得起一笔订单之前,TradingView 的经纪商会先按当时的汇率,把订单的名义价值换算成你的账户货币,再与同一币种的权益进行比较。
引擎的购买力检查跳过了这一步换算。它直接拿 USDT 名义价值与以 INR 计价的权益比较,仿佛汇率是 1:1——也就是说,声明了 currency.INR 的策略,看起来的购买力是应有水平的几十倍(一个 USDT 就值几十卢比)。在受影响的脚本上,引擎产生的交易大约是 TradingView 的两倍:它放行了 TradingView 经纪商以资金不足为由拒绝的入场。
修复方案是在购买力检查运行之前,先把账户货币的汇率应用到所需保证金上,这与 TradingView 在订单处理流程中应用汇率的位置相同;所用汇率由回测时传入(引擎默认为 1:1)。如果你之前用非美元的账户货币回测过以美元或 USDT 计价的品种,悄悄抬高你交易笔数的就是这个 bug。
Bug 三:从未上膛的移动止损
这是我们最希望用户能比我们先发现的一个,因为它是静默失败的。strategy.exit() 支持两种指定移动止损的方式:trail_points,相对入场价的距离;trail_price,绝对的激活价格。
// armed correctly today; trail_price-only calls used to never arm at all
strategy.exit("TS", from_entry = "Long", trail_price = close * 1.05, trail_offset = 10)
引擎的上膛条件只检查了 trail_points。一个纯粹通过 trail_price 指定的移动止损出场,编译不报错,运行不报错,却从未上膛——整个回测期间,仓位就这么挂着,没有任何移动止损在生效,悄无声息。没有异常,没有日志,也没有平仓交易。策略看起来有保护,实际上没有。
我们之所以发现它,是因为有一个社区分享的脚本只用 trail_price 来设置移动止损,结果只对上了 TradingView 一半的交易;修复之后,匹配率达到 99.7%。公开语料库里没有任何探针用到 trail_price,所以语料库的数字反映不出这个缺口。validation 类别正是为这类缺口而设的,就像语料库拆解那篇文章里说的:范围很窄的合成策略,不追求盈利,也不追求真实,只为单独检验某一个 Pine 特性,让这类回归没法躲在「整体数字看起来还行」的背后。
修复后,只要 trail_points 或 trail_price 任意一个存在,移动止损就会上膛。如果你写过用 trail_price 的 Pine 策略,而权益曲线看起来很可疑,就像根本没有移动止损一样——那确实没有,在首个包含这个修复的引擎版本 v0.10.14(2026-07-02)之前,所有版本都是如此。
为什么这才是语料库真正的意义
这三个 bug 没有一个是靠代码审查找到的。它们被发现,是因为某个形态恰好合适的策略跑过引擎,产出了一个与 TradingView 对不上的数字。这正是我们坚持把从 167 个到 252 个、并且还在增加的策略维护成一个独立留存、带版本管理的语料库,而不是寥寥几个冒烟测试的全部理由,也是我们让社区分享的脚本走同一套 diff 的原因,这三个 bug 正是在这些脚本里冒出来的。总数再漂亮,也可能藏着一个会改变策略盈亏的 bug,而抓住这类 bug 的唯一办法,就是不断让更多形态的策略,对照同一份基准真值,走同一套 diff。
发现缺口,我们就会继续补充探针,也会继续如实公布数字——好的部分,以及那一个异常。
接下来可以看看
- 浏览策略画廊:公开语料库目前的 312 个策略,每个策略都有自己的页面,列出一致性结果和交易笔数。
- 看看我们如何找到一个类似的保证金 bug——不同的 bug,不同的边界,同样的方法:隔离、diff、修复。
- 在 Claude 或 Cursor 中回测:使用免费的托管 MCP 服务器,或用本地 Docker 服务器在你自己的 OHLCV 上回测。
- 我们如何衡量一致性:最新结果及其数据来源。