---
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）是两个订单之间的一次撮合，而成交回报（fill）是某个账户在这次撮合中的一方。每笔成交恰好产生两条成交回报，每个账户一条。本指南逐一介绍每种记录在 HyperCore 上的负载、关联它们的键，以及在生产环境中破坏对账的故障情形。

这三种记录通过不同的流到达，并在不同的时间发生变化，因此把它们当作同一种东西处理的代码，会逐渐与交易所失去同步。下文所有内容都已对照 Hyperliquid 主网的实时数据核实过，本指南中的两个脚本目前也可以直接在公开的 Hyperliquid API 上运行。

## 订单、成交和成交回报有什么区别？

HyperCore 是 Hyperliquid 的原生交易引擎，与 HyperEVM 智能合约环境相互独立。它运行一个完全链上的订单簿，因此每一次下单、撤单和撮合都会被记录在区块中。

这三种记录从不同角度描述这些活动：

<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: "单个账户", tooltip: "", icon: "" },
        stream: {
          title:
            "<code>orderUpdates</code>（原生）、<code>userOrderUpdates</code>（Alchemy）",
          tooltip: "",
          icon: "",
        },
        id: 0,
      },
      {
        record: { title: "成交", tooltip: "", icon: "" },
        what_it_describes: {
          title: "某个市场中买方与卖方之间的一次撮合",
          tooltip: "",
          icon: "",
        },
        identifier: { title: "<code>tid</code>", tooltip: "", icon: "" },
        scope: { title: "单个市场，双方账户", tooltip: "", icon: "" },
        stream: { title: "<code>trades</code>", tooltip: "", icon: "" },
        id: 1,
      },
      {
        record: { title: "成交回报", tooltip: "", icon: "" },
        what_it_describes: {
          title: "某个账户在一笔成交中的一方，含手续费和盈亏（PnL）",
          tooltip: "",
          icon: "",
        },
        identifier: {
          title: "<code>tid</code> 和 <code>oid</code>",
          tooltip: "",
          icon: "",
        },
        scope: { title: "单个账户", 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`（仅添加流动性）在会穿越价差时被拒绝，`Ioc`（立即成交否则取消）成交能成交的部分并丢弃剩余部分。
- **`cloid`**。可选的客户端自定义订单 ID，便于把订单与你的系统发出的请求对应起来。

[userOrderUpdates 参考文档](https://www.alchemy.com/docs/chains/websockets/hypercore/streams/user-order-updates)列出了在节点输出中观察到的所有状态值，并说明该列表并不详尽。把未知的状态字符串视为一种需要记录日志并调查的终态，而不是让程序崩溃。

订单更新的三种行为会让简单的状态机出错。

部分成交不会产生订单更新。我们观察实时账户时发现，一个被部分成交的挂单会先发出 `open`，然后是一条或多条成交回报，之后便不再有任何更新，直到它进入终态。当这样的订单后来被撤销时，`canceled` 记录会在 `sz` 中携带剩余数量。如果你需要某个挂单的实时剩余数量，请根据成交回报计算，或按需读取当前挂单。

`filled` 状态并不总是意味着完全成交。一个只撮合了部分数量的 `Ioc` 订单，最终状态是 `filled`，且 `sz` 不为零，这个值就是被丢弃的未成交部分。在一个活跃账户的 45 秒样本中，达到 `filled` 的 127 个 `Ioc` 订单里，有 72 个仍有剩余数量。在把订单视为已完成之前，先检查 `sz`。

`hash` 字段只在 `open` 时有值。已撤销和被拒绝的记录 hash 为 null，因此交易哈希无法用作订单历史的键。

## 成交是什么样的？

`trades` 流会发布某个市场中的每一次撮合。每条记录都很小：

<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` 是吃单方（taker）的方向，其中 `A`（ask，卖方报价）表示吃单方卖出，`B`（bid，买方报价）表示吃单方买入。`users` 总是先列出买方、再列出卖方，因此当 `side` 为 `B` 时，吃单方是 `users[0]`；当 `side` 为 `A` 时，吃单方是 `users[1]`。在上面的例子中吃单方卖出，因此 `0xe4f5...c0c4` 是吃单方，`0x4cf1...e717` 是挂单的买方。

一个吃单订单如果吃掉多个挂单，就会产生多笔成交。每笔成交都有自己的 `tid`，而它们共享相同的 `hash` 和 `time`，因为它们全都由同一笔交易引起。这意味着 `hash` 标识的是交易，而不是成交。

有些成交的 hash 全为零。在横跨八个市场的 80 笔近期成交样本中，有 7 笔的 hash 为零。我们追溯的那一笔是一个 TWAP 子单，由交易所代表用户执行，无需新的签名交易；[Hyperliquid 的 info 端点文档](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint)也确认 TWAP 成交回报的 hash 为零。通过 hash 将成交关联到交易的代码需要处理这种情况。

`trades` 适用于全市场数据，例如逐笔成交、成交量和价格图表。凡是涉及某个特定账户的需求，请改为订阅成交回报，因为成交记录不含手续费、订单 ID 和仓位上下文。

## 成交回报是什么样的？

成交回报是某个账户在一笔成交中的一方。下面是上文那笔 ETH 成交对应的两条成交回报，取自主网：

<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
  }
]`}
/>

两条成交回报共享该成交的 `tid`、价格、数量和时间。其余内容都特定于各自的账户：

- **`oid`**。产生这条成交回报的订单。买方和卖方各有自己的订单。
- **`crossed`**。吃单方为 `true`，挂单方（maker，即挂在订单簿上的那笔订单）为 `false`。
- **`fee`**。该账户支付的手续费。负值表示挂单返佣，如上面买方的成交回报所示。`builderFee` 出现时，表示支付给构建者（builder，即路由该订单的前端或应用）的部分，并且已包含在 `fee` 中。
- **`startPosition`、`dir` 和 `closedPnl`**。成交前的仓位大小、一个展示用标签（例如 `Open Long` 或 `Close Short`），以及这次成交平仓所实现的盈亏。借助它们，仅凭成交回报就能重建仓位历史。
- **`liquidation`**。普通成交回报中为 null。对于清算，它会给出被清算的账户、标记价格，以及这次清算是通过市场还是通过后备机制（backstop）完成的。
- **`twapId`**。产生这条成交回报的 TWAP 订单（如果有的话）。

手续费、返佣、已实现盈亏和清算详情只存在于成交回报中，因此任何账本或税务报告都必须基于成交回报来构建。

## 订单、成交和成交回报如何关联在一起？

两个键承担了几乎所有的关联：

<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: "一笔成交，恰好两条成交回报",
          tooltip: "",
          icon: "",
        },
        id: 0,
      },
      {
        join: { title: "成交回报到订单", tooltip: "", icon: "" },
        key: { title: "<code>oid</code>", tooltip: "", icon: "" },
        cardinality: {
          title: "一个订单，零条或多条成交回报",
          tooltip: "",
          icon: "",
        },
        id: 1,
      },
      {
        join: { title: "订单到客户端请求", tooltip: "", icon: "" },
        key: { title: "<code>cloid</code>", tooltip: "", icon: "" },
        cardinality: {
          title: "一对一，前提是你设置了它",
          tooltip: "",
          icon: "",
        },
        id: 2,
      },
      {
        join: { title: "成交回报或成交到交易", tooltip: "", icon: "" },
        key: { title: "<code>hash</code>", tooltip: "", icon: "" },
        cardinality: {
          title: "多对一，TWAP 子单的 hash 为零",
          tooltip: "",
          icon: "",
        },
        id: 3,
      },
    ],
  }}
/>

应当以前两行为基础来构建。我们在实时数据上核验了成交到成交回报的关联：从 `trades` 中取出一笔成交，然后查询 `users` 数组中两个地址的成交回报。每个地址都恰好有一条带有该成交 `tid` 的成交回报，其中一条为 `crossed: true`，另一条为 `crossed: false`。

订单的剩余数量可以由成交回报到订单的关联推导出来：

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

这个公式对每个订单都成立，比起你最后看到的那条订单更新中的 `sz`，它更值得信赖。

## 如何根据成交回报对订单状态进行对账？

下面的脚本订阅某个账户的订单更新和成交回报，根据成交回报跟踪每个订单的已成交数量，并在订单进入终态时，将这一计算结果与交易所报告的 `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`}
/>

第一行是订阅开始时先发送的快照。接下来三行是同一个 `Ioc` 订单：它吃掉了两个挂单，产生了两条成交回报，最终以 `filled` 结束，仍有 7.52 HYPE 未成交。最后两行是一个 `Alo` 订单：它挂在订单簿上，作为挂单方成交了 0.02 ZEC，随后在剩余部分仍挂单时被撤销。在那次运行中，共有 115 个带有成交回报的订单进入了终态，根据成交回报推导出的剩余数量每一次都与交易所报告的 `sz` 一致。

脚本中有三个细节很重要。数量是字符串，如果把它们当作 JavaScript 浮点数求和，在经历多次部分成交的长期订单上会产生舍入漂移，因此脚本把它们作为缩放后的整数求和。此外，核对会在两个处理函数中都运行。收到终态时会保存交易所报告的 `sz`，之后到达的任何成交回报都会重新执行这一比较，因此如果某个订单的最后一条成交回报在其 `filled` 状态之后才到达，它会先显示 `waiting for fills`，然后显示 `ok`，而不会被误报为不一致。最后，成交回报在影响状态之前会先去重，因此同一条成交回报到达两次时（例如在重连后的快照中再次出现，或者回填与实时流发生重叠），只会被计算一次。

[Hyperliquid 的 WebSocket 文档](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions)将 `tid` 描述为一个 50 位哈希值，并指出区块时间、`coin` 和 `tid` 三者合起来才是全局唯一的成交 ID，因此脚本以 `coin`、`time` 和 `tid` 作为成交回报的键。一笔成交及其两条成交回报在这三个字段上完全相同。对单个账户而言，这个键是唯一的。如果你在一个进程中跟踪多个账户，请把 `user` 也加入键中，因为一笔成交的双方共享其余字段。

## 生产环境中哪些地方会出问题？

大多数对账错误都来自以下情况。

- **重叠投递**。当你重新订阅时，原生 `userFills` 流会先发送一份近期成交回报的快照（`isSnapshot: true`），而回填也会与实时流已经投递的内容发生重叠。成交回报按 `user`、`coin`、`time` 和 `tid` 去重，订单更新按 `oid` 加 `status` 加 `statusTimestamp` 去重。
- **跨流顺序**。订单更新和成交回报通过不同的频道到达。不要假设成交回报一定先于其订单的 `filled` 状态到达，反之亦然。像脚本那样以 `oid` 作为状态的键、允许任一事件创建记录，并在迟到的成交回报到达时重新运行核对，就能消除这种依赖。
- **断连**。WebSocket 订阅只会投递连接期间发生的事情，因此任何停机时间都需要回填。保存你处理过的最后一条成交回报的时间，并从那个时间点开始查询成交回报，如下所示。
- **TWAP 子单的成交回报**。TWAP 子单执行时 `hash` 为零。在公开的 Hyperliquid API 上，我们追溯的那条子单成交回报出现在 `userTwapSliceFills` 中，而不是 `userFillsByTime` 中，因此只读取 `userFillsByTime` 的回填可能会漏掉它们。
- **未知状态**。新的拒绝和撤单原因会不断被加入。处理未知值时不要崩溃，并将 `triggerCondition` 视为展示用字符串。机器可读的触发价格是 `triggerPx`。
- **市场标识符**。coin 字符串编码了市场类型。`BTC` 是永续合约，现货市场使用 `@index` 形式的字符串（例如 `@107`），结果市场（outcome market）使用 `#N`，由第三方部署的 HIP-3 市场使用 `dex:name`，详见[市场、资产和 DEX 文档](https://www.alchemy.com/docs/data/hypercore/concepts/markets-assets-and-dexes)。coin 字符串区分大小写，因此要精确比较，绝不要把它们转为小写。
- **过早推进检查点**。只有在成交回报写入存储之后，才持久化你的最后成交时间。如果先推进检查点，那么两步之间一旦发生崩溃，就会悄无声息地丢失一条成交回报。

