Repository Public
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