From bff6560f439a60cc5a3e32d6b48720f444d95074 Mon Sep 17 00:00:00 2001 From: u1 Date: Sat, 10 Jan 2026 23:00:56 +0000 Subject: [PATCH] feat(visualizer): add DLOB depth bands subscription hook --- .../src/features/market/useDlobDepthBands.ts | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 apps/visualizer/src/features/market/useDlobDepthBands.ts diff --git a/apps/visualizer/src/features/market/useDlobDepthBands.ts b/apps/visualizer/src/features/market/useDlobDepthBands.ts new file mode 100644 index 0000000..bf20ac6 --- /dev/null +++ b/apps/visualizer/src/features/market/useDlobDepthBands.ts @@ -0,0 +1,133 @@ +import { useEffect, useMemo, useState } from 'react'; +import { subscribeGraphqlWs } from '../../lib/graphqlWs'; + +export type DlobDepthBandRow = { + marketName: string; + bandBps: number; + midPrice: number | null; + bestBid: number | null; + bestAsk: number | null; + bidUsd: number | null; + askUsd: number | null; + bidBase: number | null; + askBase: 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; +} + +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; + band_bps: unknown; + mid_price?: unknown; + best_bid_price?: unknown; + best_ask_price?: unknown; + bid_usd?: unknown; + ask_usd?: unknown; + bid_base?: unknown; + ask_base?: unknown; + imbalance?: unknown; + updated_at?: string | null; +}; + +type SubscriptionData = { + dlob_depth_bps_latest: HasuraRow[]; +}; + +export function useDlobDepthBands( + marketName: string +): { rows: DlobDepthBandRow[]; 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 DlobDepthBands($market: String!) { + dlob_depth_bps_latest( + where: { market_name: { _eq: $market } } + order_by: [{ band_bps: asc }] + ) { + market_name + band_bps + mid_price + best_bid_price + best_ask_price + bid_usd + ask_usd + bid_base + ask_base + imbalance + updated_at + } + } + `; + + const sub = subscribeGraphqlWs({ + query, + variables: { market: normalizedMarket }, + onStatus: ({ connected }) => setConnected(connected), + onError: (e) => setError(e), + onData: (data) => { + const out: DlobDepthBandRow[] = []; + for (const r of data?.dlob_depth_bps_latest || []) { + if (!r?.market_name) continue; + const bandBps = toInt(r.band_bps); + if (bandBps == null || bandBps <= 0) continue; + out.push({ + marketName: r.market_name, + bandBps, + midPrice: toNum(r.mid_price), + bestBid: toNum(r.best_bid_price), + bestAsk: toNum(r.best_ask_price), + bidUsd: toNum(r.bid_usd), + askUsd: toNum(r.ask_usd), + bidBase: toNum(r.bid_base), + askBase: toNum(r.ask_base), + imbalance: toNum(r.imbalance), + updatedAt: r.updated_at ?? null, + }); + } + setRows(out); + }, + }); + + return () => sub.unsubscribe(); + }, [normalizedMarket]); + + return { rows, connected, error }; +}