在 Solana 上做市,首要的不是定价问题。大多数团队的模型是能用的。真正让他们亏钱的,是"判断出报价错了"到"新报价上链"之间的那段间隙。
在这段间隙里,你的旧报价仍然挂着、仍然可成交,而且现在已经错价了。会有人来吃掉它。
钱从哪里漏掉
每一次报价更新的时序:
1. 价格移动
2. ★你检测到★
3. 你做决策
4. ★你提交 撤单 + 挂新单★
5. ★它落块★
6. 新报价生效
★第 2、4、5 步全都是延迟,而只有第 2 步得到了关注。★ 团队去买更快的数据源,却把提交留在一条通用路径上,然后困惑于逆向选择为什么没有改善。
这笔账毫不留情。如果检测只花不到一个 slot、而落块要花三个 slot,你的暴露窗口里绝大部分在提交那一侧。把检测砍一半,总数几乎不动。
测窗口,不要测零件
真正重要的数字是完整的暴露窗口,而它很容易埋点:
const priceMovedAtSlot = detectedSlot;
const sig = await sendReplacement(newQuote);
const status = await waitForLanding(sig);
log({
detectLag: detectedSlot - eventSlot, // 你的数据源
decideLag: submitSlot - detectedSlot, // 你的逻辑
landLag: status.slot - submitSlot, // ★你的提交路径★
exposureSlots: status.slot - eventSlot, // ★真正让你付钱的★
});
★exposureSlots 才是该优化的那个数字。★ 它也是唯一能直接映射到亏损的:你的陈旧报价挂得越久,你的成交流里就有越多是别人在吃错的那一边。
要按分布追踪它。一个中位数暴露两个 slot、尾巴十五个 slot 的做市商,是在尾巴上被吃掉的 —— 而中位数会告诉他一切正常。
撤单改单有个顺序问题
最直观的实现藏着一个不明显的 bug:
// ★不要假设它们按这个顺序执行。★
await send(cancelTx);
await send(placeTx);
分别提交的 Solana 交易没有顺序保证。挂新单可能先于撤单落块。那一瞬间你有两个报价同时挂着 —— 陈旧的那个和新的那个 —— 这比只有陈旧那个更糟。
★只要指令装得下,就把它们合并成一笔交易。★
const tx = new Transaction().add(
cancelOrderIx(oldOrderId),
placeOrderIx(newQuote),
);
// ★一笔交易:原子、有序、一个签名、一份手续费。★
对大多数做市机器人来说,这是单项收益最高的改动,而且它是一个代码形状,不是一笔基础设施采购。当装不下时 —— 很多市场上的很多订单 —— 就把能合的合掉,并接受"分开的交易需要自己单独推理重叠情况"。
如果你的报价逻辑没问题、暴露窗口仍然很宽,免费领个 BoltTx key 改一行就能测测提交那一半。
更新频率是一个成本决策
每次更新都要花基础手续费,加上你附带的优先费。给一百个市场报价、并且每个都频繁更新,是一笔实打实的日常开销。
// ★按阈值更新,不要按每一跳更新。★
const drift = Math.abs(fairValue - quotedValue) / quotedValue;
if (drift < updateThreshold) return; // 还在容忍范围内
★设定 updateThreshold 是整个策略的核心取舍。★
太紧 —— 你不停地更新、不停地付手续费,而且大部分时间都有交易在途。
太松 —— 你的报价更经常是陈旧的,而陈旧报价正是有信息的成交流在寻找的东西。
正确的阈值取决于你的暴露窗口。★如果你的更新两个 slot 就落块,你能承受的阈值比八个 slot 落块时更紧★,因为每次更新让你暴露的时间更少。这就是提交速度改变的不只是你的指标、而是你的策略的机制所在。
逆向选择是可以测的
判断陈旧报价是不是问题的诊断方法:
// 对每一笔成交,标记它之后不久的价格。
const markout = (priceAfter - fillPrice) * side;
// ★按"该报价挂了多久"分桶。★
metrics.markoutByQuoteAge.observe(quoteAgeSlots, markout);
★如果 markout 随报价年龄系统性地变差,那你就是在被吃 —— 而修法是缩短暴露窗口,不是拉宽价差。★
拉宽价差是本能反应,而它治的是症状。它降低了你每次被吃的损失,同时也降低了你在真正想要的那部分成交流上的成交率。 缩短窗口对两者都是改善。
争用之下的手续费
做市商会反复写入同一批账户 —— 你自己的订单账户、市场的订单簿 —— 所以对这几个具体账户调 getRecentPrioritizationFees 才是正确的输入,而不是一个全网数字:
const fees = await connection.getRecentPrioritizationFees({
lockedWritableAccounts: [marketAccount, ...yourOrderAccounts],
});
★全网平均手续费是错误的输入。★ 一个只有你在报价的清淡市场几乎不需要付什么;而一个所有做市商同时在重新定价的波动事件,是完全不同的处境 —— 而那恰恰是落得晚代价最大的处境。
撤单值得有自己的手续费策略。 一笔没落块的撤单,留下的是一个可成交的陈旧报价。★在快速行情里,把撤单送上去比把新单送上去更重要★ —— 撤掉一个坏报价是防守,挂一个新报价是可选项。
计算与体积约束
批量的订单操作会撞上和任何复杂交易一样的两个天花板:
1232 字节。 每个订单都要引用账户、每个 key 32 字节。跨多个订单的撤单改单累加得很快,而 address lookup table 在这里很值,因为你的账户集合是稳定且可复用的。
计算单元。 每次撤单和挂单都消耗计算。按你实际发送的那个批次形状去模拟并设置上限,因为八个订单的批次并不等于单个订单成本的八倍。
const sim = await connection.simulateTransaction(tx, {
replaceRecentBlockhash: true,
sigVerify: false,
});
const limit = Math.ceil((sim.value.unitsConsumed ?? 200_000) * 1.2);
★做市商是唯一应该早早就建 lookup table 的那一类人★,因为不同于打新狙击,他们提前就知道自己的账户,并且要复用几千次。
上链应该是什么水平
我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。
★对做市商来说,那个数字就是一扇暴露窗口。★ 它的每一毫秒,都是你上一个报价仍然挂在一个你早已判定为错误的价格上的时间。
BoltTx 在这里的位置
我们负责你签名之后的那一跳。不做定价,不做订单管理。
提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 一笔撤单在落块之前、传输途中观察不到。而另一种选择,是把"我打算撤掉这个报价"广播给任何想抢先吃掉它的人。
你在本地签名。我们不托管资金、不代签、不修改交易内容。tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。
免费领 API key,没有月费:
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
常见问题
Solana 做市商该多快更新报价? 快到你的暴露窗口短于有信息的成交流反应所需的时间。测"价格移动到新报价落块"的 slot 数,并把这个窗口而不是更新频率当作目标。
撤单和挂单该放一笔还是两笔交易? 只要指令装得下就放一笔。分别提交的交易没有顺序保证,所以挂新单可能先于它的撤单落块,让两个报价同时挂着。
我的成交为什么比模型预期更差? 通常是陈旧报价带来的逆向选择。把 markout 按"报价挂了多久"分桶 —— 如果它随报价年龄变差,问题在你的暴露窗口,不在你的定价。
被吃了的话该拉宽价差吗? 它降低每次被吃的损失,同时也降低你在想要的成交流上的成交率。缩短暴露窗口对两者都是改善,所以值得先把那条路走尽。
Solana 上怎么测逆向选择? 记录每笔成交之后不久的价格,按方向计算带符号的 markout。按以 slot 计的报价年龄分桶,就能把陈旧报价的损失和普通定价误差分开。
做市商该付多少优先费? 从你要写入的市场账户和订单账户的近期费用推导。全网平均值忽略了你的争用是特定于你所报价的那个市场的。
撤单该比挂单付更高的费用吗? 经常该。一笔没落块的撤单留下一个可成交的陈旧报价,而一笔没落块的挂单只意味着你没在报价。撤是防守,挂是可选。
怎么跨市场批量更新报价? 把能装进 1232 字节的合并起来,并用 address lookup table 压缩账户引用。任何有顺序要求的必须放在同一笔交易里,因为分开的交易没有顺序保证。
做市商用 address lookup table 划算吗? 比大多数人都划算。你的账户集合稳定、要复用几千次,所以建表成本立刻就摊平了 —— 不像打新狙击,那时候账户还不存在。
该用多大的更新阈值? 取决于你的暴露窗口。两个 slot 落块能支撑比八个 slot 落块更紧的阈值,因为每次更新的暴露代价更小。从实测延迟推导,好过随便挑一个数。
我的撤单为什么落在挂新单之后? 因为在 Solana 上分别提交的交易没有顺序保证。把两条指令放进同一笔交易,让原子性替你保证顺序。
报价一天要花多少手续费? 每次更新的基础费加优先费,乘以你在所有市场上的更新次数。从按跳更新改成按阈值更新,通常是影响这个数字最大的杠杆。
报价更新该用 skipPreflight 吗? 该。preflight 会给你运行的最延迟敏感的操作再加一次往返,而且它模拟的是当前 slot,不是你将要落块的那个。
故障期间怎么安全地停止报价? 准备一条不依赖常规更新循环的全撤通道,并且确保它的手续费策略足够激进,能在导致故障的那种条件下落块。
落块速度改变的是我的策略还是只是指标? 是策略。更短的暴露窗口能支撑更紧的更新阈值和更窄的价差,所以这个改善会复利到"你能有利可图地报什么价"上。
批量订单操作该设多大的计算上限? 通过模拟你实际打算发送的那个批次形状测出来,再加余量。成本对订单数不是线性的,所以从单个订单外推要么高估要么低估。