feat(chart): candle build indicator as direction line #1
75
apps/visualizer/src/features/market/DlobDepthBandsPanel.tsx
Normal file
75
apps/visualizer/src/features/market/DlobDepthBandsPanel.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import type { DlobDepthBandRow } from './useDlobDepthBands';
|
||||||
|
|
||||||
|
function formatUsd(v: number | null | undefined): string {
|
||||||
|
if (v == null || !Number.isFinite(v)) return '—';
|
||||||
|
if (v >= 1_000_000) return `$${(v / 1_000_000).toFixed(2)}M`;
|
||||||
|
if (v >= 1000) return `$${(v / 1000).toFixed(0)}K`;
|
||||||
|
if (v >= 1) return `$${v.toFixed(2)}`;
|
||||||
|
return `$${v.toPrecision(4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPct(v: number | null | undefined): string {
|
||||||
|
if (v == null || !Number.isFinite(v)) return '—';
|
||||||
|
return `${v.toFixed(0)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bandRowStyle(askScale: number, bidScale: number): CSSProperties {
|
||||||
|
const a = Number.isFinite(askScale) && askScale > 0 ? Math.min(1, askScale) : 0;
|
||||||
|
const b = Number.isFinite(bidScale) && bidScale > 0 ? Math.min(1, bidScale) : 0;
|
||||||
|
return { ['--ask-scale' as any]: a, ['--bid-scale' as any]: b } as CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DlobDepthBandsPanel({ rows }: { rows: DlobDepthBandRow[] }) {
|
||||||
|
const sorted = useMemo(() => rows.slice().sort((a, b) => a.bandBps - b.bandBps), [rows]);
|
||||||
|
|
||||||
|
const maxUsd = useMemo(() => {
|
||||||
|
let max = 0;
|
||||||
|
for (const r of sorted) {
|
||||||
|
if (r.askUsd != null && Number.isFinite(r.askUsd)) max = Math.max(max, r.askUsd);
|
||||||
|
if (r.bidUsd != null && Number.isFinite(r.bidUsd)) max = Math.max(max, r.bidUsd);
|
||||||
|
}
|
||||||
|
return max;
|
||||||
|
}, [sorted]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dlobDepth">
|
||||||
|
<div className="dlobDepth__head">
|
||||||
|
<div className="dlobDepth__title">Depth (bands)</div>
|
||||||
|
<div className="dlobDepth__meta">±bps around mid</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dlobDepth__table">
|
||||||
|
<div className="dlobDepthRow dlobDepthRow--head">
|
||||||
|
<span>Band</span>
|
||||||
|
<span className="dlobDepthRow__num">Ask USD</span>
|
||||||
|
<span className="dlobDepthRow__num">Bid USD</span>
|
||||||
|
<span className="dlobDepthRow__num">Bid %</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sorted.length ? (
|
||||||
|
sorted.map((r) => (
|
||||||
|
<div
|
||||||
|
key={r.bandBps}
|
||||||
|
className="dlobDepthRow"
|
||||||
|
style={bandRowStyle(maxUsd > 0 ? (r.askUsd || 0) / maxUsd : 0, maxUsd > 0 ? (r.bidUsd || 0) / maxUsd : 0)}
|
||||||
|
title={`band=${r.bandBps}bps bid=${r.bidUsd ?? '—'} ask=${r.askUsd ?? '—'} imbalance=${r.imbalance ?? '—'}`}
|
||||||
|
>
|
||||||
|
<span className="dlobDepthRow__band">{r.bandBps} bps</span>
|
||||||
|
<span className="dlobDepthRow__num neg">{formatUsd(r.askUsd)}</span>
|
||||||
|
<span className="dlobDepthRow__num pos">{formatUsd(r.bidUsd)}</span>
|
||||||
|
<span className="dlobDepthRow__num muted">
|
||||||
|
{r.imbalance == null || !Number.isFinite(r.imbalance)
|
||||||
|
? '—'
|
||||||
|
: formatPct(((r.imbalance + 1) / 2) * 100)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="dlobDepth__empty muted">No depth band rows yet.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user