上一篇文章里,我们用 FMZ 接入 Robinhood Chain,搭建了一个 Uniswap V4 新池雷达。新池出现以后,程序能够及时抓到交易对、费率和流动性信息,这部分推进得很顺利。既然池子找到了,接下来就想试试:能不能在交易活跃的时候进去做 LP,赚一点手续费?
于是继续修改代码,加入模拟建仓、仓位估值和退出逻辑。跑起来一看,效果还不错,模拟收益曲线几乎一路向上。看着这样的结果,难免有点心动,又把授权、买币、添加流动性和撤池卖出补齐,完成了实盘代码的搭建。机器人启动以后,我兴冲冲地等着收益继续往上走,结果很快发现:群众里面有坏人啊。
一、三个币,两个没卖出去

最初尝试的三个币,两个出现了买入后卖不出去的情况。刚拿 ETH 买了目标币,对面的流动性就撤了,感觉像是专门等着我这一单。原本准备进去赚手续费,最后手续费还没赚到,币先留在了钱包里。
当时第一反应就是碰到了“貔貅币”。但回头查链上交易,还是得把原因分清楚:合约限制卖出和池子流动性被撤空,都可能造成卖不出去,不能直接当成同一种问题。perp 那次尤其明显,买入后约两秒,原来的 LP 就被撤空了,我们自己的 LP 甚至还没成功建立。后来缩小卖出数量,报价仍然提示流动性不足。调整滑点、重复授权,都没有用,兑换所需的对手方资金已经不在了。
这时候再看模拟曲线,就知道之前高兴得有点早。模拟可以按池内价格给资产估值,但这个价格是否还能成交、退出时有没有足够流动性,都需要另外验证。目标币上涨带来的仓位增值,也不能全部算成 LP 手续费。账面上看到盈利,和真正把资金收回来,中间还隔着一整套交易过程。
所以后面先改了记账方式,将预估收入、持仓估值、实际手续费和最终现金收支分开记录。币还没卖出去、交易还没确认,就不能当作这一轮已经赚钱结束。对于没有完成的交易,程序也要保留执行进度,重启后先核对原来的交易,避免把回执暂时未返回误判为失败,再买一次。
二、不抢新了,先找能持续交易的池子
吃过这次亏以后,我开始调整方向。新池刚出现,资料少,流动性又可能随时撤走,买入前检查再仔细,也很难保证下一秒不会变化。既然目标是持续赚手续费,那就先找已经运行一段时间、有人持续交易的池子,至少不用把主要精力放在争抢刚开出来的几秒钟上。
但第一版筛选又收得太紧:池龄至少七天、TVL 至少五十万美元、费率不超过 0.3%,再叠加成交量和价格条件,V4 最后能留下的池子很少。每条规则单独看都有理由,全部放在一起,机器人倒是很谨慎,就是迟迟不开仓。
后来把问题拆开看,才发现有些池是标准太严,有些是程序没找到,还有一些是协议根本没接入。于是,一边调整池龄、流动性和短时成交门槛,一边增加 V3 支持。实际查找时,V3 中存在更多值得继续审核的对象,只围着 V4 转,会限制候选范围。接入过程中也补齐了 V3 的身份核验、报价、授权、建仓和退出流程,并不是把池地址加入列表就算完成。
这里的“稳定池”,指交易和流动性状态相对稳定,并不是只做稳定币交易对。我们关注的是持续成交能否支撑手续费收入,以及资金进出是否具备基本条件。当前十二小时池龄、两万美元 TVL 等门槛,也只是这一阶段的研究配置,还需要结合实际运行继续校准。
三、页面上明明有,程序却找不到
扩大范围后,又遇到一个让人挠头的问题:人工打开页面能看到的池子,程序就是不往候选里放,MOO 就是其中一个。早期通过 ETH、WETH、USDG 地址查询相关池子时,接口各自只返回有限数量的结果,其中没有 MOO;换成 MOO 的代币地址反查,池子马上就出来了。原来它根本没进入目录,后面的筛选写得再细,也轮不到它。
我们随后补过热门池列表,还尝试从 V3、V4 的建池事件回补历史目录。技术上能够做,但当时节点套餐每次 eth_getLogs 只允许查询十个区块,回补的请求量和等待时间都很可观。做新池雷达时,跟着最新事件往前走很合适;现在要找已有活跃池,再从历史区块里重建完整目录,就成了另一项基础设施工作。
兜了一圈,最后采用了 KyberSwap Earn 的池目录 API。不指定 MOO 名称和地址,在普通分页里就找到了它。此后,我们把目录发现、行情统计和链上核验分开处理:
| 数据来源 | 负责的工作 |
|---|---|
| KyberSwap Earn API | 分页获取 V3/V4 池目录,定期更新 |
| DEX Screener | 提供成交额、买卖笔数和涨跌幅统计 |
| 链上 RPC | 核对池身份、当前价格、活跃流动性和交易状态 |
目录每页获取一百条,完成一轮后等待三十分钟,再重新分页合并更新。行情直接使用接口已有统计,链上请求集中用于候选核验和持仓管理。这样,机器人不用启动后先积累一小时行情,也不必为了筛选已有池反复扫描历史区块。
分页本身也踩过坑。接口按实时 TVL 排序,翻页过程中顺序会变化,去重后的池子数量不一定等于接口报告的总数。如果硬性要求两者相等,就可能不停从第一页重来。后来保留了数量差异记录、异常重试和定期更新,将“本轮遍历结束”与“完整覆盖全链”分开判断。API 提供了更广的候选范围,但仍然存在索引延迟和遗漏,需要持续复查。

