One-and-done counts confirmed fills; holds need a persistent refusal; wipes keep their yardstick

Fourth review round (52ed9c7):

- The one-and-done guard counts only entries the broker confirmed filled,
  so an entry that timed out or was rejected unfilled no longer benches the
  account for the day.
- TopstepX: an account is promoted only by a sync that actually checked its
  funds; one with no account type stays out (not traded) without costing a
  sync per pass, and once typed waits for that check. Wipe routes don't
  count untyped accounts as failures.
- Tradovate: an unchecked balance-hold release needs Cash History refused
  for a full hour straight (a 4xx other than 401/403/408/429, or an answer
  without data); any other outcome resets it, so one stray reply during a
  5xx outage can't release a payout.
- A wipe keeps the pre-wipe baseline and fund rows as its yardstick: the
  rebuild must explain today's balance against them, so a payout the
  rebuilt report hasn't booked yet stays held.
- The roll base of an unrolled record is kept per trade date in
  account_meta, so a restart later the same trade date counts from the same
  point instead of from a value that already includes the day's fills.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-24 20:46:35 -05:00
co-authored by Claude Opus 5.5
parent 52ed9c75e4
commit 1eb0736fbe
5 changed files with 88 additions and 27 deletions
+9 -5
View File
@@ -894,10 +894,11 @@ export class TopstepXClient implements PlatformClient {
console.error(`[topstepx] fund reconcile failed for ${acc.name}: ${errorMessage(err)}`);
}
// A freshly fetched history is tradable once a fresh pass has
// actually checked the funds behind the balance (a skipped
// check — a position still open — waits for a later pass).
const ours = Boolean(getAccountConfig(acc.id).accountType);
if ((reconciled || !ours) && this.reconcilePendingIds.delete(acc.id)) this.historyLoadedIds.add(acc.id);
// actually checked the funds behind the balance. A skipped
// check waits for a later pass — a position still open, or no
// account type yet (not traded until one is assigned, and then
// only after a check).
if (reconciled && this.reconcilePendingIds.delete(acc.id)) this.historyLoadedIds.add(acc.id);
try {
this.updateMllFloor(acc);
@@ -1290,8 +1291,11 @@ export class TopstepXClient implements PlatformClient {
// rather than leave them out for up to a minute, and before returning,
// so a wipe route reads the result. (The firm's first pass syncs from
// maybeFetchDaysTraded once readiness allows.)
// Only ours: an account with no type stays pending without costing
// a sync on every pass.
const waiting = () => this.accountList.some((a) => this.reconcilePendingIds.has(a.id)
&& (!onlyAccountIds || onlyAccountIds.includes(a.id)));
&& (!onlyAccountIds || onlyAccountIds.includes(a.id))
&& Boolean(getAccountConfig(a.id).accountType));
if (waiting() && this.historyLoaded && !this.intentionalDisconnect) {
await this.requestSync(); // a pass already running may have gone past them
if (waiting()) await this.requestSync();