メインコンテンツへ移動
AIチュートリアル

jev-traderでブロックごとに動作するMonadマーケットメイカーを構築・実行する

jev-traderのインストール方法、安全なドライランモードでの実行方法、Jevモデルの接続方法、HTTPおよびSSEフィードの確認方法、クオートと約定の解釈方法、そしてMonadの約300ミリ秒のブロック時間内で取引ループを維持するレイテンシ最適化について学びます。

jev-traderでブロックごとに動作するMonadマーケットメイカーを構築・実行する

jev-traderの動作

jev-traderは、Monad上のKuru MON-USDCオーダーブック向けのブロック単位の取引ボットです。新しいブロックごとにリアルタイムのオーダーブックを読み取り、モデルに買いまたは売りを尋ね、選択した側にpost-only指値注文の意図を作成します。

注文価格は、現在の最良気配値からQUOTE_INSIDE_TICKSティック内側に設定されます。スプレッドが狭すぎる場合、価格は最良気配値に補正されます。ボットは以前から残っている注文をキャンセルし、1回のKuru batchUpdateで置き換え注文を出します。

post-only注文であるため、即時に約定せず、オーダーブックに残ることを意図しています。別の参加者が価格を跨ぐテイカー注文を送信すると約定します。これにより、スプレッドを支払うのではなく受け取る戦略を実現できますが、約定、在庫、ガス、相場に関するリスクは依然として存在します。

主な機能

  • Monadの各ブロックで1回の判断: ボットは約300 msごとに最新ブロックを処理します。
  • リアルタイムのKuruオーダーブックデータ: カスタムリーダーがgetL2Bookをデコードし、AMMボールトをオーダーブックに統合します。
  • 2種類のモデル: デフォルトのモックモデルはモメンタムヒューリスティックを使用し、MODEL=jevを設定するとTypeSafe Jevモデルが有効になります。
  • 安全なドライラン動作: PRIVATE_KEYがない場合、ボットは実際のオーダーブックと実際の約定情報を使用しますが、自身の注文と約定はシミュレートします。
  • post-only注文の置き換え: 更新のたびに既知の残存注文をキャンセルし、1回のバッチで新しいpost-only注文を送信します。
  • ポジションと損益の追跡: イベントでポジション、実現損益と未実現損益、ガスコスト、約定回数、その他の合計値を報告します。
  • HTTPおよびSSEによる監視: 小規模なBunサーバーが、最新スナップショット、最大1,000件の過去ブロックイベント、ライブイベントストリームを提供します。
  • 非同期確認: ライブトランザクションは主要処理経路で送信し、レシートや関連する確認処理は後で実行します。

プロジェクトのアーキテクチャ

ソースツリーでは、ブロック取り込み、マーケットアクセス、モデル評価、取引ロジック、監視を分離しています。

  • src/config.tsは環境設定を読み込みます。
  • src/chain.tsはWebSocketのnewHeads更新を受信し、バックストップとしてポーリングを使用して、最新のブロックだけを保持します。
  • src/book.tsは1回のeth_callでオーダーブックを読み取り、デコードします。
  • src/market.tsはKuruのブック読み取り、マージン入金、ローカルnonce、バッチ更新、非同期確認を処理します。
  • src/model.tsはモデルインターフェースを定義し、JevModelとMockModelを提供します。
  • src/trader.tsは取引ループを実行し、ポジションと損益を追跡します。
  • src/server.tsはBun.serveでスナップショット、履歴、Server-Sent Eventsを提供します。

インストールとセットアップ

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は3つ目のモデル戦略を表すものではなく、モデルが現在のブロックの締め切りに間に合わなかった場合にのみ現れます。その場合、decision.lateはtrueになり、そのブロックでは注文を出しません。

ドライランモードで安全に実行する

PRIVATE_KEYが設定されていない場合、jev-traderはライブトランザクションを送信しません。ただし、実際のKuruオーダーブックを読み取り、実際のモデル判断を生成します。

シミュレートされた気配値にはstatus: "sim"が設定されます。これは1ブロックの間残り、実際のマーケット約定がその価格を跨いだ場合にのみ約定したと見なされます。結果として生成される約定には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イベントを送信し、その後、処理されたブロックごとに1件の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はガス上限分を請求するため、トランザクションが成功するかリバートするかにかかわらず、これを実際のコストとして扱います。

通常、レシートは1〜2ブロック後に到着し、別のquoteイベントを生成します。

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。
  • タイプ2トランザクションの手数料上限を指定するMAX_FEE_GWEI。
  • 注文板の読み取りに使用するREAD_RPC_URL。
  • トランザクション送信に使用するRPC_URL。

ボットが300ミリ秒の予算を満たす方法

ホットパスは、正確に2回のRPC往復を中心に設計されています。

  1. 1回のeth_callで、READ_RPC_URLを通じて現在の注文板を読み取ります。
  2. 1回のeth_sendRawTransactionで、署名済みの更新をRPC_URLを通じて送信し、受け付けられると戻ります。

このループでは、コストが大きくなる可能性のある処理を意図的にいくつか避けています。

  • ブロックごとの eth_estimateGas は行いません。ガス上限はハードコードするか、起動時に一度だけ算出します。
  • トランザクションが提案されるまで待機する eth_sendRawTransactionSync は使用しません。
  • ホットパスでのガス価格の取得は行いません。トランザクションには、優先手数料 2 gwei と MAX_FEE_GWEI の上限を設定した静的なタイプ2手数料を使用します。
  • 意思決定ループ内でレシートを待機しません。
  • 注文を遅延させる可能性のあるボールトや手数料のインラインチェックは行いません。

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" を注文が成立したことを示すものとして扱わないでください。後続の quote イベントで placed、reverted、または lost が通知されるまで待機します。
  • 上限に達したクォートを追跡する: capped が true の場合、送信された側と変更前のモデル確率を比較し、シグナルと実行が乖離した理由を確認します。
  • SSE スナップショットを使用する: 新しい監視クライアントは、最初の snapshot イベントから初期状態を構築し、その後のブロック、クォート、フィルの更新を適用します。
  • 両方の RPC 経路を計測する: 読み取りが遅いと意思決定が遅れ、送信が遅いと市場が動く前にクォートを約定させる可能性が低下します。
  • 請求対象のガス上限を考慮する: リバートしたトランザクションでも相応のコストが発生する可能性があります。そのため、フィルだけでなく、gasMon、gasUsd、リバートの合計も監視します。
  • ドライランの約定表示で検証する: シミュレーションのフィルは、注文価格を実際の約定値が通過することに依存します。そのため、ドライランモードは即時約定を前提とするより現実的ですが、実際のキュー内の順位や約定とは異なります。

まとめ

jev-trader は、Kuru のライブ板リーダー、モックまたは Jev の意思決定モデル、最適化された Monad トランザクション送信、ポジション管理、SSE 監視サービスを組み合わせています。PRIVATE_KEY を設定せずに開始し、スナップショットとイベントストリームを確認し、クリティカルパスのベンチマークを実行してから、ライブ実行を検討する前にエンコードを検証してください。本番環境では、確認、遅延したブロック、上限、リバート、在庫、P&L、ガスコストをまとめて監視します。