Vest: a stale sync keeps the bot's fill, a cut-off open is still the bot's, and the day turns with the numbers

A sync whose positions read went out before the bot's own open or close
finished no longer writes that account's positions back (ownOrderSeq, as
claimSeq guards balances): the record a flatten goes by stays, so a read
lagging the fill still gives FLATTEN FAILED, not "nothing to flatten".

An open is noted in account_meta before its POST and dropped once its
outcome is known. After a restart the first sync takes the note up as an
open of unknown outcome, so the position it made is marked the bot's and
guardClose sees it. Such an open is matched by the position id its answer
named, else only to a position with no exit legs opened between the send
and a few seconds past the error (was two minutes): an outside
copy-trader's open half a minute on isn't taken for the bot's, and
several matches adopt none, loudly.

VestClient.tradeDate follows the last sync's numbers: past 8 PM ET the
day turns once a sync has worked out the fresh day's realized P&L, so
the engine, reporter and dashboard never pair the new day with the ended
day's numbers. A stalled sync holds the day back only while isClientReady
still trades on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-10-27 15:31:28 -05:00
co-authored by Claude Opus 5.5
parent ab588adbac
commit 032ba1ca9c
4 changed files with 403 additions and 49 deletions
+15 -8
View File
@@ -434,17 +434,24 @@ export function awaitingLegs(p: VestPosition): VestLeg[] {
}
/**
* The position an open of unknown outcome (a timeout, a reset, a 5xx after
* the send) made, once a read shows it: on its account, in its symbol and
* side, opened between `from` and `until` — the send and the error, widened
* for the gateway's clock. A market open is IOC, so a later one isn't it.
* The positions an open of unknown outcome (a timeout, a reset, a 5xx after
* the send) may have made, as a read shows them — the bot's only when there
* is exactly one. The one the open's answer named, when it named one. Else
* on its account, in its symbol and side, with no exit legs (none went on
* it), opened between `from` and `until` — the send and the error, widened
* for the gateway's clock: a market open is IOC, so a later one (an outside
* copy-trader's, which opens the same symbol on every account at once) isn't
* it. More than one of those can't be told apart.
*/
export function unknownOpenFill(
sent: { accountId: string; symbol: string; side: 'long' | 'short'; from: number; until: number },
sent: { accountId: string; symbol: string; side: 'long' | 'short'; from: number; until: number; positionId?: string },
positions: VestPosition[],
): VestPosition | null {
return positions.find((p) => p.accountId === sent.accountId && p.symbol === sent.symbol && p.side === sent.side
&& p.openedAt >= sent.from && p.openedAt <= sent.until) ?? null;
): VestPosition[] {
const mine = positions.filter((p) => p.accountId === sent.accountId);
if (sent.positionId) return mine.filter((p) => p.positionId === sent.positionId);
return mine.filter((p) => p.symbol === sent.symbol && p.side === sent.side
&& p.stopLosses.length === 0 && p.takeProfits.length === 0
&& p.openedAt >= sent.from && p.openedAt <= sent.until);
}
export interface VestOpenOrder {