---
title: "Hyperliquidの注文・トレード・フィルの違い"
description: "Hyperliquidでは、注文、トレード、フィルはそれぞれ別のレコードです。HyperCoreの各ペイロード、oidとtidによる結合、そして照合の方法を解説します。"
---

# Hyperliquidの注文・トレード・フィルの違い

<ImageBlock
  src="https://media.alchemy.com/overviews/hyperliquid-orders-trades-fills-hero-16x9-20261020.png"
  alt="Hyperliquidの注文・トレード・フィルの違い"
  width={1920}
  height={1080}
  priority
/>

Hyperliquidでは、注文(order)は売買の指示、トレード(trade)は2つの注文の間の1回のマッチング、フィル(fill)はそのマッチングにおける1つのアカウント側の記録だ。すべてのトレードは、アカウントごとに1つずつ、ちょうど2つのフィルを生む。本ガイドでは、HyperCore上の各レコードのペイロード、それらを結び付けるキー、そして本番環境で照合を破綻させる失敗ケースを順に解説する。

3つのレコードは別々のストリームで届き、変化するタイミングも異なる。そのため、これらを同じものとして扱うコードは、取引所の状態から徐々にずれていく。以下の内容はすべてHyperliquid mainnetのライブデータで確認しており、本ガイドの2つのスクリプトは現時点で公開Hyperliquid APIに対して動作する。

## 注文、トレード、フィルの違いは何か

HyperCoreはHyperliquidのネイティブな取引エンジンで、HyperEVMのスマートコントラクト環境とは別物だ。完全にオンチェーンのオーダーブックを運用しているため、注文の発注、キャンセル、マッチングはすべてブロックに記録される。

3つのレコードは、その活動をそれぞれ異なる角度から記述する。

<EmbeddedTable
  table={{
    columns: [
      { key: "record", width: 180, title: "レコード", dataType: "object" },
      {
        key: "what_it_describes",
        width: 180,
        title: "記述する内容",
        dataType: "object",
      },
      {
        key: "identifier",
        width: 180,
        title: "識別子",
        dataType: "object",
      },
      { key: "scope", width: 180, title: "範囲", dataType: "object" },
      { key: "stream", width: 180, title: "ストリーム", dataType: "object" },
    ],
    data: [
      {
        record: { title: "注文更新", tooltip: "", icon: "" },
        what_it_describes: {
          title: "注文ステータスの変化(open、filled、canceled、rejected)",
          tooltip: "",
          icon: "",
        },
        identifier: { title: "<code>oid</code>", tooltip: "", icon: "" },
        scope: { title: "1アカウント", tooltip: "", icon: "" },
        stream: {
          title:
            "<code>orderUpdates</code>(ネイティブ)、<code>userOrderUpdates</code>(Alchemy)",
          tooltip: "",
          icon: "",
        },
        id: 0,
      },
      {
        record: { title: "トレード", tooltip: "", icon: "" },
        what_it_describes: {
          title: "ある市場における買い手と売り手の1回のマッチング",
          tooltip: "",
          icon: "",
        },
        identifier: { title: "<code>tid</code>", tooltip: "", icon: "" },
        scope: { title: "1市場、双方のアカウント", tooltip: "", icon: "" },
        stream: { title: "<code>trades</code>", tooltip: "", icon: "" },
        id: 1,
      },
      {
        record: { title: "フィル", tooltip: "", icon: "" },
        what_it_describes: {
          title: "トレードにおける1アカウント側の記録(手数料とPnLを含む)",
          tooltip: "",
          icon: "",
        },
        identifier: {
          title: "<code>tid</code>と<code>oid</code>",
          tooltip: "",
          icon: "",
        },
        scope: { title: "1アカウント", tooltip: "", icon: "" },
        stream: { title: "<code>userFills</code>", tooltip: "", icon: "" },
        id: 2,
      },
    ],
  }}
/>

トレードはマッチングを市場の視点から見たものだ。そのため両方のアカウントが記載され、手数料は含まれない。フィルは同じマッチングをアカウントの視点から見たもので、そのアカウントが支払った手数料、約定前に保有していたポジション、元になった注文を含む。注文更新は注文そのものを記述するが、次のセクションで示すように、多くの開発者が想定するよりもまばらにしか届かない。

