3D Order Book Depth Map: Plotly.js + Three.js live visualization
Architecture: - depth-map-utils.ts: L2RingBuffer, l2SnapshotsToSurface, computeImbalance └─ O(1) ring buffer, 60-snapshot capacity └─ Surface matrix: ±50 bps × 100 resolution └─ Imbalance formula: I = (V_b-V_a)/(V_b+V_a) with wall detection - depth-map-plotly.tsx: Plotly.js 3D Surface └─ 7-stop warm colorscale (dark→amber→gold) └─ contour projection, ambient+diffuse lighting └─ Live imbalance overlay: gauge bar + formula └─ uirevision for stable camera on updates - depth-map-three.tsx: Three.js high-perf alternative └─ BufferGeometry + vertex colors + OrbitControls └─ 60fps suitable, WebGL renderer with alpha └─ Warm gradient matching Plotly colorscale └─ Double-sided faces, dark grid helper - obi-detail.tsx: Combined strategy detail panel └─ Engine toggle: Plotly.js ↔ Three.js └─ Synthetic data generation for testing └─ 6-stat metrics row (PnL, BTC B&H, Sharpe, Hit Rate, Max DD, Signal) └─ Equity curve comparison + trade history table - Page integration: OBI strategy triggers dedicated 3D view
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* Order Book Depth Map — Data Utilities
|
||||
*
|
||||
* Transforms raw Hyperliquid L2 snapshots into surface matrices
|
||||
* for 3D visualization.
|
||||
*
|
||||
* Architecture:
|
||||
* Ring buffer stores last N snapshots.
|
||||
* Each snapshot: { bids: [px, sz][], asks: [px, sz][], mid: number, ts: number }
|
||||
* Output: { x: bps[], y: snapshot_index[], z: size[][] }
|
||||
*
|
||||
* Ring-buffer design:
|
||||
* - Fixed capacity (default 60 = ~1 minute at 1s updates)
|
||||
* - O(1) append via write pointer
|
||||
* - No allocations on append → suitable for 60fps streaming
|
||||
*/
|
||||
|
||||
export interface L2Level {
|
||||
px: number;
|
||||
sz: number;
|
||||
}
|
||||
|
||||
export interface L2Snapshot {
|
||||
bids: L2Level[]; // sorted descending by price
|
||||
asks: L2Level[]; // sorted ascending by price
|
||||
mid: number;
|
||||
ts: number;
|
||||
}
|
||||
|
||||
export interface SurfaceData {
|
||||
/** Distance from mid in basis points (X-axis) */
|
||||
x: number[];
|
||||
/** Snapshot index or cumulative bid count (Y-axis) */
|
||||
y: number[];
|
||||
/** Resting size matrix: z[row][col] — rows = snapshots, cols = bps */
|
||||
z: number[][];
|
||||
}
|
||||
|
||||
export interface ImbalanceMetrics {
|
||||
/** Current imbalance: (V_bid - V_ask) / (V_bid + V_ask) */
|
||||
imbalance: number;
|
||||
bidVolume: number;
|
||||
askVolume: number;
|
||||
wallSide: "bid" | "ask" | "none";
|
||||
wallStrength: number;
|
||||
snapshots: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ring buffer for L2 snapshots.
|
||||
* Fixed capacity, overwrite oldest on overflow.
|
||||
*/
|
||||
export class L2RingBuffer {
|
||||
private buffer: L2Snapshot[];
|
||||
private capacity: number;
|
||||
private writeIdx: number;
|
||||
private count: number;
|
||||
|
||||
constructor(capacity: number = 60) {
|
||||
this.capacity = capacity;
|
||||
this.buffer = new Array(capacity);
|
||||
this.writeIdx = 0;
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
push(snapshot: L2Snapshot): void {
|
||||
this.buffer[this.writeIdx] = snapshot;
|
||||
this.writeIdx = (this.writeIdx + 1) % this.capacity;
|
||||
if (this.count < this.capacity) this.count++;
|
||||
}
|
||||
|
||||
/** Returns snapshots oldest-first */
|
||||
snapshot(): L2Snapshot[] {
|
||||
if (this.count === 0) return [];
|
||||
const start = this.count < this.capacity ? 0 : this.writeIdx;
|
||||
const result: L2Snapshot[] = [];
|
||||
for (let i = 0; i < this.count; i++) {
|
||||
result.push(this.buffer[(start + i) % this.capacity]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.count;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.writeIdx = 0;
|
||||
this.count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert L2 snapshots → surface matrix.
|
||||
*
|
||||
* X-axis: distance from mid in basis points
|
||||
* Y-axis: snapshot index (0 = oldest, N = newest)
|
||||
* Z-axis: resting size at that bps level
|
||||
*
|
||||
* @param snapshots Ring buffer contents (oldest first)
|
||||
* @param bpsRange ±bps from mid to cover (default: 50)
|
||||
* @param resolution Number of bps steps (default: 100)
|
||||
*/
|
||||
export function l2SnapshotsToSurface(
|
||||
snapshots: L2Snapshot[],
|
||||
bpsRange: number = 50,
|
||||
resolution: number = 100,
|
||||
): SurfaceData {
|
||||
const bpsStep = (bpsRange * 2) / resolution;
|
||||
const x: number[] = [];
|
||||
for (let i = 0; i < resolution; i++) {
|
||||
x.push(-bpsRange + i * bpsStep);
|
||||
}
|
||||
|
||||
const y = snapshots.map((_, i) => i);
|
||||
const z: number[][] = [];
|
||||
|
||||
for (const snap of snapshots) {
|
||||
const row = new Array(resolution).fill(0);
|
||||
const mid = snap.mid;
|
||||
|
||||
// Fill bid side (negative bps)
|
||||
for (const bid of snap.bids) {
|
||||
const bps = ((bid.px - mid) / mid) * 10000;
|
||||
const idx = Math.round((bps + bpsRange) / bpsStep);
|
||||
if (idx >= 0 && idx < resolution) {
|
||||
row[idx] += bid.sz;
|
||||
}
|
||||
}
|
||||
|
||||
// Fill ask side (positive bps)
|
||||
for (const ask of snap.asks) {
|
||||
const bps = ((ask.px - mid) / mid) * 10000;
|
||||
const idx = Math.round((bps + bpsRange) / bpsStep);
|
||||
if (idx >= 0 && idx < resolution) {
|
||||
row[idx] += ask.sz;
|
||||
}
|
||||
}
|
||||
|
||||
z.push(row);
|
||||
}
|
||||
|
||||
return { x, y, z };
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute imbalance metrics from latest snapshot.
|
||||
*/
|
||||
export function computeImbalance(snapshot: L2Snapshot): ImbalanceMetrics {
|
||||
const bidVolume = snapshot.bids.reduce((sum, b) => sum + b.sz * b.px, 0);
|
||||
const askVolume = snapshot.asks.reduce((sum, a) => sum + a.sz * a.px, 0);
|
||||
const total = bidVolume + askVolume;
|
||||
const imbalance = total > 0 ? (bidVolume - askVolume) / total : 0;
|
||||
|
||||
// Wall detection: find side with largest concentration
|
||||
const maxBidSz = Math.max(...snapshot.bids.map(b => b.sz), 0);
|
||||
const maxAskSz = Math.max(...snapshot.asks.map(a => a.sz), 0);
|
||||
const wallSide: "bid" | "ask" | "none" =
|
||||
maxBidSz > maxAskSz * 1.3 ? "bid" :
|
||||
maxAskSz > maxBidSz * 1.3 ? "ask" : "none";
|
||||
const wallStrength = Math.max(maxBidSz, maxAskSz);
|
||||
|
||||
return {
|
||||
imbalance: Math.round(imbalance * 10000) / 10000,
|
||||
bidVolume: Math.round(bidVolume * 100) / 100,
|
||||
askVolume: Math.round(askVolume * 100) / 100,
|
||||
wallSide,
|
||||
wallStrength: Math.round(wallStrength * 10000) / 10000,
|
||||
snapshots: 1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate synthetic L2 data for testing/development.
|
||||
* Produces realistic order-book shapes with price movement.
|
||||
*/
|
||||
export function generateSyntheticSnapshots(
|
||||
count: number = 60,
|
||||
basePrice: number = 97800,
|
||||
): L2Snapshot[] {
|
||||
const snapshots: L2Snapshot[] = [];
|
||||
let price = basePrice;
|
||||
let trend = 0;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Random walk with mean reversion
|
||||
trend += (Math.random() - 0.5) * 2;
|
||||
trend *= 0.95; // decay
|
||||
price += trend * 50;
|
||||
price += (basePrice - price) * 0.01; // mean reversion
|
||||
|
||||
const mid = price;
|
||||
const bids: L2Level[] = [];
|
||||
const asks: L2Level[] = [];
|
||||
|
||||
// Generate 20 levels on each side
|
||||
for (let j = 0; j < 20; j++) {
|
||||
const bps = (j + 1) * 2.5;
|
||||
const bidPx = mid * (1 - bps / 10000);
|
||||
const askPx = mid * (1 + bps / 10000);
|
||||
|
||||
// Realistic size distribution: thicker near mid, thinner further out
|
||||
// Add wall at certain levels
|
||||
const baseSize = Math.exp(-j * 0.15) * 5;
|
||||
const bidWall = j === 3 ? Math.random() * 15 : 0; // occasional wall at 10bps
|
||||
const askWall = j === 5 ? Math.random() * 12 : 0;
|
||||
const noise = (Math.random() - 0.5) * 2;
|
||||
|
||||
bids.push({ px: Math.round(bidPx * 10) / 10, sz: Math.max(0.01, baseSize + bidWall + noise) });
|
||||
asks.push({ px: Math.round(askPx * 10) / 10, sz: Math.max(0.01, baseSize + askWall + noise) });
|
||||
}
|
||||
|
||||
snapshots.push({ bids, asks, mid, ts: Date.now() + i * 1000 });
|
||||
}
|
||||
|
||||
return snapshots;
|
||||
}
|
||||
Reference in New Issue
Block a user