按时间范围回填成交回报只需要一个简短的循环。根据 [Hyperliquid 的 info 端点文档](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint)，`userFillsByTime` 按从旧到新的顺序返回成交回报，每次响应最多 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 分钟的时间窗口在四页中返回了 6,049 条成交回报。把结果送入流所使用的同一个 `onFill` 处理函数，去重集合就能防止回填与实时流之间的重叠被重复计算。

同一份文档指出，通过 `userFillsByTime` 只能获取最近的 10,000 条成交回报。按上面的速率，这相当于不到一小时的历史，因此对于如此活跃的账户，更长时间的中断会超出公开端点所能返回的范围。请据此设置告警，确保断开连接的消费者能在这个时间窗口内被及早发现。

## Alchemy 的 HyperCore 流式传输带来了哪些改变？

[Alchemy 上的 HyperCore 流式传输](https://www.alchemy.com/docs/chains/websockets/hypercore)使用原生的订阅封装格式，因此上面的负载和关联逻辑保持不变。区别在于每一帧告诉你的源区块信息，以及一个订阅能够覆盖的范围。

每个数据帧除了原生负载之外，还携带顶层元数据，包括 `subscriptionId`、`blockHeight` 和 `blockTime`。订单更新和成交回报仍然通过不同的频道到达，但每一帧都会标明其来源区块，这样在对齐两个流时，你就有了一个共同的参照点。

订阅也接受数组。原生订阅只接受单个 `user` 或 `coin`，而我们的订阅接受 `users`、`coins` 和 `builders`，因此一个订阅就能覆盖一整组账户。每条成交回报和订单记录都带有其 `user`，这使得上文的多账户去重键成为默认情况，而不再是边缘情况。

有几个流专门提供现成的数据，让你无需从宽泛的数据流中自行推导：

- **`liquidationFills`**。仅包含带有清算信息的成交回报，按市场过滤。
- **`builderFills` 和 `builderOrderUpdates`**。归属于某个构建者地址的成交回报和订单更新，适用于路由订单、需要跟踪自身订单流和手续费的应用。
- **`allFills`**。所选市场中所有账户的成交回报，采用完整的成交回报结构，而不是更精简的成交记录。

要让对账脚本连接我们的端点，请将 `HL_WS_URL` 指向你在获得抢先体验资格时收到的 WebSocket URL，按照 [HyperCore 连接生命周期文档](https://www.alchemy.com/docs/chains/websockets/hypercore/connection-lifecycle)中的说明进行身份验证，并订阅 `userOrderUpdates` 而不是 `orderUpdates`。我们的订单更新帧会把状态记录包装在一个 `orders` 数组中，因此请从 `msg.data.orders` 而不是 `msg.data` 读取它们。在历史数据方面，我们的 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 key 上为你启用它。先从 [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)。
