Roll window instead of a roll-time base; transient outages keep payout holds
Third review round (088886e):
- Tradovate: a record still on the ended trade date waits only inside
17:00-17:45 CT (every account rolled 17:17-17:21 on 2026-09-24); nothing
trades on it there, so no pre-roll fill can be lost or double-counted by
the broker's roll. Past the window an unrolled record (an idle account)
counts from its value when first read, so nothing waits indefinitely —
across restarts, stale bases or accounts the broker rolls only on a fill.
- One-and-done accounts also stop once this process fired a leg today.
- A balance hold is released unchecked only when Cash History is refused
outright (4xx / errorText, e.g. a passed account's 404s); a 5xx, timeout
or queue outage keeps the payout held until it recovers.
- A Cash History is judged against the baseline as it stood when requested,
so a fold observed while it was in flight is not taken for a payout.
- TopstepX: a cache fallback for an account not loaded before waits for a
funds check; accounts with no account type (not ours) aren't left
pending; a fetch awaits a sync already running and syncs again if its
accounts still wait, so wipe routes report the real outcome.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
088886ed73
commit
52ed9c75e4
+19
-5
@@ -242,6 +242,9 @@ export class TopstepXClient implements PlatformClient {
|
||||
* fetched without it (after a wipe took the fund rows) would plan the
|
||||
* account as stage 1 until the next sync. */
|
||||
private reconcilePendingIds = new Set<number>();
|
||||
/** Settles when the sync in flight ends (requestSync hands it to a caller
|
||||
* that arrives mid-pass). */
|
||||
private syncRunning: Promise<void> = Promise.resolve();
|
||||
/** Accounts a fetchDaysTraded call is working through right now, so the
|
||||
* targeted pass doesn't fetch them again (a wipe fetches outside it). */
|
||||
private fetchingIds = new Set<number>();
|
||||
@@ -812,13 +815,16 @@ export class TopstepXClient implements PlatformClient {
|
||||
const startedAt = Date.now();
|
||||
if (this.syncInFlight) {
|
||||
const runningMs = startedAt - this.syncInFlightSince;
|
||||
if (runningMs < SYNC_WEDGED_MS) return;
|
||||
// A caller that awaits gets the running pass's end.
|
||||
if (runningMs < SYNC_WEDGED_MS) return this.syncRunning;
|
||||
console.warn(`[topstepx] sync running for ${Math.round(runningMs / 1000)}s — looks wedged, starting a fresh one`);
|
||||
}
|
||||
const gen = ++this.syncGeneration;
|
||||
const superseded = () => gen !== this.syncGeneration || this.intentionalDisconnect;
|
||||
this.syncInFlight = true;
|
||||
this.syncInFlightSince = startedAt;
|
||||
let finished!: () => void;
|
||||
this.syncRunning = new Promise<void>((resolve) => { finished = resolve; });
|
||||
try {
|
||||
const accRes = await this.post<{ accounts: { id: number; name: string; balance: number; canTrade: boolean; isVisible: boolean }[] }>(
|
||||
'/api/Account/search', { onlyActiveAccounts: false });
|
||||
@@ -890,7 +896,8 @@ export class TopstepXClient implements PlatformClient {
|
||||
// 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).
|
||||
if (reconciled && this.reconcilePendingIds.delete(acc.id)) this.historyLoadedIds.add(acc.id);
|
||||
const ours = Boolean(getAccountConfig(acc.id).accountType);
|
||||
if ((reconciled || !ours) && this.reconcilePendingIds.delete(acc.id)) this.historyLoadedIds.add(acc.id);
|
||||
|
||||
try {
|
||||
this.updateMllFloor(acc);
|
||||
@@ -931,6 +938,7 @@ export class TopstepXClient implements PlatformClient {
|
||||
this.syncInFlight = false;
|
||||
this.syncInFlightSince = 0;
|
||||
}
|
||||
finished();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1264,7 +1272,10 @@ export class TopstepXClient implements PlatformClient {
|
||||
this.dailyFees[account.id] = new Map(cached.map((e) => [e.date, e.fees ?? 0]));
|
||||
this.applyCycleSlice(account.id);
|
||||
this.lastFetchErrors[account.name] = `${msg} (using ${cached.length} cached entries from database)`;
|
||||
this.historyLoadedIds.add(account.id);
|
||||
// Not loaded before: no funds check has run on this cache,
|
||||
// and none will while the fetch error stands — so it waits
|
||||
// for a fetch that lands (reconcileFunds, requestSync).
|
||||
if (!this.historyLoadedIds.has(account.id)) this.reconcilePendingIds.add(account.id);
|
||||
} else {
|
||||
// No history at all is not an empty one: the account
|
||||
// stays out of trading until a fetch lands, instead of
|
||||
@@ -1279,8 +1290,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.)
|
||||
if (this.reconcilePendingIds.size > 0 && this.historyLoaded && !this.intentionalDisconnect) {
|
||||
await this.requestSync();
|
||||
const waiting = () => this.accountList.some((a) => this.reconcilePendingIds.has(a.id)
|
||||
&& (!onlyAccountIds || onlyAccountIds.includes(a.id)));
|
||||
if (waiting() && this.historyLoaded && !this.intentionalDisconnect) {
|
||||
await this.requestSync(); // a pass already running may have gone past them
|
||||
if (waiting()) await this.requestSync();
|
||||
}
|
||||
return { failedAccounts };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user