68 lines
2.7 KiB
TypeScript
68 lines
2.7 KiB
TypeScript
import { usePositions } from '../api/hooks';
|
|
import { DataTable, type Column } from '../components/DataTable';
|
|
import { LoadingSpinner } from '../components/ui';
|
|
import type { Position } from '../api/hooks';
|
|
|
|
function fmtUsd(v: number | null | undefined) {
|
|
if (v == null) return '—';
|
|
return `$${v.toFixed(2)}`;
|
|
}
|
|
|
|
function pnlColor(v: number | null | undefined) {
|
|
if (v == null) return 'text-gray-400';
|
|
return v >= 0 ? 'text-green-400' : 'text-red-400';
|
|
}
|
|
|
|
const columns: Column<Position>[] = [
|
|
{ key: 'ticker', header: 'Ticker', className: 'font-mono font-semibold text-brand-300' },
|
|
{ key: 'quantity', header: 'Qty' },
|
|
{ key: 'avg_entry_price', header: 'Entry', render: (r) => <span>{fmtUsd(r.avg_entry_price)}</span> },
|
|
{ key: 'current_price', header: 'Current', render: (r) => <span>{fmtUsd(r.current_price)}</span> },
|
|
{ key: 'unrealized_pnl', header: 'Unrealized P&L', render: (r) => <span className={pnlColor(r.unrealized_pnl)}>{fmtUsd(r.unrealized_pnl)}</span> },
|
|
{ key: 'realized_pnl', header: 'Realized P&L', render: (r) => <span className={pnlColor(r.realized_pnl)}>{fmtUsd(r.realized_pnl)}</span> },
|
|
{ key: 'updated_at', header: 'Updated', render: (r) => <span className="text-xs">{new Date(r.updated_at).toLocaleString()}</span> },
|
|
];
|
|
|
|
export function PositionsPage() {
|
|
const { data, isLoading } = usePositions();
|
|
|
|
if (isLoading) return <LoadingSpinner />;
|
|
|
|
const positions = data ?? [];
|
|
|
|
const totals = positions.reduce(
|
|
(acc, p) => ({
|
|
quantity: acc.quantity + p.quantity,
|
|
unrealized_pnl: acc.unrealized_pnl + (p.unrealized_pnl ?? 0),
|
|
realized_pnl: acc.realized_pnl + (p.realized_pnl ?? 0),
|
|
market_value: acc.market_value + p.quantity * (p.current_price ?? 0),
|
|
cost_basis: acc.cost_basis + p.quantity * p.avg_entry_price,
|
|
}),
|
|
{ quantity: 0, unrealized_pnl: 0, realized_pnl: 0, market_value: 0, cost_basis: 0 },
|
|
);
|
|
|
|
const footer = positions.length > 0 ? (
|
|
<tr className="font-semibold text-gray-200">
|
|
<td className="px-3 py-2">Totals</td>
|
|
<td className="px-3 py-2">{totals.quantity}</td>
|
|
<td className="px-3 py-2">{fmtUsd(totals.cost_basis)}</td>
|
|
<td className="px-3 py-2">{fmtUsd(totals.market_value)}</td>
|
|
<td className={`px-3 py-2 ${pnlColor(totals.unrealized_pnl)}`}>{fmtUsd(totals.unrealized_pnl)}</td>
|
|
<td className={`px-3 py-2 ${pnlColor(totals.realized_pnl)}`}>{fmtUsd(totals.realized_pnl)}</td>
|
|
<td className="px-3 py-2" />
|
|
</tr>
|
|
) : undefined;
|
|
|
|
return (
|
|
<div>
|
|
<h1 className="mb-4 text-xl font-semibold text-gray-100">Positions</h1>
|
|
<DataTable<Position>
|
|
data={positions}
|
|
columns={columns}
|
|
keyField="id"
|
|
footerRow={footer}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|