跳到主要内容
AI教程

使用 jev-trader 构建并运行逐区块 Monad 做市商

了解如何安装 jev-trader,在 dry-run 模式下安全运行,连接 Jev 模型,检查其 HTTP 和 SSE 数据流,解读报价与成交,并理解使交易循环保持在 Monad 约 300 毫秒区块时间内的延迟优化。

使用 jev-trader 构建并运行逐区块 Monad 做市商

jev-trader 的作用

jev-trader 是一个逐区块运行的交易机器人,用于 Monad 上 Kuru 的 MON-USDC 订单簿。每产生一个新区块,它都会读取实时订单簿,询问模型是买入还是卖出,并在选定的一侧创建只挂单限价订单意图。

订单价格位于当前最优价内侧 QUOTE_INSIDE_TICKS 个 tick。如果价差过窄,价格会被限制在最优价。机器人会取消此前仍在挂单的订单,并通过一次 Kuru batchUpdate 发布替换订单。

由于订单为只挂单订单,其设计目的是留在订单簿中,而不是立即成交。当其他参与者提交穿过该价格的吃单时,订单才会成交。这样策略可以赚取价差,而不是支付价差,但成交、库存、Gas 和市场风险仍然存在。

主要功能

  • 每个 Monad 区块做出一次决策:机器人约每 300 毫秒处理最新区块。
  • 实时 Kuru 订单簿数据:自定义读取器解码 getL2Book,并将 AMM 金库合并到订单簿中。
  • 两种模型选项:默认模拟模型使用动量启发式方法,而 MODEL=jev 会启用 TypeSafe Jev 模型。
  • 安全的试运行行为:未设置 PRIVATE_KEY 时,机器人使用真实订单簿和真实市场成交记录,但会模拟自身的订单和成交。
  • 只挂单订单替换:每次更新都会取消已知的挂单,并在一次批处理中提交新的只挂单订单。
  • 仓位与盈亏跟踪:事件会报告仓位、已实现和未实现结果、Gas 成本、成交次数及其他汇总数据。
  • HTTP 和 SSE 监控:小型 Bun 服务器提供最新快照、最多 1,000 条历史区块事件以及实时事件流。
  • 异步确认:实时交易会在主流程中提交,而收据及相关检查会在之后处理。

项目架构

源代码树将区块接收、市场访问、模型评估、交易逻辑和监控分开:

  • src/config.ts 加载环境配置。
  • src/chain.ts 接收 WebSocket newHeads 更新,使用轮询作为后备机制,并且只保留最新区块。
  • src/book.ts 通过一次 eth_call 读取并解码订单簿。
  • src/market.ts 处理 Kuru 订单簿读取、保证金存入、本地 nonce、批量更新和异步确认。
  • src/model.ts 定义模型接口,并提供 JevModel 和 MockModel。
  • src/trader.ts 运行交易循环并跟踪仓位与盈亏。
  • src/server.ts 使用 Bun.serve 提供快照、历史记录和服务器发送事件。

安装与设置

1. 准备环境文件

在仓库目录中复制示例配置:

cp .env.example .env

启动应用前,请检查生成的 .env 文件。最重要的行为取决于是否设置了 PRIVATE_KEY。

首次运行时请不要设置 PRIVATE_KEY。应用会基于真实市场数据执行试运行,而不会广播自身的订单。

2. 安装依赖

项目使用 Bun 管理依赖并执行程序:

bun install

3. 启动交易机器人

bun run start

在默认配置下,模拟动量模型会作为推理替代方案运行。随后,服务器会通过 HTTP 端点提供交易机器人状态和事件流。

选择决策模型

使用默认模拟模型

默认的 mock 模型适合在不调用 Jev 的情况下测试完整流程。它使用动量启发式方法做出决策,让你能够验证订单簿读取、模拟订单、成交、记账和事件流。

启用 Jev 模型

要使用 Jev,请在 .env 中配置以下值:

MODEL=jev
TYPESAFE_AI_API_KEY=your_api_key

每个区块都会向选定的模型询问 HORIZON_BLOCKS 范围内的预期变动。其默认值为 100 个区块,根据项目 README 约为 30 秒。

正常决策只有 buy 或 sell。hold 并不代表第三种模型策略:它只会在模型错过当前区块截止时间时出现。在这种情况下,decision.late 为 true,该区块不会发布订单。

