SPB Git

Repository Public

Router.tsx 26.7 KB 265 lines tsx

1/**2 * « Routeur (simulation) » — for one order intent, what each venue's execution would look like, side by side: eligibility, the3 * simulated fill, every cost with its inputs, margin, the risk view and the contract terms. Nothing here places an order.4 * Every number carries where it comes from: `venue` (read from the venue), `constant` (a documented fact) or `estimate`5 * (computed). The ranking is information — « le moins coûteux d'après ces estimations », never « le meilleur » — and the6 * only action is to open the PAPER order panel of Hyperliquid with the form pre-filled.7 */8import { useQuery } from '@tanstack/react-query';9import { useContext, useMemo, useState, type ReactNode } from 'react';10import { errText, I18n, useT, type Key } from '../i18n';11import { Btn, Card, Field, H, inputCls, Notice, Skeleton } from '../ui';12import { ageText, DataTable, PaperTag, Row, Seg, VENUE_NAME, VenueBadge, type Col } from './bits';13import { D, dec, fmt, parseInput } from './dec';14import { rapi, type CostComponent, type PaperPrefill, type Reason, type RouteAnswer, type RouteBody, type RoutedCandidate, type RouterSource, type TermKey } from './routerApi';15import type { DerivAccount, VenueId } from './types';1617const HORIZONS = ['1', '8', '24', '168', '720'] as const;18const TERMS: TermKey[] = ['structure', 'execution', 'collateral', 'settlement', 'quote', 'markPrice', 'fundingInterval', 'fundingRule', 'otherCarry', 'maxLeverage', 'marginModes', 'tick', 'lot', 'sizeUnit', 'minOrder', 'maintenance', 'liquidation', 'fees'];19interface Form { base: string; side: 'long' | 'short'; unit: 'usd' | 'base'; size: string; lev: string; horizon: string; type: 'market' | 'limit'; price: string; account: string }20const num = (s: string) => s.replace(/[^0-9.,\s ]/g, '');21const symbolOf = (id: string) => id.slice(id.indexOf(':') + 1);2223/** where a number comes from — always a word, the colour only repeats it */24export function SrcTag({ source }: { source: RouterSource | 'paper' }) {25  const t = useT();26  const cls = source === 'venue' ? 'border-emerald/35 bg-emerald/8 text-emerald' : source === 'constant' ? 'border-line bg-elev text-mute' : 'border-warn/40 bg-warn/10 text-warn';27  return <span className={`ml-1.5 inline-flex shrink-0 items-center rounded-md border px-1 py-px align-middle text-[10px] font-bold uppercase tracking-[0.05em] ${cls}`} title={t(`rxSrcTitle_${source}` as Key)} data-src={source}>{t(`rxSrc_${source}` as Key)}</span>;28}2930export function RouterView({ accounts, onOpenPaper }: { accounts: DerivAccount[]; onOpenPaper: (p: PaperPrefill, accountId: string | null) => void }) {31  const t = useT(); const { locale } = useContext(I18n);32  const [f, setF] = useState<Form>({ base: 'BTC', side: 'long', unit: 'usd', size: '25000', lev: '3', horizon: '24', type: 'market', price: '', account: '' });33  const [sent, setSent] = useState<RouteBody | null>(null);34  const set = (p: Partial<Form>) => setF((x) => ({ ...x, ...p }));35  const assets = useQuery({ queryKey: ['deriv', 'route', 'assets'], queryFn: rapi.assets, staleTime: 600_000 });36  const bases = useMemo(() => (assets.data ?? []).filter((a) => a.candidateVenues >= 1).slice(0, 60), [assets.data]);37  const asset = (assets.data ?? []).find((a) => a.base === f.base);38  const size = parseInput(f.size), lev = parseInput(f.lev), price = parseInput(f.price);39  const problem: Key | null = !size || !D.isPos(D.from(size)) ? (f.size.trim() ? 'rxErrSize' : null) : !lev || !D.isPos(D.from(lev)) ? 'rxErrLev' : f.type === 'limit' && (!price || !D.isPos(D.from(price))) ? (f.price.trim() ? 'rxErrPrice' : null) : null;40  const ready = !!size && !!lev && !problem && (f.type === 'market' || !!price);41  const body: RouteBody | null = ready ? { base: f.base, side: f.side, ...(f.unit === 'usd' ? { notionalUsd: size! } : { sizeBase: size! }), leverage: lev!, type: f.type, price: f.type === 'limit' ? price : null, reduceOnly: false,42    horizonHours: f.horizon, accountIds: f.account ? [f.account] : [] } : null;43  const q = useQuery({ queryKey: ['deriv', 'route', sent], enabled: !!sent, queryFn: () => rapi.route(sent!), staleTime: 0, gcTime: 0, retry: 0, refetchOnWindowFocus: false });44  const run = () => { if (!body) return; if (JSON.stringify(body) === JSON.stringify(sent)) void q.refetch(); else setSent(body); };45  const stale = !!sent && !!body && JSON.stringify(body) !== JSON.stringify(sent);46  const horizonLabel = (h: string) => t(`rxH_${h}` as Key);4748  return (49    <div className="space-y-4" data-testid="router-view">50      <Card>51        <H>{t('rxTitle')}</H>52        <div className="mb-3"><Notice tone="info"><b className="font-semibold">{t('rxSimOnly')}</b> {t('rxSimOnlyBody')}</Notice></div>53        <form className="space-y-3" onSubmit={(e) => { e.preventDefault(); run(); }} data-testid="router-form">54          <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">55            <Field label={t('rxAsset')} hint={asset ? asset.venues.map((v) => `${VENUE_NAME[v.venue] ?? v.name}${v.restriction ? ` (${t('rxUnavailableShort')})` : ''}`).join(' · ') : undefined}>56              <select className={inputCls} value={f.base} onChange={(e) => set({ base: e.target.value })} aria-label={t('rxAsset')}>57                {(bases.some((b) => b.base === f.base) ? bases : [{ base: f.base }, ...bases]).map((b) => <option key={b.base} value={b.base}>{b.base}</option>)}</select></Field>58            <div><span className="mb-1.5 block text-[13px] text-mute">{t('fxDirection')}</span>59              <Seg label={t('fxDirection')} value={f.side} onChange={(v) => set({ side: v })} options={[{ id: 'long', label: t('fxLong') }, { id: 'short', label: t('fxShort') }]} /></div>60            <Field label={t('fxSize')} hint={f.unit === 'usd' ? t('rxSizeUsdHint') : t('rxSizeBaseHint', { base: f.base })}>61              <div className="flex items-center gap-2"><input className={inputCls} inputMode="decimal" autoComplete="off" value={f.size} onChange={(e) => set({ size: num(e.target.value) })} aria-label={t('fxSize')} />62                <Seg size="sm" label={t('rxUnit')} value={f.unit} onChange={(v) => set({ unit: v })} options={[{ id: 'usd', label: 'USD' }, { id: 'base', label: f.base }]} className="shrink-0" /></div></Field>63            <Field label={t('fxLeverage')} hint={t('rxLevHint')}>64              <div className="flex items-center gap-2"><input className={inputCls} inputMode="decimal" autoComplete="off" value={f.lev} onChange={(e) => set({ lev: num(e.target.value) })} aria-label={t('fxLeverage')} /><span className="text-mute">×</span></div></Field>65          </div>66          <div className="grid grid-cols-1 gap-3 xl:grid-cols-2">67            <div><span className="mb-1.5 block text-[13px] text-mute">{t('rxHorizon')}</span>68              <Seg size="sm" label={t('rxHorizon')} value={f.horizon} onChange={(v) => set({ horizon: v })} options={HORIZONS.map((h) => ({ id: h, label: horizonLabel(h) }))} />69              <span className="mt-1.5 block text-xs text-mute">{t('rxHorizonHint')}</span></div>70            <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">71              <div><span className="mb-1.5 block text-[13px] text-mute">{t('fxOrderType')}</span>72                <Seg size="sm" label={t('fxOrderType')} value={f.type} onChange={(v) => set({ type: v })} options={[{ id: 'market', label: t('fxKind_market') }, { id: 'limit', label: t('fxKind_limit') }]} /></div>73              {f.type === 'limit' && <Field label={t('fxPrice')}><input className={inputCls} inputMode="decimal" autoComplete="off" value={f.price} onChange={(e) => set({ price: num(e.target.value) })} aria-label={t('fxPrice')} /></Field>}74            </div>75          </div>76          <Field label={t('rxAccount')} hint={t('rxAccountHint')}><select className={inputCls} value={f.account} onChange={(e) => set({ account: e.target.value })} aria-label={t('rxAccount')}>77            <option value="">{t('rxAccountNone')}</option>{accounts.map((a) => <option key={a.id} value={a.id}>{a.label} — {VENUE_NAME[a.venue]}</option>)}</select></Field>78          {problem && <Notice tone="warn">{t(problem)}</Notice>}79          <Btn full type="submit" disabled={!ready} busy={q.isFetching} data-testid="router-run">{t(sent && !stale ? 'rxRefresh' : 'rxRun')}</Btn>80        </form>81      </Card>8283      {!sent ? <Card><p className="py-6 text-center text-[13.5px] text-mute">{t('rxEmpty')}</p></Card>84        : q.isLoading ? <Skeleton h="h-64" />85        : q.isError ? <Notice tone="danger">{errText(t, q.error) || t('fxErrGeneric')}</Notice>86        : q.data ? <Results data={q.data} stale={stale} locale={locale} accounts={accounts} onOpenPaper={onOpenPaper} /> : null}87    </div>88  );89}9091function usdT(x: string | null | undefined, locale: string, sign = false): string {92  const d = dec(x);93  if (!d) return '—';94  const small = D.lt(D.abs(d), D.one) && !D.isZero(d);95  return `${fmt(d, { dp: small ? 4 : 2, locale, sign })} USD`;96}97const bpsT = (x: string | null | undefined, locale: string, sign = false) => (dec(x) ? `${fmt(x, { dp: 2, locale, sign })} ${locale === 'en' ? 'bps' : 'pb'}` : '—');98const priceT = (x: string | null | undefined, locale: string) => { const d = dec(x); if (!d) return '—'; const n = D.toNumber(d); return fmt(d, { dp: n >= 1000 ? 2 : n >= 1 ? 4 : 6, locale }); };99100function Results({ data, stale, locale, accounts, onOpenPaper }: { data: RouteAnswer; stale: boolean; locale: string; accounts: DerivAccount[]; onOpenPaper: (p: PaperPrefill, accountId: string | null) => void }) {101  const t = useT();102  const bps = locale === 'en' ? 'bps' : 'pb', fix = (s: string) => s;103  const rows = data.ranking.rows, first = rows[0];104  const name = (id: string) => { const c = data.candidates.find((x) => x.instrument === id); return c ? `${VENUE_NAME[c.venue]} · ${c.symbol}` : id; };105  const unknown = [...new Set(data.candidates.flatMap((c) => c.allIn?.unknown ?? []))];106  const restricted = data.candidates.filter((c) => c.eligibility.reasons.some((r) => r.code === 'venue_restricted'));107  const cols: Col<(typeof rows)[number]>[] = [108    { id: 'rank', label: t('rxRank'), head: true, cell: (r) => <span className="tabular-nums">{r.rank}</span> },109    { id: 'inst', label: t('fxInstrument'), head: true, cell: (r) => <span className="inline-flex flex-wrap items-center gap-1.5"><VenueBadge venue={r.venue} /><span className="font-semibold">{symbolOf(r.instrument)}</span></span> },110    { id: 'usd', label: t('rxAllIn'), num: true, cell: (r) => <>{usdT(r.allInUsd, locale)}<SrcTag source="estimate" /></> },111    { id: 'bps', label: t('rxAllInBps'), num: true, cell: (r) => fix(bpsT(r.allInBps, locale)) },112    { id: 'unc', label: t('rxUncertainty'), num: true, cell: (r) => `± ${usdT(r.uncertaintyUsd, locale)}` },113    { id: 'tie', label: t('rxTie'), cell: (r) => (r.tiedWith.length ? t('rxTieExact', { n: r.tiedWith.length }) : r.nearTieWith.length ? t('rxTieNear', { n: r.nearTieWith.length }) : t('rxTieNone')) },114    { id: 'trip', label: t('rxRoundTrip'), num: true, cell: (r) => (r.roundTripUsd ? <>{usdT(r.roundTripUsd, locale)}{r.roundTripRank !== null && r.roundTripRank !== r.rank ? <span className="block text-[11.5px] text-warn">{t('rxRoundTripRank', { n: r.roundTripRank })}</span> : null}</> : '—') },115    { id: 'now', label: t('rxExecutable'), cell: (r) => (r.executableNow ? <span className="inline-flex items-center gap-1.5">{t('rxExecPaper')}<PaperTag /></span> : <span className="text-mute">{t('rxExecNo')}</span>) },116  ];117  const diffCols: Col<(typeof data.differences.fields)[number]>[] = [118    { id: 'f', label: t('rxTerm'), head: true, cell: (d) => t(`rxTerm_${d.field}` as Key) },119    ...data.candidates.map((c): Col<(typeof data.differences.fields)[number]> => ({ id: c.instrument, label: <span className="inline-flex flex-wrap items-center gap-1"><VenueBadge venue={c.venue} /><span>{symbolOf(c.instrument)}</span></span>,120      cell: (d) => { const v = d.values.find((x) => x.instrument === c.instrument); return v ? <span className="block max-w-[300px] whitespace-normal text-[12.5px] leading-snug md:w-[230px]">{v.value}<SrcTag source={v.source} /></span> : '—'; } })),121  ];122123  return (124    <div className={`space-y-4 ${stale ? 'opacity-60' : ''}`} data-testid="router-results" data-stale={stale ? '1' : '0'}>125      {stale && <Notice tone="warn">{t('rxStale')}</Notice>}126      <Card>127        <H right={<span className="text-[12px] text-mute">{t('rxAsOf', { age: ageText(Date.now() - data.ts) })}</span>}>{t('rxRankingTitle')}</H>128        <div className="mb-3 space-y-2">129          {first ? <p className="text-[14.5px] leading-snug" data-testid="router-least"><b className="font-semibold">{t('rxLeast')}</b> {name(first.instrument)} — {usdT(first.allInUsd, locale)} ({fix(bpsT(first.allInBps, locale))}).{' '}130            {first.tiedWith.length ? t('rxLeastTied', { n: first.tiedWith.length }) : first.nearTieWith.length ? t('rxLeastNear', { n: first.nearTieWith.length }) : ''}</p>131            : <Notice tone="warn">{t('rxNoEligible')}</Notice>}132          <Notice tone="warn"><b className="font-semibold">{t('rxNotBest')}</b> {t('rxNotBestBody')}</Notice>133          {data.ranking.orderChangesWithExit && <Notice tone="warn">{t('rxOrderChanges')}</Notice>}134          {unknown.length > 0 && <Notice tone="warn">{t('rxUnknownCosts', { list: unknown.map((u) => t(`rxCost_${u}` as Key)).join(', ') })}</Notice>}135          {restricted.length > 0 && <Notice tone="info">{t('rxRestrictedNote', { venues: [...new Set(restricted.map((c) => VENUE_NAME[c.venue]))].join(', ') })}</Notice>}136          {data.errors.length > 0 && <Notice tone="warn">{t('fxVenueErrors', { venues: data.errors.map((e) => VENUE_NAME[e.venue as VenueId] ?? e.venue).join(', ') })}</Notice>}137        </div>138        <DataTable label={t('rxRankingTitle')} cols={cols} rows={rows} rowKey={(r) => r.instrument} empty={t('rxNoEligible')} />139        <p className="mt-3 text-[12px] leading-snug text-mute">{t('rxRankingNote', { bps })}</p>140      </Card>141142      <div className="grid grid-cols-1 gap-4 xl:grid-cols-2" data-testid="router-cards">143        {data.candidates.map((c) => <Candidate key={c.instrument} c={c} locale={locale} account={accounts.find((a) => a.venue === c.venue && a.kind === 'paper') ?? null} onOpenPaper={onOpenPaper} />)}144      </div>145146      <Card>147        <H>{t('rxDiffTitle')}</H>148        <div className="mb-3"><Notice tone="warn"><b className="font-semibold">{t('fxNotInterchangeable')}</b> {t('rxDiffBody', { n: data.candidates.length, base: data.intent.base.toUpperCase(), k: data.differences.fields.length })}</Notice></div>149        <DataTable label={t('rxDiffTitle')} cols={diffCols} rows={data.differences.fields} rowKey={(d) => d.field} empty={t('rxDiffEmpty')} />150        <ul className="mt-4 list-disc space-y-1.5 pl-5 text-[13px] leading-snug">{(['fxNever1', 'fxNever2', 'fxNever3', 'fxNever4'] as const).map((k) => <li key={k}>{t(k)}</li>)}</ul>151        <details className="mt-3"><summary className="flex min-h-11 cursor-pointer items-center text-[12.5px] font-medium text-blue">{t('rxEngineNotes', { n: data.warnings.length })}</summary>152          <ul className="list-disc space-y-1 pb-1 pl-5 text-[12.5px] leading-snug text-mute">{data.warnings.map((w) => <li key={w}>{w}</li>)}</ul></details>153      </Card>154    </div>155  );156}157158function Status({ c }: { c: RoutedCandidate }) {159  const t = useT();160  const restricted = c.eligibility.reasons.some((r) => r.code === 'venue_restricted');161  const [key, cls]: [Key, string] = restricted ? ['rxStRestricted', 'border-warn/40 bg-warn/10 text-warn'] : !c.eligibility.eligible ? ['rxStNotEligible', 'border-danger/40 bg-danger/10 text-danger']162    : c.eligibility.executableNow ? ['rxStPaper', 'border-emerald/35 bg-emerald/8 text-emerald'] : ['rxStSetup', 'border-line bg-elev text-ink'];163  return <span className={`inline-flex items-center rounded-full border px-2.5 py-1 text-[12px] font-semibold ${cls}`} data-testid="router-status" data-status={restricted ? 'restricted' : !c.eligibility.eligible ? 'not_eligible' : c.eligibility.executableNow ? 'paper' : 'setup'}>{t(key)}</span>;164}165function ReasonLine({ r }: { r: Reason }) {166  const t = useT();167  const k = `rxReason_${r.code}`, known = t(k as Key), text = known.startsWith('rxReason') || known === r.code.replace(/_/g, ' ') ? r.message : known;168  const tone = r.kind === 'blocker' ? 'text-danger' : r.kind === 'setup' ? 'text-warn' : 'text-mute';169  return <li className="text-[13px] leading-snug" data-reason={r.code}><span className={`font-semibold ${tone}`}>{t(`rxKind_${r.kind}` as Key)}</span> — {text}170    {text !== r.message && r.kind !== 'note' && <span className="mt-0.5 block break-words font-mono text-[11px] text-mute">{r.code} — {r.message}</span>}</li>;171}172function Sub({ title, children }: { title: string; children: ReactNode }) {173  return <section className="border-t border-line pt-3"><h3 className="mb-1.5 text-[12px] font-semibold uppercase tracking-wide text-mute">{title}</h3>{children}</section>;174}175function CostRow({ k, locale }: { k: CostComponent; locale: string }) {176  const t = useT(); const fix = (s: string) => s;177  return <li className="py-1.5" data-cost={k.id}>178    <details>179      <summary className="flex min-h-11 cursor-pointer list-none items-center justify-between gap-3 text-[13.5px]">180        <span className="min-w-0"><span className="font-medium">{t(`rxCost_${k.id}` as Key)}</span><span className="block text-[11.5px] text-mute">{t(`rxTiming_${k.timing}` as Key)}</span></span>181        <span className="shrink-0 text-right tabular-nums">{k.usd === null ? <span className="font-semibold text-warn">{t('rxUnknown')}</span> : <><span className="font-semibold">{usdT(k.usd, locale, true)}</span><SrcTag source="estimate" /><span className="block text-[11.5px] text-mute">{fix(bpsT(k.bps, locale, true))} · ± {usdT(k.uncertaintyUsd, locale)}</span></>}</span>182      </summary>183      <div className="space-y-1.5 pb-2 text-[12.5px] leading-snug text-mute">184        <p><b className="font-semibold text-ink">{t('rxBasis')}</b> {k.basis}</p>185        <p><b className="font-semibold text-ink">{t('rxUncertainty')}</b> {k.uncertainty}</p>186        {k.inputs.length > 0 && <ul className="space-y-0.5">{k.inputs.map((i) => <li key={i.name} className="break-words"><span className="text-ink">{i.name}</span> : {i.value ?? t('rxUnknown')}<SrcTag source={i.source} /></li>)}</ul>}187      </div>188    </details>189  </li>;190}191192function Candidate({ c, locale, account, onOpenPaper }: { c: RoutedCandidate; locale: string; account: DerivAccount | null; onOpenPaper: (p: PaperPrefill, accountId: string | null) => void }) {193  const t = useT(); const fix = (s: string) => s;194  const fl = c.fill, o = c.order, g = fl?.gmx ?? null, base = symbolOf(c.instrument).split(/[-/ ]/)[0]!;195  const restricted = c.eligibility.reasons.some((r) => r.code === 'venue_restricted');196  const canOpen = c.venue === 'hyperliquid' && c.eligibility.eligible && !!o && o.sizeUnit === 'base' && D.isPos(D.from(o.sizeBase)) && !!c.margin;197  const open = () => { if (!o || !c.margin) return; onOpenPaper({ instrument: c.instrument, side: o.side, kind: o.type, unit: 'base', size: o.sizeBase, lev: Math.max(1, Math.floor(Number(c.margin.leverage))), price: o.limitPrice ?? '', reduceOnly: o.reduceOnly }, account?.id ?? null); };198  return (199    <Card className={restricted ? '!border-warn/40' : ''}>200      <div data-testid="router-card" data-venue={c.venue} data-instrument={c.instrument} className="space-y-3">201        <div className="flex flex-wrap items-center gap-2"><VenueBadge venue={c.venue} /><h3 className="text-[16px] font-semibold">{c.symbol}</h3><Status c={c} /></div>202        {restricted && <Notice tone="warn"><b className="font-semibold">{t('rxRestrictedTitle', { venue: VENUE_NAME[c.venue] })}</b> {t('rxRestrictedBody')}</Notice>}203        <ul className="space-y-1.5" data-testid="router-reasons">{c.eligibility.reasons.filter((r) => !(restricted && r.code !== 'venue_restricted' && r.kind !== 'blocker')).map((r) => <ReasonLine key={r.code + r.message.slice(0, 20)} r={r} />)}</ul>204205        {fl && o && <Sub title={t('rxFillTitle')}>206          <Row label={t('fxSize')}>{o.sizeUnit === 'usd' ? usdT(o.sizeUsd, locale) : <>{fmt(o.sizeBase, { locale, trim: true })} {base}<span className="text-mute"> ≈ {usdT(o.sizeUsd, locale)}</span></>}<SrcTag source="estimate" /></Row>207          {o.rounding && <p className="text-[11.5px] leading-snug text-mute">{o.sizeUnit === 'usd' ? t('rxRoundUsd') : t('rxRoundLot')}</p>}208          <Row label={t(fl.method === 'order_book_walk' ? 'rxRefBook' : 'rxRefOracle')}>{priceT(fl.referencePrice, locale)}<SrcTag source="venue" /></Row>209          <Row label={t('rxAvgPrice')}>{priceT(fl.avgPrice, locale)}<SrcTag source="estimate" /></Row>210          {fl.method === 'order_book_walk' && <>211            <Row label={t('rxWorstPrice')}>{priceT(fl.worstPrice, locale)}<SrcTag source="venue" /></Row>212            <Row label={t('rxSlippage')}>{fix(bpsT(fl.slippageBps, locale, true))}<SrcTag source="estimate" /></Row>213            <Row label={t('rxLevels')}>{fl.levelsConsumed} / {fl.snapshotLevels}<span className="text-mute"> · {fmt(fl.depthConsumedPct, { dp: 2, locale })} %</span><SrcTag source="venue" /></Row>214            {dec(fl.unfilledBase) && D.isPos(D.from(fl.unfilledBase)) && <div className="mt-1.5"><Notice tone="danger"><b className="font-semibold">{t('rxBookEnds')}</b> {t('rxBookEndsBody', { left: fmt(fl.unfilledBase, { locale, trim: true }), base })}</Notice></div>}215            {dec(fl.restingBase) && D.isPos(D.from(fl.restingBase)) && <div className="mt-1.5"><Notice tone="warn">{t('rxResting', { size: fmt(fl.restingBase, { locale, trim: true }), base })}</Notice></div>}216          </>}217          {g && <>218            <Row label={t('rxOracleRange')}>{priceT(g.oracleMin, locale)} – {priceT(g.oracleMax, locale)}<SrcTag source="venue" /></Row>219            <Row label={t('rxImpact')}><span className={D.isNeg(D.from(g.impactCappedUsd)) ? 'text-danger' : ''}>{usdT(g.impactCappedUsd, locale, true)}</span>{g.capped ? ` (${t('rxCapped')})` : ''}<SrcTag source="estimate" /></Row>220            <Row label={t('rxImpactCaps')}>+{usdT(g.capPositiveUsd, locale)} / −{usdT(g.capNegativeUsd, locale)}<SrcTag source="venue" /></Row>221            <Row label={t('rxImbalance')}>{usdT(g.imbalanceBeforeUsd, locale)} → {usdT(g.imbalanceAfterUsd, locale)}<SrcTag source="estimate" /></Row>222            <Row label={t('rxCapacityUsed')}>{fmt(fl.depthConsumedPct, { dp: 3, locale })} %<SrcTag source="venue" /></Row>223            <p className="mt-1 text-[11.5px] leading-snug text-mute">{t('rxGmxImpactNote')} {t(`rxVirtual_${g.virtualInventory}` as Key)}</p>224          </>}225        </Sub>}226227        {c.costs.length > 0 && <Sub title={t('rxCostsTitle')}>228          <ul className="divide-y divide-line/70" data-testid="router-costs">{c.costs.map((k) => <CostRow key={k.id} k={k} locale={locale} />)}</ul>229          {c.allIn && <div className="mt-2 rounded-2xl bg-elev p-3" data-testid="router-allin">230            <div className="flex items-baseline justify-between gap-3"><span className="text-[13.5px] font-semibold">{t('rxAllInOver', { h: t(`rxH_${c.allIn.horizonHours}` as Key) })}</span>231              <span className="text-right text-[15px] font-semibold tabular-nums">{usdT(c.allIn.usd, locale, true)}<SrcTag source="estimate" /></span></div>232            <div className="mt-0.5 flex items-baseline justify-between gap-3 text-[12px] text-mute"><span>{c.allIn.complete ? t('rxComplete') : c.allIn.unknown.length ? t('rxIncomplete', { list: c.allIn.unknown.map((u) => t(`rxCost_${u}` as Key)).join(', ') }) : t('rxPartial')}</span>233              <span className="shrink-0 tabular-nums">{fix(bpsT(c.allIn.bps, locale, true))} · ± {usdT(c.allIn.uncertaintyUsd, locale)}</span></div>234            {c.exit && <p className="mt-2 border-t border-line pt-2 text-[12px] leading-snug text-mute">{t('rxExit', { exit: usdT(c.exit.usd, locale, true), trip: usdT(c.exit.roundTripUsd, locale), bps: fix(bpsT(c.exit.roundTripBps, locale)) })}</p>}235          </div>}236        </Sub>}237238        {c.margin && <Sub title={t('rxMarginTitle')}>239          <Row label={t('rxInitialMargin', { lev: fmt(c.margin.leverage, { locale, trim: true }) })}>{usdT(c.margin.initialMarginUsd, locale)}<SrcTag source="estimate" /></Row>240          <Row label={t('rxMaintenance')}>{c.margin.maintenanceFraction ? `${fmt(D.mul(D.from(c.margin.maintenanceFraction), D.from(100n)), { dp: 3, locale, trim: true })} %` : t('rxUnknown')}<SrcTag source={c.margin.maintenanceSource} /></Row>241          <Row label={t('rxLiq')}>{c.margin.liquidationPrice.value ? <>{priceT(c.margin.liquidationPrice.value, locale)}<span className="text-mute"> · {fix(bpsT(c.margin.liquidationDistanceBps, locale))}</span></> : t('rxUnknown')}<SrcTag source="estimate" /></Row>242          <details><summary className="flex min-h-11 cursor-pointer items-center text-[12.5px] font-medium text-blue">{t('rxMethod')}</summary>243            <p className="pb-1 text-[12.5px] leading-snug text-mute">{c.margin.maintenanceRule}</p><p className="pb-1 text-[12.5px] leading-snug text-mute">{c.margin.liquidationPrice.method}</p></details>244        </Sub>}245246        <Sub title={t('rxRiskTitle')}>247          <Row label={t('rxVenueLimits')}>{c.risk.venue.accepted ? <span className="text-emerald">{t('rxAccepted')}</span> : <span className="text-danger">{t('rxRefused')} — <span className="font-mono text-[12px]">{c.risk.venue.codes.join(', ')}</span></span>}</Row>248          {c.risk.accounts.length === 0 ? <p className="text-[12px] leading-snug text-mute">{t('rxNoAccountAsked')}</p>249            : c.risk.accounts.map((a) => <div key={a.accountId} data-testid="router-policy" data-accepted={String(a.accepted)}>250              <Row label={<span className="inline-flex items-center gap-1.5">{a.label}{a.kind === 'paper' && <PaperTag />}</span>}>{a.accepted === true ? <span className="text-emerald">{t('rxAccepted')}</span> : a.accepted === false ? <span className="text-danger">{t('rxRefused')} — <span className="font-mono text-[12px]">{a.code}</span></span> : <span className="text-mute">{t('rxNotAsked')}</span>}</Row>251              {a.accepted === false && a.code && <p className="text-[12px] leading-snug text-mute">{(() => { const s = t(`rk_${a.code}` as Key); return s.startsWith('rk ') || s === a.code!.replace(/_/g, ' ') ? a.reason : s; })()}</p>}252              {a.accepted === null && a.reason && <p className="text-[12px] leading-snug text-mute">{a.reason}</p>}</div>)}253        </Sub>254255        <details data-testid="router-terms"><summary className="flex min-h-11 cursor-pointer items-center text-[13px] font-semibold">{t('rxTermsTitle')}</summary>256          <dl className="space-y-2 pb-1">{TERMS.map((k) => <div key={k}><dt className="text-[11.5px] font-semibold uppercase tracking-wide text-mute">{t(`rxTerm_${k}` as Key)}</dt><dd className="text-[12.5px] leading-snug">{c.terms[k].value}<SrcTag source={c.terms[k].source} /></dd></div>)}</dl></details>257258        {c.venue === 'hyperliquid' && <div className="space-y-1.5 border-t border-line pt-3">259          <Btn full kind="soft" disabled={!canOpen} onClick={open} data-testid="router-open-paper">{t('rxOpenPaper')}</Btn>260          <p className="text-[12px] leading-snug text-mute">{t(canOpen ? 'rxOpenPaperHint' : 'rxOpenPaperOff')}</p></div>}261      </div>262    </Card>263  );264}265
Files main
Menu

Theme