トレーディングボットやポートフォリオトラッカーを構築するなら、何が約定したかについての信頼できる情報源はフィルであり、何がまだオーダーブックに残っているかについての信頼できる情報源は注文更新だ。

## 注文更新はどのような形をしているか

注文更新は、注文のステータスが変化したときに発行される。ペイロードは、注文をステータスレコードで包んだ形になっている。

<CodeSnippet
  language="json"
  code={`{
  "status": "open",
  "statusTimestamp": 1780000000000,
  "hash": "0xabc...def",
  "order": {
    "oid": 12345,
    "coin": "BTC",
    "side": "B",
    "limitPx": "100000.0",
    "sz": "0.10",
    "origSz": "0.10",
    "orderType": "Limit",
    "tif": "Gtc",
    "reduceOnly": false,
    "isTrigger": false,
    "triggerPx": "0.0",
    "cloid": null
  }
}`}
/>

状態の追跡で最も重要なフィールドは次のとおりだ。

- **`oid`**。取引所が割り当てる注文ID。フィルはこれを参照するため、注文とその約定を結び付ける結合キーになる。
- **`origSz`と`sz`**。発注時のサイズと、このステータスレコードの時点でまだオーダーブックに残っているサイズ。
- **`status`**。新しい状態。よく見られるのは`open`、`filled`、`canceled`、`triggered`で、ほかにも`badAloPxRejected`、`reduceOnlyCanceled`、`selfTradeCanceled`、`liquidatedCanceled`のような、個別の拒否理由やキャンセル理由が数多く存在する。
- **`tif`**。Time in force(有効期間)。`Gtc`はキャンセルされるまでオーダーブックに残り、`Alo`(add liquidity only)はスプレッドをまたぐ場合に拒否され、`Ioc`(immediate or cancel)は約定できる分だけ約定して残りを破棄する。
- **`cloid`**。クライアントが任意で指定する注文ID。注文を自システムが送ったリクエストと照合するのに役立つ。