四、只拿 3U 测试,gas 怎么花了 2.33U
池子能找到了,接下来就得认真核对成本。模拟阶段,gas 更多是一个估计值;真正执行起来,一轮 LP 操作可能包含包装 ETH、授权、兑换、添加流动性、撤池领取、卖回和解包。V3、V4 的授权路径也不同,不能统一按几笔交易粗略估算。
于是,我们写了独立测费脚本,用约 3U 的本金预算,在 USDG 与 ETH/WETH 配对池中完成短时建仓和退出。测试先解决两个问题:整套流程能否走完,以及每一步实际花了多少费用。
| 测试环节 | 主要检查内容 |
|---|---|
| 资金准备 | ETH 是否按需包装为 WETH,目标币是否成功买入 |
| 授权与建仓 | 授权对象和额度是否正确,LP 仓位是否实际建立 |
| 撤池与领取 | 流动性是否成功撤出,两侧资产是否返回钱包 |
| 卖回与解包 | 目标币是否卖回,属于本仓的 WETH 是否解包 |
| 费用结算 | 逐笔计算 gas,并核对整轮资金净变化 |
| 中断恢复 | 对未确认交易继续核查,避免重复执行已发送步骤 |
gas 按回执中的实际用量和单价计算,而不是根据配置里的预估值记账:
javascript
// 实际 gas 支出,单位为 wei
var gasWei =
BigInt(receipt.gasUsed) *
BigInt(receipt.effectiveGasPrice);
totalGasWei += gasWei;
测试中,第一轮 V3 的完整流程 gas 约为 0.326U,后续一轮却达到了约 2.33U。看到结果时,我也愣了一下:刚才不是三角多吗,怎么又变成两块三了?对照交易后发现,后一轮授权步骤更多,gas 单价也明显提高。两轮包装 ETH 消耗的 gas 都是 57,647,折算费用却分别约为 0.0139U 和 0.1545U。相同操作的费用也会变化,不能拿上一次的结果直接当作下一次的收费标准。随后完成的 V4 独立测试,整轮 gas 约为 0.226U,同样只能作为这次执行的成本样本。
有了这些实测,正式代码才改成按操作分别预留 gas:包装、授权、兑换、建 LP、撤池各算各的,再结合当前单价和预算余量。没有实测过的代币操作,仍然保留较高兜底。这里校准的是预算模型,实际支出继续按回执计算,不把某一次测试费用固定写进策略。
测试口径也需要交代清楚:独立测费脚本的约 3U 本金与 gas 分开计算;正式策略目前的每仓 3U,则包含开仓 gas 和执行余量。整轮资金变化还混合了兑换损耗、价格变化和 LP 收入,不能把扣除 gas 后的差额全部叫作“兑换手续费”。
这几轮测试完成了 V3、V4 的资金进出与费用核算,但不是长期盈利测试。小额资金的固定操作成本占比很高,扩大本金可能摊薄这部分成本,却也会改变价格冲击、流动性份额和资金风险,仍然需要重新评估。
五、先算这一仓能赚多少,再决定开不开
一个池子交易很多,不代表我们放进去的资金就能分到很多手续费。集中流动性仓位的收入,还取决于价格区间、当前活跃流动性及其他 LP 的仓位。因此,当前模型先估计本仓流动性份额,再计算手续费收入,不直接套用页面 APR。
成交量采用一小时成交额与六小时平均小时成交额中的较小值,避免只根据最近一小时的活跃程度作出乐观判断。结合费率、LP 可分配比例及本仓计划份额后,再与完整进出成本比较,核心逻辑可以简化为:
javascript
// 收益与成本统一为 ETH
var hourlyVolumeUSD = Math.min(volume1hUSD, volume6hUSD / 6);
var share = myLiquidity / (poolLiquidity + myLiquidity);
var expectedFeesETH = hourlyVolumeUSD / ethUSD
* feeRate * lpFeeShare * share * 0.5;
var totalCostETH =
quoteLossETH + slippageReserveETH + gasBudgetETH;
var requiredFeesETH =
totalCostETH * 1.5 + principalETH * 0.01;
var incomeConditionMet = expectedFeesETH >= requiredFeesETH;
收入侧采用 50% 的折扣系数,成本侧设置 1.5 倍覆盖要求,并另计本金 1% 的库存风险余量。满足这一条件后,还要通过资金和执行检查。这些系数目前仍是模型设定,没有充分的盈利样本支持;采用折扣和余量,是为了在历史成交与未来收入之间留出一定空间,而不是保证开仓后一定能够覆盖成本。
持仓以后,也要检查当初的判断有没有兑现。价格偏离、净亏损和流动性下降,都可以触发退出;持有满十分钟后,开始检查实际手续费积累是否明显落后于预期。入场时依靠估计,持仓后已经有实际数据,就应该用实际数据继续验证。
固定三十分钟退出的设计则取消了。池况没有变化,仅因为计时结束就重复进出,会增加成本;条件已经恶化,也不应该等到时间结束。一小时仍作为收益评估窗口使用,但不再被当作持仓期限。
六、这一版完成后的优化方向

