Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions core/src/exchanges/hyperliquid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Order[]> {
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<number, typeof rawFills>();
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<Order[]> {
const [open, closed] = await Promise.all([
this.fetchOpenOrders(),
this.fetchClosedOrders(),
]);
return [...open, ...closed];
}

// -------------------------------------------------------------------------
// Trading (EIP-712 signing required)
// -------------------------------------------------------------------------
Expand Down
38 changes: 38 additions & 0 deletions core/src/exchanges/hyperliquid/normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,13 +367,51 @@ export class HyperliquidNormalizer implements IExchangeNormalizer<HyperliquidRaw
}

normalizeUserTrade(raw: HyperliquidRawFill, _index: number): UserTrade {
const fee = parseFloat(raw.fee);
return {
id: String(raw.tid),
timestamp: raw.time,
price: parseFloat(raw.px),
amount: parseFloat(raw.sz),
side: raw.side === 'B' ? 'buy' : raw.side === 'A' ? 'sell' : 'unknown',
orderId: String(raw.oid),
marketId: this.coinToMarketId(raw.coin),
outcomeId: this.coinToOutcomeId(raw.coin),
fee: Number.isFinite(fee) ? fee : undefined,
};
}

// ponytail: HL has no closed-orders endpoint; we reconstruct from the fills of one oid.
// amount = filled (we cannot recover the unfilled-then-cancelled portion); price = VWAP across fills.
synthesizeClosedOrder(fills: HyperliquidRawFill[]): Order {
const first = fills[0];
let totalSz = 0;
let totalNotional = 0;
let totalFee = 0;
let earliest = first.time;
for (const f of fills) {
const sz = parseFloat(f.sz);
const px = parseFloat(f.px);
const fee = parseFloat(f.fee);
totalSz += sz;
totalNotional += sz * px;
if (Number.isFinite(fee)) totalFee += fee;
if (f.time < earliest) earliest = f.time;
}
const vwap = totalSz > 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,
};
}

Expand Down
4 changes: 4 additions & 0 deletions core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
Loading