Files
trade-frontend/apps/visualizer/src/features/market/useDlobStats.ts

135 lines
3.7 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { subscribeGraphqlWs } from '../../lib/graphqlWs';
import { getLatestDlobTable } from './dlobSource';
const ORDERBOOK_STATS_POLL_MS = 400;
export type DlobStats = {
marketName: string;
markPrice: number | null;
oraclePrice: number | null;
bestBid: number | null;
bestAsk: number | null;
mid: number | null;
spreadAbs: number | null;
spreadBps: number | null;
depthBidBase: number | null;
depthAskBase: number | null;
depthBidUsd: number | null;
depthAskUsd: number | null;
imbalance: number | null;
updatedAt: string | null;
};
function toNum(v: unknown): number | null {
if (v == null) return null;
if (typeof v === 'number') return Number.isFinite(v) ? v : null;
if (typeof v === 'string') {
const s = v.trim();
if (!s) return null;
const n = Number(s);
return Number.isFinite(n) ? n : null;
}
return null;
}
type HasuraDlobStatsRow = {
market_name: string;
mark_price?: unknown;
oracle_price?: unknown;
best_bid_price?: unknown;
best_ask_price?: unknown;
mid_price?: unknown;
spread_quote?: unknown;
spread_bps?: unknown;
depth_bid_base?: unknown;
depth_ask_base?: unknown;
depth_bid_quote?: unknown;
depth_ask_quote?: unknown;
imbalance?: unknown;
updated_at?: string | null;
is_indicative?: boolean | null;
};
type SubscriptionData = Record<string, HasuraDlobStatsRow[] | undefined>;
export function useDlobStats(marketName: string): { stats: DlobStats | null; connected: boolean; error: string | null } {
const [stats, setStats] = useState<DlobStats | null>(null);
const [connected, setConnected] = useState(false);
const [error, setError] = useState<string | null>(null);
const normalizedMarket = useMemo(() => (marketName || '').trim(), [marketName]);
useEffect(() => {
if (!normalizedMarket) {
setStats(null);
setError(null);
setConnected(false);
return;
}
setError(null);
const table = getLatestDlobTable(normalizedMarket);
const query = `
subscription DlobStats($market: String!) {
${table}(
where: {market_name: {_eq: $market}, is_indicative: {_eq: false}}
limit: 1
) {
market_name
mark_price
oracle_price
best_bid_price
best_ask_price
mid_price
spread_quote
spread_bps
depth_bid_base
depth_ask_base
depth_bid_quote
depth_ask_quote
imbalance
updated_at
is_indicative
}
}
`;
const sub = subscribeGraphqlWs<SubscriptionData>({
query,
variables: { market: normalizedMarket },
pollMs: ORDERBOOK_STATS_POLL_MS,
onStatus: ({ connected }) => setConnected(connected),
onError: (e) => setError(e),
onData: (data) => {
const row = data?.[table]?.[0];
if (!row?.market_name) {
setStats(null);
return;
}
setStats({
marketName: row.market_name,
markPrice: toNum(row.mark_price),
oraclePrice: toNum(row.oracle_price),
bestBid: toNum(row.best_bid_price),
bestAsk: toNum(row.best_ask_price),
mid: toNum(row.mid_price),
spreadAbs: toNum(row.spread_quote),
spreadBps: toNum(row.spread_bps),
depthBidBase: toNum(row.depth_bid_base),
depthAskBase: toNum(row.depth_ask_base),
depthBidUsd: toNum(row.depth_bid_quote),
depthAskUsd: toNum(row.depth_ask_quote),
imbalance: toNum(row.imbalance),
updatedAt: row.updated_at ?? null,
});
},
});
return () => sub.unsubscribe();
}, [normalizedMarket]);
return { stats, connected, error };
}