diff --git a/app/accounts/[id]/page.tsx b/app/accounts/[id]/page.tsx index 84f5561..07adcfd 100644 --- a/app/accounts/[id]/page.tsx +++ b/app/accounts/[id]/page.tsx @@ -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; fundMap: Map }) { +function CalendarMonth({ year, month, pnlMap, fundMap, stageMap }: { year: number; month: number; pnlMap: Map; fundMap: Map; stageMap: Map }) { 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 (
- - {day} - +
+ + {day} + + {stage !== undefined && ( + + #{stage} + + )} +
{hasData && ( [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(); - 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(); + for (const d of displayPnL) { + const stage = sortedWithdrawalDates.filter((wd) => wd < d.date).length + 1; + stageMap.set(d.date, stage); + } + return (
@@ -388,13 +408,13 @@ export default function AccountPage() {

Equity Curve

- {dailyPnL.length > 0 && ( + {displayPnL.length > 0 && ( {isPositive ? '+' : ''}${fmt(totalProfit)} )}
- {dailyPnL.length === 0 ? ( + {displayPnL.length === 0 ? (

No trading history available

) : ( @@ -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() {
{calendarMonths.map((ym) => { const [y, m] = ym.split('-').map(Number); - return ; + return ; })}
)} {/* Cash History Table */} - {(dailyPnL.length > 0 || fundTransactions.length > 0) && ( + {(displayPnL.length > 0 || fundTransactions.length > 0) && (

Cash History

@@ -573,7 +593,7 @@ export default function AccountPage() { {[ - ...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)) diff --git a/app/api/account-configs/[id]/route.ts b/app/api/account-configs/[id]/route.ts index 7ba3326..c70f8cd 100644 --- a/app/api/account-configs/[id]/route.ts +++ b/app/api/account-configs/[id]/route.ts @@ -21,9 +21,11 @@ export async function PUT( accountSize?: number; maxLoss?: number; maxPositionSize?: number; + targetSameEquity?: boolean; + withdrawalStages?: { profit: number; consistency: number; minTradingDays: number }[]; }; - const { prefix, profitTarget, consistency, minDayPnL, minTradingDays, accountSize, maxLoss, maxPositionSize } = body; + const { prefix, profitTarget, consistency, minDayPnL, minTradingDays, accountSize, maxLoss, maxPositionSize, targetSameEquity, withdrawalStages } = body; if ( typeof prefix !== 'string' || !prefix.trim() || @@ -46,6 +48,8 @@ export async function PUT( accountSize, maxLoss: maxLoss ?? 0, maxPositionSize: maxPositionSize ?? 0, + targetSameEquity: targetSameEquity ?? false, + withdrawalStages: withdrawalStages ?? [], }); if (!updated) { diff --git a/app/api/firms/[id]/accounts/route.ts b/app/api/firms/[id]/accounts/route.ts index 1992399..4441548 100644 --- a/app/api/firms/[id]/accounts/route.ts +++ b/app/api/firms/[id]/accounts/route.ts @@ -25,9 +25,11 @@ export async function POST( accountSize?: number; maxLoss?: number; maxPositionSize?: number; + targetSameEquity?: boolean; + withdrawalStages?: { profit: number; consistency: number; minTradingDays: number }[]; }; - const { prefix, profitTarget, consistency, minDayPnL, minTradingDays, accountSize, maxLoss, maxPositionSize } = body; + const { prefix, profitTarget, consistency, minDayPnL, minTradingDays, accountSize, maxLoss, maxPositionSize, targetSameEquity, withdrawalStages } = body; if ( typeof prefix !== 'string' || !prefix.trim() || @@ -50,6 +52,8 @@ export async function POST( accountSize, maxLoss: maxLoss ?? 0, maxPositionSize: maxPositionSize ?? 0, + targetSameEquity: targetSameEquity ?? false, + withdrawalStages: withdrawalStages ?? [], }); return NextResponse.json({ @@ -62,6 +66,8 @@ export async function POST( accountSize: row.account_size, maxLoss: row.max_loss, maxPositionSize: row.max_position_size, + targetSameEquity: row.target_same_equity === 1, + withdrawalStages: (() => { try { return JSON.parse(row.withdrawal_stages ?? '[]') as { profit: number; consistency: number; minTradingDays: number }[]; } catch { return []; } })(), }, { status: 201 }); } catch (err) { console.error('[POST /api/firms/:id/accounts]', err); diff --git a/app/api/firms/[id]/route.ts b/app/api/firms/[id]/route.ts index e6d0759..004b411 100644 --- a/app/api/firms/[id]/route.ts +++ b/app/api/firms/[id]/route.ts @@ -33,6 +33,8 @@ export async function GET( accountSize: a.account_size, maxLoss: a.max_loss, maxPositionSize: a.max_position_size, + targetSameEquity: a.target_same_equity === 1, + withdrawalStages: (() => { try { return JSON.parse(a.withdrawal_stages ?? '[]') as { profit: number; consistency: number; minTradingDays: number }[]; } catch { return []; } })(), })), }); } diff --git a/app/api/state/route.ts b/app/api/state/route.ts index a1265c7..954e6e9 100644 --- a/app/api/state/route.ts +++ b/app/api/state/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server'; -import { getFirms } from '@/lib/db'; +import { getFirms, loadDailyPnL } from '@/lib/db'; import { getClients } from '@/lib/clients'; -import { computeDailyTarget } from '@/lib/trading-logic'; +import { computeDailyTarget, resolveEffectiveConfig } from '@/lib/trading-logic'; import type { AccountConfigRow } from '@/lib/db'; function getAccountConfig(name: string, accounts: AccountConfigRow[]): AccountConfigRow | undefined { @@ -30,16 +30,32 @@ export async function GET() { const cfg = getAccountConfig(acc.name, f.accounts); const autoLiqThreshold = client.autoLiqThresholds[acc.id] ?? 0; const isDead = autoLiqThreshold > 0 && cash.amount <= autoLiqThreshold; + const allFundTxns = client.fundTransactions?.[acc.id] ?? []; + const priorProfit = client.priorProfit?.[acc.id] ?? 0; + // Hide initial funding (amount === accountSize) from display + const displayFundTxns = cfg + ? allFundTxns.filter((f) => f.amount !== cfg.account_size) + : allFundTxns; let targetHit = false; let dailyTarget: { amount: number; path: 'first_day' | 'normal_day' | 'reduced_day' } | null = null; if (cfg && !isDead) { - const target = computeDailyTarget(cfg.profit_target, cfg.consistency, totalProfit, dailyPnL, cfg.min_day_pnl, cfg.min_trading_days); + const withdrawalStages: { profit: number; consistency: number; minTradingDays: number }[] = (() => { try { return JSON.parse(cfg.withdrawal_stages ?? '[]'); } catch { return []; } })(); + const effective = resolveEffectiveConfig( + cfg.profit_target, + cfg.consistency, + cfg.min_trading_days, + cfg.target_same_equity === 1, + withdrawalStages, + priorProfit, + allFundTxns + ); + const target = computeDailyTarget(effective.profitTarget, effective.consistency, totalProfit, dailyPnL, cfg.min_day_pnl, effective.minTradingDays); dailyTarget = target; // Condition 1: profit target already exceeded (target=0), still need days → any activity counts // Condition 2: target > 0 → must have made at least the computed daily target targetHit = // If we are just flipping take any activity as target hit - (target.amount === 0 && Math.abs(cash.realizedPnL) > 0 && client.daysTraded[acc.id] <= cfg.min_trading_days) || + (target.amount === 0 && Math.abs(cash.realizedPnL) > 0 && client.daysTraded[acc.id] <= effective.minTradingDays) || (cash.realizedPnL >= target.amount); } @@ -56,7 +72,8 @@ export async function GET() { targetHit, dailyTarget, dailyPnL, - fundTransactions: client.fundTransactions[acc.id] ?? [], + fullDailyPnL: client.fullDailyPnL?.[acc.id] ?? loadDailyPnL(acc.id), + fundTransactions: displayFundTxns, }; }); return { firm: f.name, connected: true, accounts, perContractFees: client.perContractFees }; diff --git a/app/firms/[id]/settings/page.tsx b/app/firms/[id]/settings/page.tsx index 2c48594..786f79a 100644 --- a/app/firms/[id]/settings/page.tsx +++ b/app/firms/[id]/settings/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useRouter, useParams } from 'next/navigation'; const SIZE_PRESETS = [5_000, 10_000, 25_000, 50_000, 75_000, 100_000, 150_000]; @@ -14,6 +14,8 @@ interface AccountConfig { minTradingDays: number; accountSize: number; maxPositionSize: number; + targetSameEquity: boolean; + withdrawalStages: { profit: number; consistency: number; minTradingDays: number }[]; } interface FirmConfig { @@ -51,6 +53,8 @@ function newRow(): RowState { minTradingDays: 5, accountSize: 50_000, maxPositionSize: 0, + targetSameEquity: true, + withdrawalStages: [], dirty: true, saving: false, error: '', @@ -148,6 +152,8 @@ export default function FirmSettingsPage() { minTradingDays: row.minTradingDays, accountSize: row.accountSize, maxPositionSize: row.maxPositionSize, + targetSameEquity: row.targetSameEquity, + withdrawalStages: row.withdrawalStages, }; if (row.id < 0) { @@ -257,20 +263,22 @@ export default function FirmSettingsPage() { Min Day P&L Min Trading Days Max Contracts + After First W/D {rows.length === 0 && firmName && ( - + No account types yet )} {rows.map((row) => ( - + + {/* Prefix */} @@ -405,6 +413,21 @@ export default function FirmSettingsPage() { /> + {/* After Withdrawal */} + + + + {/* Actions */}
@@ -449,11 +472,114 @@ export default function FirmSettingsPage() {
+ + {/* Stage sub-rows — shown when targetSameEquity is OFF */} + {!row.targetSameEquity && ( + <> + {row.withdrawalStages.map((stage, idx) => { + const isLast = idx === row.withdrawalStages.length - 1; + const showPlus = isLast && row.withdrawalStages.length > 1; + return ( + + +
+ + Stage {idx + 2}{showPlus ? '+' : ''} + +
+ $ + { + const next = row.withdrawalStages.map((s, i) => + i === idx ? { ...s, profit: Number(e.target.value) } : s + ); + updateRow(row.id, { withdrawalStages: next }); + }} + /> +
+ {row.consistency > 0 && ( +
+ { + const next = row.withdrawalStages.map((s, i) => + i === idx ? { ...s, consistency: Number(e.target.value) / 100 } : s + ); + updateRow(row.id, { withdrawalStages: next }); + }} + /> + % +
+ )} +
+ { + const next = row.withdrawalStages.map((s, i) => + i === idx ? { ...s, minTradingDays: Number(e.target.value) } : s + ); + updateRow(row.id, { withdrawalStages: next }); + }} + /> + days +
+ +
+ + + ); + })} + + + + + + + )} +
))} {/* Add row */} - +