Add per-stage withdrawal targets with consistency and min trading days
- Add withdrawal stage system: each stage defines profit target, consistency, and min trading days for post-withdrawal challenge cycles - Target Same Equity mode accounts for withdrawn amounts when computing effective profit target (profitTarget - remainingProfit) - Store fund transaction timestamps for time-aware cycle filtering (withdrawals before 9 AM CT include that day in new cycle) - Expose full P&L history (fullDailyPnL) for calendar/equity curve display across all cycles, with DB fallback for pre-restart data - Show stage number (#1, #2, etc.) on calendar cells - Hide consistency reference line when consistency is 0% or 100% - Settings UI: "After First W/D" column with same-equity checkbox, expandable stage sub-rows with profit/consistency/days inputs - Default target_same_equity to 1 for new and existing account configs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
c54073e4b8
commit
70b1362d3e
+40
-20
@@ -86,7 +86,7 @@ const MONTH_NAMES = [
|
||||
];
|
||||
const DOW_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
|
||||
function CalendarMonth({ year, month, pnlMap, fundMap }: { year: number; month: number; pnlMap: Map<string, number>; fundMap: Map<string, number> }) {
|
||||
function CalendarMonth({ year, month, pnlMap, fundMap, stageMap }: { year: number; month: number; pnlMap: Map<string, number>; fundMap: Map<string, number>; stageMap: Map<string, number> }) {
|
||||
const daysInMonth = new Date(year, month, 0).getDate();
|
||||
const firstDow = new Date(year, month - 1, 1).getDay(); // 0 = Sunday
|
||||
|
||||
@@ -133,12 +133,13 @@ function CalendarMonth({ year, month, pnlMap, fundMap }: { year: number; month:
|
||||
const key = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
const pnl = pnlMap.get(key);
|
||||
const fundAmt = fundMap.get(key);
|
||||
const stage = stageMap.get(key);
|
||||
const hasData = pnl !== undefined;
|
||||
const positive = hasData && pnl! >= 0;
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={`rounded-lg px-1.5 pt-1.5 pb-2 min-h-14 flex flex-col ${
|
||||
className={`rounded-lg px-1.5 pt-1.5 pb-2 min-h-14 flex flex-col relative ${
|
||||
hasData
|
||||
? positive
|
||||
? 'bg-green-50 border border-green-100'
|
||||
@@ -148,11 +149,18 @@ function CalendarMonth({ year, month, pnlMap, fundMap }: { year: number; month:
|
||||
: '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') : fundAmt !== undefined ? 'text-amber-700' : 'text-slate-400'
|
||||
}`}>
|
||||
{day}
|
||||
</span>
|
||||
<div className="flex items-start justify-between">
|
||||
<span className={`text-[11px] font-medium leading-none ${
|
||||
hasData ? (positive ? 'text-green-700' : 'text-red-600') : fundAmt !== undefined ? 'text-amber-700' : 'text-slate-400'
|
||||
}`}>
|
||||
{day}
|
||||
</span>
|
||||
{stage !== undefined && (
|
||||
<span className="text-[8px] font-semibold text-slate-400 leading-none">
|
||||
#{stage}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{hasData && (
|
||||
<span className={`text-[11px] font-bold tabular-nums leading-tight ${
|
||||
positive ? 'text-green-700' : 'text-red-600'
|
||||
@@ -220,8 +228,9 @@ export default function AccountPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// dailyPnL comes from state — same source as dailyTarget, no separate fetch needed.
|
||||
// dailyPnL = current cycle (for targets); fullDailyPnL = all history (for display)
|
||||
const dailyPnL = account.dailyPnL;
|
||||
const displayPnL = account.fullDailyPnL ?? account.dailyPnL;
|
||||
const fundTransactions = account.fundTransactions ?? [];
|
||||
const fundMap = new Map(fundTransactions.map((f) => [f.date, f.amount]));
|
||||
|
||||
@@ -245,17 +254,17 @@ export default function AccountPage() {
|
||||
|
||||
// Consistency target: the total profit level at which the best day no longer
|
||||
// violates the consistency ratio. Only meaningful once a positive day exists.
|
||||
const maxDayPnL = dailyPnL.length > 0 ? Math.max(...dailyPnL.filter(d => d.pnl > 0).map(d => d.pnl)) : 0;
|
||||
const consistencyTarget = cfg && cfg.consistency > 0 && maxDayPnL > 0
|
||||
const maxDayPnL = displayPnL.length > 0 ? Math.max(...displayPnL.filter(d => d.pnl > 0).map(d => d.pnl)) : 0;
|
||||
const consistencyTarget = cfg && cfg.consistency > 0 && cfg.consistency < 1 && maxDayPnL > 0
|
||||
? Math.round(maxDayPnL / cfg.consistency * 100) / 100
|
||||
: null;
|
||||
|
||||
// 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 allDates = new Set([...displayPnL.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 d of displayPnL) 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 });
|
||||
@@ -304,10 +313,21 @@ export default function AccountPage() {
|
||||
const strokeGradId = 'equityStroke';
|
||||
|
||||
// Build calendar data
|
||||
const pnlMap = new Map(dailyPnL.map((d) => [d.date, d.pnl]));
|
||||
const allCalendarDates = [...dailyPnL.map((d) => d.date), ...fundTransactions.map((f) => f.date)];
|
||||
const pnlMap = new Map(displayPnL.map((d) => [d.date, d.pnl]));
|
||||
const allCalendarDates = [...displayPnL.map((d) => d.date), ...fundTransactions.map((f) => f.date)];
|
||||
const calendarMonths = [...new Set(allCalendarDates.map((d) => d.slice(0, 7)))].sort();
|
||||
|
||||
// Build stage map: stage increments after each withdrawal (negative fund txn)
|
||||
const sortedWithdrawalDates = fundTransactions
|
||||
.filter((f) => f.amount < 0)
|
||||
.map((f) => f.date)
|
||||
.sort();
|
||||
const stageMap = new Map<string, number>();
|
||||
for (const d of displayPnL) {
|
||||
const stage = sortedWithdrawalDates.filter((wd) => wd < d.date).length + 1;
|
||||
stageMap.set(d.date, stage);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
@@ -388,13 +408,13 @@ export default function AccountPage() {
|
||||
<div className="bg-white border border-slate-200 rounded-xl shadow-sm p-6 mb-5">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-slate-900 font-bold text-base">Equity Curve</h2>
|
||||
{dailyPnL.length > 0 && (
|
||||
{displayPnL.length > 0 && (
|
||||
<span className={`text-sm font-bold tabular-nums ${isPositive ? 'text-green-600' : 'text-red-500'}`}>
|
||||
{isPositive ? '+' : ''}${fmt(totalProfit)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{dailyPnL.length === 0 ? (
|
||||
{displayPnL.length === 0 ? (
|
||||
<p className="text-slate-400 italic text-sm text-center py-8">No trading history available</p>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={260}>
|
||||
@@ -485,7 +505,7 @@ export default function AccountPage() {
|
||||
strokeDasharray="4 3"
|
||||
label={{
|
||||
value: 'W/D',
|
||||
position: 'insideTopRight',
|
||||
position: 'insideBottomRight',
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
fill: '#d97706',
|
||||
@@ -552,14 +572,14 @@ 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} fundMap={fundMap} />;
|
||||
return <CalendarMonth key={ym} year={y} month={m} pnlMap={pnlMap} fundMap={fundMap} stageMap={stageMap} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cash History Table */}
|
||||
{(dailyPnL.length > 0 || fundTransactions.length > 0) && (
|
||||
{(displayPnL.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">
|
||||
@@ -573,7 +593,7 @@ export default function AccountPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
...dailyPnL.map((d) => ({ date: d.date, type: 'Trade' as const, amount: d.pnl })),
|
||||
...displayPnL.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))
|
||||
|
||||
Reference in New Issue
Block a user