Reporter: name the accounts whose final day completed their cycle

cycleCompletion() says whether today's P&L finished the current cycle
(profit target, consistency and minimum days all met) — projectPerfectPath's
completion probe run without and with today. The reporter counts those as
`completed` (a subset of targetHit) and sends their names, so the master
can keep an account a firm erases after it passes. Checked ahead of the
lockout branch: a firm locking a passed account is no DLL loss. The local
dashboard shows them with a purple Completed pill.

Because the master keeps a completion for good, doubtful reads stay
'none': weekends, a Monday with a weekend-dated row, the first minutes
after the 5 PM roll, and history that hasn't freshly loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-23 12:28:41 -05:00
co-authored by Claude Opus 5.5
parent a47a86a59c
commit acb8879575
8 changed files with 205 additions and 19 deletions
+57 -4
View File
@@ -18,8 +18,9 @@ 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, projectPerfectPath, resolveEffectiveConfig } from './trading-logic';
import { combineCmeSessions, computeDayPlan, currentTradeDateCT, cycleCompletion, LEG_COMPLETION_EPSILON, projectPerfectPath, resolveEffectiveConfig } from './trading-logic';
import type { AccountTypeStats } from './master-protocol';
import { historyFresh } from './platform-client';
function collectFirmStats() {
// Only firms this machine has credentials for. A firm it cannot log into
@@ -27,13 +28,16 @@ function collectFirmStats() {
// the master's table with rows for firms it does not run.
const firms = getTradableFirms();
const clients = getClients();
// One raw trade date for the whole report: the combined views below roll
// it forward, the completion check needs it as is.
const rawToday = currentTradeDateCT();
return firms.map((f) => {
const client = clients.get(f.id);
if (!client || client.accountList.length === 0) {
return {
firm: f.name, totalAccounts: 0, accountsTraded: 0, inTrade: 0,
targetHit: 0, partial: 0, loss: 0, personalDll: 0, accountTypes: [],
targetHit: 0, partial: 0, loss: 0, personalDll: 0, completed: 0, accountTypes: [],
};
}
@@ -44,6 +48,7 @@ function collectFirmStats() {
let partialCount = 0;
let lossCount = 0;
let personalDllCount = 0;
let completedCount = 0;
// Per account type, so the master can say what the accounts still to
// trade actually are. Keyed by account type id; unassigned accounts
@@ -91,7 +96,7 @@ function collectFirmStats() {
// projects from "on the winning path", which they are not.
// (Dead accounts never reach here — the auto-liq check above.)
const combined = cfg ? combineCmeSessions(
cfg.combineCmeHolidays, client.dailyPnL[acc.id] ?? [], currentTradeDateCT(), cash.realizedPnL,
cfg.combineCmeHolidays, client.dailyPnL[acc.id] ?? [], rawToday, cash.realizedPnL,
) : null;
// Stage-resolved config, shared by the payout state and the
@@ -208,6 +213,54 @@ function collectFirmStats() {
continue;
}
// Did today finish the cycle? Checked ahead of the lockout branch:
// firms lock a passed account (Tradovate liquidate-only, TopstepX
// canTrade=false), and a pass is no loss. Only once the account's
// history has loaded — right after a restart the missing rows and
// withdrawals would read as a fresh stage-1 cycle — and a 'today'
// only on freshly fetched rows, since the master keeps it.
const historyLoaded = client.dailyPnL[acc.id] !== undefined
&& client.fundTransactions?.[acc.id] !== undefined;
if (cfg && combined && effective && historyLoaded) {
const completion = cycleCompletion({
accountId: acc.id,
rawTradeDate: rawToday,
cycleDailyPnL: client.dailyPnL[acc.id],
rawRealizedPnL: cash.realizedPnL,
combineCmeHolidays: cfg.combineCmeHolidays,
effective,
equityProfit: cash.amount - cfg.accountSize,
splitEnabled: cfg.splitEnabled,
});
if (completion === 'today' && historyFresh(client, acc)) {
accountsTraded++;
slice.accountsTraded++;
targetHitCount++;
slice.targetHit = (slice.targetHit ?? 0) + 1;
completedCount++;
slice.completed = (slice.completed ?? 0) + 1;
if (dayEntry) {
dayEntry.targetHit++;
dayEntry.completed = (dayEntry.completed ?? 0) + 1;
}
// Same test as projectPerfectPath's `loops`: a cycle that
// keeps going pays out, one that stops just passes.
const kind = effective.drain ? 'drain'
: (cfg.payoutable === true || cfg.withdrawalStages.length > 0) ? 'payout' : 'pass';
(slice.completedAccounts ??= []).push({
name: acc.name,
date: combined.tradeDate,
stage: 1 + fundTxns.filter((f) => f.amount < 0).length,
kind,
});
continue;
}
// Passed on an earlier day and locked since: nothing traded
// today, so it reads as untraded — like a finished account
// that's still active — not as a DLL loss every day after.
if (completion === 'before' && !acc.active && combined.realizedPnL === 0) continue;
}
// DLL lockout: Tradovate flips the account to liquidate-only, so
// its day is over at a loss even when the booked realizedPnL
// hasn't caught up yet (or the account has no matching config).
@@ -269,7 +322,7 @@ function collectFirmStats() {
return {
firm: f.name, totalAccounts, accountsTraded, inTrade,
targetHit: targetHitCount, partial: partialCount, loss: lossCount,
personalDll: personalDllCount, accountTypes,
personalDll: personalDllCount, completed: completedCount, accountTypes,
};
});
}