Report each live account's payout state to the master

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 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-10 16:43:27 -05:00
co-authored by Claude Fable 5
parent abfb703f0e
commit 79ee5773cb
2 changed files with 53 additions and 1 deletions
+13
View File
@@ -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<number, number>;
/** 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 {
+40 -1
View File
@@ -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++;