import type { DlobL2, OrderbookRow } from './useDlobL2'; export const DEFAULT_DLOB_DEPTH_BANDS_BPS = [5, 10, 20, 50, 100, 200] as const; export const DEFAULT_DLOB_SLIPPAGE_SIZES_USD = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 5000, 10000, 50000] as const; export type ComputedDlobDepthBandRow = { 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; }; export type ComputedDlobSlippageRow = { 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; }; type OrderbookSide = 'buy' | 'sell'; function levelUsd(level: OrderbookRow): number { if (Number.isFinite(level.sizeUsd)) return level.sizeUsd; if (Number.isFinite(level.sizeBase) && Number.isFinite(level.price)) return level.sizeBase * level.price; return 0; } function canonicalBids(l2: DlobL2): OrderbookRow[] { return (Array.isArray(l2.bids) ? l2.bids : []) .slice() .sort((a, b) => b.price - a.price); } function canonicalAsks(l2: DlobL2): OrderbookRow[] { return (Array.isArray(l2.asks) ? l2.asks.slice().reverse() : []) .slice() .sort((a, b) => a.price - b.price); } function resolveBestBid(l2: DlobL2, bids: OrderbookRow[]): number | null { if (l2.bestBid != null && Number.isFinite(l2.bestBid)) return l2.bestBid; const price = bids[0]?.price; return Number.isFinite(price) ? price : null; } function resolveBestAsk(l2: DlobL2, asks: OrderbookRow[]): number | null { if (l2.bestAsk != null && Number.isFinite(l2.bestAsk)) return l2.bestAsk; const price = asks[0]?.price; return Number.isFinite(price) ? price : null; } function resolveMidPrice(l2: DlobL2, bestBid: number | null, bestAsk: number | null): number | null { if (l2.mid != null && Number.isFinite(l2.mid)) return l2.mid; if (bestBid != null && bestAsk != null) return (bestBid + bestAsk) / 2; return bestBid ?? bestAsk ?? null; } function computeBandDepth( bids: OrderbookRow[], asks: OrderbookRow[], midPrice: number | null, bandBps: number ): Pick { if (midPrice == null || !Number.isFinite(midPrice) || !(midPrice > 0)) { return { bidUsd: null, askUsd: null, bidBase: null, askBase: null, imbalance: null, }; } const minBidPrice = midPrice * (1 - bandBps / 10_000); const maxAskPrice = midPrice * (1 + bandBps / 10_000); let bidBase = 0; let askBase = 0; let bidUsd = 0; let askUsd = 0; for (const level of bids) { if (!Number.isFinite(level.price) || !Number.isFinite(level.sizeBase)) continue; if (level.price < minBidPrice) break; bidBase += level.sizeBase; bidUsd += levelUsd(level); } for (const level of asks) { if (!Number.isFinite(level.price) || !Number.isFinite(level.sizeBase)) continue; if (level.price > maxAskPrice) break; askBase += level.sizeBase; askUsd += levelUsd(level); } const denom = bidUsd + askUsd; return { bidUsd, askUsd, bidBase, askBase, imbalance: denom > 0 ? (bidUsd - askUsd) / denom : null, }; } function simulateFill(levels: OrderbookRow[], sizeUsd: number) { let remainingUsd = sizeUsd; let filledUsd = 0; let filledBase = 0; let worstPrice: number | null = null; let levelsConsumed = 0; for (const level of levels) { const availableUsd = levelUsd(level); if (!(availableUsd > 0) || !Number.isFinite(level.price) || !(level.price > 0)) continue; if (remainingUsd <= 0) break; levelsConsumed += 1; worstPrice = level.price; const takeUsd = Math.min(remainingUsd, availableUsd); filledUsd += takeUsd; filledBase += takeUsd / level.price; remainingUsd -= takeUsd; } return { filledUsd, filledBase, worstPrice, levelsConsumed, vwapPrice: filledBase > 0 ? filledUsd / filledBase : null, fillPct: sizeUsd > 0 ? (filledUsd / sizeUsd) * 100 : null, }; } function impactBps(side: OrderbookSide, midPrice: number | null, vwapPrice: number | null): number | null { if (midPrice == null || vwapPrice == null) return null; if (!(midPrice > 0) || !Number.isFinite(vwapPrice)) return null; if (side === 'buy') return ((vwapPrice / midPrice) - 1) * 10_000; return (1 - vwapPrice / midPrice) * 10_000; } export function computeDepthBandsFromDlobL2( l2: DlobL2 | null | undefined, bandList: readonly number[] = DEFAULT_DLOB_DEPTH_BANDS_BPS ): ComputedDlobDepthBandRow[] { if (!l2?.marketName) return []; const bids = canonicalBids(l2); const asks = canonicalAsks(l2); const bestBid = resolveBestBid(l2, bids); const bestAsk = resolveBestAsk(l2, asks); const midPrice = resolveMidPrice(l2, bestBid, bestAsk); const updatedAt = l2.updatedAt ?? null; return bandList .map((bandBps) => Number(bandBps)) .filter((bandBps, idx, arr) => Number.isFinite(bandBps) && bandBps > 0 && arr.indexOf(bandBps) === idx) .sort((a, b) => a - b) .map((bandBps) => ({ marketName: l2.marketName, bandBps, midPrice, bestBid, bestAsk, updatedAt, ...computeBandDepth(bids, asks, midPrice, bandBps), })); } export function computeSlippageFromDlobL2( l2: DlobL2 | null | undefined, sizeList: readonly number[] = DEFAULT_DLOB_SLIPPAGE_SIZES_USD ): ComputedDlobSlippageRow[] { if (!l2?.marketName) return []; const bids = canonicalBids(l2); const asks = canonicalAsks(l2); const bestBid = resolveBestBid(l2, bids); const bestAsk = resolveBestAsk(l2, asks); const midPrice = resolveMidPrice(l2, bestBid, bestAsk); const updatedAt = l2.updatedAt ?? null; if (midPrice == null || !Number.isFinite(midPrice) || !(midPrice > 0)) return []; const sizes = sizeList .map((sizeUsd) => Number(sizeUsd)) .filter((sizeUsd, idx, arr) => Number.isFinite(sizeUsd) && sizeUsd > 0 && arr.indexOf(sizeUsd) === idx) .sort((a, b) => a - b); const rows: ComputedDlobSlippageRow[] = []; for (const sizeUsd of sizes) { const buyFill = simulateFill(asks, sizeUsd); rows.push({ marketName: l2.marketName, side: 'buy', sizeUsd, midPrice, vwapPrice: buyFill.vwapPrice, worstPrice: buyFill.worstPrice, filledUsd: buyFill.filledUsd, filledBase: buyFill.filledBase, impactBps: impactBps('buy', midPrice, buyFill.vwapPrice), levelsConsumed: buyFill.levelsConsumed, fillPct: buyFill.fillPct, updatedAt, }); const sellFill = simulateFill(bids, sizeUsd); rows.push({ marketName: l2.marketName, side: 'sell', sizeUsd, midPrice, vwapPrice: sellFill.vwapPrice, worstPrice: sellFill.worstPrice, filledUsd: sellFill.filledUsd, filledBase: sellFill.filledBase, impactBps: impactBps('sell', midPrice, sellFill.vwapPrice), levelsConsumed: sellFill.levelsConsumed, fillPct: sellFill.fillPct, updatedAt, }); } return rows; }