feat(visualizer): update chart flow and DLOB docs
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -4,11 +4,20 @@ tokens/*
|
|||||||
!tokens/*.example.yml
|
!tokens/*.example.yml
|
||||||
!tokens/*.example.yaml
|
!tokens/*.example.yaml
|
||||||
|
|
||||||
|
# Local secrets (never commit)
|
||||||
|
pass/
|
||||||
|
argo/pass
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
.env
|
.env
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# Baremetal IaC local overlays (do not commit)
|
||||||
|
infra/baremetal-solana-rpc/ansible/inventory/hosts.ini
|
||||||
|
infra/baremetal-solana-rpc/ansible/group_vars/solana_rpc.yml
|
||||||
|
infra/baremetal-solana-rpc/ansible/group_vars/vault.yml
|
||||||
|
|
||||||
# Local scratch / build output
|
# Local scratch / build output
|
||||||
_tmp/
|
_tmp/
|
||||||
apps/visualizer/dist/
|
apps/visualizer/dist/
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ function envString(name: string, fallback: string): string {
|
|||||||
|
|
||||||
function formatUsd(v: number | null | undefined): string {
|
function formatUsd(v: number | null | undefined): string {
|
||||||
if (v == null || !Number.isFinite(v)) return '—';
|
if (v == null || !Number.isFinite(v)) return '—';
|
||||||
if (v >= 1000) return `$${v.toFixed(0)}`;
|
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)}`;
|
if (v >= 1) return `$${v.toFixed(2)}`;
|
||||||
return `$${v.toPrecision(4)}`;
|
return `$${v.toPrecision(4)}`;
|
||||||
}
|
}
|
||||||
@@ -42,9 +43,36 @@ function formatQty(v: number | null | undefined, decimals: number): string {
|
|||||||
return v.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
|
return v.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
|
||||||
}
|
}
|
||||||
|
|
||||||
function orderbookBarStyle(scale: number): CSSProperties {
|
function formatCompact(v: number | null | undefined): string {
|
||||||
const s = Number.isFinite(scale) && scale > 0 ? Math.min(1, scale) : 0;
|
if (v == null || !Number.isFinite(v)) return '—';
|
||||||
return { ['--ob-bar-scale' as any]: s } as CSSProperties;
|
const abs = Math.abs(v);
|
||||||
|
if (abs >= 1_000_000) return `${(v / 1_000_000).toFixed(2)}M`;
|
||||||
|
if (abs >= 1000) return `${(v / 1000).toFixed(0)}K`;
|
||||||
|
if (abs >= 1) return v.toFixed(2);
|
||||||
|
return v.toPrecision(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp01(scale: number): number {
|
||||||
|
return Number.isFinite(scale) && scale > 0 ? Math.min(1, scale) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function barCurve(scale01: number): number {
|
||||||
|
// Makes small rows visible without letting a single wall dominate.
|
||||||
|
return Math.sqrt(clamp01(scale01));
|
||||||
|
}
|
||||||
|
|
||||||
|
function orderbookRowBarStyle(totalScale: number, levelScale: number): CSSProperties {
|
||||||
|
return {
|
||||||
|
['--ob-total-scale' as any]: barCurve(totalScale),
|
||||||
|
['--ob-level-scale' as any]: barCurve(levelScale),
|
||||||
|
} as CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
function liquidityStyle(bid: number, ask: number): CSSProperties {
|
||||||
|
const max = Math.max(1e-9, bid, ask);
|
||||||
|
const b = Number.isFinite(bid) && bid > 0 ? Math.min(1, bid / max) : 0;
|
||||||
|
const a = Number.isFinite(ask) && ask > 0 ? Math.min(1, ask / max) : 0;
|
||||||
|
return { ['--liq-bid' as any]: b, ['--liq-ask' as any]: a } as CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
type WhoamiResponse = {
|
type WhoamiResponse = {
|
||||||
@@ -118,7 +146,7 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) {
|
|||||||
const [pollMs, setPollMs] = useLocalStorageState('trade.pollMs', envNumber('VITE_POLL_MS', 1000));
|
const [pollMs, setPollMs] = useLocalStorageState('trade.pollMs', envNumber('VITE_POLL_MS', 1000));
|
||||||
const [limit, setLimit] = useLocalStorageState('trade.limit', envNumber('VITE_LIMIT', 300));
|
const [limit, setLimit] = useLocalStorageState('trade.limit', envNumber('VITE_LIMIT', 300));
|
||||||
const [showIndicators, setShowIndicators] = useLocalStorageState('trade.showIndicators', true);
|
const [showIndicators, setShowIndicators] = useLocalStorageState('trade.showIndicators', true);
|
||||||
const [showBuild, setShowBuild] = useLocalStorageState('trade.showBuild', false);
|
const [showBuild, setShowBuild] = useLocalStorageState('trade.showBuild', true);
|
||||||
const [tab, setTab] = useLocalStorageState<'orderbook' | 'trades'>('trade.sidebarTab', 'orderbook');
|
const [tab, setTab] = useLocalStorageState<'orderbook' | 'trades'>('trade.sidebarTab', 'orderbook');
|
||||||
const [bottomTab, setBottomTab] = useLocalStorageState<
|
const [bottomTab, setBottomTab] = useLocalStorageState<
|
||||||
'dlob' | 'positions' | 'orders' | 'balances' | 'orderHistory' | 'positionHistory'
|
'dlob' | 'positions' | 'orders' | 'balances' | 'orderHistory' | 'positionHistory'
|
||||||
@@ -150,7 +178,7 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { stats: dlob, connected: dlobConnected, error: dlobError } = useDlobStats(symbol);
|
const { stats: dlob, connected: dlobConnected, error: dlobError } = useDlobStats(symbol);
|
||||||
const { l2: dlobL2, connected: dlobL2Connected, error: dlobL2Error } = useDlobL2(symbol, { levels: 14 });
|
const { l2: dlobL2, connected: dlobL2Connected, error: dlobL2Error } = useDlobL2(symbol, { levels: 10 });
|
||||||
const { rows: slippageRows, connected: slippageConnected, error: slippageError } = useDlobSlippage(symbol);
|
const { rows: slippageRows, connected: slippageConnected, error: slippageError } = useDlobSlippage(symbol);
|
||||||
const { rows: depthBands, connected: depthBandsConnected, error: depthBandsError } = useDlobDepthBands(symbol);
|
const { rows: depthBands, connected: depthBandsConnected, error: depthBandsError } = useDlobDepthBands(symbol);
|
||||||
|
|
||||||
@@ -160,51 +188,88 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) {
|
|||||||
first && latest && first.close > 0 ? ((latest.close - first.close) / first.close) * 100 : null;
|
first && latest && first.close > 0 ? ((latest.close - first.close) / first.close) * 100 : null;
|
||||||
|
|
||||||
const orderbook = useMemo(() => {
|
const orderbook = useMemo(() => {
|
||||||
if (dlobL2) return { asks: dlobL2.asks, bids: dlobL2.bids, mid: dlobL2.mid as number | null };
|
if (dlobL2) {
|
||||||
if (!latest) return { asks: [], bids: [], mid: null as number | null };
|
return {
|
||||||
|
asks: dlobL2.asks,
|
||||||
|
bids: dlobL2.bids,
|
||||||
|
mid: dlobL2.mid as number | null,
|
||||||
|
bestBid: dlobL2.bestBid,
|
||||||
|
bestAsk: dlobL2.bestAsk,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!latest) return { asks: [], bids: [], mid: null as number | null, bestBid: null as number | null, bestAsk: null as number | null };
|
||||||
const mid = latest.close;
|
const mid = latest.close;
|
||||||
const step = Math.max(mid * 0.00018, 0.0001);
|
const step = Math.max(mid * 0.00018, 0.0001);
|
||||||
const levels = 14;
|
const levels = 10;
|
||||||
|
|
||||||
const asksRaw = Array.from({ length: levels }, (_, i) => ({
|
const asksRaw = Array.from({ length: levels }, (_, i) => ({
|
||||||
price: mid + (i + 1) * step,
|
price: mid + (i + 1) * step,
|
||||||
size: 0.1 + ((i * 7) % 15) * 0.1,
|
sizeBase: 0.1 + ((i * 7) % 15) * 0.1,
|
||||||
}));
|
}));
|
||||||
const bidsRaw = Array.from({ length: levels }, (_, i) => ({
|
const bidsRaw = Array.from({ length: levels }, (_, i) => ({
|
||||||
price: mid - (i + 1) * step,
|
price: mid - (i + 1) * step,
|
||||||
size: 0.1 + ((i * 5) % 15) * 0.1,
|
sizeBase: 0.1 + ((i * 5) % 15) * 0.1,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let askTotal = 0;
|
let askTotalBase = 0;
|
||||||
|
let askTotalUsd = 0;
|
||||||
const asks = asksRaw
|
const asks = asksRaw
|
||||||
.slice()
|
.slice()
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((r) => {
|
.map((r) => {
|
||||||
askTotal += r.size;
|
const sizeUsd = r.sizeBase * r.price;
|
||||||
return { ...r, total: askTotal };
|
askTotalBase += r.sizeBase;
|
||||||
|
askTotalUsd += sizeUsd;
|
||||||
|
return { ...r, sizeUsd, totalBase: askTotalBase, totalUsd: askTotalUsd };
|
||||||
});
|
});
|
||||||
|
|
||||||
let bidTotal = 0;
|
let bidTotalBase = 0;
|
||||||
|
let bidTotalUsd = 0;
|
||||||
const bids = bidsRaw.map((r) => {
|
const bids = bidsRaw.map((r) => {
|
||||||
bidTotal += r.size;
|
const sizeUsd = r.sizeBase * r.price;
|
||||||
return { ...r, total: bidTotal };
|
bidTotalBase += r.sizeBase;
|
||||||
|
bidTotalUsd += sizeUsd;
|
||||||
|
return { ...r, sizeUsd, totalBase: bidTotalBase, totalUsd: bidTotalUsd };
|
||||||
});
|
});
|
||||||
|
|
||||||
return { asks, bids, mid };
|
return { asks, bids, mid, bestBid: bidsRaw[0]?.price ?? null, bestAsk: asksRaw[0]?.price ?? null };
|
||||||
}, [dlobL2, latest]);
|
}, [dlobL2, latest]);
|
||||||
|
|
||||||
const maxAskTotal = useMemo(() => {
|
const maxAskTotal = useMemo(() => {
|
||||||
let max = 0;
|
let max = 0;
|
||||||
for (const r of orderbook.asks) max = Math.max(max, r.total || 0);
|
for (const r of orderbook.asks) max = Math.max(max, (r as any).totalUsd || 0);
|
||||||
return max;
|
return max;
|
||||||
}, [orderbook.asks]);
|
}, [orderbook.asks]);
|
||||||
|
|
||||||
const maxBidTotal = useMemo(() => {
|
const maxBidTotal = useMemo(() => {
|
||||||
let max = 0;
|
let max = 0;
|
||||||
for (const r of orderbook.bids) max = Math.max(max, r.total || 0);
|
for (const r of orderbook.bids) max = Math.max(max, (r as any).totalUsd || 0);
|
||||||
return max;
|
return max;
|
||||||
}, [orderbook.bids]);
|
}, [orderbook.bids]);
|
||||||
|
|
||||||
|
const maxAskSize = useMemo(() => {
|
||||||
|
let max = 0;
|
||||||
|
for (const r of orderbook.asks) max = Math.max(max, (r as any).sizeUsd || 0);
|
||||||
|
return max;
|
||||||
|
}, [orderbook.asks]);
|
||||||
|
|
||||||
|
const maxBidSize = useMemo(() => {
|
||||||
|
let max = 0;
|
||||||
|
for (const r of orderbook.bids) max = Math.max(max, (r as any).sizeUsd || 0);
|
||||||
|
return max;
|
||||||
|
}, [orderbook.bids]);
|
||||||
|
|
||||||
|
const liquidity = useMemo(() => {
|
||||||
|
const bid = orderbook.bids.length ? (orderbook.bids[orderbook.bids.length - 1] as any).totalUsd || 0 : 0;
|
||||||
|
const ask = orderbook.asks.length ? (orderbook.asks[0] as any).totalUsd || 0 : 0;
|
||||||
|
const bestBid = orderbook.bestBid;
|
||||||
|
const bestAsk = orderbook.bestAsk;
|
||||||
|
const spreadAbs = bestBid != null && bestAsk != null ? bestAsk - bestBid : null;
|
||||||
|
const spreadPct =
|
||||||
|
spreadAbs != null && orderbook.mid != null && orderbook.mid > 0 ? (spreadAbs / orderbook.mid) * 100 : null;
|
||||||
|
return { bidUsd: bid, askUsd: ask, spreadAbs, spreadPct };
|
||||||
|
}, [orderbook.asks, orderbook.bids, orderbook.bestAsk, orderbook.bestBid, orderbook.mid]);
|
||||||
|
|
||||||
const trades = useMemo(() => {
|
const trades = useMemo(() => {
|
||||||
const slice = candles.slice(-24).reverse();
|
const slice = candles.slice(-24).reverse();
|
||||||
return slice.map((c) => {
|
return slice.map((c) => {
|
||||||
@@ -428,19 +493,22 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) {
|
|||||||
<div className="orderbook">
|
<div className="orderbook">
|
||||||
<div className="orderbook__header">
|
<div className="orderbook__header">
|
||||||
<span>Price</span>
|
<span>Price</span>
|
||||||
<span className="orderbook__num">Size</span>
|
<span className="orderbook__num">Size (USD)</span>
|
||||||
<span className="orderbook__num">Total</span>
|
<span className="orderbook__num">Total (USD)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="orderbook__rows">
|
<div className="orderbook__rows">
|
||||||
{orderbook.asks.map((r) => (
|
{orderbook.asks.map((r) => (
|
||||||
<div
|
<div
|
||||||
key={`a-${r.price}`}
|
key={`a-${r.price}`}
|
||||||
className="orderbookRow orderbookRow--ask"
|
className="orderbookRow orderbookRow--ask"
|
||||||
style={orderbookBarStyle(maxAskTotal > 0 ? r.total / maxAskTotal : 0)}
|
style={orderbookRowBarStyle(
|
||||||
|
maxAskTotal > 0 ? (r as any).totalUsd / maxAskTotal : 0,
|
||||||
|
maxAskSize > 0 ? (r as any).sizeUsd / maxAskSize : 0
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<span className="orderbookRow__price">{formatQty(r.price, 3)}</span>
|
<span className="orderbookRow__price">{formatQty(r.price, 3)}</span>
|
||||||
<span className="orderbookRow__num">{formatQty(r.size, 2)}</span>
|
<span className="orderbookRow__num">{formatCompact((r as any).sizeUsd)}</span>
|
||||||
<span className="orderbookRow__num">{formatQty(r.total, 2)}</span>
|
<span className="orderbookRow__num">{formatCompact((r as any).totalUsd)}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="orderbookMid">
|
<div className="orderbookMid">
|
||||||
@@ -451,13 +519,37 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) {
|
|||||||
<div
|
<div
|
||||||
key={`b-${r.price}`}
|
key={`b-${r.price}`}
|
||||||
className="orderbookRow orderbookRow--bid"
|
className="orderbookRow orderbookRow--bid"
|
||||||
style={orderbookBarStyle(maxBidTotal > 0 ? r.total / maxBidTotal : 0)}
|
style={orderbookRowBarStyle(
|
||||||
|
maxBidTotal > 0 ? (r as any).totalUsd / maxBidTotal : 0,
|
||||||
|
maxBidSize > 0 ? (r as any).sizeUsd / maxBidSize : 0
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<span className="orderbookRow__price">{formatQty(r.price, 3)}</span>
|
<span className="orderbookRow__price">{formatQty(r.price, 3)}</span>
|
||||||
<span className="orderbookRow__num">{formatQty(r.size, 2)}</span>
|
<span className="orderbookRow__num">{formatCompact((r as any).sizeUsd)}</span>
|
||||||
<span className="orderbookRow__num">{formatQty(r.total, 2)}</span>
|
<span className="orderbookRow__num">{formatCompact((r as any).totalUsd)}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
<div className="orderbookMeta">
|
||||||
|
<div className="orderbookMeta__row">
|
||||||
|
<span className="muted">Spread</span>
|
||||||
|
<span className="orderbookMeta__val">
|
||||||
|
{liquidity.spreadAbs == null || liquidity.spreadPct == null
|
||||||
|
? '—'
|
||||||
|
: `${formatUsd(liquidity.spreadAbs)} (${liquidity.spreadPct.toFixed(3)}%)`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="orderbookMeta__row">
|
||||||
|
<span className="muted">Liquidity (L1–L10)</span>
|
||||||
|
<span className="orderbookMeta__val">
|
||||||
|
<span className="pos">{formatUsd(liquidity.bidUsd)}</span> <span className="muted">/</span>{' '}
|
||||||
|
<span className="neg">{formatUsd(liquidity.askUsd)}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="liquidityBar" style={liquidityStyle(liquidity.bidUsd, liquidity.askUsd)}>
|
||||||
|
<div className="liquidityBar__bid" />
|
||||||
|
<div className="liquidityBar__ask" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type Props = {
|
|||||||
onToggleFullscreen: () => void;
|
onToggleFullscreen: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeframes = ['5s', '15s', '30s', '1m', '5m', '15m', '1h', '4h', '1D'] as const;
|
const timeframes = ['3s', '5s', '15s', '30s', '1m', '5m', '15m', '1h', '4h', '1D'] as const;
|
||||||
|
|
||||||
export default function ChartToolbar({
|
export default function ChartToolbar({
|
||||||
timeframe,
|
timeframe,
|
||||||
|
|||||||
@@ -639,7 +639,7 @@ export default function TradingChart({
|
|||||||
const buildSlicesPrimitive = new BuildSlicesPrimitive();
|
const buildSlicesPrimitive = new BuildSlicesPrimitive();
|
||||||
volumeSeries.attachPrimitive(buildSlicesPrimitive);
|
volumeSeries.attachPrimitive(buildSlicesPrimitive);
|
||||||
buildSlicesPrimitiveRef.current = buildSlicesPrimitive;
|
buildSlicesPrimitiveRef.current = buildSlicesPrimitive;
|
||||||
buildSlicesPrimitive.setEnabled(!showBuildRef.current);
|
buildSlicesPrimitive.setEnabled(showBuildRef.current);
|
||||||
|
|
||||||
const buildHoverSeries = chart.addSeries(LineSeries, {
|
const buildHoverSeries = chart.addSeries(LineSeries, {
|
||||||
color: BUILD_FLAT_COLOR,
|
color: BUILD_FLAT_COLOR,
|
||||||
@@ -1125,7 +1125,7 @@ export default function TradingChart({
|
|||||||
|
|
||||||
const buildPrimitive = buildSlicesPrimitiveRef.current;
|
const buildPrimitive = buildSlicesPrimitiveRef.current;
|
||||||
buildPrimitive?.setData({ candles, bucketSeconds: bs, samples: map });
|
buildPrimitive?.setData({ candles, bucketSeconds: bs, samples: map });
|
||||||
buildPrimitive?.setEnabled(!showBuild);
|
buildPrimitive?.setEnabled(showBuild);
|
||||||
|
|
||||||
if (showBuild) {
|
if (showBuild) {
|
||||||
const hoverTime = hoverCandleTime;
|
const hoverTime = hoverCandleTime;
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { subscribeGraphqlWs } from '../../lib/graphqlWs';
|
|||||||
|
|
||||||
export type OrderbookRow = {
|
export type OrderbookRow = {
|
||||||
price: number;
|
price: number;
|
||||||
size: number;
|
sizeBase: number;
|
||||||
total: number;
|
sizeUsd: number;
|
||||||
|
totalBase: number;
|
||||||
|
totalUsd: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DlobL2 = {
|
export type DlobL2 = {
|
||||||
@@ -66,11 +68,22 @@ function parseLevels(raw: unknown, pricePrecision: number, basePrecision: number
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function withTotals(levels: Array<{ price: number; size: number }>): OrderbookRow[] {
|
function withTotals(levels: Array<{ price: number; sizeBase: number }>): OrderbookRow[] {
|
||||||
let total = 0;
|
let totalBase = 0;
|
||||||
|
let totalUsd = 0;
|
||||||
|
|
||||||
return levels.map((l) => {
|
return levels.map((l) => {
|
||||||
total += l.size;
|
const sizeUsd = l.sizeBase * l.price;
|
||||||
return { ...l, total };
|
totalBase += l.sizeBase;
|
||||||
|
totalUsd += sizeUsd;
|
||||||
|
|
||||||
|
return {
|
||||||
|
price: l.price,
|
||||||
|
sizeBase: l.sizeBase,
|
||||||
|
sizeUsd,
|
||||||
|
totalBase,
|
||||||
|
totalUsd,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,14 +142,25 @@ export function useDlobL2(
|
|||||||
const row = data?.dlob_l2_latest?.[0];
|
const row = data?.dlob_l2_latest?.[0];
|
||||||
if (!row?.market_name) return;
|
if (!row?.market_name) return;
|
||||||
|
|
||||||
const bidsRaw = parseLevels(row.bids, pricePrecision, basePrecision).slice(0, levels);
|
const bidsSorted = parseLevels(row.bids, pricePrecision, basePrecision)
|
||||||
const asksRaw = parseLevels(row.asks, pricePrecision, basePrecision).slice(0, levels);
|
.slice()
|
||||||
|
.sort((a, b) => b.price - a.price)
|
||||||
|
.slice(0, levels)
|
||||||
|
.map((l) => ({ price: l.price, sizeBase: l.size }));
|
||||||
|
|
||||||
const bids = withTotals(bidsRaw);
|
const asksSorted = parseLevels(row.asks, pricePrecision, basePrecision)
|
||||||
const asks = withTotals(asksRaw).slice().reverse();
|
.slice()
|
||||||
|
.sort((a, b) => a.price - b.price)
|
||||||
|
.slice(0, levels)
|
||||||
|
.map((l) => ({ price: l.price, sizeBase: l.size }));
|
||||||
|
|
||||||
const bestBid = bidsRaw.length ? bidsRaw[0].price : null;
|
// We compute totals from best -> worse.
|
||||||
const bestAsk = asksRaw.length ? asksRaw[0].price : null;
|
// For UI we display asks with best ask closest to mid (at the bottom), so we reverse.
|
||||||
|
const bids = withTotals(bidsSorted);
|
||||||
|
const asks = withTotals(asksSorted).slice().reverse();
|
||||||
|
|
||||||
|
const bestBid = bidsSorted.length ? bidsSorted[0].price : null;
|
||||||
|
const bestAsk = asksSorted.length ? asksSorted[0].price : null;
|
||||||
const mid = bestBid != null && bestAsk != null ? (bestBid + bestAsk) / 2 : null;
|
const mid = bestBid != null && bestAsk != null ? (bestBid + bestAsk) / 2 : null;
|
||||||
|
|
||||||
setL2({
|
setL2({
|
||||||
@@ -156,4 +180,3 @@ export function useDlobL2(
|
|||||||
|
|
||||||
return { l2, connected, error };
|
return { l2, connected, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,8 +79,16 @@ export async function fetchChart(params: {
|
|||||||
flat: Number((c as any).flow.flat),
|
flat: Number((c as any).flow.flat),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
flowRows: Array.isArray((c as any)?.flowRows) ? (c as any).flowRows.map((x: any) => Number(x)) : undefined,
|
flowRows: Array.isArray((c as any)?.flowRows)
|
||||||
flowMoves: Array.isArray((c as any)?.flowMoves) ? (c as any).flowMoves.map((x: any) => Number(x)) : undefined,
|
? (c as any).flowRows.map((x: any) => Number(x))
|
||||||
|
: Array.isArray((c as any)?.flow_rows)
|
||||||
|
? (c as any).flow_rows.map((x: any) => Number(x))
|
||||||
|
: undefined,
|
||||||
|
flowMoves: Array.isArray((c as any)?.flowMoves)
|
||||||
|
? (c as any).flowMoves.map((x: any) => Number(x))
|
||||||
|
: Array.isArray((c as any)?.flow_moves)
|
||||||
|
? (c as any).flow_moves.map((x: any) => Number(x))
|
||||||
|
: undefined,
|
||||||
})),
|
})),
|
||||||
indicators: json.indicators || {},
|
indicators: json.indicators || {},
|
||||||
meta: { tf: String(json.tf || params.tf), bucketSeconds: Number(json.bucketSeconds || 0) },
|
meta: { tf: String(json.tf || params.tf), bucketSeconds: Number(json.bucketSeconds || 0) },
|
||||||
|
|||||||
@@ -1286,7 +1286,20 @@ body.chartFullscreen {
|
|||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
transform: scaleX(var(--ob-bar-scale, 0));
|
opacity: 0.35;
|
||||||
|
transform: scaleX(var(--ob-total-scale, 0));
|
||||||
|
transition: transform 220ms ease-out;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orderbookRow::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
transform: scaleX(var(--ob-level-scale, 0));
|
||||||
transition: transform 220ms ease-out;
|
transition: transform 220ms ease-out;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
@@ -1311,6 +1324,26 @@ body.chartFullscreen {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.orderbookRow--ask::after {
|
||||||
|
transform-origin: left center;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(239, 68, 68, 0.36) 0%,
|
||||||
|
rgba(239, 68, 68, 0.12) 55%,
|
||||||
|
rgba(239, 68, 68, 0) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orderbookRow--bid::after {
|
||||||
|
transform-origin: right center;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(34, 197, 94, 0) 0%,
|
||||||
|
rgba(34, 197, 94, 0.12) 45%,
|
||||||
|
rgba(34, 197, 94, 0.36) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.orderbookRow__num {
|
.orderbookRow__num {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
color: rgba(230, 233, 239, 0.75);
|
color: rgba(230, 233, 239, 0.75);
|
||||||
@@ -1344,6 +1377,69 @@ body.chartFullscreen {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.orderbookMeta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 10px 2px 2px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orderbookMeta__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orderbookMeta__val {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.liquidityBar {
|
||||||
|
position: relative;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
overflow: hidden;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.liquidityBar::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 1px;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
transform: translateX(-0.5px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.liquidityBar__bid,
|
||||||
|
.liquidityBar__ask {
|
||||||
|
height: 100%;
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: center;
|
||||||
|
transition: transform 180ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.liquidityBar__bid {
|
||||||
|
background: linear-gradient(90deg, rgba(34, 197, 94, 0.0) 0%, rgba(34, 197, 94, 0.35) 55%, rgba(34, 197, 94, 0.85) 100%);
|
||||||
|
transform-origin: right center;
|
||||||
|
transform: scaleX(var(--liq-bid, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.liquidityBar__ask {
|
||||||
|
background: linear-gradient(90deg, rgba(239, 68, 68, 0.85) 0%, rgba(239, 68, 68, 0.35) 45%, rgba(239, 68, 68, 0.0) 100%);
|
||||||
|
transform-origin: left center;
|
||||||
|
transform: scaleX(var(--liq-ask, 0));
|
||||||
|
}
|
||||||
|
|
||||||
.trades {
|
.trades {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -2,6 +2,21 @@
|
|||||||
|
|
||||||
Ten dokument opisuje rolę serwisów “DLOB” uruchomionych w namespace `trade-staging` oraz ich przepływ danych.
|
Ten dokument opisuje rolę serwisów “DLOB” uruchomionych w namespace `trade-staging` oraz ich przepływ danych.
|
||||||
|
|
||||||
|
## Czy `dlob-worker` pracuje na VPS?
|
||||||
|
|
||||||
|
Tak — wszystkie serwisy wymienione niżej działają **na VPS** jako Deploymenty w klastrze k3s, w namespace `trade-staging`.
|
||||||
|
|
||||||
|
## Czy na VPS jest GraphQL/WS dla stats i orderbook?
|
||||||
|
|
||||||
|
Tak — **GraphQL wystawia Hasura** (na VPS w k3s), a nie `dlob-server`.
|
||||||
|
|
||||||
|
- Dane L2 i liczone statsy są zapisane do Postgresa jako tabele `dlob_*_latest` i są dostępne przez Hasurę jako GraphQL (query + subscriptions).
|
||||||
|
- Z zewnątrz korzystamy przez frontend (proxy) pod:
|
||||||
|
- HTTP: `https://trade.rv32i.pl/graphql`
|
||||||
|
- WS: `wss://trade.rv32i.pl/graphql` (subskrypcje, protokół `graphql-ws`)
|
||||||
|
|
||||||
|
`dlob-server` wystawia **REST** (np. `/l2`, `/l3`) w klastrze; to jest źródło danych dla workerów albo do debugowania.
|
||||||
|
|
||||||
## TL;DR: kto co robi
|
## TL;DR: kto co robi
|
||||||
|
|
||||||
### `dlob-worker`
|
### `dlob-worker`
|
||||||
@@ -70,6 +85,12 @@ W tym stacku:
|
|||||||
- `base` = instrument bazowy (np. SOL),
|
- `base` = instrument bazowy (np. SOL),
|
||||||
- `quote` = waluta wyceny (często USD).
|
- `quote` = waluta wyceny (często USD).
|
||||||
|
|
||||||
|
## Kolory w UI (visualizer)
|
||||||
|
|
||||||
|
- `bid` / “buy side” = zielony (`.pos`, `#22c55e`)
|
||||||
|
- `ask` / “sell side” = czerwony (`.neg`, `#ef4444`)
|
||||||
|
- “flat” / brak zmiany = niebieski (`#60a5fa`) — używany m.in. w “brick stack” pod świecami
|
||||||
|
|
||||||
### Jednostki i skróty
|
### Jednostki i skróty
|
||||||
|
|
||||||
- **bps (basis points)**: 1 bps = 0.01% = `0.0001`. Np. 25 bps = 0.25%.
|
- **bps (basis points)**: 1 bps = 0.01% = `0.0001`. Np. 25 bps = 0.25%.
|
||||||
|
|||||||
Reference in New Issue
Block a user