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
42 changes: 40 additions & 2 deletions core/src/exchanges/hyperliquid/fetcher.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { MarketFilterParams, EventFetchParams, OHLCVParams, TradesParams } from '../../BaseExchange';
import { IExchangeFetcher, FetcherContext } from '../interfaces';
import { hyperliquidErrorMapper } from './errors';
import { toCoinNotation, toMidKey, fromMarketId } from './utils';
import { toCoinNotation, toMidKey, fromMarketId, fromCoinEncoding } from './utils';

// ----------------------------------------------------------------------------
// Raw venue-native types (Hyperliquid HIP-4 Outcome Markets)
Expand Down Expand Up @@ -142,11 +142,21 @@ export interface HyperliquidRawUserState {
withdrawable: string;
}

// Per-coin context from spotMetaAndAssetCtxs (only the fields we use)
export interface HyperliquidRawSpotAssetCtx {
coin: string; // "#NNNN" for outcome legs
dayNtlVlm: string; // 24h notional volume in USDC
prevDayPx?: string;
markPx?: string;
midPx?: string;
}

// Composite type: outcome + its question context
export interface HyperliquidRawOutcomeWithQuestion {
outcome: HyperliquidRawOutcome;
question: HyperliquidRawQuestion | undefined;
midPrice: string | undefined; // from allMids
volume24h?: number; // summed Yes+No dayNtlVlm from spotMetaAndAssetCtxs
}

// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -176,9 +186,10 @@ export class HyperliquidFetcher implements IExchangeFetcher<HyperliquidRawOutcom
// -- Markets (outcomes) ----------------------------------------------------

async fetchRawMarkets(params?: MarketFilterParams): Promise<HyperliquidRawOutcomeWithQuestion[]> {
const [meta, mids] = await Promise.all([
const [meta, mids, volumeMap] = await Promise.all([
this.fetchOutcomeMeta(),
this.fetchAllMids(),
this.fetchOutcomeVolumeMap(),
]);

const questionMap = new Map<number, HyperliquidRawQuestion>();
Expand All @@ -192,6 +203,7 @@ export class HyperliquidFetcher implements IExchangeFetcher<HyperliquidRawOutcom
outcome,
question: questionMap.get(outcome.outcome),
midPrice: this.getMidForOutcome(mids, outcome.outcome),
volume24h: volumeMap.get(outcome.outcome),
}));

// Filter settled outcomes out by default (active only)
Expand Down Expand Up @@ -313,6 +325,32 @@ export class HyperliquidFetcher implements IExchangeFetcher<HyperliquidRawOutcom
return this.postInfo<HyperliquidRawMid>({ type: 'allMids' });
}

/**
* Build a map of outcomeId -> 24h notional volume (Yes leg + No leg)
* by reading spotMetaAndAssetCtxs, where outcome legs appear as
* coin "#<encoding>" with `dayNtlVlm` in USDC.
*/
async fetchOutcomeVolumeMap(): Promise<Map<number, number>> {
const map = new Map<number, number>();
try {
const resp = await this.postInfo<[unknown, HyperliquidRawSpotAssetCtx[]]>({ type: 'spotMetaAndAssetCtxs' });
const ctxs = Array.isArray(resp) ? resp[1] : undefined;
if (!Array.isArray(ctxs)) return map;
for (const ctx of ctxs) {
if (!ctx?.coin || !ctx.coin.startsWith('#')) continue;
const vol = parseFloat(ctx.dayNtlVlm);
if (!Number.isFinite(vol)) continue;
const encoding = parseInt(ctx.coin.slice(1), 10);
if (!Number.isFinite(encoding)) continue;
const { outcomeId } = fromCoinEncoding(encoding);
map.set(outcomeId, (map.get(outcomeId) ?? 0) + vol);
}
} catch {
// ponytail: best-effort volume enrichment; if spotMetaAndAssetCtxs is unreachable, return empty map and callers fall back to 0
}
return map;
}

private getMidForOutcome(mids: HyperliquidRawMid, outcomeId: number): string | undefined {
const midKey = toMidKey(outcomeId);
return mids[midKey];
Expand Down
5 changes: 3 additions & 2 deletions core/src/exchanges/hyperliquid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,15 @@ export class HyperliquidExchange extends PredictionMarketExchange {
return [];
}

const [rawQuestions, meta, mids] = await Promise.all([
const [rawQuestions, meta, mids, volumeMap] = await Promise.all([
this.fetcher.fetchRawEvents(params),
this.fetcher.fetchOutcomeMeta(),
this.fetcher.fetchAllMids(),
this.fetcher.fetchOutcomeVolumeMap(),
]);

return rawQuestions
.map(q => this.normalizer.normalizeEventWithMarkets(q, meta, mids))
.map(q => this.normalizer.normalizeEventWithMarkets(q, meta, mids, volumeMap))
.filter((e): e is UnifiedEvent => e !== null);
}

Expand Down
4 changes: 3 additions & 1 deletion core/src/exchanges/hyperliquid/normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ export class HyperliquidNormalizer implements IExchangeNormalizer<HyperliquidRaw
slug: `hl-${outcomeId}`,
outcomes,
resolutionDate: expiryDate ?? new Date(0),
volume24h: 0,
volume24h: raw.volume24h ?? 0,
liquidity: 0,
url: buildMarketUrl(outcomeId),
category,
Expand Down Expand Up @@ -290,6 +290,7 @@ export class HyperliquidNormalizer implements IExchangeNormalizer<HyperliquidRaw
raw: HyperliquidRawQuestion,
outcomeMeta: HyperliquidRawOutcomeMeta,
mids: HyperliquidRawMid,
volumeMap?: Map<number, number>,
): UnifiedEvent | null {
const event = this.normalizeEvent(raw);
if (!event) return null;
Expand All @@ -308,6 +309,7 @@ export class HyperliquidNormalizer implements IExchangeNormalizer<HyperliquidRaw
outcome,
question: raw,
midPrice,
volume24h: volumeMap?.get(outcomeId),
});
if (market) {
markets.push(market);
Expand Down
12 changes: 8 additions & 4 deletions sdks/python/pmxt/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,19 @@ def __init__(self, message: str, field: str | None = None, **kwargs) -> None:
class NetworkError(PmxtError):
"""503 Service Unavailable - Network connectivity issues."""

def __init__(self, message: str, exchange: str | None = None):
super().__init__(message, code="NETWORK_ERROR", retryable=True, exchange=exchange)
def __init__(self, message: str, exchange: str | None = None, **kwargs: Any) -> None:
kwargs.setdefault("code", "NETWORK_ERROR")
kwargs.setdefault("retryable", True)
super().__init__(message, exchange=exchange, **kwargs)


class ExchangeNotAvailable(PmxtError):
"""503 Service Unavailable - Exchange is down or unreachable."""

def __init__(self, message: str, exchange: str | None = None):
super().__init__(message, code="EXCHANGE_NOT_AVAILABLE", retryable=True, exchange=exchange)
def __init__(self, message: str, exchange: str | None = None, **kwargs: Any) -> None:
kwargs.setdefault("code", "EXCHANGE_NOT_AVAILABLE")
kwargs.setdefault("retryable", True)
super().__init__(message, exchange=exchange, **kwargs)


# Mapping from server error codes to error classes
Expand Down
Loading