P1.1: Report each account's status to the master's ledger

Every FirmStats now carries statusVersion 1, the catalog firmId and, once
synced, syncedAt (from lastSyncAt), including the no-accounts branch, which
is now built by emptyFirmStats(). Each accounts[] entry adds status, reason,
balance, floor, accountTypeId, firstFundDate, positiveFunds and brokerStatus,
each left out when null or at its default.

accountStatus() in platform-client.ts is pure: a missing balance is unknown
(cash_missing), never dead; with a balance, dead is exactly the old floor
test (legacyDead); then history_pending, locked, live. Only the reporter
uses it. The aggregates, totals and every trading decision keep their own
dead test, unchanged.

The reporter reads history readiness through a new Tradovate
peekHistoryReady(), because historyReady() there can start a hold and move
the balance baseline. DXTrade exposes accountStatus and registrationTime via
brokerAccountMeta(). A throw in the new fields reports the account as
unknown instead of losing the report.

sendReport skips the socket for a firms payload over 900,000 chars (the
master's frame cap is 1,000,000 bytes) and POSTs it, logging once per switch.

scripts/check-account-status.mjs proves the legacy invariant over the full
matrix and the size budget (worst 228 B added per account, 154 B typical
live; 2000 accounts 592,501 B).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-06 14:36:55 -05:00
co-authored by Claude Opus 5.5
parent 8747dd6b08
commit 0c3a36845a
7 changed files with 527 additions and 18 deletions
+35 -2
View File
@@ -107,8 +107,41 @@ export interface FirmStats {
* withdrawals = how many times the balance has gone down for a payout
* (negative fund transactions), which tells the master a completed
* cycle's money is out. Omitted while the account's fund history hasn't
* loaded, so a restart can't read as zero withdrawals. */
accounts?: { name: string; size: number; payoutable: boolean | null; withdrawals?: number }[];
* loaded, so a restart can't read as zero withdrawals.
*
* The rest come from statusVersion builds (platform-client.ts
* accountStatus), each left out when null or at its default. status:
* unknown when the balance is missing (reason cash_missing — never dead),
* dead on the floor test, history_pending, locked when the firm stopped
* it trading (reason says how), else live. balance and floor (0 = none)
* in dollars; accountTypeId = the resolved catalog type (absent =
* unassigned); firstFundDate = the earliest positive fund row, YYYY-MM-DD
* (DXtrade: the registration date when there is none); positiveFunds
* counts those rows, absent until the fund history loads; brokerStatus =
* the broker's own flags (Tradovate closed/archived/restricted, DXtrade
* accountStatus, TopstepX canTrade:false). */
accounts?: {
name: string; size: number; payoutable: boolean | null; withdrawals?: number;
status?: 'live' | 'locked' | 'dead' | 'history_pending' | 'unknown';
reason?: string;
balance?: number;
floor?: number;
accountTypeId?: string;
firstFundDate?: string;
positiveFunds?: number;
brokerStatus?: string;
}[];
/** 1 on every firm from a build that sends the account status fields,
* including one with no accounts. Absent = an older build: its accounts
* are unknown, never dead. An empty accounts[] from a statusVersion build
* is a synced firm with no accounts. Optional. */
statusVersion?: 1;
/** The catalog firm's uuid (statusVersion builds), so the master needn't
* resolve the name. Optional. */
firmId?: string;
/** ISO time the firm's last good sync finished (statusVersion builds), sent
* once synced. The statuses count only while it is recent. Optional. */
syncedAt?: string;
/** Per account type, summing to the firm's own totals. Optional so a master
* on an older build simply ignores it. */
accountTypes?: AccountTypeStats[];