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:
co-authored by
Claude Sonnet 4.6
parent
6d960ceb7b
commit
4252adfbea
+99
-13
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user