Files
autofirmer-expanded/app/components/TradesTable.tsx
T
Brandon LiandClaude Opus 5.5 1e844178de Trades: record every firm's trades, not just Vest's, and list them on each
TopstepX and Tradovate report fills, so a trade is built from them, flat
to flat (lib/fill-trades.ts), off the window each sync already reads:
TopstepX's two days of Trade/search, Tradovate's sync snapshot. Neither
says who placed an order, so every order the bot places is noted in
own_orders, and a trade is the bot's when its opening fill's order is
one; its stop or take-profit closing it reads as such. A contract is
written only when the position before its window comes out the same
two passes running, so a fill landing between the position and history
reads can't build the wrong trades.

DXtrade reports positions: its record groups the last two days' filled
orders by position code, with each close's SETTLEMENT and COMMISSION.
The bot's own orders carry af- codes, so nothing is kept locally. Only
accounts with something going on are read, at most every 5 minutes.

The Trades button shows on every firm with a login, and the account
page lists any account's trades.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 01:27:07 -05:00

178 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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<string, { label: string; cls: string; title: string }> = {
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 <span className={`${PILL} bg-violet-100 text-violet-700`} title="Opened by the bot">Bot</span>;
if (by === 'outside') return <span className={`${PILL} bg-amber-100 text-amber-700`} title="Opened by an order the bot did not send — by hand on the platform, or another tool">Outside</span>;
return <span className="text-slate-400" title="Opened before the bot began marking its own trades">—</span>;
}
function ClosedBy({ by }: { by: string | null }) {
if (by === null) return <span className={`${PILL} bg-sky-100 text-sky-700`}>Open</span>;
const c = CLOSED_BY[by] ?? { label: by, cls: 'bg-slate-100 text-slate-600', title: by };
return <span className={`${PILL} ${c.cls}`} title={c.title}>{c.label}</span>;
}
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<TradeView[] | null>(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 && (
<span className="text-xs text-slate-400">
{trades.length} trade{trades.length === 1 ? '' : 's'}{trades.length >= limit ? ` (latest ${limit})` : ''}
{closed.length > 0 && (
<>
{' · net '}
<span className={net >= 0 ? 'text-green-600' : 'text-red-500'}>{signed(net)}</span>
{` · fees $${money(fees)}`}
</>
)}
</span>
);
const table = !trades
? <div className="text-sm text-slate-400 italic py-2">{failed ? 'Couldn’t load trades' : 'Loading trades…'}</div>
: trades.length === 0
? <div className="text-sm text-slate-400 italic py-2">No trades recorded yet — they appear a few minutes after a fill, once the platform’s history is read</div>
: (
<div className="overflow-x-auto max-h-[28rem] overflow-y-auto">
<table className="w-full text-xs">
<thead className="sticky top-0 bg-white">
<tr className="border-b border-slate-100 text-slate-400">
<th className="text-left font-medium py-2 pr-3">Opened</th>
<th className="text-left font-medium py-2 pr-3">Closed</th>
{showAccount && <th className="text-left font-medium py-2 pr-3">Account</th>}
<th className="text-left font-medium py-2 pr-3">Market</th>
<th className="text-left font-medium py-2 pr-3">Side</th>
<th className="text-right font-medium py-2 pr-3">Size</th>
<th className="text-right font-medium py-2 pr-3">Open</th>
<th className="text-right font-medium py-2 pr-3">Close</th>
<th className="text-right font-medium py-2 pr-3" title="Realized P&L before fees and funding">P&L</th>
<th className="text-right font-medium py-2 pr-3">Fees</th>
<th className="text-right font-medium py-2 pr-3" title="P&L − fees + funding">Net</th>
<th className="text-left font-medium py-2 pr-3">Opened by</th>
<th className="text-left font-medium py-2">Closed by</th>
</tr>
</thead>
<tbody>
{trades.map((t) => (
<tr key={t.positionId} className="border-b border-slate-50 last:border-0 hover:bg-slate-50">
<td className="py-1.5 pr-3 text-slate-500 tabular-nums whitespace-nowrap">{when(t.openedAt)}</td>
<td className="py-1.5 pr-3 text-slate-500 tabular-nums whitespace-nowrap">{t.closedAt !== null ? when(t.closedAt) : '—'}</td>
{showAccount && <td className="py-1.5 pr-3 font-mono text-slate-700 whitespace-nowrap">{formatAccount(t.accountName)}</td>}
<td className="py-1.5 pr-3 text-slate-700 whitespace-nowrap">{t.symbol}</td>
<td className={`py-1.5 pr-3 font-semibold ${t.side === 'long' ? 'text-green-600' : 'text-red-500'}`}>{t.side === 'long' ? 'Long' : 'Short'}</td>
<td className="py-1.5 pr-3 text-right tabular-nums text-slate-600">{t.quantity > 0 ? t.quantity.toLocaleString(undefined, { maximumFractionDigits: 4 }) : '—'}</td>
<td className="py-1.5 pr-3 text-right tabular-nums text-slate-600">{price(t.openPrice)}</td>
<td className="py-1.5 pr-3 text-right tabular-nums text-slate-600">{price(t.closePrice)}</td>
<td className={`py-1.5 pr-3 text-right tabular-nums ${t.pnl === null ? 'text-slate-400' : t.pnl >= 0 ? 'text-green-600' : 'text-red-500'}`}>
{t.pnl === null ? '—' : signed(t.pnl)}
</td>
<td className="py-1.5 pr-3 text-right tabular-nums text-slate-500">{t.fee === null ? '—' : `$${money(Math.abs(t.fee))}`}</td>
<td className={`py-1.5 pr-3 text-right tabular-nums font-semibold ${t.net === null ? 'text-slate-400' : t.net >= 0 ? 'text-green-600' : 'text-red-500'}`}>
{t.net === null ? '—' : signed(t.net)}
</td>
<td className="py-1.5 pr-3"><OpenedBy by={t.openedBy} /></td>
<td className="py-1.5"><ClosedBy by={t.closedBy} /></td>
</tr>
))}
</tbody>
</table>
</div>
);
if (title) {
return (
<div className="bg-white border border-slate-200 rounded-xl shadow-sm p-6 mt-5">
<div className="flex items-baseline justify-between mb-4">
<h2 className="text-slate-900 font-bold text-base">{title}</h2>
{summary}
</div>
{table}
</div>
);
}
return (
<div>
<div className="flex items-baseline justify-between mb-1">
<span className="text-xs font-semibold text-slate-600">Trades</span>
{summary}
</div>
{table}
</div>
);
}