安全地运行试运行模式

未配置 PRIVATE_KEY 时,jev-trader 不会提交实时交易。它仍会读取真实的 Kuru 订单簿,并生成真实的模型决策。

模拟报价的 status: "sim"。它会挂单一个区块,只有当真实市场成交记录穿过其价格时,才会被视为已成交。生成的成交记录包含 simulated: true。

此模式适合验证:

  • 订单簿更新是否稳定到达。
  • 模型延迟是否保持在区块时间预算内。
  • 报价可能成交的频率。
  • 仓位和盈亏如何根据模拟规则变化。
  • 仪表板或其他 SSE 客户端是否能正确处理每种事件类型。

检查 HTTP 端点

一个使用模拟模型部署的试运行实例位于 https://jev-trader-production.up.railway.app。

阅读最新快照

根端点会报告当前模型、钱包、试运行状态以及最近的区块事件:

curl https://jev-trader-production.up.railway.app/

读取近期历史记录

历史记录端点会返回最近 1,000 个区块事件:

curl https://jev-trader-production.up.railway.app/history

关注实时 SSE 流

使用 curl -N 保持连接开启,并在事件到达时显示它们:

curl -N https://jev-trader-production.up.railway.app/events

客户端连接时,数据流会发送一个 snapshot 事件,随后每处理一个区块就发送一个 block 事件。实时交易回执会生成 quote 更新,检测到的成交会生成 fill 事件。

了解区块事件

区块事件汇总了市场状态、模型决策、新的报价意图、挂单流动性、持仓信息以及累计总数。简化示例如下:

{
  "block": 105488269,
  "mid": 0.022636,
  "bestBid": 0.022628,
  "bestAsk": 0.022644,
  "spreadBps": 7.07,
  "decision": {
    "action": "buy",
    "probabilities": { "buy": 0.77, "sell": 0.23, "hold": 0 },
    "upIn10": 0.77,
    "latencyMs": 81,
    "late": false
  },
  "quote": {
    "side": "buy",
    "price": 0.022629,
    "size": 200,
    "status": "sent",
    "capped": false
  }
}
  • mid、bestBid 和 bestAsk 描述观测到的市场状态。
  • spreadBps 以基点表示当前价差。
  • decision.action 是模型选择的方向。
  • decision.probabilities 保留模型对买入和卖出的概率。
  • upIn10 等于买入概率。
  • decision.latencyMs 报告模型评估延迟。
  • quote 描述本区块尝试提交的订单。
  • quote.cancel 标识需要取消的既有挂单。
  • resting 记录更新后机器人已知的买单和卖单数量。
  • position 报告方向、数量、入场价格和未实现结果。
  • totals 累计区块数、决策数、报价数、成交数、反转数、延迟区块数、模型成本、燃料费和盈亏。

持仓与资金上限

如果持仓上限阻止请求的方向,或实时保证金资金不足以执行该方向,报价就会被放置在相反方向。在这种情况下,quote.capped 为 true。原始模型概率保持不变,因此消费者可以区分模型的判断与实际报价的方向。

了解实时报价确认

实时发送采用触发后不等待回执的方式,因此交易循环无需等待回执。初始区块事件包含的是一个 意图,其 status: "sent"。

报告中的 gasMon 由燃料上限乘以最近已知的基础费加优先费计算得出。项目说明 Monad 会收取燃料上限对应的费用,因此无论交易成功还是回滚,这都会被视为实际成本。

回执通常会在一两个区块后到达,并生成单独的报价事件:

event: quote
data: {
  "block": 105488269,
  "quote": {
    "status": "placed",
    "orderId": 100295812,
    "gasMon": 0.0357
  }
}

实时报价可能进入以下状态之一:

  • placed:交易成功,并包含订单 ID。
  • reverted:订单簿在交易纳入前穿过了提交价格,或者请求取消的目标订单已经成交。
  • lost:10 个区块后仍未找到回执。

了解成交事件

成交不会发生在机器人自己的下单交易中,而是发生在另一位交易者的吃单击中挂单时。机器人通过同一个 eth_getLogs 轮询流程检测相应的 Trade 日志,该流程也为模型提供市场信息。