到这里,完整策略已经搭建完成:API 获取候选,V3/V4 分别执行,入场前评估收益成本,持仓期间持续检查,退出后按实际收支结算。未确认交易和未处理资产保留在账本中,重启后先恢复已有流程,避免一次中断留下无法追踪的仓位。
除了小额完整流程测试,代码检查还覆盖了目录分页异常、池身份不匹配、报价失败、预算不足、交易回执延迟和重启恢复等情况。重点是确认程序在条件不满足时能够停止新增操作,在交易状态不明确时先核查已有交易,而不是重复发送。执行过程逐步完善后,接下来才有条件评估选池和收益模型的实际效果。
后续的调整会围绕实际运行数据展开。尤其要避免看到不开仓就降低门槛,看到亏损又立即收紧参数,最后只是在不同条件之间来回切换。每次改动都应该对应一个具体问题,并有可以对照的指标:
| 优化方向 | 下一步具体工作 | 评估指标 |
|---|---|---|
| 收益预测 | 逐仓对比入场预测与实际手续费积累,校准成交持续性假设 | 预测偏差、实际费用覆盖率 |
| gas 预算 | 根据当前授权状态减少不必要的预留,补充不同代币的操作样本 | 预算与实际支出的偏差 |
| 资金与区间 | 比较不同本金规模和区间宽度下的运行结果 | 净收益、价格冲击、有效在区间时间 |
| 池子质量 | 加强主要 LP 撤离、异常成交和代币权限识别 | 风险拦截效果、误过滤情况 |
| 持仓退出 | 检查触发退出到实际卖回的全过程,分析延迟和成交损耗 | 退出耗时、实际亏损、残留资产 |
回头看,这次实践的主要收获,是把原来模拟里容易略过的部分逐项补齐了:池子从哪里来,资金怎么进去,费用如何计算,退出是否完成,都有对应的处理逻辑。后续优化可以在这些基础上继续推进,不必每遇到一个问题就重新设计整套策略。
- 1

