Hyperliquid 訂單、成交與成交明細比較
作者 Uttam Singh

在 Hyperliquid 上,訂單(order)是買進或賣出的指令,成交(trade)是兩筆訂單之間的一次撮合,而成交明細(fill)則是單一帳戶在這次撮合中的那一方。每筆成交都會產生剛好兩筆成交明細,每個帳戶各一筆。本指南逐一說明 HyperCore 上每種紀錄的 payload、將它們關聯起來的鍵,以及在正式環境中會破壞對帳的失效情境。
這三種紀錄來自不同的串流,也在不同的時間點變化,因此把它們當成同一件事處理的程式碼,會逐漸與交易所的狀態脫節。以下內容皆以 Hyperliquid 主網的即時資料驗證過,本指南中的兩支腳本目前也都能直接對公開的 Hyperliquid API 執行。
訂單、成交與成交明細有什麼差別?
HyperCore 是 Hyperliquid 的原生交易引擎,與 HyperEVM 智能合約環境彼此獨立。它運行一個完全鏈上的訂單簿,因此每一次下單、取消與撮合都會記錄在區塊中。
這三種紀錄從不同角度描述這些活動:
成交是市場視角下的撮合,所以它會列出雙方帳戶,但不含手續費。成交明細是帳戶視角下的同一次撮合,因此帶有該帳戶支付的手續費、成交前的持倉,以及它來自哪一筆訂單。訂單更新描述的是訂單本身,而如下一節所示,它們比大多數開發者預期的還要稀疏。
如果你正在打造交易機器人或投資組合追蹤工具,成交明細是「實際執行了什麼」的唯一可信來源,而訂單更新則是「還有什麼掛在訂單簿上」的唯一可信來源。
訂單更新長什麼樣子?
訂單狀態改變時,就會發出一筆訂單更新。payload 會把訂單包在一筆狀態紀錄中:
{
"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 參考文件列出了在節點輸出中觀察到的所有狀態值,並說明這份清單並不完整。遇到未知的狀態字串時,請把它當成需要記錄並調查的終止狀態,而不是讓程式崩潰。
訂單更新有三種行為,會讓過於簡單的狀態機出錯。
部分成交不會產生訂單更新。我們觀察即時帳戶時發現,一筆部分成交的掛單會先發出 open,接著是一筆或多筆成交明細,之後就不再有任何訂單更新,直到它進入終止狀態為止。當這類訂單之後被取消時,canceled 紀錄會在 sz 中帶出剩餘數量。如果你需要某筆掛單中訂單的即時剩餘數量,請從成交明細計算,或在需要時查詢目前的掛單。
filled 狀態不一定代表完全成交。只撮合到部分數量的 Ioc 訂單,最後的狀態會是 filled,而 sz 不為零,這個值就是被捨棄的未成交剩餘數量。在一個活躍帳戶的 45 秒樣本中,127 筆進入 filled 的 Ioc 訂單裡,有 72 筆仍有剩餘數量。在把訂單視為已完成之前,請先檢查 sz。
hash 欄位只有在 open 時才有值。已取消與已拒絕的紀錄 hash 為 null,因此交易 hash 無法作為查詢訂單歷史的鍵。
成交長什麼樣子?
trades 串流會發布單一市場中的每一次撮合。每筆紀錄都很精簡:
{
"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 端點文件也證實 TWAP 成交明細的 hash 為零。以 hash 將成交關聯到交易的程式碼,必須處理這種情況。
trades 適合用於全市場層級的資料,例如逐筆成交紀錄、交易量與價格圖表。凡是與特定帳戶相關的需求,請改為訂閱成交明細,因為成交紀錄中沒有手續費、沒有訂單 ID,也沒有持倉資訊。
成交明細長什麼樣子?
成交明細是單一帳戶在一筆成交中的那一方。以下是上面那筆 ETH 成交對應的兩筆成交明細,取自主網:
[
{
"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,掛單方(原本掛在訂單簿上的那筆訂單)為false。fee:此帳戶支付的手續費。負值代表掛單返佣,如上方買方的成交明細。builderFee若存在,代表支付給 builder(傳遞該訂單的前端或應用程式)的部分,且已包含在fee中。startPosition、dir與closedPnl:成交前的持倉數量、Open Long或Close Short之類的顯示標籤,以及這筆成交明細平倉所實現的 PnL。有了這些欄位,就能只靠成交明細重建持倉歷史。liquidation:一般成交明細為 null。若為清算,則會列出被清算的帳戶、標記價格,以及清算是透過市場還是後備機制(backstop)執行。twapId:產生這筆成交明細的 TWAP 訂單(若有)。
手續費、返佣、已實現 PnL 與清算細節只存在於成交明細中,因此任何帳本或稅務報表都必須以成交明細為基礎來建立。
訂單、成交與成交明細如何相互關聯?
幾乎所有關聯都靠兩個鍵完成:
前兩列才是值得依賴的基礎。我們用即時資料驗證了成交與成交明細的關聯:先從 trades 取得一筆成交,再查詢 users 陣列中兩個地址的成交明細。每個地址都剛好有一筆帶有該成交 tid 的成交明細,一筆為 crossed: true,另一筆為 crossed: false。
訂單的剩餘數量可以從成交明細與訂單的關聯推導出來:
remaining = origSz - sum(fill.sz for every fill where fill.oid == order.oid)這個公式適用於每一筆訂單,比起你最後看到的那筆訂單更新中的 sz,更應該信任這個公式。
如何根據成交明細核對訂單狀態?
下面的腳本會訂閱單一帳戶的訂單更新與成交明細,根據成交明細追蹤每筆訂單的已成交數量,並在訂單進入終止狀態時,將計算結果與交易所回報的 sz 相互核對。它可在內建 WebSocket 支援的較新 Node 版本上執行,不需要任何相依套件。
// 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 秒後,輸出如下:
loaded 30 fills from the snapshot
580847595557 HYPE fill tid=524471650157202 B [email protected] taker fee=1.800529
580847595557 HYPE fill tid=1042881790178215 B [email protected] taker fee=0.71124
580847595557 HYPE filled: filled 214.47 of 221.99, sz=7.52 ok
580847576684 ZEC fill tid=38729269326008 B [email protected] 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 文件將 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 markets)使用#N,由第三方部署的 HIP-3 市場則使用dex:name,詳見市場、資產與 DEX 文件。coin 字串區分大小寫,因此請精確比對,絕對不要轉成小寫。 - 過早推進檢查點:只有在成交明細寫入儲存空間之後,才持久化你的最後成交明細時間。如果先推進檢查點,兩個步驟之間一旦當機,就會無聲無息地遺漏一筆成交明細。
回填某段時間範圍內的成交明細,只需要一個簡短的迴圈。根據 Hyperliquid 的 info 端點文件,userFillsByTime 會從最舊的成交明細開始回傳,每次回應最多 2,000 筆,因此迴圈會從目前看到的最新時間重新開始,並讓同一個去重鍵吸收重疊的部分:
// 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 串流使用原生的訂閱封裝格式,因此上述 payload 與關聯邏輯都維持不變。差異在於每個訊框能告訴你多少關於其來源區塊的資訊,以及單一訂閱可以涵蓋多少範圍。
每個資料訊框除了原生 payload 之外,還帶有頂層的中繼資料,包括 subscriptionId、blockHeight 與 blockTime。訂單更新與成交明細仍然透過不同的頻道送達,但每個訊框都會標明它來自哪個區塊,讓你在對齊兩個串流時有一個共同的參考點。
訂閱也接受陣列。原生訂閱只能指定單一 user 或 coin,我們的訂閱則接受 users、coins 與 builders,因此一個訂閱就能涵蓋一整組帳戶。每筆成交明細與訂單紀錄都帶有對應的 user,因此上述的多帳戶去重鍵也就從邊緣情況變成了預設情況。
另外有幾個串流,讓你不必再從範圍廣泛的資料流中自行推導:
liquidationFills:只包含帶有清算資訊的成交明細,可依市場篩選。builderFills與builderOrderUpdates:歸屬於某個 builder 地址的成交明細與訂單更新,適合負責傳遞訂單、需要追蹤自身訂單流量與手續費的應用程式。allFills:選定市場中所有帳戶的成交明細,採用完整的成交明細結構,而不是較精簡的成交紀錄。
若要對我們的端點執行對帳腳本,請將 HL_WS_URL 指向你在取得搶先體驗資格時收到的 WebSocket URL,依照 HyperCore 連線生命週期文件完成驗證,並改為訂閱 userOrderUpdates 而非 orderUpdates。我們的訂單更新訊框會把狀態紀錄包在 orders 陣列中,因此請從 msg.data.orders 讀取,而不是 msg.data。在歷史資料方面,我們的 HyperCore REST API 將會新增支援游標分頁的 userFillsByTime 與 historicalOrders。兩者都已記錄在查詢錢包交易歷史指南中,但目前尚無法呼叫,因此上面的回填腳本暫時使用公開的 Hyperliquid 端點。
如何取得 HyperCore 串流的使用權限?
HyperCore 串流目前處於搶先體驗階段,是我們在 6 月宣布的 HyperCore 計畫的一部分。你直接從我們營運的 Hyperliquid 節點接收串流,不需要自行運行 hl-node;如果你已經在 Alchemy 上開發,我們會直接在你目前使用的 API 金鑰上啟用這項功能。請先從 HyperCore Data API 總覽了解有哪些串流與 REST 方法,再申請 HyperCore 搶先體驗,以取得連線資訊並直接與我們的團隊合作。
如果你正在為技術堆疊的其他部分挑選串流傳輸方式,我們的 webhooks、WebSockets 與 gRPC 比較文章說明了各自的取捨。至於 Hyperliquid 的 HyperEVM 部分,請參閱我們的 Hyperliquid RPC 指南。
相關總覽
技術2026年5月19日
Webhooks、WebSockets 與 gRPC 比較
即時資料傳輸主要由三種協定主導。以下說明 webhooks、WebSockets 與 gRPC 的差異、各自的失效情境,以及該如何選擇。
基礎設施2026年1月15日
Hyperliquid RPC 供應商完整指南:2026 年八大解決方案
想找 2026 年最佳的 Hyperliquid RPC?本指南介紹最優秀的供應商。
錢包2026年10月19日
什麼是帳戶抽象(account abstraction)?ERC-4337 詳解
帳戶抽象讓智慧合約充當你的帳戶。ERC-4337 無需改動以太坊即可實現,EIP-7702 則將其帶給現有的 EOA。

打造區塊鏈魔法
Alchemy 結合最強大的 Web3 開發者產品與工具,並提供資源、社群與卓越的支援。