Repository Public
1// Perp agents (V6): creation form and detail page. SIMULATION only — every figure here belongs to a derivatives paper account.2// Reuses the Futures terminal's building blocks (exact decimals, side / PnL tags, tables) and the agent screen's cards.3import { useQuery, useQueryClient } from '@tanstack/react-query';4import { useContext, useState } from 'react';5import { get, post } from '../api';6import type { Nav } from '../App';7import { PriceChart } from '../chart';8import { DataTable, PaperTag, Pnl, Row, Seg, SideTag, Stat, timeOf, VenueBadge, type Col } from '../derivatives/bits';9import { D, dec, fmt, usd } from '../derivatives/dec';10import type { Fill, FundingPayment, Order, Position, RiskPolicy } from '../derivatives/types';11import { errText, I18n, useT, type Key } from '../i18n';12import { Btn, Card, Field, H, Icon, inputCls, Notice, Skeleton, when } from '../ui';13import { AgentState } from './Agents';14import { AgentModeBand, GoLiveCard, LivePanel, PerpLiveState, Refusal } from './PerpAgentLive'; // v7 perp live: the real mode15import { ResearchPanel } from './Research'; // v7 research runtime1617interface PerpRisk { maxLeverage: number; maxGrossExposureX: number; maxPositionX: number; maxDailyLossPct: number; maxTotalLossPct: number; slippageBps: number; minTradePct: number }18interface VenueInst { symbol: string; id: string; maxLeverage: string; lotSize: string; mark: string | null; volume24h: string | null; eligible: boolean; why: string | null }19interface Venues { venues: { id: string; name: string; instruments: VenueInst[] }[]; maxInstruments: number; intervals: string[]; riskDefaults: PerpRisk; minCapital: string; live: { available: boolean; reason: string } }20const sym = (instrument: string) => instrument.slice(instrument.indexOf(':') + 1);2122/** the label nobody can miss: this agent trades a simulated account */23export function SimulationBanner() {24 const t = useT();25 return <div role="note" className="flex items-center gap-3 rounded-2xl border-2 border-blue/50 bg-blue/10 px-4 py-3">26 <span className="rounded-md bg-blue px-2.5 py-1 text-[13px] font-extrabold uppercase tracking-[0.12em] text-white">{t('paSim')}</span>27 <span className="text-[13.5px] leading-snug">{t('paSimText')}</span></div>;28}2930// ------------------------------------------------------------------ creation3132export function PerpAgentForm({ nav, onCreated }: { nav: Nav; onCreated: () => Promise<void> }) {33 const t = useT();34 const q = useQuery({ queryKey: ['perp-venues'], queryFn: () => get<Venues>('/agents/perp/venues'), staleTime: 60_000 });35 const [f, setF] = useState({ name: '', mission: '', capital: '1000', interval: '1h', instruments: [] as string[], search: '' });36 const [risk, setRisk] = useState<PerpRisk | null>(null), [busy, setBusy] = useState(false), [err, setErr] = useState('');37 const r = risk ?? q.data?.riskDefaults ?? { maxLeverage: 2, maxGrossExposureX: 1.5, maxPositionX: 1, maxDailyLossPct: 3, maxTotalLossPct: 10, slippageBps: 30, minTradePct: 2 };38 const max = q.data?.maxInstruments ?? 8, cap = Number(f.capital) || 0;39 const all = q.data?.venues[0]?.instruments ?? [];40 const shown = all.filter((x) => x.eligible && (f.search ? x.symbol.includes(f.search.toUpperCase()) : true)).slice(0, f.search ? 40 : 24);41 const num = (k: keyof PerpRisk, label: string, hint: string, suffix: string, min: number, mx: number, int = false) => <Field label={label} hint={hint}><div className="flex items-center gap-2">42 <input className={inputCls} inputMode="decimal" value={String(r[k])} onChange={(e) => { const v = Number(e.target.value.replace(',', '.').replace(/[^0-9.]/g, '')) || 0; setRisk({ ...r, [k]: Math.max(min, Math.min(mx, int ? Math.round(v) : v)) }); }} />43 <span className="w-12 shrink-0 text-[13px] text-mute">{suffix}</span></div></Field>;44 const create = async () => {45 setBusy(true); setErr('');46 try {47 const a = await post<{ id: string }>('/agents', { kind: 'perp', name: f.name.trim(), mission: f.mission.trim(), instruments: f.instruments, interval: f.interval, capitalUsdc: f.capital, risk: r });48 await onCreated(); nav({ tab: 'agent', asset: a.id });49 } catch (e) { setErr(errText(t, e)); } finally { setBusy(false); }50 };51 const money = (x: number) => x.toLocaleString('fr-CA', { maximumFractionDigits: 0 });52 return (53 <div className="space-y-4">54 <SimulationBanner />55 <Field label={t('agName')}><input className={inputCls} maxLength={40} value={f.name} onChange={(e) => setF({ ...f, name: e.target.value })} placeholder="BTC–ETH pairs" /></Field>56 <Field label={t('agMission')}><textarea className={`${inputCls} min-h-[72px] resize-none`} maxLength={400} value={f.mission} onChange={(e) => setF({ ...f, mission: e.target.value })} /></Field>57 <div><div className="mb-2 flex items-center justify-between gap-2 text-[13px] text-mute"><span>{t('paInstruments', { max })}</span><VenueBadge venue="hyperliquid" /></div>58 <input className={`${inputCls} mb-2`} value={f.search} onChange={(e) => setF({ ...f, search: e.target.value })} placeholder={t('paSearch')} aria-label={t('paSearch')} />59 {q.isLoading ? <Skeleton h="h-16" /> : q.isError ? <Notice tone="danger">{errText(t, q.error)}</Notice> : <div className="flex flex-wrap gap-2">{shown.map((x) => { const on = f.instruments.includes(x.symbol), full = !on && f.instruments.length >= max; return (60 <button key={x.symbol} type="button" role="checkbox" aria-checked={on} disabled={full} title={`max ${x.maxLeverage}x · lot ${x.lotSize}`} onClick={() => setF({ ...f, instruments: on ? f.instruments.filter((s) => s !== x.symbol) : [...f.instruments, x.symbol] })}61 className={`inline-flex min-h-11 items-center gap-2 rounded-full border px-4 text-[14px] font-medium ${on ? 'border-primary bg-primary text-onprimary' : 'border-line'} ${full ? 'opacity-40' : ''}`}>{x.symbol}<span className="text-[11.5px] font-normal opacity-75">{x.maxLeverage}x</span></button>); })}</div>}62 {f.instruments.length > 0 && <p className="mt-2 text-[13px]">{t('paChosen')} <span className="font-semibold">{f.instruments.join(' · ')}</span></p>}63 <p className="mt-2 text-[12.5px] text-mute">{t('paInstrumentsNote')}</p></div>64 <div className="grid grid-cols-2 gap-3">65 <Field label={t('paCapital')} hint={t('paCapitalHint', { min: q.data?.minCapital ?? '50' })}><div className="flex items-center gap-2"><input className={inputCls} inputMode="decimal" value={f.capital} onChange={(e) => setF({ ...f, capital: e.target.value.replace(',', '.').replace(/[^0-9.]/g, '') })} /><span className="w-12 shrink-0 text-[13px] text-mute">USDC</span></div></Field>66 <Field label={t('agInterval')} hint={t('paIntervalHint')}><select className={inputCls} value={f.interval} onChange={(e) => setF({ ...f, interval: e.target.value })}>{(q.data?.intervals ?? ['1h', '4h']).map((i) => <option key={i} value={i}>{i.replace('h', ' h')}</option>)}</select></Field>67 </div>68 <div><div className="mb-2 text-[15px] font-semibold">{t('paLimits')}</div><p className="mb-3 text-[12.5px] text-mute">{t('paLimitsNote')}</p>69 <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">70 {num('maxLeverage', t('paMaxLev'), t('paMaxLevHint', { usd: money(cap * r.maxLeverage) }), '×', 1, 5, true)}71 {num('maxGrossExposureX', t('paMaxGross'), t('paMaxGrossHint', { usd: money(cap * r.maxGrossExposureX) }), '× cap.', 0.1, 5)}72 {num('maxPositionX', t('paMaxPos'), t('paMaxPosHint', { usd: money(cap * r.maxPositionX) }), '× cap.', 0.05, 5)}73 {num('slippageBps', t('paSlip'), t('paSlipHint'), 'bps', 5, 100, true)}74 {num('maxDailyLossPct', t('paDailyLoss'), t('paDailyLossHint', { usd: money((cap * r.maxDailyLossPct) / 100) }), '%', 0.5, 50)}75 {num('maxTotalLossPct', t('paTotalLoss'), t('paTotalLossHint', { usd: money((cap * r.maxTotalLossPct) / 100) }), '%', 1, 90)}76 </div></div>77 <Notice tone="warn">{t('paLossNote')}</Notice>78 {err && <Notice tone="danger">{err}</Notice>}79 <Btn full busy={busy} disabled={f.name.trim().length < 2 || !f.instruments.length || f.instruments.length > max || cap < Number(q.data?.minCapital ?? 50) || r.maxDailyLossPct > r.maxTotalLossPct} onClick={() => void create()}>{t('paCreate')}</Btn>80 </div>81 );82}8384// ------------------------------------------------------------------ detail8586type Tab = 'orders' | 'fills' | 'funding' | 'events';87export function PerpAgentDetail({ a, id, nav }: { a: any; id: string; nav: Nav }) {88 const t = useT(); const { locale } = useContext(I18n); const qc = useQueryClient();89 const lib = useQuery({ queryKey: ['strategies-perp'], queryFn: () => get<{ strategies: any[] }>('/strategies?kind=perp'), staleTime: 300_000 });90 const [busy, setBusy] = useState(''), [err, setErr] = useState(''), [note, setNote] = useState(''), [tab, setTab] = useState<Tab>('orders');91 const act = async (name: string, fn: () => Promise<unknown>) => { setBusy(name); setErr(''); setNote(''); try { await fn(); await qc.invalidateQueries({ queryKey: ['agent', id] }); await qc.invalidateQueries({ queryKey: ['agents'] }); } catch (e) { const m = errText(t, e); if (m) setErr(m); } finally { setBusy(''); } };92 const acct = a.account as null | { state: any; policy: RiskPolicy; totals: any; positions: Position[]; orders: Order[]; fills: Fill[]; funding: FundingPayment[]; riskEvents: { id: number; ts: number; kind: string; code: string | null; message: string }[]; equity: { ts: number; equity: string }[] };93 const risk = a.risk as PerpRisk, approved = (a.versions as any[]).find((v) => v.id === a.strategyVersionId);94 // >>> v7 perp live: in real mode the page shows the VENUE account (PerpAgentLive.tsx); the paper account is not shown as if it were it95 const isLive = a.mode === 'LIVE', liveActive = !!a.live?.active, pol = isLive ? undefined : acct?.policy;96 // <<< v7 perp live97 const eq = (acct?.equity ?? []).map((e) => ({ t: e.ts, p: Number(e.equity) }));98 const result = acct ? D.sub(D.from(acct.state.equity), D.from(a.capitalUsdc)) : null;99 const pctOf = (x: unknown) => (typeof x === 'number' ? `${(x * 100).toFixed(1)} %` : '—'), n2 = (x: unknown) => (typeof x === 'number' ? x.toFixed(2) : '—');100 const px = (x: string | null | undefined) => fmt(x, { locale, trim: true });101102 const posCols: Col<Position>[] = [103 { id: 'i', label: t('paColInst'), head: true, cell: (p) => <span className="font-semibold">{sym(p.instrument)}</span> },104 { id: 's', label: t('paColSide'), head: true, cell: (p) => <SideTag side={p.side} /> },105 { id: 'z', label: t('paColSize'), num: true, cell: (p) => px(p.size) },106 { id: 'e', label: t('paColEntry'), num: true, cell: (p) => px(p.entryPrice) },107 { id: 'm', label: t('paColMark'), num: true, cell: (p) => px(p.markPrice) },108 { id: 'l', label: t('paColLev'), num: true, cell: (p) => `${px(p.leverage)}×` },109 { id: 'q', label: t('paColLiq'), num: true, cell: (p) => (p.liquidationPrice ? <span title={t('paLiqTitle')}>≈ {px(p.liquidationPrice)}</span> : <span className="text-mute">—</span>) },110 { id: 'u', label: t('paColPnl'), num: true, cell: (p) => <Pnl value={p.unrealizedPnl} locale={locale} /> },111 ];112 const ordCols: Col<Order>[] = [113 { id: 't', label: t('paColTime'), cell: (o) => timeOf(o.createdAt, locale, true) },114 { id: 'i', label: t('paColInst'), head: true, cell: (o) => <span className="font-semibold">{sym(o.instrument)}</span> },115 { id: 's', label: t('paColSide'), head: true, cell: (o) => <span className="inline-flex items-center gap-2"><SideTag side={o.side} />{o.reduceOnly && <span className="rounded bg-elev px-1.5 text-[11px] font-semibold text-mute">{t('paReduceOnly')}</span>}</span> },116 { id: 'z', label: t('paColSize'), num: true, cell: (o) => `${px(o.filled)} / ${px(o.size)}` },117 { id: 'p', label: t('paColAvg'), num: true, cell: (o) => px(o.avgFillPrice) },118 { id: 'w', label: t('paColWorst'), num: true, cell: (o) => px(o.price) },119 { id: 'x', label: t('paColStatus'), cell: (o) => <span title={o.rejectReason ?? ''}>{o.status}{o.rejectReason ? ' ⓘ' : ''}</span> },120 ];121 const fillCols: Col<Fill>[] = [122 { id: 't', label: t('paColTime'), cell: (x) => timeOf(x.ts, locale, true) },123 { id: 'i', label: t('paColInst'), head: true, cell: (x) => <span className="font-semibold">{sym(x.instrument)}</span> },124 { id: 's', label: t('paColSide'), head: true, cell: (x) => <SideTag side={x.side} /> },125 { id: 'z', label: t('paColSize'), num: true, cell: (x) => px(x.size) },126 { id: 'p', label: t('paColPrice'), num: true, cell: (x) => px(x.price) },127 { id: 'f', label: t('paColFee'), num: true, cell: (x) => usd(x.fee, locale) },128 { id: 'c', label: t('paColClosed'), num: true, cell: (x) => <Pnl value={x.closedPnl} locale={locale} /> },129 ];130 const fundCols: Col<FundingPayment>[] = [131 { id: 't', label: t('paColTime'), cell: (x) => timeOf(x.ts, locale, true) },132 { id: 'i', label: t('paColInst'), head: true, cell: (x) => <span className="font-semibold">{sym(x.instrument)}</span> },133 { id: 'r', label: t('paColRate'), num: true, cell: (x) => `${fmt(D.mul(D.from(x.rate), D.from(100n)), { dp: 5, locale })} %` },134 { id: 'z', label: t('paColSize'), num: true, cell: (x) => px(x.size) },135 { id: 'a', label: t('paColAmount'), num: true, cell: (x) => <Pnl value={x.amount} locale={locale} dp={4} /> },136 ];137 const evCols: Col<{ id: number; ts: number; kind: string; code: string | null; message: string }>[] = [138 { id: 't', label: t('paColTime'), cell: (x) => timeOf(x.ts, locale, true) },139 { id: 'k', label: t('paColKind'), head: true, cell: (x) => <span className="font-semibold">{x.kind}{x.code ? ` · ${x.code}` : ''}</span> },140 { id: 'm', label: '', full: true, cell: (x) => <span className="whitespace-normal text-mute">{x.message}</span> },141 ];142143 const have = new Set((a.versions as any[]).map((v) => v.code_sha256)), uni = [...(a.universe as string[])].sort().join();144 const fit = (lib.data?.strategies ?? []).filter((x) => !have.has(x.sha256) && !x.spotLegs && x.meta.interval === a.interval && [...x.meta.universe].sort().join() === uni);145146 return (147 <div className="space-y-4">148 <div className="flex items-center gap-2 pt-1"><button onClick={() => nav({ tab: 'agents' })} aria-label={t('back')} className="-ml-2 grid h-11 w-11 place-items-center rounded-full"><Icon n="chev" className="h-6 w-6 rotate-180" /></button>149 <div className="min-w-0 flex-1"><h1 className="truncate text-[24px] font-semibold tracking-tight">{a.name}</h1><div className="flex flex-wrap items-center gap-2 text-[13px] text-mute"><VenueBadge venue="hyperliquid" />{(a.universe as string[]).join(' · ')} · {a.interval} · {t('paPerps')}</div></div>{!isLive && <AgentState state={a.state} />}</div>150 {/* v7 perp live: the state and the money it moves, on their own line (the title keeps its width on a phone) */}151 {isLive && <div className="flex justify-start"><PerpLiveState a={a} /></div>}152 {isLive ? <AgentModeBand a={a} /> : <SimulationBanner />}153 {a.mission && <p className="text-[14px] text-mute">{a.mission}</p>}154 {a.stopReason && !(isLive && a.live?.exit) && <Notice tone="warn">{a.stopReason}</Notice>}{/* v7: a live agent's stop is said in the screen's language by LivePanel; the server's sentence is its detail */}155 {a.flattening && a.state !== 'STOPPED' && <Notice tone="danger">{t('paFlattening')} {a.flattening.reason}</Notice>}156 {a.flag && <Notice tone="warn">{t('paFlag')} {a.flag.message} ({when(new Date(a.flag.ts), locale)})</Notice>}157 {a.hold && (a.state === 'PAPER_TRADING' || a.state === 'LIVE') && <Notice tone="info">{t('paHold')} {a.hold.reason}</Notice>}158 {err && <Notice tone="danger">{err}</Notice>}{note && <Notice tone="ok">{note}</Notice>}159160 {isLive && <LivePanel a={a} id={id} nav={nav} />}161 {!isLive && <Card><H right={<PaperTag />}>{t('paAccount')}</H>162 {!acct ? <p className="text-[14px] text-mute">{t('paNoAccount')}</p> : <>163 <div className="text-[30px] font-semibold tracking-tight">{usd(acct.state.equity, locale)}</div>164 <div className="text-[13px] text-mute">{t('paAllocated', { capital: a.capitalUsdc })} · {t('paResult')} <Pnl value={result ? D.str(result) : null} locale={locale} /></div>165 <div className="mt-3 grid grid-cols-2 gap-x-4 gap-y-2 sm:grid-cols-4">166 <Stat label={t('paCollateral')}>{usd(acct.state.collateral, locale)}</Stat><Stat label={t('paMarginUsed')}>{usd(acct.state.marginUsed, locale)}</Stat>167 <Stat label={t('paFree')}>{usd(acct.state.freeCollateral, locale)}</Stat><Stat label={t('paRealised')}><Pnl value={acct.totals?.realizedPnl} locale={locale} /></Stat>168 <Stat label={t('paFees')}>{usd(acct.totals?.feesPaid, locale)}</Stat><Stat label={t('paFundingNet')}><Pnl value={acct.totals?.fundingNet} locale={locale} dp={4} /></Stat>169 <Stat label={t('paLiquidations')}>{acct.totals?.liquidations ?? 0}</Stat><Stat label={t('paLastBar')}>{a.lastBar ? when(a.lastBar, locale) : '—'}</Stat>170 </div>171 <div className="mt-4"><div className="mb-1 text-[13px] font-semibold text-mute">{t('paPositions')}</div>172 <DataTable cols={posCols} rows={acct.positions} rowKey={(p) => p.instrument} empty={t('paNoPositions')} label={t('paPositions')} /></div>173 <p className="mt-3 text-[12px] text-mute">{t('paSimplifications')}</p>174 {acct.positions.length > 0 && <Btn kind="ghost" full className="mt-3" busy={busy === 'flat'} onClick={() => act('flat', async () => { const r = await post<{ orders: unknown[]; positionsLeft: number }>(`/agents/${id}/perp/flatten`); setNote(t('paFlattened', { n: r.orders.length, left: r.positionsLeft })); })}>{t('paFlatten')}</Btn>}175 </>}176 </Card>}177178 {!isLive && eq.length > 1 && <Card><H right={<PaperTag />}>{t('paEquity')}</H><PriceChart points={eq} format={(v) => usd(dec(v.toFixed(2)), locale)} formatTime={(ts) => timeOf(ts, locale, true)} /></Card>}179180 <Card><H>{t('paLimits')}</H>181 <Row label={t('paMaxLev')}>{risk.maxLeverage}× <span className="text-mute">{!isLive && <>({t('paPolicy')} {pol?.maxLeverage ?? '—'}×)</>}</span></Row>182 <Row label={t('paMaxGross')}>{risk.maxGrossExposureX}×{!isLive && <> → {usd(pol?.maxGrossExposure, locale)}</>}</Row>183 <Row label={t('paMaxPos')}>{risk.maxPositionX}×{!isLive && <> → {usd(pol?.maxPositionNotional, locale)}</>}</Row>184 <Row label={t('paDailyLoss')}>{risk.maxDailyLossPct} %{!isLive && <> → {usd(pol?.maxDailyLoss, locale)}</>}</Row>185 <Row label={t('paTotalLoss')}>{risk.maxTotalLossPct} %{!isLive && <> → {usd(pol?.maxTotalLoss, locale)}</>}</Row>186 <Row label={t('paSlip')}>{risk.slippageBps} bps</Row>187 {pol?.halt && <div className="mt-2"><Notice tone="danger">{t('paHalt')} {pol.halt}</Notice></div>}188 <p className="mt-2 text-[12px] text-mute">{t('paLimitsNote')}</p>189 </Card>190191 <div id="pl-strategy" className="scroll-mt-24" /><Card><H>{t('agStrategy')}</H>192 {(a.versions as any[]).length === 0 ? <p className="text-[14px] text-mute">{t('paNoStrategy')}</p> : <ul className="space-y-3">{(a.versions as any[]).map((v) => { const s = v.summary; return <li key={v.id} className="rounded-2xl bg-elev/70 p-3.5">193 <div className="flex items-center justify-between gap-2"><span className="font-semibold">{v.name} <span className="font-normal text-mute">v{v.version}</span></span><span className="rounded-full bg-surface px-2 py-0.5 text-[11.5px] font-semibold">{t(`agV_${v.status}` as Key)}</span></div>194 <div className="mt-1 break-all font-mono text-[11px] text-mute">sha256 {v.code_sha256}</div>195 {s ? <>196 <div className="mt-2.5 grid grid-cols-3 gap-2 text-center text-[12.5px]">197 <div className="rounded-xl bg-surface p-2"><div className="text-mute">{t('agRet')}</div><div className="text-[14px] font-semibold">{pctOf(s.metrics.test?.total_return)}</div><div className="text-[11px] text-mute">{t('paVsPerp')} {pctOf(s.benchmarks.perp_long_1x?.total_return)}</div></div>198 <div className="rounded-xl bg-surface p-2"><div className="text-mute">Sharpe</div><div className="text-[14px] font-semibold">{n2(s.metrics.test?.sharpe)}</div><div className="text-[11px] text-mute">[{n2(s.robustness?.bootstrap_sharpe_ci?.[0])} … {n2(s.robustness?.bootstrap_sharpe_ci?.[1])}]</div></div>199 <div className="rounded-xl bg-surface p-2"><div className="text-mute">{t('agDD')}</div><div className="text-[14px] font-semibold">{pctOf(s.metrics.test?.max_drawdown)}</div><div className="text-[11px] text-mute">{t('paIntrabar')} {pctOf(s.metrics.test?.worst_intrabar_drawdown)}</div></div></div>200 <div className="mt-2.5 rounded-xl border border-warn/30 bg-warn/8 p-3 text-[12.5px] leading-snug"><div className="mb-1 font-semibold">{t('paCaveats')}</div><ul className="list-disc space-y-1 pl-4">201 <li>{t('paCavHistory', { days: s.historyDays ?? '?', bars: s.config?.bars ?? '?', interval: s.config?.interval ?? a.interval })}</li>202 <li>{t('paCavEstimates', { fee: s.config?.fee_bps ?? '?', slip: s.config?.slippage_bps ?? '?', pen: s.config?.liquidation_penalty_bps ?? '?' })}</li>203 <li className={s.liquidations.train + s.liquidations.validation + s.liquidations.test > 0 || s.metrics.test?.wiped_out ? 'font-semibold text-danger' : ''}>{t('paCavLiq', { train: s.liquidations.train, val: s.liquidations.validation, test: s.liquidations.test })}{s.metrics.test?.wiped_out ? ` ${t('paCavWiped')}` : ''}</li>204 <li>{t('paCavDsr', { dsr: typeof s.robustness?.deflated_sharpe === 'number' ? s.robustness.deflated_sharpe.toFixed(3) : '?', trials: s.robustness?.trials_tested ?? '?' })}</li>205 <li>{t('paCavChecks', { look: s.checks?.lookahead ?? '?', data: s.checks?.data_quality ?? '?', ledger: s.checks?.ledger ?? '?', fund: s.checks?.funding_alignment ?? '?' })}</li>206 {!s.buyAndHoldIsSpot && <li>{t('paCavNoSpot')}</li>}207 <li>{t('paCavNever')}</li></ul></div>208 {s.problem && <div className="mt-2"><Notice tone="danger">{t('paNotApprovable')} {s.problem}</Notice></div>}209 </> : <p className="mt-2 text-[12.5px] text-mute">{v.status === 'backtesting' ? t('paBacktesting') : v.notes ? v.notes : t('agNoReport')}</p>}210 {v.status !== 'approved' && s && !s.problem && !liveActive && <Btn kind="soft" full className="mt-3" busy={busy === `a${v.id}`} onClick={() => act(`a${v.id}`, () => post(`/agents/${id}/strategies/${v.id}/approve`))}>{t('agApproveVersion')}</Btn>}211 </li>; })}</ul>}212 <div className="mt-4"><div className="mb-2 text-[13px] font-semibold text-mute">{t('paLibrary')}</div>213 {lib.isLoading ? <Skeleton h="h-16" /> : fit.length === 0 ? <p className="text-[13px] text-mute">{t('paLibNone', { assets: (a.universe as string[]).join(', '), interval: a.interval })}</p> :214 <ul className="space-y-2">{fit.map((x) => <li key={x.sha256} className="flex items-center gap-3 rounded-2xl border border-line p-3">215 <span className="min-w-0 flex-1"><span className="block font-semibold">{x.meta.name} <span className="font-normal text-mute">v{x.meta.version}</span></span>216 <span className="block truncate text-[12px] text-mute">{x.meta.universe.join(' · ')} · {x.meta.interval} · {t('paBounds', { lev: x.meta.max_leverage, gross: x.meta.max_gross_exposure })}</span></span>217 <Btn kind="soft" busy={busy === `r${x.sha256}`} onClick={() => act(`r${x.sha256}`, () => post(`/agents/${id}/strategies`, { path: x.path.replace(/^strategies\//, '') }))}>{t('agLibTest')}</Btn></li>)}</ul>}</div>218 </Card>219220 {/* v7 research runtime */}<ResearchPanel agent={a} nav={nav} />221222 <Card><H>{t('agControl')}</H>223 <div className="space-y-2.5">224 {approved && !liveActive && ['AWAITING_APPROVAL', 'PAUSED', 'STOPPED', 'CREATED'].includes(a.state) && <Btn kind="soft" full busy={busy === 'paper'} onClick={() => act('paper', () => post(`/agents/${id}/mode`, { mode: 'PAPER' }))} data-testid="pa-start-paper">{t(isLive ? 'plBackPaper' : 'paStart')}</Btn>}225 {a.state === 'PAPER_TRADING' && <Btn kind="ghost" full busy={busy === 'pause'} onClick={() => act('pause', () => post(`/agents/${id}/mode`, { mode: 'PAUSED' }))}>{t('agPause')}</Btn>}226 {!liveActive && ['PAPER_TRADING', 'PAUSED'].includes(a.state) && <Btn kind="ghost" full busy={busy === 'stop'} onClick={() => act('stop', () => post(`/agents/${id}/mode`, { mode: 'STOPPED' }))}>{t('paStop')}</Btn>}227 {!approved && <p className="text-[13.5px] text-mute">{t('agNeedsStrategy')}</p>}228 </div>229 {/* >>> v7 perp live: the go-live checklist (the pause / resume / stop of a live agent are in LivePanel) */}230 {liveActive ? <p className="text-[13.5px] text-mute">{t('plBandLive')}</p> : <div className="mt-4"><GoLiveCard a={a} id={id} nav={nav} /></div>}231 {/* <<< v7 perp live */}232 </Card>233234 {acct && !isLive && <Card><H right={<PaperTag />}>{t('paActivity')}</H>235 <Seg<Tab> value={tab} onChange={setTab} label={t('paActivity')} tabs size="sm" className="mb-3" options={[{ id: 'orders', label: `${t('paOrders')} (${acct.orders.length})` }, { id: 'fills', label: `${t('paFills')} (${acct.fills.length})` }, { id: 'funding', label: `${t('paFunding')} (${acct.funding.length})` }, { id: 'events', label: `${t('paEvents')} (${acct.riskEvents.length})` }]} />236 {tab === 'orders' && <DataTable cols={ordCols} rows={acct.orders} rowKey={(o) => o.clientId} empty={t('paNone')} label={t('paOrders')} />}237 {tab === 'fills' && <DataTable cols={fillCols} rows={acct.fills} rowKey={(x) => x.id} empty={t('paNone')} label={t('paFills')} />}238 {tab === 'funding' && <DataTable cols={fundCols} rows={acct.funding} rowKey={(x) => `${x.instrument}:${x.ts}`} empty={t('paNone')} label={t('paFunding')} />}239 {tab === 'events' && <DataTable cols={evCols} rows={acct.riskEvents} rowKey={(x) => String(x.id)} empty={t('paNone')} label={t('paEvents')} />}240 </Card>}241242 <Card><H>{t('paSignals')}</H>243 {(a.signals as any[]).length === 0 ? <p className="text-[14px] text-mute">{t('paNoSignals')}</p> : <ul className="space-y-2.5 text-[13px]">{(a.signals as any[]).slice(0, 24).map((s) => <li key={s.bar} className="rounded-2xl border border-line p-3">244 <div className="flex justify-between gap-2"><span className="font-semibold">{when(s.bar, locale)}</span><span className={`rounded-full px-2 py-0.5 text-[11.5px] font-semibold ${s.status === 'done' ? 'bg-emerald/10 text-emerald' : s.status === 'hold' ? 'bg-warn/12 text-warn' : 'bg-elev text-mute'}`}>{s.status === 'decided' ? t('plSig_decided') : t(`paSig_${s.status}` as Key)}</span></div>245 {s.reason && <div className="mt-1 text-mute">{s.reason}</div>}246 {s.weights && <div className="mt-1 font-mono text-[11.5px] text-mute">{t('paWeights')} {Object.entries(s.weights as Record<string, number>).map(([k, w]) => `${k} ${w >= 0 ? '+' : ''}${w.toFixed(3)}`).join(' ')}{s.scale && s.scale !== '1' ? ` × ${Number(s.scale).toFixed(3)} (${s.orders?.binding})` : ''}</div>}247 {s.targets && <div className="font-mono text-[11.5px] text-mute">{t('paTargets')} {Object.entries(s.targets as Record<string, string>).map(([k, z]) => `${k} ${z}`).join(' ')}</div>}248 {(s.orders?.list ?? []).length > 0 && <ul className="mt-1.5 space-y-0.5">{(s.orders.list as any[]).map((o, k) => <li key={k} className="text-[12px]"><SideTag side={o.side} /> {o.symbol} {o.size}{o.reduceOnly ? ` · ${t('paReduceOnly')}` : ''} → <span className="font-medium">{o.status === 'deferred' ? t('plSig_deferred') : o.status}</span>{o.code && isLive ? <span className="mt-0.5 block text-mute"><Refusal code={String(o.code)} reason={o.why ? String(o.why).slice(0, 200) : undefined} /></span> : o.why ? <span className="text-mute"> — {String(o.why).slice(0, 160)}</span> : null}</li>)}</ul>}249 </li>)}</ul>}250 </Card>251252 <Card><H>{t('agLog')}</H>253 {(a.log as any[]).length === 0 ? <p className="text-[14px] text-mute">{t('agNoLog')}</p> : <ul className="space-y-2.5 text-[13px]">{(a.log as any[]).map((l, k) => <li key={k}><div className="flex justify-between gap-2"><span className="font-medium">{l.title}</span><span className="shrink-0 text-[12px] text-mute">{when(l.ts, locale)}</span></div>{l.body && <div className="whitespace-pre-wrap text-mute">{String(l.body).slice(0, 600)}</div>}</li>)}</ul>}</Card>254 </div>255 );256}257