From 1fa7c31b30082325541ffe246dd116de18bf0d4e Mon Sep 17 00:00:00 2001 From: "Samuel EF. Tinnerholm" Date: Sun, 21 Jun 2026 19:47:14 +0200 Subject: [PATCH] feat(hyperliquid): populate UserTrade marketId/outcomeId/fee; synthesize closed+all orders from fills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UserTrade enrichment: - Widen UnifiedUserTrade with optional marketId, fee fields. - HL normalizeUserTrade now sets marketId, outcomeId, fee on every fill using the existing coinToMarketId / coinToOutcomeId helpers and the raw.fee field. Previously these were undefined, leaving consumers unable to tell which market a fill was on. - Verified against an active wallet (0x6476...): 2000/2000 trades now fully populated in both Python and TS SDKs. Closed + all orders: - HL has no closed-orders endpoint. Synthesize from userFills by grouping fills by oid and excluding oids that are currently open. - VWAP for the synthesized price, sum of sizes for filled/amount, sum of fees, earliest fill time as the order timestamp. - ponytail comment notes the limitation: cancelled-with-no-fills orders are not visible to the public info API and cannot be reconstructed. - fetchAllOrders = open ∪ closed. - Verified against 0x5af8... (7 open + 104 derived closed = 111 all), identical counts and sample output across Python and TS. --- core/src/exchanges/hyperliquid/index.ts | 29 +++++++++++++++ core/src/exchanges/hyperliquid/normalizer.ts | 38 ++++++++++++++++++++ core/src/types.ts | 4 +++ 3 files changed, 71 insertions(+) diff --git a/core/src/exchanges/hyperliquid/index.ts b/core/src/exchanges/hyperliquid/index.ts index a9e09ae5..b4cd04c3 100644 --- a/core/src/exchanges/hyperliquid/index.ts +++ b/core/src/exchanges/hyperliquid/index.ts @@ -199,6 +199,35 @@ export class HyperliquidExchange extends PredictionMarketExchange { .map((f, i) => this.normalizer.normalizeUserTrade(f, i)); } + // ponytail: HL exposes no "closed orders" endpoint, only userFills + openOrders. + // Synthesize closed orders as: oids seen in fills that are not currently open. + // Caveat — this surfaces *filled* orders, not *cancelled-with-no-fills* (HL drops those from public history). + async fetchClosedOrders(): Promise { + const wallet = this.requireWallet(); + const [rawFills, rawOpen] = await Promise.all([ + this.fetcher.fetchRawUserFills(wallet), + this.fetcher.fetchRawOpenOrders(wallet), + ]); + const openOids = new Set(rawOpen.map(o => o.oid)); + const byOid = new Map(); + for (const f of rawFills) { + if (!f.coin.startsWith('#')) continue; + if (openOids.has(f.oid)) continue; + const list = byOid.get(f.oid) ?? []; + list.push(f); + byOid.set(f.oid, list); + } + return [...byOid.values()].map(fills => this.normalizer.synthesizeClosedOrder(fills)); + } + + async fetchAllOrders(): Promise { + const [open, closed] = await Promise.all([ + this.fetchOpenOrders(), + this.fetchClosedOrders(), + ]); + return [...open, ...closed]; + } + // ------------------------------------------------------------------------- // Trading (EIP-712 signing required) // ------------------------------------------------------------------------- diff --git a/core/src/exchanges/hyperliquid/normalizer.ts b/core/src/exchanges/hyperliquid/normalizer.ts index e818e484..906191c6 100644 --- a/core/src/exchanges/hyperliquid/normalizer.ts +++ b/core/src/exchanges/hyperliquid/normalizer.ts @@ -367,6 +367,7 @@ export class HyperliquidNormalizer implements IExchangeNormalizer 0 ? totalNotional / totalSz : parseFloat(first.px); + return { + id: String(first.oid), + marketId: this.coinToMarketId(first.coin), + outcomeId: this.coinToOutcomeId(first.coin), + side: first.side === 'B' ? 'buy' : 'sell', + type: 'limit', + price: vwap, + amount: totalSz, + status: 'filled', + filled: totalSz, + remaining: 0, + timestamp: earliest, + fee: totalFee, }; } diff --git a/core/src/types.ts b/core/src/types.ts index a578c10b..9de5d0d0 100644 --- a/core/src/types.ts +++ b/core/src/types.ts @@ -209,6 +209,10 @@ export interface Trade { export interface UserTrade extends Trade { /** The order that produced this trade, if known. */ orderId?: string; + /** The market this trade belongs to, when the venue exposes it (e.g. derivable from the fill's coin/asset). */ + marketId?: string; + /** Trading fee paid by the user for this fill, when the venue exposes it. */ + fee?: number; /** Populated in hosted mode after on-chain settlement; null for local-mode and for non-on-chain venues. */ txHash?: string | null; /** Populated in hosted mode after on-chain settlement; null for local-mode and for non-on-chain venues. */