Vest: a claim within $25 of target names its cycle, the newest goes first, and a batch on a turned day stops
The auto-claim takes a cycle as done within LEG_COMPLETION_EPSILON of its target, so the cycle it names is judged at that tolerance too. Each saved cycle keeps its claim's time, and the report lists a day's claims newest first, so the master's per-slice cap can't hold the latest back. A batch or copy whose client's day turned while it chose (Vest, at the first sync after 8 PM ET) is abandoned rather than sized and counted on the old day. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dc210ffcb7
commit
bf7e147507
+13
-1
@@ -1224,6 +1224,13 @@ async function runTradeLocked(
|
||||
console.warn('[auto-trade] superseded while collecting accounts — abandoning this batch');
|
||||
return { outcome: 'skipped-in-flight', results: [], detail: 'superseded by another batch while choosing' };
|
||||
}
|
||||
// A Vest lane's day turns with its client's numbers (tradeDateFor). One
|
||||
// that turned while choosing would size and count these legs on the day
|
||||
// that ended — the next tick plans on the new one.
|
||||
if (batch.some((item) => tradeDateFor(item.client) !== tradeDate)) {
|
||||
console.warn(`[auto-trade] ${laneName(lane)}: the trade date turned while choosing — abandoning this batch`);
|
||||
return { outcome: 'skipped-in-flight', results: [], detail: 'the trade date turned while choosing' };
|
||||
}
|
||||
|
||||
console.log(`[auto-trade] ${laneName(lane)}: ${allEligible.length} eligible account(s) across ${groups.size} prefix group(s) — firing batch of ${batch.length} ${batchPrefix || '(no prefix)'} on ${resolvedSymbol}`);
|
||||
runtime().lastFireAt = Date.now();
|
||||
@@ -1826,6 +1833,10 @@ async function copyTradeLocked(lease: number, lane: string): Promise<unknown[]>
|
||||
console.warn('[copy-trade] superseded while collecting accounts — abandoning this copy');
|
||||
return [];
|
||||
}
|
||||
if (batch.some((item) => tradeDateFor(item.client) !== tradeDate)) {
|
||||
console.warn('[copy-trade] the trade date turned while choosing — abandoning this copy');
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`[copy-trade] copying ${resolvedAction} ${resolvedSymbol} to ${batch.length} account(s)`);
|
||||
runtime().lastFireAt = Date.now();
|
||||
@@ -2074,7 +2085,8 @@ async function claimTick(): Promise<void> {
|
||||
function claimEnds(client: PlatformClient, acc: { id: number; name: string }): ClaimedCycle | null {
|
||||
try {
|
||||
const cfg = getAccountConfig(acc.id).accountType;
|
||||
return cfg && !client.lastFetchErrors?.[acc.name] ? claimedCycle(client, acc.id, cfg) : null;
|
||||
// Judged as claimDue claims: within LEG_COMPLETION_EPSILON of the target.
|
||||
return cfg && !client.lastFetchErrors?.[acc.name] ? claimedCycle(client, acc.id, cfg, new Date(), LEG_COMPLETION_EPSILON) : null;
|
||||
} catch (err) {
|
||||
console.error(`[claim] ${acc.name}: the cycle the claim ends couldn't be read — ${errorMessage(err)}`);
|
||||
return null;
|
||||
|
||||
@@ -574,7 +574,7 @@ export function loadClaimedCycles(accountId: number): ClaimedCycle[] {
|
||||
if (!Array.isArray(list)) return [];
|
||||
return list.filter((c): c is ClaimedCycle => typeof c?.date === 'string' && typeof c.cycle === 'string'
|
||||
&& Number.isInteger(c.stage) && typeof c.drain === 'boolean' && Number.isInteger(c.cycleSeq))
|
||||
.map(({ date, stage, drain, cycle, cycleSeq }) => ({ date, stage, drain, cycle, cycleSeq }));
|
||||
.map(({ date, stage, drain, cycle, cycleSeq, at }) => ({ date, stage, drain, cycle, cycleSeq, ...(Number.isFinite(at) ? { at } : {}) }));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
+8
-3
@@ -98,6 +98,8 @@ function collectFirmStats() {
|
||||
// The completed cycles this instance's own claims ended, by slice
|
||||
// (listed after the accounts — see below).
|
||||
const claimedBySlice = new Map<AccountTypeStats, NonNullable<AccountTypeStats['completedAccounts']>>();
|
||||
/** When each claimed entry's claim was made, to list the newest first. */
|
||||
const claimedAt = new Map<object, number>();
|
||||
|
||||
for (const acc of client.accountList) {
|
||||
const reportedCash = client.accountCashBalances[acc.id];
|
||||
@@ -331,14 +333,16 @@ function collectFirmStats() {
|
||||
if (!withinEndedCycleWindow(c.date, rawToday)) continue;
|
||||
let list = claimedBySlice.get(slice);
|
||||
if (!list) claimedBySlice.set(slice, list = []);
|
||||
list.push({
|
||||
const entry = {
|
||||
name: acc.name,
|
||||
date: c.date,
|
||||
stage: c.stage,
|
||||
kind: completionKind(cfg, c.drain),
|
||||
cycle: c.cycle,
|
||||
...(c.cycleSeq > 0 ? { cycleSeq: c.cycleSeq } : {}),
|
||||
});
|
||||
};
|
||||
claimedAt.set(entry, c.at ?? 0);
|
||||
list.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,7 +463,8 @@ function collectFirmStats() {
|
||||
const list = slice.completedAccounts ?? [];
|
||||
const key = (e: (typeof list)[number]) => `${e.name}\n${e.cycle}\n${e.cycleSeq ?? 0}`;
|
||||
const named = new Set(list.map(key));
|
||||
for (const e of claimed.sort((a, b) => b.date.localeCompare(a.date))) {
|
||||
// Newest first across accounts: a day's ties go by the claim's time.
|
||||
for (const e of claimed.sort((a, b) => b.date.localeCompare(a.date) || (claimedAt.get(b) ?? 0) - (claimedAt.get(a) ?? 0))) {
|
||||
if (named.has(key(e))) continue;
|
||||
named.add(key(e));
|
||||
list.push(e);
|
||||
|
||||
@@ -982,7 +982,7 @@ export function endedCycleCompletion(opts: {
|
||||
}
|
||||
|
||||
/** A completed cycle a payout claim ended (claimedCycle). */
|
||||
export interface ClaimedCycle { date: string; stage: number; drain: boolean; cycle: string; cycleSeq: number }
|
||||
export interface ClaimedCycle { date: string; stage: number; drain: boolean; cycle: string; cycleSeq: number; at?: number }
|
||||
|
||||
/**
|
||||
* The cycle a payout claim about to go out ends (PlatformClient.claimProfit),
|
||||
@@ -1020,6 +1020,7 @@ export function claimedCycle(
|
||||
combineCmeHolidays?: boolean; tradesEveryDay?: boolean; dailyLossLimit?: number; stagesSameDay?: boolean;
|
||||
},
|
||||
now: Date = new Date(),
|
||||
tolerance = 0,
|
||||
): ClaimedCycle | null {
|
||||
const cash = client.accountCashBalances[accountId];
|
||||
const cycleDailyPnL = client.dailyPnL[accountId];
|
||||
@@ -1029,11 +1030,15 @@ export function claimedCycle(
|
||||
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity, cfg.withdrawalStages,
|
||||
client.priorProfit?.[accountId] ?? 0, fundTransactions, cfg.minDayPnL, stageInputsFor(client, accountId, cfg),
|
||||
);
|
||||
// `tolerance`: how far short of the target the claim still ends the
|
||||
// cycle — the engine's day is done within LEG_COMPLETION_EPSILON of it.
|
||||
const judged = tolerance > 0 && effective.profitTarget > tolerance
|
||||
? { ...effective, profitTarget: effective.profitTarget - tolerance } : effective;
|
||||
const rawTradeDate = tradeDateFor(client, now);
|
||||
const completion = cycleCompletion({
|
||||
accountId, rawTradeDate, client, cycleDailyPnL, rawRealizedPnL: cash.realizedPnL,
|
||||
combineCmeHolidays: cfg.combineCmeHolidays === true, tradesEveryDay: cfg.tradesEveryDay === true,
|
||||
effective, equityProfit: cash.amount - cfg.accountSize, splitEnabled: cfg.splitEnabled, now,
|
||||
effective: judged, equityProfit: cash.amount - cfg.accountSize, splitEnabled: cfg.splitEnabled, now,
|
||||
});
|
||||
if (completion === 'none') return null;
|
||||
const cycle = client.lastFundDates?.[accountId] ?? '';
|
||||
@@ -1041,7 +1046,7 @@ export function claimedCycle(
|
||||
const place = completionPlace({ payoutDays, fundTransactions, cycle });
|
||||
return {
|
||||
date: combineCmeSessions(effectiveCombine(cfg), [], rawTradeDate, 0).tradeDate,
|
||||
stage: place.stage, drain: effective.drain, cycle, cycleSeq: place.cycleSeq,
|
||||
stage: place.stage, drain: effective.drain, cycle, cycleSeq: place.cycleSeq, at: now.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user