feat: wire up stop levels, circuit breaker daily loss, profit-taking, real portfolio/decisions/history endpoints
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { usePositions } from '../../api/hooks';
|
||||
import type { Position } from '../../api/hooks';
|
||||
import { useTradingStatus } from '../../api/tradingHooks';
|
||||
import { Card, LoadingSpinner } from '../../components/ui';
|
||||
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
@@ -8,57 +10,54 @@ const COLORS = [
|
||||
'#ec4899', '#8b5cf6', '#14b8a6', '#f97316', '#64748b',
|
||||
];
|
||||
|
||||
interface Position {
|
||||
ticker: string;
|
||||
entry_price: number;
|
||||
current_price: number;
|
||||
unrealized_pnl: number;
|
||||
stop_loss: number | null;
|
||||
take_profit: number | null;
|
||||
sector: string | null;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
function fmtUsd(v: number | null | undefined) {
|
||||
if (v == null) return '—';
|
||||
return `$${v.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
return `$${Number(v).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
function pnlColor(v: number | null | undefined) {
|
||||
if (v == null) return 'text-gray-400';
|
||||
return v >= 0 ? 'text-green-400' : 'text-red-400';
|
||||
return Number(v) >= 0 ? 'text-green-400' : 'text-red-400';
|
||||
}
|
||||
|
||||
export function PortfolioComposition() {
|
||||
const { data: status, isLoading } = useTradingStatus();
|
||||
const { data: positions, isLoading: posLoading } = usePositions();
|
||||
const { data: status } = useTradingStatus();
|
||||
|
||||
// Extract positions from the status — the API may embed them or we derive from open_position_count
|
||||
// For now, we use a typed cast since the status object may carry positions in an extended response
|
||||
const positions: Position[] = useMemo(() => {
|
||||
if (!status) return [];
|
||||
const raw = (status as unknown as Record<string, unknown>)['positions'];
|
||||
if (Array.isArray(raw)) return raw as Position[];
|
||||
return [];
|
||||
}, [status]);
|
||||
|
||||
const sectorData = useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
for (const p of positions) {
|
||||
const sector = p.sector ?? 'Unknown';
|
||||
const value = (p.current_price ?? 0) * (p.quantity ?? 0);
|
||||
map.set(sector, (map.get(sector) ?? 0) + value);
|
||||
}
|
||||
return Array.from(map.entries()).map(([name, value]) => ({ name, value }));
|
||||
const tickerData = useMemo(() => {
|
||||
if (!positions) return [];
|
||||
return positions.map((p: Position) => ({
|
||||
name: p.ticker,
|
||||
value: Math.abs(Number(p.quantity) * Number(p.avg_entry_price)),
|
||||
}));
|
||||
}, [positions]);
|
||||
|
||||
if (isLoading) return <LoadingSpinner />;
|
||||
const totalInvested = useMemo(() => {
|
||||
if (!positions) return 0;
|
||||
return positions.reduce((sum: number, p: Position) => sum + Math.abs(Number(p.quantity) * Number(p.avg_entry_price)), 0);
|
||||
}, [positions]);
|
||||
|
||||
const totalUnrealized = useMemo(() => {
|
||||
if (!positions) return 0;
|
||||
return positions.reduce((sum: number, p: Position) => sum + Number(p.unrealized_pnl ?? 0), 0);
|
||||
}, [positions]);
|
||||
|
||||
if (posLoading) return <LoadingSpinner />;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Summary Stats */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<StatCard label="Active Pool" value={fmtUsd(status?.active_pool)} />
|
||||
<StatCard label="Reserve Pool" value={fmtUsd(status?.reserve_pool)} />
|
||||
<StatCard label="Total Invested" value={fmtUsd(totalInvested)} />
|
||||
<StatCard label="Unrealized P&L" value={fmtUsd(totalUnrealized)} color={pnlColor(totalUnrealized)} />
|
||||
</div>
|
||||
|
||||
{/* Positions Table */}
|
||||
<Card>
|
||||
<h2 className="mb-3 text-sm font-medium text-gray-400">Current Positions</h2>
|
||||
{positions.length === 0 ? (
|
||||
<h2 className="mb-3 text-sm font-medium text-gray-400">Current Positions ({positions?.length ?? 0})</h2>
|
||||
{!positions?.length ? (
|
||||
<p className="text-sm text-gray-500">No open positions</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
@@ -66,40 +65,48 @@ export function PortfolioComposition() {
|
||||
<thead>
|
||||
<tr className="border-b border-surface-700 text-left text-gray-500">
|
||||
<th className="px-3 py-2">Ticker</th>
|
||||
<th className="px-3 py-2">Qty</th>
|
||||
<th className="px-3 py-2">Entry</th>
|
||||
<th className="px-3 py-2">Current</th>
|
||||
<th className="px-3 py-2">Market Value</th>
|
||||
<th className="px-3 py-2">Unrealized P&L</th>
|
||||
<th className="px-3 py-2">Stop-Loss</th>
|
||||
<th className="px-3 py-2">Take-Profit</th>
|
||||
<th className="px-3 py-2">Sector</th>
|
||||
<th className="px-3 py-2">Return %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{positions.map((p) => (
|
||||
<tr key={p.ticker} className="border-b border-surface-700/50">
|
||||
<td className="px-3 py-2 font-mono font-semibold text-brand-300">{p.ticker}</td>
|
||||
<td className="px-3 py-2 text-gray-300">{fmtUsd(p.entry_price)}</td>
|
||||
<td className="px-3 py-2 text-gray-300">{fmtUsd(p.current_price)}</td>
|
||||
<td className={`px-3 py-2 ${pnlColor(p.unrealized_pnl)}`}>{fmtUsd(p.unrealized_pnl)}</td>
|
||||
<td className="px-3 py-2 text-gray-300">{fmtUsd(p.stop_loss)}</td>
|
||||
<td className="px-3 py-2 text-gray-300">{fmtUsd(p.take_profit)}</td>
|
||||
<td className="px-3 py-2 text-gray-400">{p.sector ?? '—'}</td>
|
||||
</tr>
|
||||
))}
|
||||
{positions.map((p: Position) => {
|
||||
const qty = Number(p.quantity);
|
||||
const entry = Number(p.avg_entry_price);
|
||||
const current = Number(p.current_price ?? entry);
|
||||
const marketValue = qty * current;
|
||||
const pnl = Number(p.unrealized_pnl ?? 0);
|
||||
const returnPct = entry > 0 ? ((current - entry) / entry) * 100 : 0;
|
||||
return (
|
||||
<tr key={p.id} className="border-b border-surface-700/50">
|
||||
<td className="px-3 py-2 font-mono font-semibold text-brand-300">{p.ticker}</td>
|
||||
<td className="px-3 py-2 text-gray-300">{qty}</td>
|
||||
<td className="px-3 py-2 text-gray-300">{fmtUsd(entry)}</td>
|
||||
<td className="px-3 py-2 text-gray-300">{fmtUsd(current)}</td>
|
||||
<td className="px-3 py-2 text-gray-300">{fmtUsd(marketValue)}</td>
|
||||
<td className={`px-3 py-2 ${pnlColor(pnl)}`}>{fmtUsd(pnl)}</td>
|
||||
<td className={`px-3 py-2 ${pnlColor(returnPct)}`}>{returnPct.toFixed(2)}%</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Sector Allocation Pie Chart */}
|
||||
{sectorData.length > 0 && (
|
||||
{/* Allocation Pie Chart */}
|
||||
{tickerData.length > 0 && (
|
||||
<Card>
|
||||
<h2 className="mb-3 text-sm font-medium text-gray-400">Sector Allocation</h2>
|
||||
<h2 className="mb-3 text-sm font-medium text-gray-400">Position Allocation</h2>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={sectorData}
|
||||
data={tickerData}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
@@ -107,7 +114,7 @@ export function PortfolioComposition() {
|
||||
outerRadius={100}
|
||||
label={({ name, percent }) => `${name} ${((percent ?? 0) * 100).toFixed(0)}%`}
|
||||
>
|
||||
{sectorData.map((_, i) => (
|
||||
{tickerData.map((_, i) => (
|
||||
<Cell key={i} fill={COLORS[i % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
@@ -123,3 +130,12 @@ export function PortfolioComposition() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({ label, value, color = 'text-gray-100' }: { label: string; value: string; color?: string }) {
|
||||
return (
|
||||
<Card className="text-center">
|
||||
<div className={`text-lg font-bold ${color}`}>{value}</div>
|
||||
<div className="text-xs text-gray-500">{label}</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user