'use client'; import { useCallback, useEffect, useState } from 'react'; import type { TradeView } from '@/types'; /** * Recorded positions from /api/trades — every trade on the accounts, the * bot's and anyone else's, with who opened it and how it closed. On the * dashboard under each firm (inline) and on the account page (as a card, * hidden while there's nothing to show). */ const REFRESH_MS = 30_000; const PILL = 'inline-block px-1.5 py-0.5 rounded text-[10px] font-semibold whitespace-nowrap'; const CLOSED_BY: Record = { bot: { label: 'Bot', cls: 'bg-violet-100 text-violet-700', title: "The bot's own close order" }, outside: { label: 'Outside', cls: 'bg-amber-100 text-amber-700', title: 'Closed by an order the bot did not send — by hand on the platform, another tool, or the firm’s own liquidation' }, stop_loss: { label: 'Stop loss', cls: 'bg-red-100 text-red-700', title: 'Its stop-loss triggered' }, take_profit: { label: 'Take profit', cls: 'bg-green-100 text-green-700', title: 'Its take-profit triggered' }, liquidation: { label: 'Liquidation', cls: 'bg-red-200 text-red-800', title: 'Liquidated by Vest' }, auto_deleverage: { label: 'Auto-deleverage', cls: 'bg-red-100 text-red-700', title: 'Closed by Vest’s auto-deleverage' }, temporary_leverage_limit: { label: 'Leverage limit', cls: 'bg-orange-100 text-orange-700', title: 'Reduced by Vest’s temporary (weekend) leverage limit' }, corporate_action: { label: 'Corporate action', cls: 'bg-slate-100 text-slate-600', title: 'Closed by a corporate action' }, user_requested: { label: 'Self-requested', cls: 'bg-slate-100 text-slate-600', title: 'An order on the account closed it — from before the bot marked its own closes, so whose is unknown' }, unknown: { label: 'Unknown', cls: 'bg-slate-100 text-slate-500', title: 'Closed while the platform’s history wasn’t being read — when and how aren’t on record' }, }; function money(v: number): string { return v.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function signed(v: number): string { return `${v >= 0 ? '+' : '−'}$${money(Math.abs(v))}`; } function price(v: number | null): string { return v === null ? '—' : v.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 4 }); } function when(ms: number): string { return new Date(ms).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); } function OpenedBy({ by }: { by: TradeView['openedBy'] }) { if (by === 'bot') return Bot; if (by === 'outside') return Outside; return —; } function ClosedBy({ by }: { by: string | null }) { if (by === null) return Open; const c = CLOSED_BY[by] ?? { label: by, cls: 'bg-slate-100 text-slate-600', title: by }; return {c.label}; } export function TradesTable({ query, title, showAccount = false, formatAccount = (n) => n, limit = 200 }: { /** accountId=… or firmId=… */ query: string; /** Set: render as a titled card, and nothing at all while empty. */ title?: string; showAccount?: boolean; formatAccount?: (name: string) => string; limit?: number; }) { const [trades, setTrades] = useState(null); const [failed, setFailed] = useState(false); const load = useCallback(async () => { try { const res = await fetch(`/api/trades?${query}&limit=${limit}`); if (!res.ok) throw new Error(String(res.status)); setTrades(await res.json()); setFailed(false); } catch { setFailed(true); } }, [query, limit]); useEffect(() => { void load(); const t = setInterval(() => { void load(); }, REFRESH_MS); return () => clearInterval(t); }, [load]); if (title && (!trades || trades.length === 0)) return null; const closed = (trades ?? []).filter((t) => t.net !== null); const net = closed.reduce((s, t) => s + (t.net ?? 0), 0); const fees = closed.reduce((s, t) => s + Math.abs(t.fee ?? 0), 0); const summary = trades && trades.length > 0 && ( {trades.length} trade{trades.length === 1 ? '' : 's'}{trades.length >= limit ? ` (latest ${limit})` : ''} {closed.length > 0 && ( <> {' · net '} = 0 ? 'text-green-600' : 'text-red-500'}>{signed(net)} {` · fees $${money(fees)}`} )} ); const table = !trades ?
{failed ? 'Couldn’t load trades' : 'Loading trades…'}
: trades.length === 0 ?
No trades recorded yet — they appear a few minutes after a fill, once the platform’s history is read
: (
{showAccount && } {trades.map((t) => ( {showAccount && } ))}
Opened ClosedAccountMarket Side Size Open Close P&L Fees Net Opened by Closed by
{when(t.openedAt)} {t.closedAt !== null ? when(t.closedAt) : '—'}{formatAccount(t.accountName)}{t.symbol} {t.side === 'long' ? 'Long' : 'Short'} {t.quantity > 0 ? t.quantity.toLocaleString(undefined, { maximumFractionDigits: 4 }) : '—'} {price(t.openPrice)} {price(t.closePrice)} = 0 ? 'text-green-600' : 'text-red-500'}`}> {t.pnl === null ? '—' : signed(t.pnl)} {t.fee === null ? '—' : `$${money(Math.abs(t.fee))}`} = 0 ? 'text-green-600' : 'text-red-500'}`}> {t.net === null ? '—' : signed(t.net)}
); if (title) { return (

{title}

{summary}
{table}
); } return (
Trades {summary}
{table}
); }