627 lines
24 KiB
TypeScript
627 lines
24 KiB
TypeScript
import type { CSSProperties } from 'react';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import { useLocalStorageState } from './app/hooks/useLocalStorageState';
|
|
import AppShell from './layout/AppShell';
|
|
import ChartPanel from './features/chart/ChartPanel';
|
|
import { useChartData } from './features/chart/useChartData';
|
|
import TickerBar from './features/tickerbar/TickerBar';
|
|
import Card from './ui/Card';
|
|
import Tabs from './ui/Tabs';
|
|
import MarketHeader from './features/market/MarketHeader';
|
|
import Button from './ui/Button';
|
|
import TopNav from './layout/TopNav';
|
|
import AuthStatus from './layout/AuthStatus';
|
|
import LoginScreen from './layout/LoginScreen';
|
|
import { useDlobStats } from './features/market/useDlobStats';
|
|
import { useDlobL2 } from './features/market/useDlobL2';
|
|
import { useDlobSlippage } from './features/market/useDlobSlippage';
|
|
import { useDlobDepthBands } from './features/market/useDlobDepthBands';
|
|
import DlobDashboard from './features/market/DlobDashboard';
|
|
|
|
function envNumber(name: string, fallback: number): number {
|
|
const v = (import.meta as any).env?.[name];
|
|
if (v == null) return fallback;
|
|
const n = Number(v);
|
|
return Number.isFinite(n) ? n : fallback;
|
|
}
|
|
|
|
function envString(name: string, fallback: string): string {
|
|
const v = (import.meta as any).env?.[name];
|
|
return v == null ? fallback : String(v);
|
|
}
|
|
|
|
function formatUsd(v: number | null | undefined): string {
|
|
if (v == null || !Number.isFinite(v)) return '—';
|
|
if (v >= 1000) return `$${v.toFixed(0)}`;
|
|
if (v >= 1) return `$${v.toFixed(2)}`;
|
|
return `$${v.toPrecision(4)}`;
|
|
}
|
|
|
|
function formatQty(v: number | null | undefined, decimals: number): string {
|
|
if (v == null || !Number.isFinite(v)) return '—';
|
|
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;
|
|
}
|
|
|
|
type WhoamiResponse = {
|
|
ok?: boolean;
|
|
user?: string | null;
|
|
mode?: string;
|
|
};
|
|
|
|
export default function App() {
|
|
const [user, setUser] = useState<string | null>(null);
|
|
const [authLoading, setAuthLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
setAuthLoading(true);
|
|
fetch('/whoami', { cache: 'no-store' })
|
|
.then(async (res) => {
|
|
const json = (await res.json().catch(() => null)) as WhoamiResponse | null;
|
|
const u = typeof json?.user === 'string' ? json.user.trim() : '';
|
|
return u || null;
|
|
})
|
|
.then((u) => {
|
|
if (cancelled) return;
|
|
setUser(u);
|
|
})
|
|
.catch(() => {
|
|
if (cancelled) return;
|
|
setUser(null);
|
|
})
|
|
.finally(() => {
|
|
if (cancelled) return;
|
|
setAuthLoading(false);
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
const logout = async () => {
|
|
try {
|
|
await fetch('/auth/logout', { method: 'POST' });
|
|
} finally {
|
|
setUser(null);
|
|
}
|
|
};
|
|
|
|
if (authLoading) {
|
|
return (
|
|
<div className="loginScreen">
|
|
<div className="loginCard" role="status" aria-label="Ładowanie">
|
|
Ładowanie…
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!user) {
|
|
return <LoginScreen onLoggedIn={setUser} />;
|
|
}
|
|
|
|
return <TradeApp user={user} onLogout={() => void logout()} />;
|
|
}
|
|
|
|
function TradeApp({ user, onLogout }: { user: string; onLogout: () => void }) {
|
|
const markets = useMemo(() => ['PUMP-PERP', 'SOL-PERP', '1MBONK-PERP', 'BTC-PERP', 'ETH-PERP'], []);
|
|
|
|
const [symbol, setSymbol] = useLocalStorageState('trade.symbol', envString('VITE_SYMBOL', 'SOL-PERP'));
|
|
const [source, setSource] = useLocalStorageState('trade.source', envString('VITE_SOURCE', ''));
|
|
const [tf, setTf] = useLocalStorageState('trade.tf', envString('VITE_TF', '1m'));
|
|
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 [tab, setTab] = useLocalStorageState<'orderbook' | 'trades'>('trade.sidebarTab', 'orderbook');
|
|
const [bottomTab, setBottomTab] = useLocalStorageState<
|
|
'dlob' | 'positions' | 'orders' | 'balances' | 'orderHistory' | 'positionHistory'
|
|
>('trade.bottomTab', 'positions');
|
|
const [tradeSide, setTradeSide] = useLocalStorageState<'long' | 'short'>('trade.form.side', 'long');
|
|
const [tradeOrderType, setTradeOrderType] = useLocalStorageState<'market' | 'limit' | 'other'>(
|
|
'trade.form.type',
|
|
'market'
|
|
);
|
|
const [tradePrice, setTradePrice] = useLocalStorageState<number>('trade.form.price', 0);
|
|
const [tradeSize, setTradeSize] = useLocalStorageState<number>('trade.form.size', 0.1);
|
|
|
|
useEffect(() => {
|
|
if (symbol === 'BONK-PERP') {
|
|
setSymbol('1MBONK-PERP');
|
|
return;
|
|
}
|
|
if (!markets.includes(symbol)) {
|
|
setSymbol('SOL-PERP');
|
|
}
|
|
}, [markets, setSymbol, symbol]);
|
|
|
|
const { candles, indicators, meta, loading, error, refresh } = useChartData({
|
|
symbol,
|
|
source: source.trim() ? source : undefined,
|
|
tf,
|
|
limit,
|
|
pollMs,
|
|
});
|
|
|
|
const { stats: dlob, connected: dlobConnected, error: dlobError } = useDlobStats(symbol);
|
|
const { l2: dlobL2, connected: dlobL2Connected, error: dlobL2Error } = useDlobL2(symbol, { levels: 14 });
|
|
const { rows: slippageRows, connected: slippageConnected, error: slippageError } = useDlobSlippage(symbol);
|
|
const { rows: depthBands, connected: depthBandsConnected, error: depthBandsError } = useDlobDepthBands(symbol);
|
|
|
|
const latest = candles.length ? candles[candles.length - 1] : null;
|
|
const first = candles.length ? candles[0] : null;
|
|
const changePct =
|
|
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 };
|
|
const mid = latest.close;
|
|
const step = Math.max(mid * 0.00018, 0.0001);
|
|
const levels = 14;
|
|
|
|
const asksRaw = Array.from({ length: levels }, (_, i) => ({
|
|
price: mid + (i + 1) * step,
|
|
size: 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,
|
|
}));
|
|
|
|
let askTotal = 0;
|
|
const asks = asksRaw
|
|
.slice()
|
|
.reverse()
|
|
.map((r) => {
|
|
askTotal += r.size;
|
|
return { ...r, total: askTotal };
|
|
});
|
|
|
|
let bidTotal = 0;
|
|
const bids = bidsRaw.map((r) => {
|
|
bidTotal += r.size;
|
|
return { ...r, total: bidTotal };
|
|
});
|
|
|
|
return { asks, bids, mid };
|
|
}, [dlobL2, latest]);
|
|
|
|
const maxAskTotal = useMemo(() => {
|
|
let max = 0;
|
|
for (const r of orderbook.asks) max = Math.max(max, r.total || 0);
|
|
return max;
|
|
}, [orderbook.asks]);
|
|
|
|
const maxBidTotal = useMemo(() => {
|
|
let max = 0;
|
|
for (const r of orderbook.bids) max = Math.max(max, r.total || 0);
|
|
return max;
|
|
}, [orderbook.bids]);
|
|
|
|
const trades = useMemo(() => {
|
|
const slice = candles.slice(-24).reverse();
|
|
return slice.map((c) => {
|
|
const isBuy = c.close >= c.open;
|
|
return {
|
|
time: c.time,
|
|
price: c.close,
|
|
size: c.volume ?? null,
|
|
side: isBuy ? ('buy' as const) : ('sell' as const),
|
|
};
|
|
});
|
|
}, [candles]);
|
|
|
|
const effectiveTradePrice = useMemo(() => {
|
|
if (tradeOrderType === 'limit') return tradePrice;
|
|
return latest?.close ?? tradePrice;
|
|
}, [latest?.close, tradeOrderType, tradePrice]);
|
|
|
|
const orderValueUsd = useMemo(() => {
|
|
if (!Number.isFinite(tradeSize) || tradeSize <= 0) return null;
|
|
if (!Number.isFinite(effectiveTradePrice) || effectiveTradePrice <= 0) return null;
|
|
const v = effectiveTradePrice * tradeSize;
|
|
return Number.isFinite(v) && v > 0 ? v : null;
|
|
}, [effectiveTradePrice, tradeSize]);
|
|
|
|
const dynamicSlippage = useMemo(() => {
|
|
if (orderValueUsd == null) return null;
|
|
const side = tradeSide === 'short' ? 'sell' : 'buy';
|
|
const rows = slippageRows.filter((r) => r.side === side).slice();
|
|
rows.sort((a, b) => a.sizeUsd - b.sizeUsd);
|
|
if (!rows.length) return null;
|
|
const biggest = rows[rows.length - 1];
|
|
const match = rows.find((r) => r.sizeUsd >= orderValueUsd) || biggest;
|
|
return match;
|
|
}, [orderValueUsd, slippageRows, tradeSide]);
|
|
|
|
const topItems = useMemo(
|
|
() => [
|
|
{ key: 'BTC', label: 'BTC', changePct: 1.28, active: false },
|
|
{ key: 'SOL', label: 'SOL', changePct: 1.89, active: false },
|
|
],
|
|
[]
|
|
);
|
|
|
|
const stats = useMemo(() => {
|
|
return [
|
|
{
|
|
key: 'last',
|
|
label: 'Last',
|
|
value: formatUsd(latest?.close),
|
|
sub:
|
|
changePct == null ? (
|
|
'—'
|
|
) : (
|
|
<span className={changePct >= 0 ? 'pos' : 'neg'}>
|
|
{changePct >= 0 ? '+' : ''}
|
|
{changePct.toFixed(2)}%
|
|
</span>
|
|
),
|
|
},
|
|
{ key: 'oracle', label: 'Oracle', value: formatUsd(latest?.oracle ?? null) },
|
|
{ key: 'bid', label: 'Bid', value: formatUsd(dlob?.bestBid ?? null) },
|
|
{ key: 'ask', label: 'Ask', value: formatUsd(dlob?.bestAsk ?? null) },
|
|
{
|
|
key: 'spread',
|
|
label: 'Spread',
|
|
value: dlob?.spreadBps == null ? '—' : `${dlob.spreadBps.toFixed(1)} bps`,
|
|
sub: formatUsd(dlob?.spreadAbs ?? null),
|
|
},
|
|
{
|
|
key: 'dlob',
|
|
label: 'DLOB',
|
|
value: dlobConnected ? 'live' : '—',
|
|
sub: dlobError ? <span className="neg">{dlobError}</span> : dlob?.updatedAt || '—',
|
|
},
|
|
{
|
|
key: 'l2',
|
|
label: 'L2',
|
|
value: dlobL2Connected ? 'live' : '—',
|
|
sub: dlobL2Error ? <span className="neg">{dlobL2Error}</span> : dlobL2?.updatedAt || '—',
|
|
},
|
|
];
|
|
}, [latest?.close, latest?.oracle, changePct, dlob, dlobConnected, dlobError, dlobL2, dlobL2Connected, dlobL2Error]);
|
|
|
|
const seriesLabel = useMemo(() => `Candles: Mark (oracle overlay)`, []);
|
|
const seriesKey = useMemo(() => `${symbol}|${source}|${tf}`, [symbol, source, tf]);
|
|
const bucketSeconds = meta?.bucketSeconds ?? 60;
|
|
|
|
return (
|
|
<AppShell
|
|
header={<TopNav active="trade" rightEndSlot={<AuthStatus user={user} onLogout={onLogout} />} />}
|
|
top={<TickerBar items={topItems} />}
|
|
main={
|
|
<div className="tradeMain">
|
|
<Card
|
|
className="marketCard"
|
|
title={
|
|
<MarketHeader
|
|
market={symbol}
|
|
markets={markets}
|
|
onMarketChange={setSymbol}
|
|
leftSlot={
|
|
<label className="inlineField">
|
|
<span className="inlineField__label">Source</span>
|
|
<input
|
|
className="inlineField__input"
|
|
value={source}
|
|
onChange={(e) => setSource(e.target.value)}
|
|
placeholder="(any)"
|
|
/>
|
|
</label>
|
|
}
|
|
stats={stats}
|
|
rightSlot={
|
|
<div className="marketHeader__actions">
|
|
<label className="inlineField">
|
|
<span className="inlineField__label">Poll</span>
|
|
<input
|
|
className="inlineField__input"
|
|
value={pollMs}
|
|
type="number"
|
|
min={250}
|
|
step={250}
|
|
onChange={(e) => setPollMs(Number(e.target.value))}
|
|
/>
|
|
</label>
|
|
<label className="inlineField">
|
|
<span className="inlineField__label">Limit</span>
|
|
<input
|
|
className="inlineField__input"
|
|
value={limit}
|
|
type="number"
|
|
min={50}
|
|
step={50}
|
|
onChange={(e) => setLimit(Number(e.target.value))}
|
|
/>
|
|
</label>
|
|
<Button onClick={() => void refresh()} disabled={loading} type="button">
|
|
Refresh
|
|
</Button>
|
|
</div>
|
|
}
|
|
/>
|
|
}
|
|
>
|
|
{error ? <div className="uiError">{error}</div> : null}
|
|
</Card>
|
|
|
|
<ChartPanel
|
|
candles={candles}
|
|
indicators={indicators}
|
|
timeframe={tf}
|
|
bucketSeconds={bucketSeconds}
|
|
seriesKey={seriesKey}
|
|
onTimeframeChange={setTf}
|
|
showIndicators={showIndicators}
|
|
onToggleIndicators={() => setShowIndicators((v) => !v)}
|
|
showBuild={showBuild}
|
|
onToggleBuild={() => setShowBuild((v) => !v)}
|
|
seriesLabel={seriesLabel}
|
|
dlobQuotes={{ bid: dlob?.bestBid ?? null, ask: dlob?.bestAsk ?? null, mid: dlob?.mid ?? null }}
|
|
/>
|
|
|
|
<Card className="bottomCard">
|
|
<Tabs
|
|
items={[
|
|
{
|
|
id: 'dlob',
|
|
label: 'DLOB',
|
|
content: (
|
|
<DlobDashboard
|
|
market={symbol}
|
|
stats={dlob}
|
|
statsConnected={dlobConnected}
|
|
statsError={dlobError}
|
|
depthBands={depthBands}
|
|
depthBandsConnected={depthBandsConnected}
|
|
depthBandsError={depthBandsError}
|
|
slippageRows={slippageRows}
|
|
slippageConnected={slippageConnected}
|
|
slippageError={slippageError}
|
|
/>
|
|
),
|
|
},
|
|
{ id: 'positions', label: 'Positions', content: <div className="placeholder">Positions (next)</div> },
|
|
{ id: 'orders', label: 'Orders', content: <div className="placeholder">Orders (next)</div> },
|
|
{ id: 'balances', label: 'Balances', content: <div className="placeholder">Balances (next)</div> },
|
|
{
|
|
id: 'orderHistory',
|
|
label: 'Order History',
|
|
content: <div className="placeholder">Order history (next)</div>,
|
|
},
|
|
{
|
|
id: 'positionHistory',
|
|
label: 'Position History',
|
|
content: <div className="placeholder">Position history (next)</div>,
|
|
},
|
|
]}
|
|
activeId={bottomTab}
|
|
onChange={setBottomTab}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
}
|
|
sidebar={
|
|
<Card
|
|
className="orderbookCard"
|
|
title={
|
|
<div className="sideHead">
|
|
<div className="sideHead__title">Orderbook</div>
|
|
<div className="sideHead__subtitle">{loading ? 'loading…' : orderbook.mid != null ? formatUsd(orderbook.mid) : latest ? formatUsd(latest.close) : '—'}</div>
|
|
</div>
|
|
}
|
|
>
|
|
<Tabs
|
|
items={[
|
|
{
|
|
id: 'orderbook',
|
|
label: 'Orderbook',
|
|
content: (
|
|
<div className="orderbook">
|
|
<div className="orderbook__header">
|
|
<span>Price</span>
|
|
<span className="orderbook__num">Size</span>
|
|
<span className="orderbook__num">Total</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)}
|
|
>
|
|
<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>
|
|
</div>
|
|
))}
|
|
<div className="orderbookMid">
|
|
<span className="orderbookMid__price">{formatQty(orderbook.mid, 3)}</span>
|
|
<span className="orderbookMid__label">mid</span>
|
|
</div>
|
|
{orderbook.bids.map((r) => (
|
|
<div
|
|
key={`b-${r.price}`}
|
|
className="orderbookRow orderbookRow--bid"
|
|
style={orderbookBarStyle(maxBidTotal > 0 ? r.total / maxBidTotal : 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>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'trades',
|
|
label: 'Recent Trades',
|
|
content: (
|
|
<div className="trades">
|
|
<div className="trades__header">
|
|
<span>Time</span>
|
|
<span className="trades__num">Price</span>
|
|
<span className="trades__num">Size</span>
|
|
</div>
|
|
<div className="trades__rows">
|
|
{trades.map((t) => (
|
|
<div key={`${t.time}-${t.price}`} className="tradeRow">
|
|
<span className="tradeRow__time">
|
|
{new Date(t.time * 1000).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
|
|
</span>
|
|
<span className={['tradeRow__price', t.side === 'buy' ? 'pos' : 'neg'].join(' ')}>
|
|
{formatQty(t.price, 3)}
|
|
</span>
|
|
<span className="tradeRow__num">{t.size == null ? '—' : formatQty(t.size, 2)}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
]}
|
|
activeId={tab}
|
|
onChange={setTab}
|
|
/>
|
|
</Card>
|
|
}
|
|
rightbar={
|
|
<Card
|
|
className="tradeFormCard"
|
|
title={
|
|
<div className="tradeFormHead">
|
|
<div className="tradeFormHead__left">
|
|
<button className="chipBtn" type="button">
|
|
Cross
|
|
</button>
|
|
<button className="chipBtn" type="button">
|
|
20x
|
|
</button>
|
|
</div>
|
|
<div className="tradeFormHead__right">{symbol}</div>
|
|
</div>
|
|
}
|
|
>
|
|
<div className="tradeForm">
|
|
<div className="segmented">
|
|
<button
|
|
className={['segmented__btn', tradeSide === 'long' ? 'segmented__btn--activeLong' : ''].filter(Boolean).join(' ')}
|
|
type="button"
|
|
onClick={() => setTradeSide('long')}
|
|
>
|
|
Long
|
|
</button>
|
|
<button
|
|
className={['segmented__btn', tradeSide === 'short' ? 'segmented__btn--activeShort' : ''].filter(Boolean).join(' ')}
|
|
type="button"
|
|
onClick={() => setTradeSide('short')}
|
|
>
|
|
Short
|
|
</button>
|
|
</div>
|
|
|
|
<div className="tradeTabs">
|
|
<button
|
|
type="button"
|
|
className={['tradeTabs__btn', tradeOrderType === 'market' ? 'tradeTabs__btn--active' : ''].filter(Boolean).join(' ')}
|
|
onClick={() => setTradeOrderType('market')}
|
|
>
|
|
Market
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={['tradeTabs__btn', tradeOrderType === 'limit' ? 'tradeTabs__btn--active' : ''].filter(Boolean).join(' ')}
|
|
onClick={() => setTradeOrderType('limit')}
|
|
>
|
|
Limit
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={['tradeTabs__btn', tradeOrderType === 'other' ? 'tradeTabs__btn--active' : ''].filter(Boolean).join(' ')}
|
|
onClick={() => setTradeOrderType('other')}
|
|
>
|
|
Others
|
|
</button>
|
|
</div>
|
|
|
|
<div className="tradeFields">
|
|
<label className="formField">
|
|
<span className="formField__label">Price</span>
|
|
<input
|
|
className="formField__input"
|
|
value={tradeOrderType === 'market' ? '' : String(tradePrice)}
|
|
placeholder={tradeOrderType === 'market' ? formatQty(latest?.close ?? null, 3) : '0'}
|
|
disabled={tradeOrderType !== 'limit'}
|
|
onChange={(e) => setTradePrice(Number(e.target.value))}
|
|
inputMode="decimal"
|
|
/>
|
|
</label>
|
|
<label className="formField">
|
|
<span className="formField__label">Size</span>
|
|
<input
|
|
className="formField__input"
|
|
value={String(tradeSize)}
|
|
onChange={(e) => setTradeSize(Number(e.target.value))}
|
|
inputMode="decimal"
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
<Button className="tradeCta" type="button" disabled>
|
|
Enable Trading
|
|
</Button>
|
|
|
|
<div className="tradeMeta">
|
|
<div className="tradeMeta__row">
|
|
<span className="tradeMeta__label">Order Value</span>
|
|
<span className="tradeMeta__value">{effectiveTradePrice ? formatUsd(effectiveTradePrice * tradeSize) : '—'}</span>
|
|
</div>
|
|
<div className="tradeMeta__row">
|
|
<span className="tradeMeta__label">Slippage (Dynamic)</span>
|
|
<span className="tradeMeta__value">
|
|
{slippageError ? (
|
|
<span className="neg">{slippageError}</span>
|
|
) : dynamicSlippage?.impactBps == null ? (
|
|
slippageConnected ? (
|
|
'—'
|
|
) : (
|
|
'offline'
|
|
)
|
|
) : (
|
|
<>
|
|
{dynamicSlippage.impactBps.toFixed(1)} bps{' '}
|
|
<span className="muted">
|
|
({dynamicSlippage.sizeUsd.toLocaleString()} USD)
|
|
{dynamicSlippage.fillPct != null && dynamicSlippage.fillPct < 99.9
|
|
? `, ${dynamicSlippage.fillPct.toFixed(0)}% fill`
|
|
: ''}
|
|
</span>
|
|
</>
|
|
)}
|
|
</span>
|
|
</div>
|
|
<div className="tradeMeta__row">
|
|
<span className="tradeMeta__label">Margin Required</span>
|
|
<span className="tradeMeta__value">—</span>
|
|
</div>
|
|
<div className="tradeMeta__row">
|
|
<span className="tradeMeta__label">Liq. Price</span>
|
|
<span className="tradeMeta__value">—</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
}
|
|
/>
|
|
);
|
|
}
|