SPB Git

Repository Public

Book.tsx 8.3 KB 102 lines tsx

1/**2 * Order book (depth bars, spread in basis points, grouping), recent trades, and — for GMX, which has no order book — the3 * liquidity / price-impact / funding-and-borrowing panel. Each component subscribes to its own slice of the market store.4 */5import { memo, useContext, useMemo, useState } from 'react';6import { I18n, useT } from '../i18n';7import { Notice } from '../ui';8import { Row, timeOf } from './bits';9import { compactUsd, D, dec, dpOf, fmt, naturalStep, pct, priceDp } from './dec';10import { groupBook, type BookRow } from './reducers';11import { useSlice, type MarketStore } from './store';12import type { Instrument } from './types';1314const MULTS = [1, 2, 5, 10, 50, 100];1516const Level = memo(function Level({ r, side, pdp, sdp, locale }: { r: BookRow; side: 'bid' | 'ask'; pdp: number; sdp: number; locale: string }) {17  return <div className="relative grid h-[22px] grid-cols-[1fr_1fr_1fr] items-center gap-2 px-2 text-[12.5px] leading-none">18    <span className={`absolute inset-y-px right-0 rounded-sm ${side === 'bid' ? 'bg-emerald/14' : 'bg-danger/12'}`} style={{ width: `${(r.depth * 100).toFixed(1)}%` }} aria-hidden="true" />19    <span className={`relative font-medium ${side === 'bid' ? 'text-emerald' : 'text-danger'}`}>{fmt(r.price, { dp: pdp, locale })}</span>20    <span className="relative text-right">{fmt(r.size, { dp: sdp, locale })}</span>21    <span className="relative text-right text-mute">{fmt(r.total, { dp: sdp, locale })}</span>22  </div>;23});2425export function BookPanel({ store, instrument, rows }: { store: MarketStore; instrument: Instrument; rows: number }) {26  const t = useT(); const { locale } = useContext(I18n);27  const book = useSlice(store, 'book');28  const [mult, setMult] = useState(1);29  const base = useMemo(() => (book ? naturalStep([book.bids, book.asks], instrument.tickSize) : instrument.tickSize), [book === null, instrument.id]);   // eslint-disable-line react-hooks/exhaustive-deps30  const step = useMemo(() => { const b = dec(base); return b ? D.str(D.mul(b, D.from(BigInt(mult)))) : instrument.tickSize; }, [base, mult, instrument.tickSize]);31  const v = useMemo(() => groupBook(book, step, rows), [book, step, rows]);32  const pdp = Math.min(dpOf(step), priceDp(instrument.tickSize, v.mid)), sdp = Math.min(dpOf(instrument.lotSize), 4);33  if (!book) return <p className="py-10 text-center text-[13px] text-mute">{t('fxLoading')}</p>;34  return (35    <div data-testid="book">36      <div className="mb-2 flex items-center justify-between gap-2">37        <label className="flex items-center gap-2 text-[12.5px] text-mute">{t('fxGrouping')}38          <select aria-label={t('fxGrouping')} value={mult} onChange={(e) => setMult(Number(e.target.value))} className="min-h-11 rounded-xl border border-line bg-surface px-2.5 text-[14px] text-ink">39            {MULTS.map((m) => { const b = dec(base); return <option key={m} value={m}>{b ? fmt(D.mul(b, D.from(BigInt(m))), { locale, trim: true }) : m}</option>; })}</select></label>40        <span className="text-[11.5px] text-mute">{t('fxBookAge', { time: timeOf(book.ts, locale) })}</span>41      </div>42      <div className="grid grid-cols-[1fr_1fr_1fr] gap-2 px-2 pb-1 text-[11px] text-mute"><span>{t('fxPrice')} ({instrument.quote})</span><span className="text-right">{t('fxSize')} ({instrument.base})</span><span className="text-right">{t('fxCumul')}</span></div>43      <div role="group" aria-label={t('fxAsks')}>44        <div className="px-2 text-[10.5px] font-semibold uppercase tracking-wide text-danger">▼ {t('fxAsks')}</div>45        {[...v.asks].reverse().map((r) => <Level key={r.price} r={r} side="ask" pdp={pdp} sdp={sdp} locale={locale} />)}46      </div>47      <div className="my-1 flex items-center justify-between rounded-lg bg-elev px-2 py-1.5 text-[12.5px]" data-testid="spread">48        <span className="text-mute">{t('fxSpread')}</span>49        <span className="font-semibold">{v.spread ? `${fmt(v.spread, { locale, trim: true })} · ${fmt(v.spreadBps, { dp: 2, locale })} ${t('fxBps')}` : '—'}</span>50      </div>51      <div role="group" aria-label={t('fxBids')}>52        {v.bids.map((r) => <Level key={r.price} r={r} side="bid" pdp={pdp} sdp={sdp} locale={locale} />)}53        <div className="px-2 pt-0.5 text-[10.5px] font-semibold uppercase tracking-wide text-emerald">▲ {t('fxBids')}</div>54      </div>55    </div>56  );57}5859export function TradesPanel({ store, instrument, rows }: { store: MarketStore; instrument: Instrument; rows: number }) {60  const t = useT(); const { locale } = useContext(I18n);61  const trades = useSlice(store, 'trades');62  const pdp = priceDp(instrument.tickSize, trades[0]?.price), sdp = Math.min(dpOf(instrument.lotSize), 5);63  if (!trades.length) return <p className="py-8 text-center text-[13px] text-mute">{t('fxNoTrades')}</p>;64  return <div data-testid="trades">65    <div className="grid grid-cols-[auto_1fr_1fr] gap-3 px-2 pb-1 text-[11px] text-mute"><span className="w-[58px]">{t('fxTime')}</span><span className="text-right">{t('fxPrice')}</span><span className="text-right">{t('fxSize')}</span></div>66    {trades.slice(0, rows).map((x) => <div key={x.id} className="grid h-[22px] grid-cols-[auto_1fr_1fr] items-center gap-3 px-2 text-[12.5px] leading-none">67      <span className="w-[58px] text-mute">{timeOf(x.ts, locale)}</span>68      <span className={`text-right font-medium ${x.side === 'buy' ? 'text-emerald' : 'text-danger'}`}><span aria-hidden="true">{x.side === 'buy' ? '▲ ' : '▼ '}</span><span className="sr-only">{t(x.side === 'buy' ? 'fxSide_buy' : 'fxSide_sell')} </span>{fmt(x.price, { dp: pdp, locale })}</span>69      <span className="text-right">{fmt(x.size, { dp: sdp, locale })}</span></div>)}70  </div>;71}7273/** GMX: what the venue has instead of a book */74export function LiquidityPanel({ store }: { store: MarketStore }) {75  const t = useT(); const { locale } = useContext(I18n);76  const { liquidity: l } = useSlice(store, 'meta');77  const rate = (x: string | undefined) => (x === undefined ? '—' : `${pct(x, 5, locale, true)} / h`);78  const two = (a: string | undefined, b: string | undefined, f: (x: string | undefined) => string) => <span className="block whitespace-nowrap"><span className="block"><span className="text-mute">{t('fxSide_long')}</span> {f(a)}</span><span className="block"><span className="text-mute">{t('fxSide_short')}</span> {f(b)}</span></span>;79  const money = (x: string | undefined) => (x === undefined ? '—' : compactUsd(x, locale));80  return (81    <div className="space-y-3" data-testid="gmx-liquidity">82      <Notice tone="info">{t('fxGmxNoBook')}</Notice>83      {!l ? <p className="py-6 text-center text-[13px] text-mute">{t('fxGmxNoLiquidity')}</p> : <>84        <div><h4 className="mb-1 text-[12.5px] font-semibold">{t('fxGmxLiquidity')}</h4>85          <Row label={t('fxOpenInterest')}>{two(l.openInterestLongUsd, l.openInterestShortUsd, money)}</Row>86          <Row label={t('fxGmxAvailable')}>{two(l.availableLiquidityLongUsd, l.availableLiquidityShortUsd, money)}</Row>87          <Row label={t('fxGmxCapacity')}>{two(l.capacityLongUsd, l.capacityShortUsd, money)}</Row>88          {l.oracleMin && l.oracleMax && <Row label={t('fxGmxOracle')}><span className="whitespace-nowrap">{fmt(l.oracleMin, { dp: 2, locale })} – {fmt(l.oracleMax, { dp: 2, locale })}</span></Row>}</div>89        <div><h4 className="mb-1 text-[12.5px] font-semibold">{t('fxGmxRates')}</h4>90          <Row label={t('fxFunding')}>{two(l.fundingRateLongPerHour, l.fundingRateShortPerHour, rate)}</Row>91          <Row label={t('fxGmxBorrow')}>{two(l.borrowingRateLongPerHour, l.borrowingRateShortPerHour, rate)}</Row>92          <Row label={t('fxGmxNet')}>{two(l.netRateLongPerHour, l.netRateShortPerHour, rate)}</Row>93          <p className="mt-1 text-[12px] leading-snug text-mute">{t('fxGmxRateSign')}</p></div>94        <div><h4 className="mb-1 text-[12.5px] font-semibold">{t('fxGmxImpact')}</h4>95          {l.positionFeeFactor && <Row label={t('fxGmxPositionFee')}>{pct(l.positionFeeFactor.balanceImproved, 3, locale)} / {pct(l.positionFeeFactor.balanceNotImproved, 3, locale)}</Row>}96          {l.priceImpact && <Row label={t('fxGmxImpactCap')}>{pct(l.priceImpact.maxFactorPositive, 2, locale, true)} / {l.priceImpact.maxFactorNegative ? `−${pct(l.priceImpact.maxFactorNegative, 2, locale)}` : '—'}</Row>}97          <p className="mt-1 text-[12px] leading-snug text-mute">{t('fxGmxImpactNote')}</p></div>98      </>}99    </div>100  );101}102
Files main
Menu

Theme