chore(snapshot): sync frontend workspace state
This commit is contained in:
244
apps/visualizer/src/features/market/dlobDerivedMetrics.ts
Normal file
244
apps/visualizer/src/features/market/dlobDerivedMetrics.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
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<ComputedDlobDepthBandRow, 'bidUsd' | 'askUsd' | 'bidBase' | 'askBase' | 'imbalance'> {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user