← back to Bertha

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>
  );
}