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.yaml
|
||||
|
||||
# Local secrets (never commit)
|
||||
pass/
|
||||
argo/pass
|
||||
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.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
|
||||
_tmp/
|
||||
apps/visualizer/dist/
|
||||
|
||||
@@ -32,7 +32,8 @@ function envString(name: string, fallback: string): string {
|
||||
|
||||
function formatUsd(v: number | null | undefined): string {
|
||||
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)}`;
|
||||
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 });
|
||||
}
|
||||
|
||||
function orderbookBarStyle(scale: number): CSSProperties {
|
||||
const s = Number.isFinite(scale) && scale > 0 ? Math.min(1, scale) : 0;
|
||||
return { ['--ob-bar-scale' as any]: s } as CSSProperties;
|
||||
function formatCompact(v: number | null | undefined): string {
|
||||
if (v == null || !Number.isFinite(v)) return '—';
|
||||
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 = {
|
||||
@@ -118,7 +146,7 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) {
|
||||
const [pollMs, setPollMs] = useLocalStorageState('trade.pollMs', envNumber('VITE_POLL_MS', 1000));
|
||||
const [limit, setLimit] = useLocalStorageState('trade.limit', envNumber('VITE_LIMIT', 300));
|
||||
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 [bottomTab, setBottomTab] = useLocalStorageState<
|
||||
'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 { 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: 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;
|
||||
|
||||
const orderbook = useMemo(() => {
|
||||
if (dlobL2) return { asks: dlobL2.asks, bids: dlobL2.bids, mid: dlobL2.mid as number | null };
|
||||
if (!latest) return { asks: [], bids: [], mid: null as number | null };
|
||||
if (dlobL2) {
|
||||
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 step = Math.max(mid * 0.00018, 0.0001);
|
||||
const levels = 14;
|
||||
const levels = 10;
|
||||
|
||||
const asksRaw = Array.from({ length: levels }, (_, i) => ({
|
||||
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) => ({
|
||||
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
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((r) => {
|
||||
askTotal += r.size;
|
||||
return { ...r, total: askTotal };
|
||||
const sizeUsd = r.sizeBase * r.price;
|
||||
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) => {
|
||||
bidTotal += r.size;
|
||||
return { ...r, total: bidTotal };
|
||||
const sizeUsd = r.sizeBase * r.price;
|
||||
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]);
|
||||
|
||||
const maxAskTotal = useMemo(() => {
|
||||
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;
|
||||
}, [orderbook.asks]);
|
||||
|
||||
const maxBidTotal = useMemo(() => {
|
||||
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;
|
||||
}, [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 slice = candles.slice(-24).reverse();
|
||||
return slice.map((c) => {
|
||||
@@ -428,19 +493,22 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) {
|
||||
<div className="orderbook">
|
||||
<div className="orderbook__header">
|
||||
<span>Price</span>
|
||||
<span className="orderbook__num">Size</span>
|
||||
<span className="orderbook__num">Total</span>
|
||||
<span className="orderbook__num">Size (USD)</span>
|
||||
<span className="orderbook__num">Total (USD)</span>
|
||||
</div>
|
||||
<div className="orderbook__rows">
|
||||
{orderbook.asks.map((r) => (
|
||||
<div
|
||||
key={`a-${r.price}`}
|
||||
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__num">{formatQty(r.size, 2)}</span>
|
||||
<span className="orderbookRow__num">{formatQty(r.total, 2)}</span>
|
||||
<span className="orderbookRow__num">{formatCompact((r as any).sizeUsd)}</span>
|
||||
<span className="orderbookRow__num">{formatCompact((r as any).totalUsd)}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="orderbookMid">
|
||||
@@ -451,13 +519,37 @@ function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) {
|
||||
<div
|
||||
key={`b-${r.price}`}
|
||||
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__num">{formatQty(r.size, 2)}</span>
|
||||
<span className="orderbookRow__num">{formatQty(r.total, 2)}</span>
|
||||
<span className="orderbookRow__num">{formatCompact((r as any).sizeUsd)}</span>
|
||||
<span className="orderbookRow__num">{formatCompact((r as any).totalUsd)}</span>
|
||||
</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>
|
||||
),
|
||||
|
||||
@@ -14,7 +14,7 @@ type Props = {
|
||||
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({
|
||||
timeframe,
|
||||
|
||||
@@ -639,7 +639,7 @@ export default function TradingChart({
|
||||
const buildSlicesPrimitive = new BuildSlicesPrimitive();
|
||||
volumeSeries.attachPrimitive(buildSlicesPrimitive);
|
||||
buildSlicesPrimitiveRef.current = buildSlicesPrimitive;
|
||||
buildSlicesPrimitive.setEnabled(!showBuildRef.current);
|
||||
buildSlicesPrimitive.setEnabled(showBuildRef.current);
|
||||
|
||||
const buildHoverSeries = chart.addSeries(LineSeries, {
|
||||
color: BUILD_FLAT_COLOR,
|
||||
@@ -1125,7 +1125,7 @@ export default function TradingChart({
|
||||
|
||||
const buildPrimitive = buildSlicesPrimitiveRef.current;
|
||||
buildPrimitive?.setData({ candles, bucketSeconds: bs, samples: map });
|
||||
buildPrimitive?.setEnabled(!showBuild);
|
||||
buildPrimitive?.setEnabled(showBuild);
|
||||
|
||||
if (showBuild) {
|
||||
const hoverTime = hoverCandleTime;
|
||||
|
||||
@@ -3,8 +3,10 @@ import { subscribeGraphqlWs } from '../../lib/graphqlWs';
|
||||
|
||||
export type OrderbookRow = {
|
||||
price: number;
|
||||
size: number;
|
||||
total: number;
|
||||
sizeBase: number;
|
||||
sizeUsd: number;
|
||||
totalBase: number;
|
||||
totalUsd: number;
|
||||
};
|
||||
|
||||
export type DlobL2 = {
|
||||
@@ -66,11 +68,22 @@ function parseLevels(raw: unknown, pricePrecision: number, basePrecision: number
|
||||
return out;
|
||||
}
|
||||
|
||||
function withTotals(levels: Array<{ price: number; size: number }>): OrderbookRow[] {
|
||||
let total = 0;
|
||||
function withTotals(levels: Array<{ price: number; sizeBase: number }>): OrderbookRow[] {
|
||||
let totalBase = 0;
|
||||
let totalUsd = 0;
|
||||
|
||||
return levels.map((l) => {
|
||||
total += l.size;
|
||||
return { ...l, total };
|
||||
const sizeUsd = l.sizeBase * l.price;
|
||||
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];
|
||||
if (!row?.market_name) return;
|
||||
|
||||
const bidsRaw = parseLevels(row.bids, pricePrecision, basePrecision).slice(0, levels);
|
||||
const asksRaw = parseLevels(row.asks, pricePrecision, basePrecision).slice(0, levels);
|
||||
const bidsSorted = parseLevels(row.bids, pricePrecision, basePrecision)
|
||||
.slice()
|
||||
.sort((a, b) => b.price - a.price)
|
||||
.slice(0, levels)
|
||||
.map((l) => ({ price: l.price, sizeBase: l.size }));
|
||||
|
||||
const bids = withTotals(bidsRaw);
|
||||
const asks = withTotals(asksRaw).slice().reverse();
|
||||
const asksSorted = parseLevels(row.asks, pricePrecision, basePrecision)
|
||||
.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;
|
||||
const bestAsk = asksRaw.length ? asksRaw[0].price : null;
|
||||
// We compute totals from best -> worse.
|
||||
// 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;
|
||||
|
||||
setL2({
|
||||
@@ -156,4 +180,3 @@ export function useDlobL2(
|
||||
|
||||
return { l2, connected, error };
|
||||
}
|
||||
|
||||
|
||||
@@ -79,8 +79,16 @@ export async function fetchChart(params: {
|
||||
flat: Number((c as any).flow.flat),
|
||||
}
|
||||
: undefined,
|
||||
flowRows: Array.isArray((c as any)?.flowRows) ? (c as any).flowRows.map((x: any) => Number(x)) : undefined,
|
||||
flowMoves: Array.isArray((c as any)?.flowMoves) ? (c as any).flowMoves.map((x: any) => Number(x)) : undefined,
|
||||
flowRows: Array.isArray((c as any)?.flowRows)
|
||||
? (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 || {},
|
||||
meta: { tf: String(json.tf || params.tf), bucketSeconds: Number(json.bucketSeconds || 0) },
|
||||
|
||||
@@ -1286,7 +1286,20 @@ body.chartFullscreen {
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
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;
|
||||
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 {
|
||||
text-align: right;
|
||||
color: rgba(230, 233, 239, 0.75);
|
||||
@@ -1344,6 +1377,69 @@ body.chartFullscreen {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
|
||||
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
|
||||
|
||||
### `dlob-worker`
|
||||
@@ -70,6 +85,12 @@ W tym stacku:
|
||||
- `base` = instrument bazowy (np. SOL),
|
||||
- `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
|
||||
|
||||
- **bps (basis points)**: 1 bps = 0.01% = `0.0001`. Np. 25 bps = 0.25%.
|
||||
|
||||
Reference in New Issue
Block a user