Show fund transactions (W/D) on calendar, equity curve, and cash history table

- Persist fund transactions to SQLite (fund_transactions table) so they
  survive beyond Tradovate's 28-day report window
- Calendar: highlight W/D dates in amber with the amount shown below the day
- Equity curve: reduce running equity at withdrawal dates and show a vertical
  dashed amber line labelled W/D
- New Cash History table below calendar listing all trades and W/D events
  sorted newest-first

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-03-21 04:16:24 -05:00
co-authored by Claude Sonnet 4.6
parent 6d960ceb7b
commit 4252adfbea
5 changed files with 144 additions and 18 deletions
+99 -13
View File
@@ -86,7 +86,7 @@ const MONTH_NAMES = [
];
const DOW_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
function CalendarMonth({ year, month, pnlMap }: { year: number; month: number; pnlMap: Map<string, number> }) {
function CalendarMonth({ year, month, pnlMap, fundMap }: { year: number; month: number; pnlMap: Map<string, number>; fundMap: Map<string, number> }) {
const daysInMonth = new Date(year, month, 0).getDate();
const firstDow = new Date(year, month - 1, 1).getDay(); // 0 = Sunday
@@ -132,6 +132,7 @@ function CalendarMonth({ year, month, pnlMap }: { year: number; month: number; p
if (day === null) return <div key={`empty-${i}`} />;
const key = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const pnl = pnlMap.get(key);
const fundAmt = fundMap.get(key);
const hasData = pnl !== undefined;
const positive = hasData && pnl! >= 0;
return (
@@ -142,11 +143,13 @@ function CalendarMonth({ year, month, pnlMap }: { year: number; month: number; p
? positive
? 'bg-green-50 border border-green-100'
: 'bg-red-50 border border-red-100'
: 'bg-slate-50 border border-transparent'
: fundAmt !== undefined
? 'bg-amber-50 border border-amber-100'
: 'bg-slate-50 border border-transparent'
}`}
>
<span className={`text-[11px] font-medium leading-none mb-1.5 ${
hasData ? (positive ? 'text-green-700' : 'text-red-600') : 'text-slate-400'
hasData ? (positive ? 'text-green-700' : 'text-red-600') : fundAmt !== undefined ? 'text-amber-700' : 'text-slate-400'
}`}>
{day}
</span>
@@ -157,6 +160,11 @@ function CalendarMonth({ year, month, pnlMap }: { year: number; month: number; p
{positive ? '+' : ''}${fmt(Math.abs(pnl!))}
</span>
)}
{fundAmt !== undefined && (
<span className="text-[10px] font-semibold tabular-nums leading-tight mt-0.5 text-amber-600">
W/D {fundAmt >= 0 ? '+' : ''}${fmt(Math.abs(fundAmt))}
</span>
)}
</div>
);
})}
@@ -214,6 +222,8 @@ export default function AccountPage() {
// dailyPnL comes from state — same source as dailyTarget, no separate fetch needed.
const dailyPnL = account.dailyPnL;
const fundTransactions = account.fundTransactions ?? [];
const fundMap = new Map(fundTransactions.map((f) => [f.date, f.amount]));
const hasLossLimit = cfg != null && cfg.minDayPnL !== -999;
const daysPassed = cfg != null && account.daysTraded >= cfg.minTradingDays;
@@ -240,22 +250,40 @@ export default function AccountPage() {
? Math.round(maxDayPnL / cfg.consistency * 100) / 100
: null;
// Build equity curve: FIFO daily increments, origin at $0
// Build equity curve: FIFO daily increments + withdrawal step-downs, origin at $0
// Merge daily P&L and fund transactions into a single sorted timeline
type EquityEvent = { date: string; pnl?: number; fundAmt?: number };
const allDates = new Set([...dailyPnL.map((d) => d.date), ...fundTransactions.map((f) => f.date)]);
const eventsByDate = new Map<string, EquityEvent>();
for (const d of dailyPnL) eventsByDate.set(d.date, { date: d.date, pnl: d.pnl });
for (const f of fundTransactions) {
const existing = eventsByDate.get(f.date);
eventsByDate.set(f.date, { ...existing, date: f.date, fundAmt: f.amount });
}
const sortedEvents = [...allDates].sort().map((d) => eventsByDate.get(d)!);
let running = 0;
const equityData = [
{ label: '', equity: 0, pnl: 0, origin: true, pos: 0, neg: 0 },
...dailyPnL.map((d) => {
running += d.pnl;
{ label: '', equity: 0, pnl: 0, origin: true, pos: 0, neg: 0, isWithdrawal: false },
...sortedEvents.map((ev) => {
if (ev.fundAmt !== undefined) running += ev.fundAmt; // withdrawals reduce equity
if (ev.pnl !== undefined) running += ev.pnl;
const equity = Math.round(running * 100) / 100;
return {
label: fmtDate(d.date),
label: fmtDate(ev.date),
equity,
pnl: d.pnl,
pos: Math.max(0, equity), // above-zero portion for green fill
neg: Math.min(0, equity), // below-zero portion for red fill
pnl: ev.pnl ?? 0,
pos: Math.max(0, equity),
neg: Math.min(0, equity),
isWithdrawal: ev.fundAmt !== undefined,
fundAmt: ev.fundAmt,
date: ev.date,
};
}),
];
// Dates with fund transactions that appear in the chart (for vertical reference lines)
const withdrawalLabels = equityData.filter((d) => d.isWithdrawal).map((d) => d.label);
const isPositive = totalProfit >= 0;
// Equity range
@@ -277,7 +305,8 @@ export default function AccountPage() {
// Build calendar data
const pnlMap = new Map(dailyPnL.map((d) => [d.date, d.pnl]));
const calendarMonths = [...new Set(dailyPnL.map((d) => d.date.slice(0, 7)))].sort();
const allCalendarDates = [...dailyPnL.map((d) => d.date), ...fundTransactions.map((f) => f.date)];
const calendarMonths = [...new Set(allCalendarDates.map((d) => d.slice(0, 7)))].sort();
return (
<div className="min-h-screen bg-slate-50 p-8">
@@ -446,6 +475,23 @@ export default function AccountPage() {
}}
/>
)}
{/* Vertical lines at fund transaction dates */}
{withdrawalLabels.map((lbl) => (
<ReferenceLine
key={`wd-${lbl}`}
x={lbl}
stroke="#f59e0b"
strokeWidth={1.5}
strokeDasharray="4 3"
label={{
value: 'W/D',
position: 'insideTopRight',
fontSize: 10,
fontWeight: 700,
fill: '#d97706',
}}
/>
))}
{/* Green fill: positive equity only, fills down to y=0 */}
<Area
type="monotone"
@@ -506,12 +552,52 @@ export default function AccountPage() {
<div className="grid grid-cols-1 gap-8" style={{ gridTemplateColumns: `repeat(${Math.min(calendarMonths.length, 3)}, minmax(0, 1fr))` }}>
{calendarMonths.map((ym) => {
const [y, m] = ym.split('-').map(Number);
return <CalendarMonth key={ym} year={y} month={m} pnlMap={pnlMap} />;
return <CalendarMonth key={ym} year={y} month={m} pnlMap={pnlMap} fundMap={fundMap} />;
})}
</div>
</div>
)}
{/* Cash History Table */}
{(dailyPnL.length > 0 || fundTransactions.length > 0) && (
<div className="bg-white border border-slate-200 rounded-xl shadow-sm p-6 mt-5">
<h2 className="text-slate-900 font-bold text-base mb-4">Cash History</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-100">
<th className="text-left text-slate-400 font-medium py-2 pr-4">Date</th>
<th className="text-left text-slate-400 font-medium py-2 pr-4">Type</th>
<th className="text-right text-slate-400 font-medium py-2">Amount</th>
</tr>
</thead>
<tbody>
{[
...dailyPnL.map((d) => ({ date: d.date, type: 'Trade' as const, amount: d.pnl })),
...fundTransactions.map((f) => ({ date: f.date, type: 'W/D' as const, amount: f.amount })),
]
.sort((a, b) => b.date.localeCompare(a.date))
.map((row, i) => (
<tr key={i} className="border-b border-slate-50 last:border-0">
<td className="py-2.5 pr-4 text-slate-500 tabular-nums">{row.date}</td>
<td className="py-2.5 pr-4">
{row.type === 'W/D' ? (
<span className="text-xs font-semibold px-2 py-0.5 rounded bg-amber-100 text-amber-700">W/D</span>
) : (
<span className="text-xs font-semibold px-2 py-0.5 rounded bg-slate-100 text-slate-500">Trade</span>
)}
</td>
<td className={`py-2.5 text-right font-bold tabular-nums ${row.amount >= 0 ? 'text-green-600' : 'text-red-500'}`}>
{row.amount >= 0 ? '+' : ''}${fmt(Math.abs(row.amount))}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
</div>
);
+1
View File
@@ -56,6 +56,7 @@ export async function GET() {
targetHit,
dailyTarget,
dailyPnL,
fundTransactions: client.fundTransactions[acc.id] ?? [],
};
});
return { firm: f.name, connected: true, accounts, perContractFees: client.perContractFees };