← back to Ken
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>
);
}