jev-trader 如何捕获真实成交?trades.ts 用 eth_getLogs 追踪 Maker 成交事件全解析
【免费下载链接】jev-traderOne AI trade decision every Monad block. Jev on Kuru MON-USDC.项目地址: https://gitcode.com/gh_mirrors/je/jev-trader
jev-trader 是部署在 Monad 链上的 AI 交易机器人:AI 模型 Jev 每个 300ms 区块给出买/卖决策,并以 post-only 限价单挂在 Kuru MON-USDC 订单簿上。它的订单是"挂单等成交"(Maker),成交来自别人打进来,自己的交易回执里根本看不到。本文完整解析 trades.ts 如何用eth_getLogs轮询链上Trade事件,捕获每一笔真实的 Maker 成交。
为什么自己的回执里看不到成交?
传统思路是:发单 → 等eth_getTransactionReceipt→ 判断成没成交。这只适用于 Taker(主动吃单)。而 jev-trader 的每一笔单都是 post-only 挂单,成交发生在别人的 Taker 交易里:
别人的 Taker 买单打穿我们的卖单 → 成交记录在对方的交易中产生,回执与我们无关。
所以 jev-trader 换了个思路:不盯自己的交易,而是盯整条链的成交流。Kuru 订单簿合约每次撮合都会发出一个Trade事件,只要轮询这些日志,既能看到市场全部成交,也能从中挑出"打到我身上"的那些。
TradeFeed:一个类抓住全部成交
核心实现在 TradeFeed 类里,它由 trader.ts 的attachTradeFeed挂上,市场地址、RPC 地址、我们的钱包地址(maker)三个参数注入。整个捕获流程分三步。
第一步:用 topic0 锁定 Trade 事件
事件签名和预计算的 keccak256 哈希直接写在模块顶部(src/trades.ts#L20-L22):
Trade(uint40 orderId, address makerAddress, bool isBuy, uint256 price, uint96 updatedSize, address takerAddress, address txOrigin, uint96 filledSize)注意makerAddress、takerAddress等字段全部未做索引,所以不能靠 topic 过滤"我的成交",只能把事件整体拉回来,在内存里按地址比对。
第二步:分块轮询 eth_getLogs,绕开公共节点限制
rpc.monad.xyz公共节点拒绝跨度超过 100 个区块的eth_getLogs请求,于是 poll() 做了三层防御(src/trades.ts#L44-L49):
| 常量 | 值 | 作用 |
|---|---|---|
FIRST_LOOKBACK | 300 | 首次启动回看约 90 秒的成交热身窗口 |
MAX_RANGE | 100 | 每次eth_getLogs最多跨 100 块,超出就切块 |
MAX_CATCHUP | 1000 | 断线恢复时单次最多补 1000 块,防止一次性回放太多 |
细节很讲究:
- 按块推进
lastBlock:切块循环里每拉完一块就更新游标,中途失败也只损失当前块,下次轮询从断点续传; inFlight守卫:上一次轮询还没跑完就直接丢弃新请求,绝不并发,也绝不抛异常——轮询永远不能拖垮 300ms 主循环;removed: true的日志(被回滚的区块)直接跳过。
第三步:手动解码,避开"1e18 价格陷阱"
decode() 不用 SDK 而是手写解析 8 个 word:word(2)是isBuy,word(3)是价格,word(7)是成交量。
这里藏着项目注释里反复强调的坑(src/trades.ts#L9-L12):Trade.price无论市场价格精度是多少,链上都是 1e18 定点数。MON-USDC 市场的pricePrecision是 1e8,若误用会把价格缩小一亿倍;而filledSize才按sizePrecision(1e10)缩放。常量 TRADE_PRICE_DEC 固定为 18,正是为了防这个错。
💡 一箭双雕:makerAddress 同时喂两个系统
解码时做了一次地址比对(src/trades.ts#L123):makerAddress等于我们的钱包地址吗?
- 等于→ 这是一笔我们的真实成交,连同
orderId、updatedSize(剩余未成交量)一起塞进fills队列。方向还要翻转:Taker 买单(isBuy: true)打的是我们的卖单,所以记录为side: "sell"; - 不等于→ 只是市场行情,进入 500 条环形缓冲(RING)。
同一份日志于是派生出两种价值:
- 给 AI 看:summary() 聚合出窗口内的成交笔数、买卖量差(CVD)、VWAP、最新价和方向,经 buildState 送入模型,成为决策的动量特征;
- 给自己记账:
drainFills()(src/trades.ts#L158-L160)取出我们的成交,交给 harvest() 更新持仓、已实现盈亏,并通过 SSE 推给前端面板。
从日志到面板:fill 事件的完整链路
整条数据流串起来是这样的:
eth_getLogs → decode() → fills 队列 → drainFills() → harvest() → liveFills() 核对订单簿 → applyFill() 记账 → onFill() → SSE "fill" 事件 → 前端成交带其中 liveFills() 还会用updatedSize同步订单簿状态:部分成交就更新剩余数量,全部成交(updatedSize <= 0)就摘掉这笔订单。注意轮询是挂在热路径之外的——trader.ts#L102 里poll(block).then(() => this.harvest())不阻塞本块的"读簿→决策→发单",保证 300ms 预算不被日志拉取吃掉。
🧪 没有私钥也能验证:dry run 模拟成交
没配PRIVATE_KEY时进入 dry run:订单不真正上链,但成交判定逻辑完全一样——模拟单从下一区块开始"挂"在簿上,一旦真实成交流里出现打穿其价格的打印(simFills()),就按打印数量成交,事件里标simulated: true。真实成交与模拟成交走的是同一套判定规则,这让策略在零资金下就能被完整验证。
快速上手:本地验证 trades.ts
cp .env.example .env bun install bun run scripts/trades-smoke.tstrades-smoke.ts 会真实回看约 1000 个区块的Trade日志,打印 CVD/VWAP 汇总和最近 5 笔成交,是检验eth_getLogs分块逻辑最快的冒烟脚本。配置项集中在 src/config.ts,其中READ_RPC_URL专供日志轮询与读簿,与发单通道RPC_URL分离。更多背景见 README.md 的 Endpoints 一节。
小结
jev-trader 捕获真实成交的关键不是"盯自己的回执",而是:
- 换视角:轮询全链
Trade日志(topic0 过滤 + 100 块分片 + 断点续传); - 一次解码两用途:行情喂 AI,
makerAddress命中即真实成交; - 精度纪律:价格恒按 1e18 还原,数量按
sizePrecision还原; - 不占热路径:日志轮询与 300ms 决策循环解耦,成交晚一两块到达也不影响下一单。
这套模式对任何想做链上 Maker 策略的项目都通用:只要合约在撮合时发带 maker 地址的事件,eth_getLogs就是捕获真实成交最可靠、也最便宜的路径。
【免费下载链接】jev-traderOne AI trade decision every Monad block. Jev on Kuru MON-USDC.项目地址: https://gitcode.com/gh_mirrors/je/jev-trader
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考