Unknown entries settle only on a sync started after them; filled-after-all and working orders seen
Sixth review round (8e66631):
- An entry of unknown outcome is judged against a good sync that STARTED
at least 90s after it was declared unknown (lastSyncStartedAt: TopstepX's
pass start, Tradovate's request time), not one that merely ended then —
a sequential pass can read the account before a late fill.
- Once the broker shows its fill (a position, or realized P&L moved), the
entry becomes an ordinary filled one instead of holding the account for
the rest of the day.
- It never settles while the broker still shows a working order on the
account: Tradovate now reads working orders from the sync snapshot, and
its fill-timeout cancel checks Tradovate's answer instead of assuming it.
- A second wipe (after a restart) keeps the pending wipe's pre-wipe fund
signature instead of overwriting it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8e66631b0e
commit
57835c01f0
+18
-8
@@ -132,12 +132,14 @@ interface EntryOutcome {
|
||||
at: number;
|
||||
realizedBefore: number;
|
||||
legAmount: number;
|
||||
/** When its outcome was declared unknown (the send's error). */
|
||||
unknownAt?: number;
|
||||
}
|
||||
|
||||
/** An unknown entry counts as never filled only once a good sync at least
|
||||
* this long after it shows the account flat with realized P&L unchanged — a
|
||||
* market order that could fill has done so by then (and a Tradovate order
|
||||
* that timed out waiting for its fill was cancelled). */
|
||||
/** An unknown entry counts as never filled only once a good sync that
|
||||
* STARTED at least this long after it was declared unknown shows the account
|
||||
* flat, with realized P&L unchanged and no order still working — a market
|
||||
* order that could fill has done so by then. */
|
||||
const UNKNOWN_ENTRY_SETTLE_MS = 90_000;
|
||||
|
||||
function entryOutcomeToday(accountId: number, tradeDate: string): EntryOutcome | null {
|
||||
@@ -156,10 +158,18 @@ function unknownEntryHolds(client: any, accountId: number, tradeDate: string): b
|
||||
const outcome = entryOutcomeToday(accountId, tradeDate);
|
||||
if (!outcome || outcome.state !== 'unknown') return false;
|
||||
const cash = client.accountCashBalances[accountId];
|
||||
const settled = client.lastSyncAt >= outcome.at + UNKNOWN_ENTRY_SETTLE_MS
|
||||
&& !client.positions[accountId]
|
||||
// The broker shows the fill (the position, or the P&L it booked): it
|
||||
// filled after all. From here it's an ordinary filled entry — the
|
||||
// position gate holds it while open, one-and-done is done for the day.
|
||||
if (client.positions[accountId] || (cash && Math.abs(cash.realizedPnL - outcome.realizedBefore) >= 0.005)) {
|
||||
outcome.state = 'filled';
|
||||
console.log(`[auto-trade] account ${accountId}: the entry of unknown outcome filled after all`);
|
||||
return false;
|
||||
}
|
||||
const syncedFrom = client.lastSyncStartedAt ?? client.lastSyncAt;
|
||||
const settled = syncedFrom >= (outcome.unknownAt ?? outcome.at) + UNKNOWN_ENTRY_SETTLE_MS
|
||||
&& cash !== undefined
|
||||
&& Math.abs(cash.realizedPnL - outcome.realizedBefore) < 0.005;
|
||||
&& !client.hasWorkingOrders?.(accountId);
|
||||
if (!settled) return true;
|
||||
runtime().entryOutcomes?.delete(accountId);
|
||||
unrecordLeg(accountId, tradeDate, outcome.legAmount);
|
||||
@@ -226,7 +236,7 @@ async function sendEntry(
|
||||
unrecordLeg(acc.id, tradeDate, legAmount);
|
||||
console.warn(`${tag} ${acc.name}: entry never reached the broker — leg un-counted`);
|
||||
} else {
|
||||
outcomes.set(acc.id, { tradeDate, state: 'unknown', at, realizedBefore, legAmount });
|
||||
outcomes.set(acc.id, { tradeDate, state: 'unknown', at, realizedBefore, legAmount, unknownAt: Date.now() });
|
||||
console.warn(`${tag} ${acc.name}: entry outcome unknown — leg kept and the account held until the broker shows it`);
|
||||
}
|
||||
throw err;
|
||||
|
||||
Reference in New Issue
Block a user