diff --git a/apps/visualizer/src/App.tsx b/apps/visualizer/src/App.tsx index 5e529b3..38f842f 100644 --- a/apps/visualizer/src/App.tsx +++ b/apps/visualizer/src/App.tsx @@ -1,3 +1,4 @@ +import type { CSSProperties } from 'react'; import { useEffect, useMemo, useState } from 'react'; import { useLocalStorageState } from './app/hooks/useLocalStorageState'; import AppShell from './layout/AppShell'; @@ -11,6 +12,11 @@ import Button from './ui/Button'; import TopNav from './layout/TopNav'; import AuthStatus from './layout/AuthStatus'; import LoginScreen from './layout/LoginScreen'; +import { useDlobStats } from './features/market/useDlobStats'; +import { useDlobL2 } from './features/market/useDlobL2'; +import { useDlobSlippage } from './features/market/useDlobSlippage'; +import { useDlobDepthBands } from './features/market/useDlobDepthBands'; +import DlobDashboard from './features/market/DlobDashboard'; function envNumber(name: string, fallback: number): number { const v = (import.meta as any).env?.[name]; @@ -36,6 +42,11 @@ function formatQty(v: number | null | undefined, decimals: number): string { return v.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); } +function orderbookBarStyle(scale: number): CSSProperties { + const s = Number.isFinite(scale) && scale > 0 ? Math.min(1, scale) : 0; + return { ['--ob-bar-scale' as any]: s } as CSSProperties; +} + type WhoamiResponse = { ok?: boolean; user?: string | null; @@ -99,9 +110,9 @@ export default function App() { } function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) { - const markets = useMemo(() => ['PUMP-PERP', 'SOL-PERP', 'BTC-PERP', 'ETH-PERP'], []); + const markets = useMemo(() => ['PUMP-PERP', 'SOL-PERP', '1MBONK-PERP', 'BTC-PERP', 'ETH-PERP'], []); - const [symbol, setSymbol] = useLocalStorageState('trade.symbol', envString('VITE_SYMBOL', 'PUMP-PERP')); + const [symbol, setSymbol] = useLocalStorageState('trade.symbol', envString('VITE_SYMBOL', 'SOL-PERP')); const [source, setSource] = useLocalStorageState('trade.source', envString('VITE_SOURCE', '')); const [tf, setTf] = useLocalStorageState('trade.tf', envString('VITE_TF', '1m')); const [pollMs, setPollMs] = useLocalStorageState('trade.pollMs', envNumber('VITE_POLL_MS', 1000)); @@ -110,7 +121,7 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) { const [showBuild, setShowBuild] = useLocalStorageState('trade.showBuild', false); const [tab, setTab] = useLocalStorageState<'orderbook' | 'trades'>('trade.sidebarTab', 'orderbook'); const [bottomTab, setBottomTab] = useLocalStorageState< - 'positions' | 'orders' | 'balances' | 'orderHistory' | 'positionHistory' + 'dlob' | 'positions' | 'orders' | 'balances' | 'orderHistory' | 'positionHistory' >('trade.bottomTab', 'positions'); const [tradeSide, setTradeSide] = useLocalStorageState<'long' | 'short'>('trade.form.side', 'long'); const [tradeOrderType, setTradeOrderType] = useLocalStorageState<'market' | 'limit' | 'other'>( @@ -120,6 +131,16 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) { const [tradePrice, setTradePrice] = useLocalStorageState('trade.form.price', 0); const [tradeSize, setTradeSize] = useLocalStorageState('trade.form.size', 0.1); + useEffect(() => { + if (symbol === 'BONK-PERP') { + setSymbol('1MBONK-PERP'); + return; + } + if (!markets.includes(symbol)) { + setSymbol('SOL-PERP'); + } + }, [markets, setSymbol, symbol]); + const { candles, indicators, meta, loading, error, refresh } = useChartData({ symbol, source: source.trim() ? source : undefined, @@ -128,12 +149,18 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) { pollMs, }); + const { stats: dlob, connected: dlobConnected, error: dlobError } = useDlobStats(symbol); + const { l2: dlobL2, connected: dlobL2Connected, error: dlobL2Error } = useDlobL2(symbol, { levels: 14 }); + const { rows: slippageRows, connected: slippageConnected, error: slippageError } = useDlobSlippage(symbol); + const { rows: depthBands, connected: depthBandsConnected, error: depthBandsError } = useDlobDepthBands(symbol); + const latest = candles.length ? candles[candles.length - 1] : null; const first = candles.length ? candles[0] : null; const changePct = first && latest && first.close > 0 ? ((latest.close - first.close) / first.close) * 100 : null; const orderbook = useMemo(() => { + if (dlobL2) return { asks: dlobL2.asks, bids: dlobL2.bids, mid: dlobL2.mid as number | null }; if (!latest) return { asks: [], bids: [], mid: null as number | null }; const mid = latest.close; const step = Math.max(mid * 0.00018, 0.0001); @@ -164,7 +191,19 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) { }); return { asks, bids, mid }; - }, [latest]); + }, [dlobL2, latest]); + + const maxAskTotal = useMemo(() => { + let max = 0; + for (const r of orderbook.asks) max = Math.max(max, r.total || 0); + return max; + }, [orderbook.asks]); + + const maxBidTotal = useMemo(() => { + let max = 0; + for (const r of orderbook.bids) max = Math.max(max, r.total || 0); + return max; + }, [orderbook.bids]); const trades = useMemo(() => { const slice = candles.slice(-24).reverse(); @@ -184,6 +223,24 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) { return latest?.close ?? tradePrice; }, [latest?.close, tradeOrderType, tradePrice]); + const orderValueUsd = useMemo(() => { + if (!Number.isFinite(tradeSize) || tradeSize <= 0) return null; + if (!Number.isFinite(effectiveTradePrice) || effectiveTradePrice <= 0) return null; + const v = effectiveTradePrice * tradeSize; + return Number.isFinite(v) && v > 0 ? v : null; + }, [effectiveTradePrice, tradeSize]); + + const dynamicSlippage = useMemo(() => { + if (orderValueUsd == null) return null; + const side = tradeSide === 'short' ? 'sell' : 'buy'; + const rows = slippageRows.filter((r) => r.side === side).slice(); + rows.sort((a, b) => a.sizeUsd - b.sizeUsd); + if (!rows.length) return null; + const biggest = rows[rows.length - 1]; + const match = rows.find((r) => r.sizeUsd >= orderValueUsd) || biggest; + return match; + }, [orderValueUsd, slippageRows, tradeSide]); + const topItems = useMemo( () => [ { key: 'BTC', label: 'BTC', changePct: 1.28, active: false }, @@ -209,12 +266,28 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) { ), }, { key: 'oracle', label: 'Oracle', value: formatUsd(latest?.oracle ?? null) }, - { key: 'funding', label: 'Funding / 24h', value: '—', sub: '—' }, - { key: 'oi', label: 'Open Interest', value: '—' }, - { key: 'vol', label: '24h Volume', value: '—' }, - { key: 'details', label: 'Market Details', value: View }, + { key: 'bid', label: 'Bid', value: formatUsd(dlob?.bestBid ?? null) }, + { key: 'ask', label: 'Ask', value: formatUsd(dlob?.bestAsk ?? null) }, + { + key: 'spread', + label: 'Spread', + value: dlob?.spreadBps == null ? '—' : `${dlob.spreadBps.toFixed(1)} bps`, + sub: formatUsd(dlob?.spreadAbs ?? null), + }, + { + key: 'dlob', + label: 'DLOB', + value: dlobConnected ? 'live' : '—', + sub: dlobError ? {dlobError} : dlob?.updatedAt || '—', + }, + { + key: 'l2', + label: 'L2', + value: dlobL2Connected ? 'live' : '—', + sub: dlobL2Error ? {dlobL2Error} : dlobL2?.updatedAt || '—', + }, ]; - }, [latest?.close, latest?.oracle, changePct]); + }, [latest?.close, latest?.oracle, changePct, dlob, dlobConnected, dlobError, dlobL2, dlobL2Connected, dlobL2Error]); const seriesLabel = useMemo(() => `Candles: Mark (oracle overlay)`, []); const seriesKey = useMemo(() => `${symbol}|${source}|${tf}`, [symbol, source, tf]); @@ -292,11 +365,30 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) { showBuild={showBuild} onToggleBuild={() => setShowBuild((v) => !v)} seriesLabel={seriesLabel} + dlobQuotes={{ bid: dlob?.bestBid ?? null, ask: dlob?.bestAsk ?? null, mid: dlob?.mid ?? null }} /> + ), + }, { id: 'positions', label: 'Positions', content:
Positions (next)
}, { id: 'orders', label: 'Orders', content:
Orders (next)
}, { id: 'balances', label: 'Balances', content:
Balances (next)
}, @@ -323,7 +415,7 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) { title={
Orderbook
-
{loading ? 'loading…' : latest ? formatUsd(latest.close) : '—'}
+
{loading ? 'loading…' : orderbook.mid != null ? formatUsd(orderbook.mid) : latest ? formatUsd(latest.close) : '—'}
} > @@ -341,18 +433,26 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) {
{orderbook.asks.map((r) => ( -
+
0 ? r.total / maxAskTotal : 0)} + > {formatQty(r.price, 3)} {formatQty(r.size, 2)} {formatQty(r.total, 2)}
))}
- {latest ? formatQty(latest.close, 3) : '—'} + {formatQty(orderbook.mid, 3)} mid
{orderbook.bids.map((r) => ( -
+
0 ? r.total / maxBidTotal : 0)} + > {formatQty(r.price, 3)} {formatQty(r.size, 2)} {formatQty(r.total, 2)} @@ -487,7 +587,27 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) {
Slippage (Dynamic) - + + {slippageError ? ( + {slippageError} + ) : dynamicSlippage?.impactBps == null ? ( + slippageConnected ? ( + '—' + ) : ( + 'offline' + ) + ) : ( + <> + {dynamicSlippage.impactBps.toFixed(1)} bps{' '} + + ({dynamicSlippage.sizeUsd.toLocaleString()} USD) + {dynamicSlippage.fillPct != null && dynamicSlippage.fillPct < 99.9 + ? `, ${dynamicSlippage.fillPct.toFixed(0)}% fill` + : ''} + + + )} +
Margin Required