diff --git a/lib/master-protocol.ts b/lib/master-protocol.ts index 53d3457..97e4ae8 100644 --- a/lib/master-protocol.ts +++ b/lib/master-protocol.ts @@ -28,6 +28,19 @@ export interface AccountTypeStats { * calendar only projects accounts still on the winning path. Optional so * an older master simply ignores it. */ daysHist?: Record; + /** One entry per live account (negative ones included, dead ones not): + * its actual progress toward the current target, so the master can + * simulate the real required profit instead of assuming the winning + * path. A withdrawal resets the path, so days/maxDay/sum cover only the + * current cycle (client.dailyPnL is already the active-cycle slice), + * while profit stays the full equity profit. withdrawals > 0 tells the + * master the trail locked long ago; target/consistency/minDays carry + * this cycle's effective values from resolveEffectiveConfig. Optional so + * an older master simply ignores it. */ + payoutStates?: { + days: number; profit: number; maxDay: number; sum: number; high: number; + withdrawals?: number; target?: number; consistency?: number; minDays?: number; + }[]; } export interface FirmStats { diff --git a/lib/reporter.ts b/lib/reporter.ts index 4140415..1e34777 100644 --- a/lib/reporter.ts +++ b/lib/reporter.ts @@ -17,7 +17,7 @@ import { getClients } from './clients'; import { getTradableFirms } from './firm-catalog'; import { getAccountConfig } from './account-type-resolver'; import { getSchedulerStatus } from './auto-trade'; -import { combineCmeSessions, computeDayPlan, currentTradeDateCT, LEG_COMPLETION_EPSILON } from './trading-logic'; +import { combineCmeSessions, computeDayPlan, currentTradeDateCT, LEG_COMPLETION_EPSILON, resolveEffectiveConfig } from './trading-logic'; import type { AccountTypeStats } from './master-protocol'; function collectFirmStats() { @@ -95,6 +95,45 @@ function collectFirmStats() { hist[combined.daysTraded] = (hist[combined.daysTraded] ?? 0) + 1; } + // Exact payout state for every live account — negative ones + // included, since the master sims their real required profit. + // client.dailyPnL is already the active-cycle slice, so a + // withdrawal resets days/maxDay/sum here automatically. + if (cfg && combined) { + const r2 = (n: number) => Math.round(n * 100) / 100; + const equityProfit = r2(cash.amount - cfg.accountSize); + const qualifying = cfg.minDayPnL === 0 + ? combined.dailyPnL + : combined.dailyPnL.filter((d) => d.pnl >= cfg.minDayPnL); + const sum = r2(combined.dailyPnL.reduce((s, d) => s + d.pnl, 0)); + // End-of-day equity-profit high-water mark for this cycle, + // floored at the $0 start: walk the P&L from the cycle base. + let run = r2(equityProfit - sum); + let highWater = Math.max(0, run); + for (const d of combined.dailyPnL) { + run = r2(run + d.pnl); + if (run > highWater) highWater = run; + } + // Effective target for this cycle: withdrawal stages applied. + const fundTxns = client.fundTransactions?.[acc.id] ?? []; + const effective = resolveEffectiveConfig( + cfg.profitTarget, cfg.consistency, cfg.minTradingDays, + cfg.targetSameEquity, cfg.withdrawalStages, + client.priorProfit?.[acc.id] ?? 0, fundTxns, + ); + (slice.payoutStates ??= []).push({ + days: qualifying.length, + profit: equityProfit, + maxDay: qualifying.length > 0 ? Math.max(...qualifying.map((d) => d.pnl)) : 0, + sum, + high: highWater, + withdrawals: fundTxns.filter((f) => f.amount < 0).length, + target: effective.profitTarget, + consistency: effective.consistency, + minDays: effective.minTradingDays, + }); + } + if (client.positions[acc.id]) { inTrade++; accountsTraded++;