Vest: the bot activates a passed evaluation's funded account, and a passed one no longer holds the sync
A passed Vest evaluation sits at GOAL_PASSED (status 3) until someone presses
"Go to funded account", which sends POST /v3/capital/activate-funded
{eval_account_id} on the evaluation's own token (read from the Vest app's
bundle, 2026-10-29); Vest then opens the funded account and marks the
evaluation CLAIMED (6). The 5 evaluations that passed on 2026-10-29 waited
for a hand activation.
runActivations now asks for it, the way runClaims claims: from the
scheduler's tick (not awaited, so a slow ask doesn't hold the lane's entry)
and from the claim watcher while the scheduler is off, for evaluations with
an account type only, 15 minutes apart after a failure. Off with the
auto_activate setting at 'off' (dashboard: "Auto-activate funded"). The
client asks for an evaluation once at a time, never again after Vest
answered, and after a timeout not until a list read a minute on says either
way; a refusal frees it at once. The rule is the Vest app's own: an
evaluation at GOAL_PASSED, not blocked, on its plan's last step.
Vest's active balance list leaves a passed evaluation out, and a missing
balance made every sync short of good (15:31-15:34Z), which holds and then
quarantines the lane. Only a live account's missing balance does now. Log
lines name Vest's statuses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0eb56b3949
commit
8a5b2a64e7
+60
-5
@@ -72,6 +72,11 @@ interface TradeRuntime {
|
||||
sideReservation?: { side: 'Buy' | 'Sell'; lane: string; lease: number };
|
||||
/** Account id → when its last payout claim failed (runClaims backoff). */
|
||||
claimFailedAt?: Map<number, number>;
|
||||
/** Account id → when its last funded-account activation failed
|
||||
* (runActivations backoff). */
|
||||
activateFailedAt?: Map<number, number>;
|
||||
/** Lanes whose runActivations is out (one at a time per lane). */
|
||||
activationsRunning?: Set<string>;
|
||||
/** Accounts whose exit the broker refused — a bracket while a hard daily
|
||||
* loss limit was on, or a lone take-profit: the position was flattened
|
||||
* and the account sits out the trade date rather than bleed a round trip
|
||||
@@ -2038,6 +2043,49 @@ async function runClaims(lane: string, ready: FirmClient[]): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** How long a failed activation waits before it's tried again — a refusal
|
||||
* (Vest's cap on funded accounts, say) holds for a while. */
|
||||
const ACTIVATE_RETRY_MS = 15 * 60_000;
|
||||
|
||||
/**
|
||||
* Activate the funded account of every passed evaluation in the lane that
|
||||
* the bot trades (one with an account type, as runClaims goes by — Vest caps
|
||||
* funded accounts, and one kept by hand isn't the bot's to spend a slot on),
|
||||
* where the platform does that by API (Vest: a passed evaluation waits until
|
||||
* it's asked). It opens and closes nothing — the evaluation no longer
|
||||
* trades, and the funded account shows on the next sync and trades from
|
||||
* there — so it takes no lane mutex, and the scheduler doesn't wait on it.
|
||||
* One run per lane at a time. Run by the scheduler's tick, or by the claim
|
||||
* watcher while the scheduler is off. Off with the auto_activate setting at
|
||||
* 'off'.
|
||||
*/
|
||||
async function runActivations(lane: string, ready: FirmClient[]): Promise<void> {
|
||||
if (getSetting('auto_activate') === 'off') return;
|
||||
const running = (runtime().activationsRunning ??= new Set());
|
||||
if (running.has(lane)) return;
|
||||
running.add(lane);
|
||||
try {
|
||||
for (const { firm, client } of inLane(ready, lane)) {
|
||||
if (typeof client.activatable !== 'function' || typeof client.activateFunded !== 'function') continue;
|
||||
for (const id of client.activatable()) {
|
||||
const acc = client.accountList.find((a) => a.id === id);
|
||||
if (!acc || !getAccountConfig(id).accountType) continue;
|
||||
if (Date.now() - (runtime().activateFailedAt?.get(id) ?? 0) < ACTIVATE_RETRY_MS) continue;
|
||||
try {
|
||||
const { funded } = await client.activateFunded(id);
|
||||
runtime().activateFailedAt?.delete(id);
|
||||
console.log(`[auto-activate] ${firm.name} ${acc.name}: passed — funded account ${funded ?? '(listed on the next sync)'} activated`);
|
||||
} catch (err) {
|
||||
(runtime().activateFailedAt ??= new Map()).set(id, Date.now());
|
||||
console.error(`[auto-activate] ${firm.name} ${acc.name}: activation failed — ${errorMessage(err)}; retrying in ${ACTIVATE_RETRY_MS / 60_000}m`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
running.delete(lane);
|
||||
}
|
||||
}
|
||||
|
||||
/** How often auto-claims run while the scheduler is off — its tick's pace. */
|
||||
const CLAIM_WATCH_MS = 60_000;
|
||||
|
||||
@@ -2049,8 +2097,9 @@ const _claimWatch = globalThis as typeof globalThis & { __claimWatcher?: ReturnT
|
||||
* 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.
|
||||
* of the way. Off with the auto_claim setting at 'off'. Activations of passed
|
||||
* evaluations' funded accounts ride along the same way (auto_activate).
|
||||
* Started once, with the clients.
|
||||
*/
|
||||
export function startClaimWatcher(): void {
|
||||
if (_claimWatch.__claimWatcher) return;
|
||||
@@ -2058,15 +2107,18 @@ export function startClaimWatcher(): void {
|
||||
}
|
||||
|
||||
async function claimTick(): Promise<void> {
|
||||
if (getState().running || getSetting('auto_claim') === 'off' || _claimWatch.__claimTickBusy) return;
|
||||
if (getState().running || _claimWatch.__claimTickBusy) return;
|
||||
if (getSetting('auto_claim') === 'off' && getSetting('auto_activate') === 'off') 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.
|
||||
// position. runClaims takes each lane's mutex itself; each step
|
||||
// checks its own setting.
|
||||
const { ready } = firmClients();
|
||||
for (const lane of tradableLanes()) {
|
||||
if (getState().running) return; // the scheduler took over
|
||||
await runClaims(lane, ready);
|
||||
await runActivations(lane, ready);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[auto-claim] ${errorMessage(err)}`);
|
||||
@@ -2343,8 +2395,11 @@ export function startScheduler(action: 'Buy' | 'Sell' | 'Auto', symbol: string,
|
||||
continue;
|
||||
}
|
||||
// Payouts first, account by account — a claim doesn't wait
|
||||
// for the rest of the lane to go flat.
|
||||
// for the rest of the lane to go flat. Nor does a passed
|
||||
// evaluation's funded account, and the batch doesn't wait
|
||||
// on it: a slow one would hold back the lane's entry.
|
||||
await runClaims(lane, ready);
|
||||
void runActivations(lane, ready).catch((err) => console.error(`[auto-activate] ${laneName(lane)}: ${errorMessage(err)}`));
|
||||
// Skip this lane if any of its accounts still has an open
|
||||
// position from its previous batch — a batch fires only
|
||||
// when every (ready) account in the lane is flat.
|
||||
|
||||
Reference in New Issue
Block a user