Pre-roll fills stay visible; holds survive restarts and judge fund amounts
Second review round (e496404,9b2d677): - Tradovate: past the 5 PM roll an old-dated cash record counts only what it gained since the roll (its realizedPnL as last seen while current is kept as the roll base), so tonight's fills booked onto it before Tradovate rolls the account stay visible — no re-trading a one-and-done account. A record this process never saw before the roll leaves the account waiting for the broker's roll (ROLL_PENDING, counted in play for stop-after-all). The 60s snapshot carries the roll base and can't undo a newer record the socket delivered. - A wipe's hold is kept in account_meta, so a restart onto a failing Cash History still holds the account. - A Cash History whose pre-request balance the last vouched-for baseline can't explain, and which booked nothing, holds the account instead of rebasing onto an unbooked payout. - Holds compare fund transactions by date and amount (a second payout on a date that already has one), and an in-flight hold is judged against the rows from before its report. - The saved baseline tracks the day's realized P&L and folds, so a restart judges against current figures; held refetches back off to an hour, and a balance hold no Cash History can check is released after an hour. - TopstepX: a fetched account is promoted only by a sync whose reconcileFunds actually checked the books (not one skipped for an open position), and a direct fetch awaits that sync, 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
9b2d677662
commit
088886ed73
+20
-13
@@ -881,14 +881,16 @@ export class TopstepXClient implements PlatformClient {
|
||||
// Withdrawal/deposit detection — the books must explain the balance.
|
||||
// Never off a failed fetch: a fresh balance against stale trades
|
||||
// would book the unseen P&L as a (permanent) fund event.
|
||||
let reconciled = false;
|
||||
try {
|
||||
if (accountFresh) this.reconcileFunds(acc);
|
||||
if (accountFresh) reconciled = this.reconcileFunds(acc);
|
||||
} catch (err) {
|
||||
console.error(`[topstepx] fund reconcile failed for ${acc.name}: ${errorMessage(err)}`);
|
||||
}
|
||||
// A freshly fetched history is tradable once a fresh pass has
|
||||
// had its go at booking the funds behind the balance.
|
||||
if (accountFresh && this.reconcilePendingIds.delete(acc.id)) this.historyLoadedIds.add(acc.id);
|
||||
// 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);
|
||||
|
||||
try {
|
||||
this.updateMllFloor(acc);
|
||||
@@ -965,21 +967,24 @@ export class TopstepXClient implements PlatformClient {
|
||||
* the drain flow is well above it. Skipped while a position is open (fills
|
||||
* in flight) or while the trade history is known-stale.
|
||||
*/
|
||||
private reconcileFunds(account: PlatformAccount): void {
|
||||
if (this.positions[account.id]) return;
|
||||
if (this.lastFetchErrors[account.name]) return;
|
||||
/** Books a withdrawal/deposit the trades can't explain. True once it has
|
||||
* actually checked the books against the balance (false when it had to
|
||||
* skip: a position open, no history or balance yet, no account type). */
|
||||
private reconcileFunds(account: PlatformAccount): boolean {
|
||||
if (this.positions[account.id]) return false;
|
||||
if (this.lastFetchErrors[account.name]) return false;
|
||||
const history = this.fullDailyPnL[account.id];
|
||||
if (!history) return; // first daily-P&L fetch hasn't landed yet
|
||||
if (!history) return false; // first daily-P&L fetch hasn't landed yet
|
||||
|
||||
const cash = this.accountCashBalances[account.id];
|
||||
if (!cash) return;
|
||||
if (!cash) return false;
|
||||
// The account's starting balance ($0 for Express, the funded size
|
||||
// otherwise) is part of the balance the books must explain — without
|
||||
// it a funded account books a phantom deposit for its whole size. Until
|
||||
// the account type is resolved that number is unknown, so hold off
|
||||
// rather than guess $0 and misfire on a funded account.
|
||||
const acctType = getAccountConfig(account.id).accountType;
|
||||
if (!acctType) return;
|
||||
if (!acctType) return false;
|
||||
const startBal = acctType.accountSize;
|
||||
const today = currentTradeDateCT();
|
||||
const tradePnL = history
|
||||
@@ -993,7 +998,7 @@ export class TopstepXClient implements PlatformClient {
|
||||
// P&L and past payouts, not the end of a cycle this instance watched.
|
||||
const firstCheck = loadAccountMeta(account.id, 'funds_reconciled') === null;
|
||||
if (firstCheck) saveAccountMeta(account.id, 'funds_reconciled', today);
|
||||
if (Math.abs(delta) < 200) return;
|
||||
if (Math.abs(delta) < 200) return true;
|
||||
|
||||
console.log(`[topstepx] ${account.name}: balance ${cash.amount} vs books ${Math.round((tradePnL + funds) * 100) / 100} — recording ${delta < 0 ? 'withdrawal' : 'deposit'} of ${delta}`);
|
||||
// A new cycle starts today: note where the one it ends began, by
|
||||
@@ -1014,6 +1019,7 @@ export class TopstepXClient implements PlatformClient {
|
||||
this.fundTransactions[account.id] = loadFundTransactions(account.id);
|
||||
this.lastFundDates[account.id] = today;
|
||||
this.applyCycleSlice(account.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1270,10 +1276,11 @@ export class TopstepXClient implements PlatformClient {
|
||||
}
|
||||
this.fetchDaysComplete = true;
|
||||
// The accounts just fetched wait on a reconciling sync: run one now
|
||||
// rather than leave them out for up to a minute. (The firm's first
|
||||
// pass syncs from maybeFetchDaysTraded once readiness allows.)
|
||||
// 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) {
|
||||
void this.requestSync();
|
||||
await this.requestSync();
|
||||
}
|
||||
return { failedAccounts };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user