回测显示一个很强的优势。机器人上线,赚到的只是其中一小部分,或者什么都没有。
★这通常不是策略建模错了,而是回测在重放历史时,把你当成不存在。★
回测默默做了哪些假设
| 假设 | 现实 |
|---|---|
| ★你会成交★ | ★你在和别人抢同一个 slot★ |
| ★按看到的价格★ | 你的规模会推动它 |
| ★立即成交★ | 决策到执行之间要过好几个 slot |
| ★每次都成★ | revert 和过期都要花手续费 |
| ★不影响市场★ | 别人会对你的成交流做出反应 |
★上面每一条都朝同一个方向乐观。★ 而它们是复利的 —— 这就是为什么回测和实盘的差距,通常远大于任何单一假设能解释的程度。
争用问题
★你重放的那个机会,在同一时刻对所有人都是可见的。★
回测问的是"这里有没有一笔赚钱的交易?" 实盘问的是"我抢不抢得到?" —— 这是一个不同的问题、有不同的答案,而历史数据里根本没有记录还有谁在抢。
// ★回测:机会存在,所以你拿到了。★
if (spread > threshold) profit += spread * size;
// ★实盘:只有你先落块才拿得到。★
if (spread > threshold && landedBefore(competitors)) profit += ...;
没有任何诚实的办法能从历史数据模拟出第二行。 现有最好的替代品是:假设你只赢下一部分被争抢的机会,然后看策略扛不扛得住。
★一个只有在 100% 成交率下才成立的策略,不是一个策略。★
把能算的执行成本算进去
上面五条假设里有三条是可计算的,修好它们能消除掉大部分幻觉:
function backtestTrade(opportunity, pool, size) {
// ★1. 价格冲击 —— 由储备确定性推出。★
const out = amountOut(size, pool.reserveIn, pool.reserveOut, pool.feeBps);
// ★2. 每一次尝试都要手续费,不只是成功的那些。★
const fees = BASE_FEE + assumedPriorityFee;
// ★3. 失败率 —— 花了手续费、什么都没换回来的那些尝试。★
const landed = Math.random() > assumedFailureRate;
return landed ? out - size - fees : -fees;
}
★第三行是大多数回测完全省略掉的。★ 一个成交率 60% 的机器人,在另外 40% 上照样付手续费 —— 而在高频下,这是一个从不出现在简单重放里的实质拖累。
如果你的回测已经算进了执行成本、实盘结果还是不一样,免费领个 BoltTx key 改一行就能测测提交路径。
用 slot 计时,不要用墙上时钟
// ★错:秒对"能不能被收录"没有意义。★
const executed = decision.timestamp + 500;
// ★对:按 slot 建模延迟。★
const executionSlot = decisionSlot + assumedLandingDelay;
const state = reservesAtSlot(executionSlot); // ★不是决策时刻的★
★你执行时面对的是落块那一刻的状态,不是你做决定时的那个。★ 一个按决策时刻状态定价的回测,量的是一笔不可能发生的交易。
光这一处修正,往往就能抹掉回测里大部分表面优势 —— 因为能扛过三个 slot 的机会,比"某一瞬间存在"的机会稀有得多。
数据本身在哪里误导你
幸存者偏差。 已经 rug 或死掉的代币常常不在历史数据集里,所以一个在剩下的东西上测出来的策略,是在赢家身上测的。
聚合的 K 线。 一根一分钟的柱子掩盖了内部的顺序。你的策略"抓到"的那根影线,可能发生在你的入场信号之前。
★重建出来的储备。★ ★从 swap 事件推出的池子状态是一个近似,而误差会在长时间重放里累积。★
看不见的失败。 链上历史记录的是已落块的交易。★过期的交易在任何地方都没留下记录★,所以一个基于链上数据的重放,看不到当时有多少人没抢到。
那该怎么做
★回测是用来否决策略的,不是用来验证策略的。★
一个通不过回测的策略,实盘一定不行。 这是一个有用的筛子,而且很便宜。
一个通过了的策略说明不了什么,因为上面那些假设全都在帮它。
真正能产出信息的顺序:
1. 用回测来否决。 便宜、快,能抓住明显不赚钱的。
2. 压测那些假设。 按 50% 成交率、双倍手续费、三个 slot 延迟重跑一遍。★如果优势消失了,那它本来就是个人造产物。★
3. 实时纸上交易。 对着实时状态算决策但不提交。这能在真实条件下抓出检测和逻辑错误。
4. 最小规模上实盘。 ★这是唯一能测出你真实成交率和 slot 分布的办法。★ 把它当成一项测量成本做预算。
5. 拿实盘对比回测。 两者的差距就是你的执行成本 —— 而那才是值得优化的数字。
有意识地测量那个差距
metrics.record({
backtestExpected: expectedProfit,
actualRealised: realisedProfit,
slotDelay: landSlot - decisionSlot,
outcome, // ★success | reverted | expired★
});
★一个随 slot 延迟增大的持续差距,说明问题在提交。一个不管延迟多少都恒定的差距,说明策略本身过拟合了。★
这个区分比任何回测精细化都值钱,而且只有同时跑两边才拿得到。
上链应该是什么水平
我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。
★这才是该喂进回测的分布,而不是假设立即执行。★ 按一个真实的落块延迟去重放,是让回测最接近实盘结果的那一个改动。
BoltTx 在这里的位置
我们负责提交。回测和策略设计完全留在你的代码里。
提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前、传输途中观察不到,这减少的正是回测模拟不了的执行成本之一。
你在本地签名。我们不托管资金、不代签、不修改交易内容。tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费,这让一次小规模的实盘测量很便宜。
免费领 API key,没有月费:
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
常见问题
Solana 机器人的回测为什么高估表现? 因为它重放历史时当你是唯一参与者。争用、执行延迟、价格冲击、失败的尝试,要么缺失、要么被乐观处理。
回测最大的错误是什么? 假设立即执行。 按决策时刻的状态定价成交,量的是一笔不可能发生的交易 —— 因为你执行时面对的是落块那一刻的状态。
回测里怎么给竞争建模? 没法直接建模,因为历史数据里没有"还有谁在抢"的记录。改成假设你只赢下一部分被争抢的机会,然后看优势还在不在。
失败的交易该算进回测吗? 该。一个成交率 60% 的机器人在另外 40% 上照样付手续费,高频下这是实质拖累。简单重放只统计成功的那些。
回测里怎么给价格冲击建模? 按你打算的规模、从池子储备算出输出,而不是用观察到的价格。冲击是确定性的,所以这是最容易做的一处修正。
该用 slot 还是秒来计时? 用 slot。 秒对"能不能被收录"没有意义,而且 slot 产出速度会变。把延迟建模成若干个 slot,并按落块那一刻的状态求值。
幸存者偏差为什么对代币策略是个问题? 因为死掉的代币常常不在历史数据集里。一个在剩下的东西上测出来的策略,是在赢家身上测的 —— 这会美化任何入场逻辑。
能从历史 swap 重建池子储备吗? 能近似重建,但误差会在长时间重放里累积。把重建出来的状态当作比实时读取更嘈杂的数据,尤其对小差异敏感的策略。
回测为什么看不到失败的交易? 链上历史记录的是已落块的交易。过期的交易没留下任何痕迹,所以基于链上数据的重放,量不出当时有多少人没抢到。
成交率假设成多少合理? 你实盘测出来多少就是多少 —— 这正是第四步存在的原因。在拿到那个数字之前,按 50% 压测是检验优势是否稳健的合理办法。
一个显示出巨大优势的回测该信吗? 应该更怀疑,而不是更相信。 每一个没被建模的假设都在帮它,所以表面优势越大,往往意味着越多的活是假设干的。
纸上交易有什么用? 在实时条件下抓出检测和逻辑错误,而不用冒资金风险。 它验证你的决策是对的,但验证不了你能不能赢下那场竞速。
第一次实盘该投多少? 最小规模,当成一项测量成本而不是一次盈利尝试。成交率和 slot 分布没有别的办法能观察到。
怎么区分过拟合和执行问题? 追踪"回测对实盘"的差距随 slot 延迟的变化。 随延迟增大的差距是提交问题;不管延迟多少都恒定的差距,是策略问题。
更快的提交路径会提高回测准确度吗? 它通过减少执行延迟缩小回测和实盘的差距,但回测本身仍然需要诚实地把那个延迟建模进去,而不是假设为零。
回测到底该拿来干什么? 否决策略。 一个不通过的回测是可靠的信息;一个通过的回测,大多只是告诉你假设对它很友好。