[userOrderUpdatesのリファレンス](https://www.alchemy.com/docs/chains/websockets/hypercore/streams/user-order-updates)は、ノード出力で観測されたすべてのステータス値を列挙しているが、このリストは網羅的ではないと明記している。未知のステータス文字列は、クラッシュの原因にするのではなく、ログに記録して調査すべき終了状態として扱う。

注文更新には、素朴なステートマシンを破綻させる挙動が3つある。

部分約定では注文更新は発生しない。ライブのアカウントを観察したところ、オーダーブックに残っていて部分的に約定した注文は、`open`を発行し、続いて1件以上のフィルが届き、その後は終了状態に達するまで何も発行しなかった。そうした注文が後でキャンセルされると、`canceled`レコードの`sz`に残りのサイズが入る。オープン注文の現在の残りサイズが必要なら、フィルから計算するか、必要なときにオープン注文を照会する。

`filled`ステータスは、必ずしも全量約定を意味しない。サイズの一部だけがマッチした`Ioc`注文は、ステータス`filled`で終わり、`sz`はゼロ以外の値になる。これは破棄された未約定の残りだ。アクティブなアカウントから取得した45秒間のサンプルでは、`filled`に達した127件の`Ioc`注文のうち72件にサイズが残っていた。注文を完了として扱う前に、`sz`を確認すること。

`hash`フィールドに値が入るのは`open`の場合だけだ。キャンセルや拒否のレコードではhashがnullになるため、トランザクションハッシュは注文履歴のキーとして使えない。

## トレードはどのような形をしているか

`trades`ストリームは、1つの市場で起きたすべてのマッチングを配信する。各レコードは小さい。

<CodeSnippet
  language="json"
  code={`{
  "coin": "ETH",
  "side": "A",
  "px": "2658.9",
  "sz": "1.0",
  "time": 1790608928951,
  "hash": "0xd8c5bd7a...25ae9e33",
  "tid": 825882878864874,
  "users": ["0x4cf1...e717", "0xe4f5...c0c4"]
}`}
/>

`side`はテイカー側を示し、`A`(ask)はテイカーが売ったこと、`B`(bid)はテイカーが買ったことを意味する。`users`は常に買い手を先、売り手を後に並べるため、`side`が`B`ならテイカーは`users[0]`、`side`が`A`なら`users[1]`になる。上の例ではテイカーが売ったので、`0xe4f5...c0c4`がテイカー、`0x4cf1...e717`がオーダーブックに注文を置いていた買い手となる。

1つのテイカー注文がオーダーブック上の複数の注文を一掃すると、複数のトレードが生成される。それぞれが固有の`tid`を持ち、1つのトランザクションがすべてを引き起こしたため、同じ`hash`と`time`を共有する。つまり`hash`が識別するのはトランザクションであって、トレードではない。

一部のトレードは、すべてゼロのhashを持つ。8つの市場にまたがる直近80件のトレードのサンプルでは、7件のhashがゼロだった。追跡した1件はTWAPスライスで、これは取引所が新たな署名付きトランザクションなしにユーザーに代わって執行するものだ。[Hyperliquidのinfoエンドポイントのドキュメント](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint)でも、TWAPのフィルがゼロのhashを持つことが確認できる。hashでトレードとトランザクションを結合するコードは、このケースに対処する必要がある。

`trades`は、歩み値、出来高、価格チャートなど、市場全体のデータに使う。特定のアカウントに関することには、代わりにフィルを購読する。トレードレコードには、手数料も注文IDもポジションの文脈もないからだ。

## フィルはどのような形をしているか

フィルは、トレードにおける1つのアカウント側の記録だ。以下は、上のETHトレードに対応する2つのフィルをmainnetから取得したものだ。

<CodeSnippet
  language="json"
  code={`[
  {
    "coin": "ETH", "px": "2658.9", "sz": "1.0", "side": "B",
    "time": 1790608928951, "startPosition": "19.3599", "dir": "Open Long",
    "closedPnl": "0.0", "hash": "0xd8c5bd7a...25ae9e33",
    "oid": 559115773954, "crossed": false, "fee": "-0.079767",
    "tid": 825882878864874, "feeToken": "USDC", "twapId": null,
    "cloid": "0xdd0cd482496f45f79c5ffbe7c27e1511"
  },
  {
    "coin": "ETH", "px": "2658.9", "sz": "1.0", "side": "A",
    "time": 1790608928951, "startPosition": "-776.0", "dir": "Open Short",
    "closedPnl": "0.0", "hash": "0xd8c5bd7a...25ae9e33",
    "oid": 559115788953, "crossed": true, "fee": "1.196505",
    "builderFee": "0.26589",
    "tid": 825882878864874, "feeToken": "USDC", "twapId": null
  }
]`}
/>

2つのフィルは、トレードの`tid`、価格、サイズ、時刻を共有する。それ以外はすべてアカウント固有だ。

- **`oid`**。このフィルを生んだ注文。買い手と売り手はそれぞれ自分の注文を持つ。
- **`crossed`**。テイカーなら`true`、メイカー(オーダーブックに残っていた注文)なら`false`。
- **`fee`**。このアカウントが支払った手数料。上の買い手のフィルのように、負の値はメイカーリベートを表す。`builderFee`がある場合、それはビルダー(注文をルーティングしたフロントエンドやアプリ)に支払われた部分で、すでに`fee`に含まれている。
- **`startPosition`、`dir`、`closedPnl`**。フィル前のポジションサイズ、`Open Long`や`Close Short`のような表示用ラベル、そしてこのフィルで確定した実現PnL。これらを使えば、フィルだけからポジション履歴を再構築できる。
- **`liquidation`**。通常のフィルではnull。清算の場合は、清算されたアカウント、マーク価格、そして清算がマーケット経由かバックストップ経由かを示す。
- **`twapId`**。フィルを生んだTWAP注文(存在する場合)。

手数料、リベート、実現PnL、清算の詳細はフィルにしか存在しない。そのため、台帳や税務レポートはすべてフィルから構築する必要がある。

## 注文、トレード、フィルはどのように結合されるか

ほぼすべての結合は、2つのキーで成り立つ。

<EmbeddedTable
  table={{
    columns: [
      { key: "join", width: 260, title: "結合", dataType: "object" },
      { key: "key", width: 260, title: "キー", dataType: "object" },
      {
        key: "cardinality",
        width: 260,
        title: "カーディナリティ",
        dataType: "object",
      },
    ],
    data: [
      {
        join: { title: "トレードからフィル", tooltip: "", icon: "" },
        key: {
          title: "<code>tid</code>(<code>coin</code>と<code>time</code>を併用)",
          tooltip: "",
          icon: "",
        },
        cardinality: {
          title: "1トレードにつき、ちょうど2つのフィル",
          tooltip: "",
          icon: "",
        },
        id: 0,
      },
      {
        join: { title: "フィルから注文", tooltip: "", icon: "" },
        key: { title: "<code>oid</code>", tooltip: "", icon: "" },
        cardinality: {
          title: "1注文につき、0個以上のフィル",
          tooltip: "",
          icon: "",
        },
        id: 1,
      },
      {
        join: {
          title: "注文からクライアントリクエスト",
          tooltip: "",
          icon: "",
        },
        key: { title: "<code>cloid</code>", tooltip: "", icon: "" },
        cardinality: {
          title: "設定した場合は1対1",
          tooltip: "",
          icon: "",
        },
        id: 2,
      },
      {
        join: {
          title: "フィルまたはトレードからトランザクション",
          tooltip: "",
          icon: "",
        },
        key: { title: "<code>hash</code>", tooltip: "", icon: "" },
        cardinality: {
          title: "多対1、TWAPスライスではhashがすべてゼロ",
          tooltip: "",
          icon: "",
        },
        id: 3,
      },
    ],
  }}
/>

土台にすべきなのは最初の2行だ。トレードからフィルへの結合は、`trades`からトレードを1件取得し、`users`配列にある両方のアドレスのフィルを照会することで、ライブデータ上で確認した。各アドレスには、そのトレードの`tid`を持つフィルがちょうど1件ずつあり、一方は`crossed: true`、もう一方は`crossed: false`だった。

注文の残りサイズは、フィルから注文への結合から求められる。

<CodeSnippet
  language="text"
  code={`remaining = origSz - sum(fill.sz for every fill where fill.oid == order.oid)`}
/>

この式はすべての注文で成り立つ。最後に受け取った注文更新の`sz`よりも、この式を信頼すべきだ。

## フィルから注文の状態をどう照合するか

以下のスクリプトは、1つのアカウントの注文更新とフィルを購読し、各注文の約定済みサイズをフィルから追跡し、注文が終了状態に達したときに取引所が報告する`sz`とその計算結果を突き合わせる。WebSocketを標準でサポートする最近のNodeリリースで動作し、依存関係は不要だ。

<CodeSnippet
  language="javascript"
  code={`// Tracks one account's orders by joining order-status events with fills.
// Usage: node reconcile.mjs 0xYourAddress
const WS_URL = process.env.HL_WS_URL ?? "wss://api.hyperliquid.xyz/ws";
const user = process.argv[2];
if (!/^0x[0-9a-fA-F]{40}$/.test(user ?? "")) throw new Error("pass a 0x address");
// Sizes arrive as decimal strings. Sum them as scaled integers so partial fills add up exactly.
const SCALE = 10;
const units = (s) => {
  const [i, f = ""] = s.split(".");
  return BigInt(i + f.padEnd(SCALE, "0").slice(0, SCALE));
};
const fmt = (u) => (Number(u) / 10 ** SCALE).toString();
const orders = new Map(); // oid -> { coin, origSz, filled, status, reported }
const seenFills = new Set(); // coin|time|tid, so a fill delivered twice is counted once
const fillKey = (f) => \`\${f.coin}|\${f.time}|\${f.tid}\`;
const order = (oid) => {
  if (!orders.has(oid)) orders.set(oid, { filled: 0n, status: "unknown" });
  return orders.get(oid);
};
// Runs whenever a terminal order learns something new, so either stream can arrive first.
function check(oid, o) {
  if (o.reported === undefined) return; // not terminal yet
  const remaining = o.origSz - o.filled;
  if (remaining === o.reported && o.filled === 0n) return; // plain cancel or reject
  const result =
    remaining === o.reported
      ? "ok"
      : remaining > o.reported
        ? \`waiting for fills (\${fmt(remaining - o.reported)} outstanding)\`
        : \`MISMATCH (fills say \${fmt(remaining)})\`;
  console.log(\`\${oid} \${o.coin} \${o.status}: filled \${fmt(o.filled)} of \${fmt(o.origSz)}, sz=\${fmt(o.reported)} \${result}\`);
}
function onOrderUpdate(u) {
  const o = order(u.order.oid);
  Object.assign(o, { coin: u.order.coin, origSz: units(u.order.origSz), status: u.status });
  if (u.status === "open") return;
  o.reported = units(u.order.sz);
  check(u.order.oid, o);
}
function onFill(f, quiet = false) {
  const key = fillKey(f);
  if (seenFills.has(key)) return;
  seenFills.add(key);
  const o = order(f.oid);
  o.filled += units(f.sz);
  if (!quiet) {
    const role = f.crossed ? "taker" : "maker";
    console.log(\`\${f.oid} \${f.coin} fill tid=\${f.tid} \${f.side} \${f.sz}@\${f.px} \${role} fee=\${f.fee}\${f.liquidation ? " LIQUIDATION" : ""}\`);
  }
  check(f.oid, o);
}
const ws = new WebSocket(WS_URL);
ws.onopen = () => {
  for (const type of ["orderUpdates", "userFills"]) {
    ws.send(JSON.stringify({ method: "subscribe", subscription: { type, user } }));
  }
};
ws.onmessage = ({ data }) => {
  const msg = JSON.parse(data);
  if (msg.channel === "orderUpdates") msg.data.forEach(onOrderUpdate);
  if (msg.channel !== "userFills") return;
  // The first message is a snapshot of recent fills. Count it, so orders already partly
  // filled at startup add up, but don't print it. The dedupe set absorbs it on reconnect.
  const { fills, isSnapshot } = msg.data;
  fills.forEach((f) => onFill(f, isSnapshot));
  if (isSnapshot) console.log(\`loaded \${fills.length} fills from the snapshot\`);
};
ws.onclose = () => console.log("closed; reconnect and backfill from your last stored fill time");`}
/>

任意のアクティブなアドレス、たとえば直近のトレードの`users`配列から取得したアドレスに対して実行してみてほしい。取引量の多いアカウントに対して45秒間実行したときの出力は次のようになった。

<CodeSnippet
  language="text"
  code={`loaded 30 fills from the snapshot
580847595557 HYPE fill tid=524471650157202 B 153.74@81.33 taker fee=1.800529
580847595557 HYPE fill tid=1042881790178215 B 60.73@81.33 taker fee=0.71124
580847595557 HYPE filled: filled 214.47 of 221.99, sz=7.52 ok
580847576684 ZEC fill tid=38729269326008 B 0.02@1250.0 maker fee=-0.0005
580847576684 ZEC canceled: filled 0.02 of 2.47, sz=2.45 ok`}
/>

最初の行は、サブスクリプション開始時に届くスナップショットだ。続く3行は1つの`Ioc`注文で、オーダーブック上の2つの注文を一掃して2つのフィルを生み、7.52 HYPEが未約定のまま`filled`で終わった。最後の2行は1つの`Alo`注文で、オーダーブックに残ったままメイカーとして0.02 ZECが約定し、残りがオープンのままキャンセルされた。この実行でフィルを伴って終了状態に達した115件の注文すべてで、フィルから導いた残りサイズは取引所が報告した`sz`と一致した。

スクリプトには重要な点が3つある。サイズは文字列であり、JavaScriptの浮動小数点数として合計すると、部分約定の多い長寿命の注文で丸め誤差が蓄積する。そのため、このスクリプトはスケーリングした整数として合計している。また、チェックは両方のハンドラーから実行される。終了ステータスが届くと報告された`sz`が保存され、その後に届くフィルはどれも比較を再実行する。そのため、最後のフィルが`filled`ステータスより後に届いた注文は、誤って不一致と判定されることなく、`waiting for fills`の後に`ok`と表示される。最後に、フィルは状態に反映される前に重複排除されるため、たとえば再接続後のスナップショットや、ライブストリームと重なるバックフィルなどで2回届いたフィルも、1回しかカウントされない。

[HyperliquidのWebSocketドキュメント](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions)は、`tid`を50ビットのハッシュと説明し、ブロック時刻、`coin`、`tid`の組み合わせをグローバルに一意なトレードIDとしている。そのため、このスクリプトは`coin`、`time`、`tid`をフィルのキーにしている。トレードとその2つのフィルは、この3つをすべて共有する。このキーは1つのアカウント内では一意だ。1つのプロセスで多数のアカウントを追跡する場合は、トレードの双方が残りの要素を共有するため、キーに`user`を加える。

## 本番環境では何が壊れるか

照合のバグのほとんどは、次のケースから生じる。

- **配信の重複**。再購読すると、ネイティブの`userFills`ストリームは直近のフィルのスナップショット(`isSnapshot: true`)から始まり、バックフィルはライブストリームがすでに配信した分と重なる。フィルは`user`、`coin`、`time`、`tid`の組み合わせで、注文更新は`oid`、`status`、`statusTimestamp`の組み合わせで重複排除する。
- **ストリーム間の順序**。注文更新とフィルは別々のチャネルで届く。フィルがその注文の`filled`ステータスより先に届くとは限らず、その逆も同様だ。スクリプトのように`oid`をキーにして状態を管理し、どちらのイベントからでもレコードを作成できるようにし、遅れて届いたフィルでチェックを再実行すれば、この依存関係はなくなる。
- **切断**。WebSocketのサブスクリプションは接続中に起きたことしか配信しないため、ダウンタイムが生じたらバックフィルが必要になる。最後に処理したフィルの時刻を保存し、以下に示すように、その時点からフィルを照会する。
- **TWAPスライスのフィル**。TWAPスライスはゼロの`hash`で執行される。公開Hyperliquid APIでは、追跡したスライスのフィルは`userFillsByTime`ではなく`userTwapSliceFills`に現れた。そのため、`userFillsByTime`しか読まないバックフィルでは取りこぼす可能性がある。
- **未知のステータス**。拒否理由やキャンセル理由は新たに追加されていく。未知の値はクラッシュさせずに処理し、`triggerCondition`は表示用の文字列として扱う。機械可読なトリガー価格は`triggerPx`だ。
- **市場識別子**。コイン文字列は市場の種類をエンコードしている。[市場、資産、DEXに関するドキュメント](https://www.alchemy.com/docs/data/hypercore/concepts/markets-assets-and-dexes)にあるとおり、`BTC`は無期限先物、現物市場は`@107`のような`@index`文字列、アウトカム市場は`#N`、サードパーティがデプロイしたHIP-3市場は`dex:name`を使う。コイン文字列は大文字と小文字を区別するため、厳密に比較し、決して小文字に変換しないこと。
- **チェックポイントの早すぎる更新**。最後のフィルの時刻は、そのフィルをストレージに書き込んだ後にだけ永続化する。先に更新すると、2つのステップの間でクラッシュした場合に、フィルが気付かれないまま失われる。

ある時間範囲のフィルのバックフィルは、短いループで済む。[Hyperliquidのinfoエンドポイントのドキュメント](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint)によると、`userFillsByTime`はフィルを古い順に、1レスポンスあたり最大2,000件返す。そのためループは、これまでに見た最新の時刻から再開し、重なった分は同じ重複排除キーで吸収する。

<CodeSnippet
  language="javascript"
  code={`// Pulls every fill for an account between two times, for use after a stream gap.
// Usage: node backfill.mjs 0xYourAddress <startMs> [endMs]
const INFO_URL = process.env.HL_INFO_URL ?? "https://api.hyperliquid.xyz/info";
const [user, start, end = Date.now()] = process.argv.slice(2);
async function fillsBetween(user, startTime, endTime) {
  const byKey = new Map();
  let from = Number(startTime);
  while (true) {
    const res = await fetch(INFO_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ type: "userFillsByTime", user, startTime: from, endTime: Number(endTime) }),
    });
    const page = await res.json();
    const before = byKey.size;
    for (const f of page) byKey.set(\`\${f.coin}|\${f.time}|\${f.tid}\`, f);
    // Pages are capped. Restart at the newest time seen; the dedupe key absorbs the overlap.
    if (page.length === 0 || byKey.size === before) break;
    from = Math.max(...page.map((f) => f.time));
  }
  return [...byKey.values()].sort((a, b) => a.time - b.time);
}
const fills = await fillsBetween(user, start, end);
console.log(\`\${fills.length} fills\`);`}
/>

同じ取引量の多いアカウントに対して、30分間のウィンドウで4ページにわたり6,049件のフィルが返ってきた。結果をストリームと同じ`onFill`ハンドラーに渡せば、重複排除用のセットによって、バックフィルとライブストリームの重なりが二重にカウントされることはない。

同じドキュメントには、`userFillsByTime`で取得できるのは直近10,000件のフィルだけだと記載されている。上記のペースでは1時間に満たない履歴にすぎないため、これほどアクティブなアカウントでそれより長く停止すると、公開エンドポイントが返せる範囲を超えてしまう。切断されたコンシューマーにその時間枠よりも十分早く気付けるよう、アラートを設計すること。

## AlchemyのHyperCoreストリーミングで何が変わるか

[AlchemyのHyperCoreストリーミング](https://www.alchemy.com/docs/chains/websockets/hypercore)はネイティブのサブスクリプションエンベロープを使うため、上記のペイロードと結合ロジックはそのまま変わらない。違いは、各フレームが元のブロックについて何を伝えるかと、1つのサブスクリプションでどこまでカバーできるかにある。

すべてのデータフレームは、ネイティブのペイロードに加えて、`subscriptionId`、`blockHeight`、`blockTime`などのトップレベルのメタデータを持つ。注文更新とフィルは引き続き別々のチャネルで届くが、各フレームが元のブロックを示すため、2つのストリームを突き合わせる際の共通の基準点になる。

サブスクリプションは配列も受け付ける。ネイティブのサブスクリプションは単一の`user`または`coin`しか取らないが、当社のサブスクリプションは`users`、`coins`、`builders`を受け付けるため、1つのサブスクリプションで複数のアカウントをまとめてカバーできる。すべてのフィルと注文のレコードには`user`が含まれるため、上述した複数アカウント向けの重複排除キーが、例外ではなく標準のケースになる。

広範なフィードから自分で導き出さなくても済むよう、いくつかのストリームが用意されている。

- **`liquidationFills`**。清算を伴うフィルだけを、市場ごとにフィルタして配信する。
- **`builderFills`と`builderOrderUpdates`**。ビルダーアドレスに帰属するフィルと注文更新。注文をルーティングし、自らのフローと手数料を追跡する必要があるアプリ向け。
- **`allFills`**。選択した市場における全アカウントのフィル。簡素なトレードレコードではなく、完全なフィルのスキーマで配信される。

照合スクリプトを当社のエンドポイントで実行するには、`HL_WS_URL`を早期アクセスで受け取るWebSocket URLに向け、[HyperCore接続ライフサイクルのドキュメント](https://www.alchemy.com/docs/chains/websockets/hypercore/connection-lifecycle)の説明に従って認証し、`orderUpdates`の代わりに`userOrderUpdates`を購読する。当社の注文更新フレームはステータスレコードを`orders`配列で包んでいるため、`msg.data`ではなく`msg.data.orders`から読み取る。履歴については、当社のHyperCore REST APIに、カーソルベースのページネーションに対応した`userFillsByTime`と`historicalOrders`が追加される予定だ。どちらも[ウォレットの取引履歴を照会するガイド](https://www.alchemy.com/docs/data/hypercore/guides/query-wallet-trading-history)に記載されているが、まだ呼び出せないため、当面は上記のバックフィルスクリプトで公開Hyperliquidエンドポイントを使っている。

## HyperCoreストリーミングはどうすれば利用できるか

HyperCoreストリーミングは早期アクセス段階にあり、[6月に発表したHyperCoreへの取り組み](https://www.alchemy.com/blog/alchemy-going-all-in-on-hyperliquid)の一環だ。当社が運用するHyperliquidノードからストリーミングするため、自分で`hl-node`を動かす必要はない。すでにAlchemyで開発しているなら、現在使っているAPIキーで有効化する。まず[HyperCore Data APIの概要](https://www.alchemy.com/docs/data/hypercore/overview)でストリームとRESTメソッドを確認し、次に[HyperCoreの早期アクセスをリクエスト](https://www.alchemy.com/hypercore-early-access)して接続情報を入手し、当社チームと直接連携してほしい。

スタックの他の部分でストリーミングのトランスポートを選ぶなら、[webhooks、WebSockets、gRPC](https://www.alchemy.com/overviews/webhooks-vs-websockets-vs-grpc)の比較記事でトレードオフを解説している。HyperliquidのHyperEVM側については、[Hyperliquid RPCガイド](https://www.alchemy.com/overviews/complete-guide-to-hyperliquid-rpc-providers-top-8-solutions-for-2026)を参照してほしい。