event: fill
data: {
  "block": 105488271,
  "fill": {
    "side": "buy",
    "size": 200,
    "price": 0.022629,
    "txHash": "0x…",
    "orderId": 100295812,
    "simulated": false
  }
}

成交的 txHash 属于吃单交易。处理成交时,数据流还会反映更新后的持仓数据、已实现盈亏以及累计成交总数。

切换到实时交易

设置 PRIVATE_KEY 会将应用从试运行行为切换为实时交易提交。在此之前,请确认环境配置、保证金资金、持仓限制、交易规模、报价下单、费用上限和 RPC 可靠性。

实时模式会提交真实的 Kuru 只做挂单订单,并产生真实燃料成本。启用已注资的密钥前,请先在试运行模式下测试完整流程,并使用保守的限制。

项目描述的主要策略相关设置包括:

  • TRADE_SIZE_MON:订单规模。
  • QUOTE_INSIDE_TICKS:相对于最优价的下单位置。
  • HORIZON_BLOCKS:模型的预测时间范围。
  • MAX_FEE_GWEI:类型 2 交易的费用上限。
  • READ_RPC_URL:读取订单簿。
  • RPC_URL:提交交易。

机器人如何满足 300 毫秒预算

热路径的设计严格基于两次 RPC 往返:

  1. 通过 READ_RPC_URL 执行一次 eth_call,读取当前订单簿。
  2. 通过 RPC_URL 执行一次 eth_sendRawTransaction,提交签名后的更新,并在交易被接受后返回。

该循环会有意避开多项可能成本高昂的操作:

  • 不针对每个区块调用 eth_estimateGas;gas 上限采用硬编码,或在启动时统一计算一次。
  • 不调用 eth_sendRawTransactionSync,因为该方法会一直等待交易被提议。
  • 热路径中不查询 gas 价格;交易使用静态的 type-2 费用设置,优先费用为 2 gwei,并受 MAX_FEE_GWEI 上限约束。
  • 决策循环中不等待收据。
  • 不执行可能延误下单的内联 vault 或费用检查。

README 报告的干运行模拟模型基准显示:盘口读取的 p50 约为 18 ms,完整循环的 p50 约为 100 ms,其中约 80 ms 归因于模拟推理替身。

验证与基准测试

对自定义盘口读取器进行基准测试

运行随附的基准测试,将自定义读取器与 SDK 在准确性和延迟方面进行比较:

bun run scripts/bench-read.ts

离线验证交易编码

干编码脚本会在不广播交易的情况下,对买入和卖出交易进行签名,然后检查生成的 calldata 是否与 SDK 一致:

bun run scripts/dry-encode.ts

在启用实时模式前,这项检查尤其有用,因为面向性能的市场模块使用手动编码的 batchUpdate calldata。

高级运行提示

  • 关注延迟决策:跟踪 decision.late 和 totals.lateBlocks。模型响应延迟意味着该区块不会提交订单。
  • 区分意图与确认:不要将 status: "sent" 视为订单已下达。应等待后续包含 placed、reverted 或 lost 的 quote 事件。
  • 跟踪触及上限的报价:当 capped 为 true 时,将提交的方向与未改变的模型概率进行比较,以了解执行结果为何偏离信号。
  • 使用 SSE 快照:新的监控客户端应从第一个 snapshot 事件初始化,然后应用后续的区块、报价和成交更新。
  • 测量两条 RPC 路径:读取缓慢会延误决策,而提交缓慢会降低报价在市场变动前成交的机会。
  • 考虑实际收取的 gas 上限:交易即使回退,仍可能产生显著成本,因此应监控 gasMon、gasUsd 和回退总数,而不能只评估成交情况。
  • 通过干运行输出进行验证:模拟成交取决于真实成交价穿过订单价格。与立即成交假设相比,干运行模式更贴近实际,但仍与真实排队位置和执行结果存在差异。

结论

jev-trader 结合了实时 Kuru 盘口读取器、模拟或 Jev 决策模型、经过优化的 Monad 交易提交、持仓记账以及 SSE 监控服务。在不设置 PRIVATE_KEY 的情况下启动,检查快照和事件流,对关键路径进行基准测试,并在考虑实时执行前验证编码。在生产环境中,应综合监控确认状态、延迟区块、上限、回退、库存、P&L 和 gas 成本。