Auto-claim without the scheduler, and no claim twice
A payout locks in profit and opens nothing, so stopping the trading no longer stops it: a claim watcher runs the same claims once a minute while the scheduler is off (its tick still claims while it runs), and the dashboard's Auto-claim payouts box turns them off. From two independent reviews of the claim path: - A claim holds every further claim on the account until Vest's transactions show its WITHDRAW. A sync already out when the claim lands (by sync generation, not the clock) no longer writes the pre-claim balance back, and a fresh sync follows the claim. - A withdraw that fails in transit (timeout, reset, 5xx) may have gone in: it is recorded as of unknown outcome and holds claims the same way, until the transactions show it, or an hour of them shows it didn't. - claimProfit checks Vest's own account: funded (or instant), and no more than the balance above its starting capital. - A claiming account is marked in flight; runClaims sends the amount it re-checked under the mutex. - The watcher starts before the rest of the init can fail; settings are validated before any is written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
016444239e
commit
6f6660fdcc
+50
-7
@@ -1947,6 +1947,7 @@ function claimDue(client: any, acc: { id: number; active: boolean }, now: number
|
||||
if (!cfg || cfg.payoutable !== true || !acc.active) return null;
|
||||
if (client.positions[acc.id] || client.hasWorkingOrders?.(acc.id) || runtime().accountsInFlight.has(acc.id)) return null;
|
||||
if (!client.historyReady?.(acc.id)) return null;
|
||||
if (client.claimPending?.(acc.id)) return null;
|
||||
if (now - (runtime().claimFailedAt?.get(acc.id) ?? 0) < CLAIM_RETRY_MS) return null;
|
||||
const cash = client.accountCashBalances[acc.id];
|
||||
if (!cash) return null;
|
||||
@@ -1966,8 +1967,9 @@ function claimDue(client: any, acc: { id: number; active: boolean }, now: number
|
||||
* Profit). The account then starts its next stage the same day: its leg
|
||||
* counts and entry outcome for the day are cleared, and a type whose stages
|
||||
* reset each day trades its next stage on the room the claim freed. Under the
|
||||
* lane's mutex, so no batch reads an account mid-claim. Off with the
|
||||
* auto_claim setting at 'off'.
|
||||
* lane's mutex, so no batch reads an account mid-claim. Run by the
|
||||
* scheduler's tick, or by the claim watcher while the scheduler is off. Off
|
||||
* with the auto_claim setting at 'off'.
|
||||
*/
|
||||
async function runClaims(lane: string, ready: FirmClient[]): Promise<void> {
|
||||
if (getSetting('auto_claim') === 'off') return;
|
||||
@@ -1983,12 +1985,15 @@ async function runClaims(lane: string, ready: FirmClient[]): Promise<void> {
|
||||
if (lease === null) return;
|
||||
try {
|
||||
const tradeDate = currentTradeDateCT();
|
||||
for (const { firm, client, acc, amount } of due) {
|
||||
for (const { firm, client, acc } of due) {
|
||||
if (!holdsTradeMutex(lease, lane)) return;
|
||||
if (claimDue(client, acc, Date.now()) === null) continue; // settled since
|
||||
// Settled since, or a different amount now: claim what is due now.
|
||||
const amount = claimDue(client, acc, Date.now());
|
||||
if (amount === null) continue;
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { claimed, received } = await (client as any).claimProfit(acc.id, amount);
|
||||
// In flight while it claims: a batch that takes over a wedged
|
||||
// lane, or a Claim now, leaves the account alone.
|
||||
const { claimed, received } = await withAccountInFlight(acc.id, () => client.claimProfit!(acc.id, amount));
|
||||
afterClaim('[auto-claim]', firm, client, acc, claimed, received, tradeDate);
|
||||
} catch (err) {
|
||||
(runtime().claimFailedAt ??= new Map()).set(acc.id, Date.now());
|
||||
@@ -2000,6 +2005,43 @@ async function runClaims(lane: string, ready: FirmClient[]): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** How often auto-claims run while the scheduler is off — its tick's pace. */
|
||||
const CLAIM_WATCH_MS = 60_000;
|
||||
|
||||
const _claimWatch = globalThis as typeof globalThis & { __claimWatcher?: ReturnType<typeof setInterval>; __claimTickBusy?: boolean };
|
||||
|
||||
/**
|
||||
* Auto-claims don't wait on the scheduler: a payout locks in profit and
|
||||
* opens nothing, so stopping the trading doesn't stop it — a funded account
|
||||
* at its target while the bot is off is claimed all the same, before
|
||||
* whatever trades it next can take the profit back. While the scheduler
|
||||
* runs, its own tick claims (first thing in each lane), and this stays out
|
||||
* of the way. Off with the auto_claim setting at 'off'. Started once, with
|
||||
* the clients.
|
||||
*/
|
||||
export function startClaimWatcher(): void {
|
||||
if (_claimWatch.__claimWatcher) return;
|
||||
_claimWatch.__claimWatcher = setInterval(() => { void claimTick(); }, CLAIM_WATCH_MS);
|
||||
}
|
||||
|
||||
async function claimTick(): Promise<void> {
|
||||
if (getState().running || getSetting('auto_claim') === 'off' || _claimWatch.__claimTickBusy) return;
|
||||
_claimWatch.__claimTickBusy = true;
|
||||
try {
|
||||
// Ready clients only, as the tick: a stale snapshot may hide a
|
||||
// position. runClaims takes each lane's mutex itself.
|
||||
const { ready } = firmClients();
|
||||
for (const lane of tradableLanes()) {
|
||||
if (getState().running) return; // the scheduler took over
|
||||
await runClaims(lane, ready);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[auto-claim] ${errorMessage(err)}`);
|
||||
} finally {
|
||||
_claimWatch.__claimTickBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** A claim went through: the account starts its next stage — the day's leg
|
||||
* count and entry outcome are cleared, as if it hadn't traded today. */
|
||||
function afterClaim(
|
||||
@@ -2043,6 +2085,7 @@ function manualClaim(accountId: number):
|
||||
return { reason: 'a position or order is open — the account must be flat' };
|
||||
}
|
||||
if (!client.historyReady(acc.id)) return { reason: 'its history is still loading' };
|
||||
if (client.claimPending?.(acc.id)) return { reason: 'the last claim is still settling — wait for the next sync' };
|
||||
const cash = client.accountCashBalances[acc.id];
|
||||
if (!cash) return { reason: 'its balance has not been read yet' };
|
||||
const profit = Math.floor((cash.amount - cfg.accountSize) * 100) / 100;
|
||||
@@ -2084,7 +2127,7 @@ export async function claimAccountNow(accountId: number, amount: number): Promis
|
||||
if ('reason' in c) throw new ClaimRefusedError(c.reason);
|
||||
if (!(amount >= 1)) throw new ClaimRefusedError(`$${amount} is below the $1 minimum claim`);
|
||||
if (amount > c.amount + 0.005) throw new ClaimRefusedError(`only $${c.amount.toFixed(2)} is claimable now`);
|
||||
const { claimed, received } = await c.client.claimProfit!(c.acc.id, amount);
|
||||
const { claimed, received } = await withAccountInFlight(c.acc.id, () => c.client.claimProfit!(c.acc.id, amount));
|
||||
const stage = afterClaim('[claim-now]', c.firm, c.client, c.acc, claimed, received, currentTradeDateCT());
|
||||
return { claimed, received, stage };
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user