From 79ee5773cb62992f6a0c6cc39b7376c7c01c61df Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Thu, 10 Sep 2026 16:43:27 -0500 Subject: [PATCH] Report each live account's payout state to the master MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit payoutStates: one entry per live account (negative included, dead not) with its current-cycle progress — qualifying days, full equity profit, max qualifying day, P&L sum, and the cycle's end-of-day high-water mark — plus the withdrawal count and the cycle's effective target/consistency/ min-days from resolveEffectiveConfig. client.dailyPnL is already the active-cycle slice, so a withdrawal resets the reported path on its own. Feeds the master's payout calendar; an older master ignores the field. Co-Authored-By: Claude Fable 5 --- lib/master-protocol.ts | 13 +++++++++++++ lib/reporter.ts | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) 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++;