← back to Ken
react-dash/src/pages/Trades.jsx
135 lines
import React, { useState, useEffect } from 'react';
import { api } from '../api';
import { BarChart, Bar, AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Cell } from 'recharts';
const ChartTooltip = ({ active, payload, label, prefix = '$' }) => {
if (!active || !payload?.length) return null;
return (
<div className="bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-xs shadow-xl">
<p className="text-gray-400 mb-1">{label}</p>
{payload.map((p, i) => (
<p key={i} style={{ color: p.color }} className="font-mono">{p.name}: {prefix}{typeof p.value === 'number' ? p.value.toFixed(2) : p.value}</p>
))}
</div>
);
};
export default function Trades() {
const [orders, setO] = useState([]);
const [trades, setT] = useState([]);
const [loading, setL] = useState(true);
useEffect(() => {
api('/trades').then(d => { setO(d.orders || []); setT(d.trades || []); setL(false); });
const i = setInterval(() => api('/trades').then(d => { setO(d.orders || []); setT(d.trades || []); }), 15000);
return () => clearInterval(i);
}, []);
if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
const pnl = trades.reduce((s, t) => s + parseFloat(t.pnl || 0), 0);
const fees = trades.reduce((s, t) => s + parseFloat(t.fee_usd || 0), 0);
const wr = trades.length > 0 ? (trades.filter(t => parseFloat(t.pnl || 0) > 0).length / trades.length * 100) : 0;
const avgSize = trades.length > 0 ? trades.reduce((s, t) => s + parseFloat(t.size_usd || 0), 0) / trades.length : 0;
// P&L per trade chart
const pnlData = trades.slice().reverse().map((t, i) => ({
idx: i + 1,
time: new Date(t.match_time).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
pnl: parseFloat(t.pnl || 0),
size: parseFloat(t.size_usd || 0),
}));
// Cumulative P&L
let cum = 0;
const cumData = pnlData.map(d => {
cum += d.pnl;
return { ...d, cumPnl: cum };
});
return (
<div className="space-y-6">
<div><h1 className="text-2xl font-bold">Trades & Orders</h1><p className="text-gray-500 text-sm">Execution history & analysis</p></div>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Orders</p><p className="stat-v text-blue-400">{orders.length}</p></div>
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Fills</p><p className="stat-v text-green-400">{trades.length}</p></div>
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">P&L</p><p className={`stat-v ${pnl >= 0 ? 'text-green-400' : 'text-red-400'}`}>${pnl.toFixed(2)}</p></div>
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Win Rate</p><p className="stat-v text-purple-400">{wr.toFixed(0)}%</p></div>
<div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Avg Size</p><p className="stat-v text-yellow-400">${avgSize.toFixed(2)}</p></div>
</div>
{/* Charts */}
{trades.length > 0 && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="card">
<h2 className="font-semibold mb-3">P&L Per Trade</h2>
<ResponsiveContainer width="100%" height={220}>
<BarChart data={pnlData} barSize={16}>
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
<XAxis dataKey="time" tick={{ fill: '#6b7280', fontSize: 10 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={v => `$${v}`} />
<Tooltip content={<ChartTooltip />} />
<Bar dataKey="pnl" name="P&L">
{pnlData.map((entry, i) => (
<Cell key={i} fill={entry.pnl >= 0 ? '#10b981' : '#ef4444'} fillOpacity={0.8} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
<div className="card">
<h2 className="font-semibold mb-3">Cumulative P&L</h2>
<ResponsiveContainer width="100%" height={220}>
<AreaChart data={cumData}>
<defs>
<linearGradient id="cumTrade" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={cum >= 0 ? '#10b981' : '#ef4444'} stopOpacity={0.3} />
<stop offset="95%" stopColor={cum >= 0 ? '#10b981' : '#ef4444'} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
<XAxis dataKey="time" tick={{ fill: '#6b7280', fontSize: 10 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={v => `$${v}`} />
<Tooltip content={<ChartTooltip />} />
<Area type="monotone" dataKey="cumPnl" stroke={cum >= 0 ? '#10b981' : '#ef4444'} strokeWidth={2} fill="url(#cumTrade)" name="Cumulative P&L" />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
)}
{/* Orders Table */}
<div className="card overflow-x-auto"><h2 className="font-semibold mb-3">Orders</h2>
<table><thead><tr><th>ID</th><th>Side</th><th>Outcome</th><th>Price</th><th>Size</th><th>Status</th><th>Paper</th><th>Time</th></tr></thead><tbody>
{orders.length === 0 ? <tr><td colSpan={8} className="text-center text-gray-500 py-8">No orders yet</td></tr>
: orders.map(o => <tr key={o.order_id}>
<td className="font-mono text-xs">{o.order_id.substring(0, 12)}</td>
<td><span className={`badge ${o.side === 'buy' ? 'bg-green' : 'bg-red'}`}>{o.side?.toUpperCase()}</span></td>
<td>{o.outcome}</td><td className="font-mono">${parseFloat(o.price).toFixed(4)}</td>
<td className="font-mono">${parseFloat(o.size_usd || 0).toFixed(2)}</td>
<td><span className={`badge ${o.status === 'filled' ? 'bg-green' : o.status === 'rejected' ? 'bg-red' : 'bg-yellow'}`}>{o.status}</span></td>
<td>{o.paper_trade ? <span className="badge bg-yellow">PAPER</span> : <span className="badge bg-green">LIVE</span>}</td>
<td className="text-gray-400 text-sm">{new Date(o.submitted_ts).toLocaleString()}</td>
</tr>)}
</tbody></table>
</div>
{/* Trades Table */}
{trades.length > 0 && (
<div className="card overflow-x-auto"><h2 className="font-semibold mb-3">Trade Fills</h2>
<table><thead><tr><th>Time</th><th>Size</th><th>Price</th><th>Fee</th><th>P&L</th></tr></thead><tbody>
{trades.map((t, i) => <tr key={i}>
<td className="text-gray-400 text-sm">{t.match_time ? new Date(t.match_time).toLocaleString() : '-'}</td>
<td className="font-mono">${parseFloat(t.size_usd || 0).toFixed(2)}</td>
<td className="font-mono">${parseFloat(t.fill_price || 0).toFixed(4)}</td>
<td className="font-mono text-yellow-400">${parseFloat(t.fee_usd || 0).toFixed(4)}</td>
<td className={`font-mono ${parseFloat(t.pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>${parseFloat(t.pnl || 0).toFixed(2)}</td>
</tr>)}
</tbody></table>
</div>
)}
</div>
);
}