← back to Bertha

react-dash/src/pages/Bankroll.jsx

154 lines

import React, { useState, useEffect } from 'react';
import { api } from '../api';
import { AreaChart, Area, LineChart, Line, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Cell, ComposedChart } 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 Bankroll() {
  const [ledger, setLe] = useState([]);
  const [sum, setSum] = useState({});
  const [loading, setL] = useState(true);

  useEffect(() => { api('/bankroll').then(d => { setLe(d.ledger || []); setSum(d.summary || {}); setL(false); }); }, []);

  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 ec = { deposit: 'bg-green', withdrawal: 'bg-red', trade_pnl: 'bg-blue', fee: 'bg-yellow', adjustment: 'bg-purple' };

  // Balance over time chart data
  const balanceData = ledger.slice().reverse().map((e, i) => ({
    idx: i,
    time: new Date(e.ts).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }),
    balance: parseFloat(e.balance_after || 0),
    amount: parseFloat(e.amount || 0),
    event: e.event_type,
  }));

  // Running P&L chart
  const pnlData = ledger.slice().reverse().filter(e => e.event_type === 'trade_pnl' || e.event_type === 'fee').map((e, i) => ({
    idx: i,
    time: new Date(e.ts).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
    pnl: parseFloat(e.amount || 0),
    event: e.event_type,
  }));

  // Cumulative P&L
  let cumPnl = 0;
  const cumData = pnlData.map(d => {
    cumPnl += d.pnl;
    return { ...d, cumPnl };
  });

  // Event type breakdown
  const eventCounts = {};
  ledger.forEach(e => { eventCounts[e.event_type] = (eventCounts[e.event_type] || 0) + 1; });
  const eventData = Object.entries(eventCounts).map(([name, count]) => ({ name, count }));

  const curBal = parseFloat(sum.current_balance || 50);
  const pnlPct = ((curBal - 50) / 50 * 100);

  return (
    <div className="space-y-6">
      <div><h1 className="text-2xl font-bold">Bankroll</h1><p className="text-gray-500 text-sm">Balance tracking and P&L 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">Balance</p><p className="stat-v text-green-400">${curBal.toFixed(2)}</p></div>
        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Deposits</p><p className="stat-v text-blue-400">${parseFloat(sum.total_deposits || 50).toFixed(2)}</p></div>
        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Total P&L</p><p className={`stat-v ${parseFloat(sum.total_pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>${parseFloat(sum.total_pnl || 0).toFixed(2)}</p></div>
        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Fees</p><p className="stat-v text-yellow-400">${parseFloat(sum.total_fees || 0).toFixed(2)}</p></div>
        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">ROI</p><p className={`stat-v ${pnlPct >= 0 ? 'text-green-400' : 'text-red-400'}`}>{pnlPct >= 0 ? '+' : ''}{pnlPct.toFixed(1)}%</p></div>
      </div>

      {/* Balance History Chart */}
      <div className="card">
        <h2 className="font-semibold mb-3">Balance History</h2>
        {balanceData.length > 1 ? (
          <ResponsiveContainer width="100%" height={280}>
            <AreaChart data={balanceData}>
              <defs>
                <linearGradient id="balGrad" x1="0" y1="0" x2="0" y2="1">
                  <stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
                  <stop offset="95%" stopColor="#10b981" stopOpacity={0} />
                </linearGradient>
              </defs>
              <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
              <XAxis dataKey="time" tick={{ fill: '#6b7280', fontSize: 10 }} axisLine={false} tickLine={false} interval="preserveStartEnd" />
              <YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} domain={['dataMin - 1', 'dataMax + 1']} tickFormatter={v => `$${v}`} />
              <Tooltip content={<ChartTooltip />} />
              <Area type="monotone" dataKey="balance" stroke="#10b981" strokeWidth={2} fill="url(#balGrad)" name="Balance" dot={false} />
            </AreaChart>
          </ResponsiveContainer>
        ) : <p className="text-gray-500 text-sm text-center py-12">Chart appears after multiple entries</p>}
      </div>

      {/* P&L Charts */}
      <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
        {/* Individual P&L events */}
        <div className="card">
          <h2 className="font-semibold mb-3">P&L Events</h2>
          {pnlData.length > 0 ? (
            <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>
          ) : <p className="text-gray-500 text-sm text-center py-12">No P&L events yet</p>}
        </div>

        {/* Cumulative P&L */}
        <div className="card">
          <h2 className="font-semibold mb-3">Cumulative P&L</h2>
          {cumData.length > 0 ? (
            <ResponsiveContainer width="100%" height={220}>
              <AreaChart data={cumData}>
                <defs>
                  <linearGradient id="cumGrad" x1="0" y1="0" x2="0" y2="1">
                    <stop offset="5%" stopColor={cumPnl >= 0 ? '#10b981' : '#ef4444'} stopOpacity={0.3} />
                    <stop offset="95%" stopColor={cumPnl >= 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={cumPnl >= 0 ? '#10b981' : '#ef4444'} strokeWidth={2} fill="url(#cumGrad)" name="Cumulative P&L" />
              </AreaChart>
            </ResponsiveContainer>
          ) : <p className="text-gray-500 text-sm text-center py-12">No P&L events yet</p>}
        </div>
      </div>

      {/* Ledger Table */}
      <div className="card overflow-x-auto"><h2 className="font-semibold mb-3">Ledger Entries</h2>
        <table><thead><tr><th>Time</th><th>Event</th><th>Amount</th><th>Balance</th><th>Note</th></tr></thead><tbody>
          {ledger.map((e, i) => <tr key={i}>
            <td className="text-gray-400 text-sm">{new Date(e.ts).toLocaleString()}</td>
            <td><span className={`badge ${ec[e.event_type] || 'bg-blue'}`}>{e.event_type}</span></td>
            <td className={`font-mono ${parseFloat(e.amount) >= 0 ? 'text-green-400' : 'text-red-400'}`}>{parseFloat(e.amount) >= 0 ? '+' : ''}${parseFloat(e.amount).toFixed(2)}</td>
            <td className="font-mono font-medium">${parseFloat(e.balance_after).toFixed(2)}</td>
            <td className="text-gray-400 text-sm max-w-[200px] truncate">{e.note || '-'}</td>
          </tr>)}
        </tbody></table>
      </div>
    </div>
  );
}