feat(visualizer): update chart flow and DLOB docs

This commit is contained in:
u1
2026-03-12 23:24:49 +01:00
parent f76045a9a5
commit 5a9d61d674
8 changed files with 296 additions and 47 deletions

9
.gitignore vendored
View File

@@ -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/

View File

@@ -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 (L1L10)</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>
), ),

View File

@@ -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,

View File

@@ -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;

View File

@@ -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 };
} }

View File

@@ -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) },

View File

@@ -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;

View File

@@ -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%.