Hold a payout that isn't booked yet, instead of re-earning it
On 2026-10-06 two Topstep Express accounts sat finished, waiting for their payouts. The hub moved their balances the moment the payouts landed, but reconcileFunds books a withdrawal only in the next sync. 28 seconds later the plan read the drop as lost profit, reopened the cycle as stage 1 and fired a full 3xNQ leg with no stop, on what should have been a drain day. Both were liquidated against the $0 floor a payout leaves. - TopstepX: unbookedBalanceChange holds an account while its balance is $200+ off its books (today's realized read fresh off the trades, so the 5 PM roll doesn't trip it); the sync books it as before. - Every platform: a finished cycle marks its books in account_meta; a later drop of $200+ with no fund row booked since holds the account until the payout books and moves it to its next stage. Relative to the mark, so history missing from before the cache cancels out; a wipe clears it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0c3a36845a
commit
c3b58c8d8b
@@ -1064,6 +1064,49 @@ export function computeDayPlan(
|
||||
};
|
||||
}
|
||||
|
||||
// ── payouts not yet booked ───────────────────────────────────────────────────
|
||||
|
||||
/** A drop at least this far below a finished cycle's books holds the account. */
|
||||
export const PAYOUT_HOLD_MIN = 200;
|
||||
|
||||
/**
|
||||
* Where a finished cycle (day target 0) left the books: its fund rows, and
|
||||
* day-start equity less the P&L booked by then. Until a fund row is added,
|
||||
* booked P&L is all that may move day-start equity — relative, so history
|
||||
* missing from before the cache (or an initial deposit row some platforms
|
||||
* book and others don't) cancels out.
|
||||
*/
|
||||
export interface FinishedCycleMark { funds: string; offset: number }
|
||||
|
||||
/** Fund rows as one comparable value: a second payout on a date that already
|
||||
* has one changes the amount, not the row count. */
|
||||
export function fundRowsKey(txns: { date: string; amount: number }[]): string {
|
||||
return txns.map((f) => `${f.date}:${f.amount}`).join('|');
|
||||
}
|
||||
|
||||
export function markFinishedCycle(funds: string, dayStartEquity: number, bookedPnL: number): FinishedCycleMark {
|
||||
return { funds, offset: round2(dayStartEquity - bookedPnL) };
|
||||
}
|
||||
|
||||
/**
|
||||
* How far day-start equity has fallen below a finished cycle's books with no
|
||||
* fund row booked since — a payout the platform hasn't booked yet. null when
|
||||
* there's no mark, a fund row has moved on from it, or the drop is fee noise.
|
||||
* Planned on, that drop reads as lost profit: the finished cycle reopens as
|
||||
* the same stage and re-earns the payout at full size, instead of moving to
|
||||
* the next stage (a drain day) once the payout books.
|
||||
*/
|
||||
export function unbookedPayoutDrop(
|
||||
mark: FinishedCycleMark | null,
|
||||
funds: string,
|
||||
dayStartEquity: number,
|
||||
bookedPnL: number,
|
||||
): number | null {
|
||||
if (!mark || mark.funds !== funds) return null;
|
||||
const drop = round2(mark.offset - (dayStartEquity - bookedPnL));
|
||||
return drop >= PAYOUT_HOLD_MIN ? drop : null;
|
||||
}
|
||||
|
||||
// ── fees ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** An entry's fee when the balance update hasn't shown it yet — at least one
|
||||
|
||||
Reference in New Issue
Block a user