Órdenes vs. trades vs. fills en Hyperliquid
Escrito por Uttam Singh

En Hyperliquid, una orden es una instrucción de compra o venta, un trade es un emparejamiento entre dos órdenes, y un fill es el lado de una cuenta en ese emparejamiento. Cada trade produce exactamente dos fills, uno por cuenta. Esta guía recorre el payload de cada registro en HyperCore, las claves que los unen y los casos de fallo que rompen la conciliación en producción.
Los tres registros llegan por streams distintos y cambian en momentos distintos, así que el código que los trata como lo mismo termina desincronizado del exchange. Todo lo que sigue se verificó con datos en vivo de la mainnet de Hyperliquid, y los dos scripts de esta guía funcionan hoy contra la API pública de Hyperliquid.
¿Cuál es la diferencia entre una orden, un trade y un fill?
HyperCore es el motor de exchange nativo de Hyperliquid, separado del entorno de smart contracts HyperEVM. Ejecuta un libro de órdenes completamente onchain, así que cada colocación de orden, cada cancelación y cada emparejamiento queda registrado en un bloque.
Los tres registros describen esa actividad desde ángulos distintos:
Un trade es la vista que tiene el mercado de un emparejamiento, por eso lista ambas cuentas y ninguna comisión. Un fill es la vista que tiene la cuenta del mismo emparejamiento, así que incluye la comisión que pagó esa cuenta, la posición que tenía antes y la orden de la que proviene. Las actualizaciones de orden describen la orden en sí y, como muestra la siguiente sección, son más escasas de lo que la mayoría de los desarrolladores espera.
Si estás construyendo un bot de trading o un rastreador de portafolio, los fills son la fuente de verdad de lo que se ejecutó, y las actualizaciones de orden son la fuente de verdad de lo que sigue en el libro.
¿Cómo es una actualización de orden?
Una actualización de orden se emite cuando cambia el estado de una orden. El payload envuelve la orden en un registro de estado:
{
"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
}
}Los campos más importantes para el seguimiento del estado son:
oid. El ID de orden asignado por el exchange. Los fills lo referencian, así que es la clave de unión entre una orden y sus ejecuciones.origSzysz. El tamaño al colocar la orden y el tamaño que sigue en el libro al momento de este registro de estado.status. El nuevo estado.open,filled,canceledytriggeredson los más comunes, y hay una larga cola de motivos específicos de rechazo y cancelación comobadAloPxRejected,reduceOnlyCanceled,selfTradeCanceledyliquidatedCanceled.tif. Time in force (vigencia de la orden).Gtcpermanece en el libro hasta que se cancela,Alo(add liquidity only) se rechaza si cruzaría el spread, yIoc(immediate or cancel) toma lo que puede y descarta el resto.cloid. Un ID de orden opcional que define el cliente, útil para asociar una orden con la solicitud que envió tu sistema.
La referencia de userOrderUpdates lista cada valor de estado observado en la salida de los nodos e indica que la lista no es exhaustiva. Trata un string de estado desconocido como un estado terminal que registras e investigas, no como un error fatal.
Tres comportamientos de las actualizaciones de orden rompen las máquinas de estado ingenuas.
Un fill parcial no produce una actualización de orden. Cuando observamos cuentas en vivo, una orden en el libro que se ejecutó parcialmente emitió open, luego uno o más fills, y después nada hasta llegar a un estado terminal. Cuando una orden así se cancela más tarde, el registro canceled incluye el tamaño restante en sz. Si necesitas el tamaño restante en vivo de una orden abierta, calcúlalo a partir de los fills o consulta las órdenes abiertas bajo demanda.
Un estado filled no siempre significa ejecución completa. Una orden Ioc que empareja parte de su tamaño termina con estado filled y un sz distinto de cero, que es el remanente no ejecutado que se descartó. En una muestra de 45 segundos de una cuenta activa, 72 de las 127 órdenes Ioc que llegaron a filled todavía tenían tamaño restante. Revisa sz antes de tratar una orden como completa.
El campo hash solo se completa en open. Los registros cancelados y rechazados tienen un hash nulo, así que el hash de transacción no sirve como clave para el historial de órdenes.
¿Cómo es un trade?
El stream trades publica cada emparejamiento de un mercado. Cada registro es pequeño:
{
"coin": "ETH",
"side": "A",
"px": "2658.9",
"sz": "1.0",
"time": 1790608928951,
"hash": "0xd8c5bd7a...25ae9e33",
"tid": 825882878864874,
"users": ["0x4cf1...e717", "0xe4f5...c0c4"]
}side es el lado del taker, donde A (ask) significa que el taker vendió y B (bid) significa que el taker compró. users siempre lista primero al comprador y después al vendedor, así que el taker es users[0] cuando side es B y users[1] cuando side es A. En el ejemplo de arriba el taker vendió, lo que hace que 0xe4f5...c0c4 sea el taker y 0x4cf1...e717 el comprador cuya orden estaba en el libro.
Una orden taker que barre varias órdenes del libro produce varios trades. Cada uno tiene su propio tid, y comparten el mismo hash y time porque una sola transacción los causó a todos. Eso significa que hash identifica la transacción, no el trade.
Algunos trades llevan un hash todo en ceros. En una muestra de 80 trades recientes en ocho mercados, 7 tenían hash en cero. El que rastreamos resultó ser un tramo de TWAP, que el exchange ejecuta en nombre del usuario sin una nueva transacción firmada, y la documentación del endpoint info de Hyperliquid confirma que los fills de TWAP llevan un hash en cero. El código que une trades con transacciones por hash tiene que manejar ese caso.
Usa trades para datos de todo el mercado, como la cinta de operaciones, el volumen y los gráficos de precios. Para cualquier cosa sobre una cuenta específica, suscríbete a los fills, porque el registro de trade no tiene comisiones, ni ID de orden, ni contexto de posición.
¿Cómo es un fill?
Un fill es el lado de una cuenta en un trade. Estos son los dos fills del trade de ETH de arriba, obtenidos de mainnet:
[
{
"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
}
]Ambos fills comparten el tid, el precio, el tamaño y el tiempo del trade. Todo lo demás es específico de la cuenta:
oid. La orden que produjo este fill. El comprador y el vendedor tienen cada uno la suya.crossed.truepara el taker,falsepara el maker (la orden que estaba en el libro).fee. La comisión que pagó esta cuenta. Un valor negativo es un reembolso para el maker, como en el fill del comprador de arriba.builderFee, cuando está presente, es la parte que se paga a un builder (un frontend o app que enrutó la orden) y ya está incluida enfee.startPosition,diryclosedPnl. El tamaño de la posición antes del fill, una etiqueta de visualización comoOpen LongoClose Short, y el PnL realizado que cerró este fill. Con ellos puedes reconstruir el historial de posiciones solo a partir de los fills.liquidation. Nulo en los fills normales. En una liquidación, indica la cuenta liquidada, el precio de marca y si la liquidación pasó por el mercado o por el backstop.twapId. La orden TWAP que produjo el fill, cuando la hay.
Las comisiones, los reembolsos, el PnL realizado y los detalles de liquidación existen solo en los fills, así que cualquier libro contable o reporte de impuestos tiene que construirse a partir de ellos.
¿Cómo se unen las órdenes, los trades y los fills?
Dos claves cubren casi todas las uniones:
Las dos primeras filas son la base sobre la que conviene construir. Verificamos la unión de trade a fill con datos en vivo: tomamos un trade de trades y luego consultamos los fills de ambas direcciones del array users. Cada dirección tenía exactamente un fill con el tid del trade, uno con crossed: true y otro con crossed: false.
El tamaño restante de una orden se deriva de la unión de fill a orden:
remaining = origSz - sum(fill.sz for every fill where fill.oid == order.oid)Esa fórmula se cumple para toda orden, y es la que debes preferir en lugar del sz de la última actualización de orden que viste.
¿Cómo concilias el estado de las órdenes a partir de los fills?
El script de abajo se suscribe a las actualizaciones de orden y a los fills de una cuenta, lleva la cuenta del tamaño ejecutado de cada orden a partir de sus fills y verifica ese cálculo contra el sz que reporta el exchange cuando la orden llega a un estado terminal. Funciona con una versión reciente de Node con soporte integrado de WebSocket y no necesita dependencias.
// 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");Ejecútalo contra cualquier dirección activa, por ejemplo una tomada del array users de un trade reciente. La salida de una ejecución de 45 segundos contra una cuenta con mucha actividad se veía así:
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 okLa primera línea es el snapshot con el que empieza la suscripción. Las tres siguientes son una orden Ioc que barrió dos órdenes del libro, produjo dos fills y terminó como filled con 7.52 HYPE sin ejecutar. Las dos últimas son una orden Alo que quedó en el libro, ejecutó 0.02 ZEC como maker y se canceló con el resto todavía abierto. En las 115 órdenes que llegaron a un estado terminal con fills en esa ejecución, el tamaño restante derivado de los fills coincidió siempre con el sz que reportó el exchange.
Tres detalles del script son importantes. Los tamaños son strings, y sumarlos como floats de JavaScript produce desviaciones de redondeo en órdenes de larga duración con muchos fills parciales, así que el script los suma como enteros escalados. Además, la verificación se ejecuta desde ambos handlers. Un estado terminal guarda el sz reportado, y cualquier fill que llegue después vuelve a ejecutar la comparación, así que una orden cuyo último fill llega después de su estado filled muestra waiting for fills y luego ok en lugar de una falsa discrepancia. Por último, los fills se deduplican antes de tocar el estado, así que un fill que llega dos veces, por ejemplo en el snapshot después de una reconexión o desde un backfill que se superpone con el stream en vivo, se cuenta una sola vez.
La documentación de WebSocket de Hyperliquid describe tid como un hash de 50 bits e indica que el tiempo de bloque, coin y tid juntos forman el ID de trade único a nivel global, así que el script usa como clave de los fills coin, time y tid. Un trade y sus dos fills comparten los tres. Esa clave es única para una sola cuenta. Si sigues muchas cuentas en un mismo proceso, agrégale user, porque ambos lados de un trade comparten el resto.
¿Qué falla en producción?
La mayoría de los bugs de conciliación vienen de los siguientes casos.
- Entregas superpuestas. Cuando vuelves a suscribirte, el stream nativo
userFillsempieza con un snapshot de fills recientes (isSnapshot: true), y un backfill se superpone con lo que el stream en vivo ya entregó. Deduplica los fills poruser,coin,timeytid, y las actualizaciones de orden poroidmásstatusmásstatusTimestamp. - Secuencia entre streams. Las actualizaciones de orden y los fills llegan por canales separados. No asumas que un fill llega antes que el estado
filledde su orden, ni al revés. Indexar el estado poroid, dejar que cualquiera de los dos eventos cree el registro y volver a ejecutar la verificación cuando llega un fill tardío, como hace el script, elimina esa dependencia. - Desconexiones. Una suscripción WebSocket solo entrega lo que ocurre mientras está conectada, así que cualquier caída necesita un backfill. Guarda el tiempo del último fill que procesaste y consulta los fills desde ese punto, como se muestra abajo.
- Fills de tramos de TWAP. Los tramos de TWAP se ejecutan con un
hashen cero. En la API pública de Hyperliquid, el fill de tramo que rastreamos apareció enuserTwapSliceFillsy no enuserFillsByTime, así que un backfill que solo leeuserFillsByTimepuede perderlos. - Estados desconocidos. Se agregan nuevos motivos de rechazo y cancelación. Maneja los valores desconocidos sin que el programa falle, y trata
triggerConditioncomo un string de visualización. El precio de activación legible por máquina estriggerPx. - Identificadores de mercado. Los valores de coin codifican el tipo de mercado.
BTCes el perpetuo, los mercados spot usan strings@indexcomo@107, los mercados de resultados usan#N, y los mercados HIP-3 desplegados por terceros usandex:name, como se describe en la documentación de mercados, activos y DEX. Los valores de coin distinguen mayúsculas de minúsculas, así que compáralos de forma exacta y nunca los pases a minúsculas. - Avanzar un checkpoint demasiado pronto. Persiste el tiempo de tu último fill solo después de que el fill se haya escrito en el almacenamiento. Si avanzas primero, una caída entre los dos pasos pierde un fill sin dejar rastro.
Hacer backfill de los fills de un rango de tiempo requiere un bucle corto. userFillsByTime devuelve los fills del más antiguo al más reciente, con un máximo de 2,000 por respuesta según la documentación del endpoint info de Hyperliquid, así que el bucle se reinicia en el tiempo más reciente que vio y deja que la misma clave de deduplicación absorba la superposición:
// 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`);Contra la misma cuenta con mucha actividad, una ventana de 30 minutos devolvió 6,049 fills en cuatro páginas. Pasa el resultado por el mismo handler onFill que usa el stream, y el conjunto de deduplicación evita que la superposición entre el backfill y el stream en vivo se cuente dos veces.
La misma documentación indica que solo los 10,000 fills más recientes están disponibles a través de userFillsByTime. Al ritmo anterior, eso equivale a menos de una hora de historial, así que una caída más larga en una cuenta así de activa queda fuera de lo que el endpoint público puede devolver. Dimensiona tus alertas para que un consumidor desconectado se detecte con holgura dentro de esa ventana.
¿Cómo cambia esto el streaming de HyperCore de Alchemy?
El streaming de HyperCore en Alchemy usa el mismo envelope que las suscripciones nativas, así que los payloads de arriba y la lógica de unión no cambian. Las diferencias están en lo que cada frame te dice sobre su bloque de origen y en cuánto puede cubrir una sola suscripción.
Cada frame de datos incluye metadatos de nivel superior junto con el payload nativo, entre ellos subscriptionId, blockHeight y blockTime. Las actualizaciones de orden y los fills siguen llegando por canales separados, pero cada frame indica el bloque del que proviene, lo que te da un punto de referencia común cuando alineas los dos streams.
Las suscripciones también aceptan arrays. Las suscripciones nativas toman un solo user o coin, mientras que las nuestras aceptan users, coins y builders, así que una sola suscripción puede cubrir todo un conjunto de cuentas. Cada registro de fill y de orden incluye su user, lo que convierte la clave de deduplicación multicuenta de arriba en el caso por defecto en lugar del caso límite.
Algunos streams existen para que no tengas que derivarlos de un feed amplio:
liquidationFills. Solo los fills que incluyen una liquidación, filtrados por mercado.builderFillsybuilderOrderUpdates. Fills y actualizaciones de orden atribuidos a una dirección de builder, para apps que enrutan órdenes y necesitan seguir su propio flujo y sus comisiones.allFills. Fills de todas las cuentas en los mercados seleccionados, con el esquema completo de fill en lugar del registro de trade, más reducido.
Para ejecutar el script de conciliación contra nuestro endpoint, apunta HL_WS_URL a la URL de WebSocket que recibes con el acceso anticipado, autentícate como se describe en la documentación del ciclo de vida de la conexión de HyperCore y suscríbete a userOrderUpdates en lugar de orderUpdates. Nuestros frames de actualización de orden envuelven los registros de estado en un array orders, así que léelos desde msg.data.orders en lugar de msg.data. Para el historial, nuestra API REST de HyperCore agregará userFillsByTime y historicalOrders con paginación basada en cursores. Ambos están documentados en la guía para consultar el historial de trading de una wallet, pero todavía no se pueden llamar, así que mientras tanto el script de backfill de arriba usa el endpoint público de Hyperliquid.
¿Cómo obtienes acceso al streaming de HyperCore?
El streaming de HyperCore está en acceso anticipado, como parte del trabajo en HyperCore que anunciamos en junio. Haces streaming desde nodos de Hyperliquid que operamos nosotros, así que no hay ningún hl-node que correr, y si ya construyes sobre Alchemy, lo habilitamos en la API key que usas hoy. Empieza con la descripción general de la HyperCore Data API para ver los streams y los métodos REST, y luego solicita acceso anticipado a HyperCore para obtener los detalles de conexión y trabajar directamente con nuestro equipo.
Si estás eligiendo entre transportes de streaming para el resto de tu stack, nuestra comparación de webhooks, WebSockets y gRPC cubre los trade-offs. Para el lado HyperEVM de Hyperliquid, consulta nuestra guía de RPC de Hyperliquid.
Resúmenes relacionados
Técnico19 de mayo de 2026
Webhooks vs. WebSockets vs. gRPC
Tres protocolos dominan la entrega de datos en tiempo real. Así es como se diferencian webhooks, WebSockets y gRPC, cuándo falla cada uno y cómo elegir entre ellos.
Infra15 de enero de 2026
Guía completa de proveedores de Hyperliquid RPC: las 8 mejores soluciones para 2026
¿Buscas el mejor Hyperliquid RPC en 2026? Esta guía presenta a los mejores proveedores.
Wallets19 de octubre de 2026
¿Qué es account abstraction? ERC-4337 explicado
Account abstraction permite que un smart contract actúe como tu cuenta. ERC-4337 lo logra sin cambiar Ethereum, y EIP-7702 lo lleva a las EOAs existentes.

Construye magia blockchain
Alchemy combina los productos y herramientas de desarrollo Web3 más potentes con recursos, comunidad y un soporte legendario.