Vest: a claim names the completed cycle it ends, a day's second and one after the reset too
Two of the cycles a claim ends never got a completion entry. A Vest day's second cycle is one: the auto-claim at the next tick beats the 30 s report, and the payout books into that day's fund row, so the latest fund date never moves for the catch-up. A cycle claimed right after the 20:00 ET reset is the other: the reset puts it back on stage 1, and all of its P&L sits in the previous claim's day row, so endedCycleCompletion reads none of it. Before a claim goes out, claimedCycle (trading-logic) checks whether the cycle stands complete: cycleCompletion 'today' or 'before' on the client's numbers. It keys the cycle the way the final day would (completionPlace on the payout list), so the master dedupes the two. afterClaim keeps the result in account_meta. The reporter names each kept cycle for the catch-up's week. It lists them last in the slice, newest first, under the master's per-slice cap, and adds no counter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
4a8e027056
commit
e02838409b
+35
-6
@@ -19,9 +19,10 @@ import {
|
||||
FALLBACK_COMMISSION_PER_CONTRACT, MAX_COMMISSION_PER_CONTRACT, DLL_MIN_STOP,
|
||||
CME_EVENING_OPEN_MIN, foldReason, withinFirmCaps,
|
||||
fundRowsKey, markFinishedCycle, unbookedPayoutDrop, machineSide, stageInputsFor, floorRoom, shortestNextLeg,
|
||||
type DayPlan, type FinishedCycleMark,
|
||||
claimedCycle, withinEndedCycleWindow,
|
||||
type DayPlan, type FinishedCycleMark, type ClaimedCycle,
|
||||
} from './trading-logic';
|
||||
import { getSetting, setSetting, loadAccountMeta, saveAccountMeta } from './db';
|
||||
import { getSetting, setSetting, loadAccountMeta, saveAccountMeta, loadClaimedCycles, saveClaimedCycles } from './db';
|
||||
import type { LaneTick, TickReason } from './master-protocol';
|
||||
import { isNotionalPlatform } from './master-protocol';
|
||||
import { POINT_VALUES, housekeepingRoot, HOUSEKEEPING_ROOT, MAX_HOLD_HOURS, futuresContracts } from './instrument-specs';
|
||||
@@ -2011,10 +2012,11 @@ async function runClaims(lane: string, ready: FirmClient[]): Promise<void> {
|
||||
const amount = claimDue(client, acc, Date.now());
|
||||
if (amount === null) continue;
|
||||
try {
|
||||
const ends = claimEnds(client, acc);
|
||||
// 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, tradeDateFor(client));
|
||||
afterClaim('[auto-claim]', firm, client, acc, claimed, received, tradeDateFor(client), ends);
|
||||
} catch (err) {
|
||||
(runtime().claimFailedAt ??= new Map()).set(acc.id, Date.now());
|
||||
console.error(`[auto-claim] ${firm.name} ${acc.name}: claim of $${Math.floor(amount * 100) / 100} failed — ${errorMessage(err)}; retrying in ${CLAIM_RETRY_MS / 60_000}m`);
|
||||
@@ -2062,15 +2064,41 @@ async function claimTick(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** The completed cycle a claim about to go out ends (claimedCycle), taken
|
||||
* before the claim books its payout. Not on rows a failed read left behind
|
||||
* — the master keeps it — which is historyFresh's own test for the
|
||||
* account; not its client-wide one, no read in flight, as each claim
|
||||
* starts a read and the next claim of the tick would never name its
|
||||
* cycle. The claim already stands on the account's read (historyReady).
|
||||
* Never throws: a payout isn't held back for its record. */
|
||||
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;
|
||||
} catch (err) {
|
||||
console.error(`[claim] ${acc.name}: the cycle the claim ends couldn't be read — ${errorMessage(err)}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
* count and entry outcome are cleared, as if it hadn't traded today — and
|
||||
* the completed cycle it ended (claimEnds) is kept for the reporter, which
|
||||
* may never have seen it complete. */
|
||||
function afterClaim(
|
||||
tag: string, firm: TradableFirm, client: PlatformClient, acc: { id: number; name: string },
|
||||
claimed: number, received: number | null, tradeDate: string,
|
||||
claimed: number, received: number | null, tradeDate: string, ends: ClaimedCycle | null,
|
||||
): number {
|
||||
runtime().legCounts.delete(acc.id);
|
||||
runtime().entryOutcomes?.delete(acc.id);
|
||||
runtime().claimFailedAt?.delete(acc.id);
|
||||
if (ends) {
|
||||
try {
|
||||
saveClaimedCycles(acc.id, [...loadClaimedCycles(acc.id).filter((c) => withinEndedCycleWindow(c.date, ends.date)), ends]);
|
||||
} catch (err) {
|
||||
console.error(`${tag} ${firm.name} ${acc.name}: the completed cycle the claim ended wasn't saved — ${errorMessage(err)}`);
|
||||
}
|
||||
}
|
||||
const stage = 1 + (client.sameDayClaims?.(acc.id) ?? 0);
|
||||
console.log(`${tag} ${firm.name} ${acc.name}: claimed $${claimed}${received !== null ? ` (you get $${received})` : ''}`
|
||||
+ ` — trading stage ${stage} next (${tradeDate})`);
|
||||
@@ -2147,8 +2175,9 @@ 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 ends = claimEnds(c.client, c.acc);
|
||||
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, tradeDateFor(c.client));
|
||||
const stage = afterClaim('[claim-now]', c.firm, c.client, c.acc, claimed, received, tradeDateFor(c.client), ends);
|
||||
return { claimed, received, stage };
|
||||
} finally {
|
||||
releaseTradeMutex(lease, lane);
|
||||
|
||||
@@ -2,6 +2,7 @@ import Database from 'better-sqlite3';
|
||||
import { mkdirSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { sortableIso, stampRulesDate } from './kept-rules';
|
||||
import type { ClaimedCycle } from './trading-logic';
|
||||
|
||||
// AUTOFIRMER_DB lets several instances share one checkout, each with its own
|
||||
// database. It is read once, at module load — schema creation and seeding are
|
||||
@@ -556,6 +557,29 @@ export function loadEndedCycleStart(accountId: number): { fundDate: string; incl
|
||||
}
|
||||
}
|
||||
|
||||
/** The completed cycles this instance's own claims ended (trading-logic
|
||||
* claimedCycle), oldest first — saved by the claim (auto-trade afterClaim),
|
||||
* since it can end one before any report sees it complete; the reporter
|
||||
* names each for the catch-up's week. Gone with a wipe, as the claims
|
||||
* themselves are. */
|
||||
export function saveClaimedCycles(accountId: number, list: ClaimedCycle[]): void {
|
||||
saveAccountMeta(accountId, 'claimed_cycles', JSON.stringify(list));
|
||||
}
|
||||
|
||||
export function loadClaimedCycles(accountId: number): ClaimedCycle[] {
|
||||
const raw = loadAccountMeta(accountId, 'claimed_cycles');
|
||||
if (raw === null) return [];
|
||||
try {
|
||||
const list = JSON.parse(raw) as unknown;
|
||||
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 }));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Deletes every cached row for an account — daily P&L, fund transactions and
|
||||
* meta — so the next report fetch rebuilds it from scratch. Rows older than
|
||||
* the report's 28-day window are gone for good; that is the point (stale
|
||||
|
||||
@@ -43,8 +43,11 @@ export interface AccountTypeStats {
|
||||
* keys on it tells them apart; one that doesn't folds them into the
|
||||
* day's first. The list also carries caught-up entries: for up to 7
|
||||
* days after a fund transaction starts a new cycle, it includes the cycle
|
||||
* that just ended if that cycle had met the rule. Caught-up entries are
|
||||
* not counted in `completed`. Optional: older instances omit it. */
|
||||
* that just ended if that cycle had met the rule — and, for 7 days after
|
||||
* it, each cycle the instance's own payout claim ended complete, dated
|
||||
* the claim's day and listed last, newest first (a claim can end one
|
||||
* before any report sees it). Caught-up and claimed entries are not
|
||||
* counted in `completed`. Optional: older instances omit it. */
|
||||
completedAccounts?: { name: string; date: string; stage: number; kind: 'pass' | 'payout' | 'drain'; cycle: string; cycleSeq?: number }[];
|
||||
/** Live accounts by trading days completed toward the current target:
|
||||
* {daysTraded: accountCount}. Feeds the master's payout calendar, so
|
||||
|
||||
+46
-2
@@ -11,7 +11,7 @@
|
||||
* POST keeps those working unchanged.
|
||||
*/
|
||||
|
||||
import { getSetting, loadEndedCycleStart } from './db';
|
||||
import { getSetting, loadClaimedCycles, loadEndedCycleStart } from './db';
|
||||
import { pushReport } from './master-link';
|
||||
import { getBuildVersion } from './version';
|
||||
import { tradeMixFor } from './trade-mix';
|
||||
@@ -20,7 +20,10 @@ import { getClients } from './clients';
|
||||
import { getAccountTypeById, getKeptRuleIdsForFirm, getTradableFirms } from './firm-catalog';
|
||||
import { getAccountConfig } from './account-type-resolver';
|
||||
import { getSchedulerStatus } from './auto-trade';
|
||||
import { botStopsActive, combineCmeSessions, computeDayPlan, completionPlace, cycleCompletion, endedCycleCompletion, LEG_COMPLETION_EPSILON, projectPerfectPath, resolveEffectiveConfig, stageInputsFor, tradeDateFor } from './trading-logic';
|
||||
import {
|
||||
botStopsActive, combineCmeSessions, computeDayPlan, completionPlace, cycleCompletion, endedCycleCompletion, LEG_COMPLETION_EPSILON,
|
||||
projectPerfectPath, resolveEffectiveConfig, stageInputsFor, tradeDateFor, withinEndedCycleWindow,
|
||||
} from './trading-logic';
|
||||
import type { AccountTypeStats, FirmStats, InstrumentStats, SymbolStats } from './master-protocol';
|
||||
import { accountNextLeg, accountLegShape } from './leg-estimate';
|
||||
import { sizingHoldHours } from './person-size';
|
||||
@@ -92,6 +95,9 @@ function collectFirmStats() {
|
||||
}
|
||||
return entry;
|
||||
};
|
||||
// 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']>>();
|
||||
|
||||
for (const acc of client.accountList) {
|
||||
const reportedCash = client.accountCashBalances[acc.id];
|
||||
@@ -314,6 +320,28 @@ function collectFirmStats() {
|
||||
}
|
||||
}
|
||||
|
||||
// The completed cycles this instance's own claims ended
|
||||
// (afterClaim), named for the catch-up's week: a claim can end
|
||||
// one before any report saw it complete, where the catch-up
|
||||
// can't find it again (a Vest day's second claim, or one right
|
||||
// after the reset — trading-logic claimedCycle). Named only, as
|
||||
// the catch-up's: no counter moves.
|
||||
if (cfg) {
|
||||
for (const c of loadClaimedCycles(acc.id).reverse()) {
|
||||
if (!withinEndedCycleWindow(c.date, rawToday)) continue;
|
||||
let list = claimedBySlice.get(slice);
|
||||
if (!list) claimedBySlice.set(slice, list = []);
|
||||
list.push({
|
||||
name: acc.name,
|
||||
date: c.date,
|
||||
stage: c.stage,
|
||||
kind: completionKind(cfg, c.drain),
|
||||
cycle: c.cycle,
|
||||
...(c.cycleSeq > 0 ? { cycleSeq: c.cycleSeq } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (client.positions[acc.id]) {
|
||||
inTrade++;
|
||||
accountsTraded++;
|
||||
@@ -423,6 +451,22 @@ function collectFirmStats() {
|
||||
}
|
||||
}
|
||||
|
||||
// Claimed cycles go last in each slice's list, newest first: the
|
||||
// master reads only so many entries a slice (twice its accounts), so
|
||||
// what it has long had drops off before a final day, a catch-up or a
|
||||
// fresh claim. One those already name goes once.
|
||||
for (const [slice, claimed] of claimedBySlice) {
|
||||
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))) {
|
||||
if (named.has(key(e))) continue;
|
||||
named.add(key(e));
|
||||
list.push(e);
|
||||
}
|
||||
if (list.length > 0) slice.completedAccounts = list;
|
||||
}
|
||||
|
||||
// Biggest shortfall first — the account types with the most left to do
|
||||
// are the ones worth reading, and Unassigned sinks to the bottom only
|
||||
// when it is genuinely small.
|
||||
|
||||
+72
-2
@@ -742,6 +742,13 @@ const ROLL_SETTLE_MS = 5 * 60 * 1000;
|
||||
* still checked for a missed completion. */
|
||||
const ENDED_CYCLE_WINDOW_DAYS = 7;
|
||||
|
||||
/** Is `date` (a fund date, or a claimed cycle's — claimedCycle) within the
|
||||
* week its ended cycle is still named? False for an unparseable date. */
|
||||
export function withinEndedCycleWindow(date: string, rawTradeDate: string): boolean {
|
||||
const ageDays = (Date.parse(`${rawTradeDate}T00:00:00Z`) - Date.parse(`${date}T00:00:00Z`)) / (24 * 60 * 60 * 1000);
|
||||
return ageDays <= ENDED_CYCLE_WINDOW_DAYS; // NaN: false
|
||||
}
|
||||
|
||||
/** Nothing left to trade AND the days booked: with no min-day amount the
|
||||
* plan coasts at 0 while days are still owed, so count them too. */
|
||||
function cycleOwesNothing(
|
||||
@@ -924,8 +931,7 @@ export function endedCycleCompletion(opts: {
|
||||
const everyDay = cfg.tradesEveryDay === true;
|
||||
const combine = effectiveCombine(cfg);
|
||||
if (lastFundDate === null || start === null) return null;
|
||||
const ageDays = (Date.parse(`${rawTradeDate}T00:00:00Z`) - Date.parse(`${lastFundDate}T00:00:00Z`)) / (24 * 60 * 60 * 1000);
|
||||
if (!(ageDays <= ENDED_CYCLE_WINDOW_DAYS)) return null; // NaN included: an unparseable date
|
||||
if (!withinEndedCycleWindow(lastFundDate, rawTradeDate)) return null;
|
||||
if (sessionDoubtful(rawTradeDate, opts.cycleDailyPnL, opts.now ?? new Date(), everyDay, opts.client)) return null;
|
||||
|
||||
const latest = (a: string, b: string) => (b > a ? b : a);
|
||||
@@ -975,6 +981,70 @@ 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 }
|
||||
|
||||
/**
|
||||
* The cycle a payout claim about to go out ends (PlatformClient.claimProfit),
|
||||
* when it stands complete — cycleCompletion's 'today' or 'before' on the
|
||||
* client's numbers just before the claim — named and numbered as the final
|
||||
* day names it (completionPlace), so the master dedupes the two. date = the
|
||||
* claim's trade date. Null for a claim short of the rule (a Claim now below
|
||||
* the target) or on numbers not read yet.
|
||||
*
|
||||
* Taken before the claim books its payout, and kept once it goes through
|
||||
* (auto-trade afterClaim). The claim starts a new cycle at once, and two of
|
||||
* the cycles it can end no report would name otherwise. A Vest day's second
|
||||
* cycle: the auto-claim at the next tick beats the 30 s report, and its
|
||||
* payout books into that day's fund row, so the latest fund date doesn't
|
||||
* move for the catch-up. And one claimed right after the 20:00 ET reset,
|
||||
* which puts it back on the first stage, whose target the day's leftover
|
||||
* profit meets: all of its P&L sits in the day row of the cycle before it,
|
||||
* so endedCycleCompletion reads none of it.
|
||||
*/
|
||||
export function claimedCycle(
|
||||
client: {
|
||||
accountCashBalances: { [accountId: number]: { amount: number; realizedPnL: number } | undefined };
|
||||
dailyPnL: { [accountId: number]: { date: string; pnl: number }[] | undefined };
|
||||
fundTransactions?: { [accountId: number]: { date: string; amount: number }[] | undefined };
|
||||
lastFundDates?: { [accountId: number]: string | null | undefined };
|
||||
priorProfit?: { [accountId: number]: number | undefined };
|
||||
tradeDate?: (now: number) => string;
|
||||
sameDayClaims?: (accountId: number) => number;
|
||||
payoutDays?: (accountId: number) => string[] | undefined;
|
||||
},
|
||||
accountId: number,
|
||||
cfg: {
|
||||
profitTarget: number; consistency: number; minTradingDays: number; minDayPnL: number; accountSize: number;
|
||||
targetSameEquity: boolean; withdrawalStages: WithdrawalStage[]; splitEnabled: boolean;
|
||||
combineCmeHolidays?: boolean; tradesEveryDay?: boolean; dailyLossLimit?: number; stagesSameDay?: boolean;
|
||||
},
|
||||
now: Date = new Date(),
|
||||
): ClaimedCycle | null {
|
||||
const cash = client.accountCashBalances[accountId];
|
||||
const cycleDailyPnL = client.dailyPnL[accountId];
|
||||
const fundTransactions = client.fundTransactions?.[accountId];
|
||||
if (!cash || !cycleDailyPnL || !fundTransactions) return null;
|
||||
const effective = resolveEffectiveConfig(
|
||||
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity, cfg.withdrawalStages,
|
||||
client.priorProfit?.[accountId] ?? 0, fundTransactions, cfg.minDayPnL, stageInputsFor(client, accountId, cfg),
|
||||
);
|
||||
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,
|
||||
});
|
||||
if (completion === 'none') return null;
|
||||
const cycle = client.lastFundDates?.[accountId] ?? '';
|
||||
const payoutDays = client.payoutDays?.(accountId) ?? fundTransactions.filter((f) => f.amount < 0).map((f) => f.date);
|
||||
const place = completionPlace({ payoutDays, fundTransactions, cycle });
|
||||
return {
|
||||
date: combineCmeSessions(effectiveCombine(cfg), [], rawTradeDate, 0).tradeDate,
|
||||
stage: place.stage, drain: effective.drain, cycle, cycleSeq: place.cycleSeq,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the next trading day's profit target for an account.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user