diff --git a/apps/visualizer/src/features/market/useDlobSlippage.ts b/apps/visualizer/src/features/market/useDlobSlippage.ts new file mode 100644 index 0000000..a44b26a --- /dev/null +++ b/apps/visualizer/src/features/market/useDlobSlippage.ts @@ -0,0 +1,137 @@ +import { useEffect, useMemo, useState } from 'react'; +import { subscribeGraphqlWs } from '../../lib/graphqlWs'; + +export type DlobSlippageRow = { + marketName: string; + side: 'buy' | 'sell'; + sizeUsd: number; + midPrice: number | null; + vwapPrice: number | null; + worstPrice: number | null; + filledUsd: number | null; + filledBase: number | null; + impactBps: number | null; + levelsConsumed: number | null; + fillPct: 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; +} + +function toInt(v: unknown): number | null { + if (v == null) return null; + if (typeof v === 'number') return Number.isFinite(v) ? Math.trunc(v) : null; + if (typeof v === 'string') { + const s = v.trim(); + if (!s) return null; + const n = Number.parseInt(s, 10); + return Number.isFinite(n) ? n : null; + } + return null; +} + +type HasuraRow = { + market_name: string; + side: string; + size_usd: unknown; + mid_price?: unknown; + vwap_price?: unknown; + worst_price?: unknown; + filled_usd?: unknown; + filled_base?: unknown; + impact_bps?: unknown; + levels_consumed?: unknown; + fill_pct?: unknown; + updated_at?: string | null; +}; + +type SubscriptionData = { + dlob_slippage_latest: HasuraRow[]; +}; + +export function useDlobSlippage(marketName: string): { rows: DlobSlippageRow[]; connected: boolean; error: string | null } { + const [rows, setRows] = useState([]); + const [connected, setConnected] = useState(false); + const [error, setError] = useState(null); + + const normalizedMarket = useMemo(() => (marketName || '').trim(), [marketName]); + + useEffect(() => { + if (!normalizedMarket) { + setRows([]); + setError(null); + setConnected(false); + return; + } + + setError(null); + + const query = ` + subscription DlobSlippage($market: String!) { + dlob_slippage_latest( + where: { market_name: { _eq: $market } } + order_by: [{ side: asc }, { size_usd: asc }] + ) { + market_name + side + size_usd + mid_price + vwap_price + worst_price + filled_usd + filled_base + impact_bps + levels_consumed + fill_pct + updated_at + } + } + `; + + const sub = subscribeGraphqlWs({ + query, + variables: { market: normalizedMarket }, + onStatus: ({ connected }) => setConnected(connected), + onError: (e) => setError(e), + onData: (data) => { + const out: DlobSlippageRow[] = []; + for (const r of data?.dlob_slippage_latest || []) { + if (!r?.market_name) continue; + const side = String(r.side || '').trim(); + if (side !== 'buy' && side !== 'sell') continue; + const sizeUsd = toInt(r.size_usd); + if (sizeUsd == null || sizeUsd <= 0) continue; + out.push({ + marketName: r.market_name, + side, + sizeUsd, + midPrice: toNum(r.mid_price), + vwapPrice: toNum(r.vwap_price), + worstPrice: toNum(r.worst_price), + filledUsd: toNum(r.filled_usd), + filledBase: toNum(r.filled_base), + impactBps: toNum(r.impact_bps), + levelsConsumed: toInt(r.levels_consumed), + fillPct: toNum(r.fill_pct), + updatedAt: r.updated_at ?? null, + }); + } + setRows(out); + }, + }); + + return () => sub.unsubscribe(); + }, [normalizedMarket]); + + return { rows, connected, error }; +}