Reporter: catch up completed cycles on reset, report each firm's account list
When a fund transaction starts a new cycle (the days revert to 0), the cycle it ended is checked against the completion rule for a week after and named if it stood complete — its final day may never have been reported. Only for a cycle this instance saw begin: the clients now note where the ended cycle started as they move last_fund_date forward. Completions carry the cycle they belong to, and each firm reports its whole account list so the master can tell erased accounts and spot new funded ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
acb8879575
commit
a7e01b0138
+144
-26
@@ -637,6 +637,48 @@ export type CycleCompletion = 'none' | 'today' | 'before';
|
||||
* number — already booked under its own date — would count as a second day. */
|
||||
const ROLL_SETTLE_MS = 5 * 60 * 1000;
|
||||
|
||||
/** How long after a fund transaction starts a new cycle the cycle it ended is
|
||||
* still checked for a missed completion. */
|
||||
const ENDED_CYCLE_WINDOW_DAYS = 7;
|
||||
|
||||
/** 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(
|
||||
accountId: number,
|
||||
eff: EffectiveConfig,
|
||||
rows: { date: string; pnl: number }[],
|
||||
date: string,
|
||||
equityProfit: number,
|
||||
realized: number,
|
||||
splitEnabled: boolean,
|
||||
): boolean {
|
||||
const plan = computeDayPlan(
|
||||
accountId, date, eff.profitTarget, eff.consistency, rows, eff.minDayPnL,
|
||||
eff.minTradingDays, equityProfit, realized, splitEnabled, eff.drain,
|
||||
);
|
||||
const qualifying = rows.filter((d) => d.date !== date
|
||||
&& (eff.minDayPnL > 0 ? d.pnl >= eff.minDayPnL : d.pnl !== 0)).length;
|
||||
return plan !== null && plan.dayTarget <= 0 && qualifying >= eff.minTradingDays;
|
||||
}
|
||||
|
||||
/** Is the live realizedPnL too doubtful to count as today's session? A
|
||||
* weekend (it is the last session's stale number, already booked under its
|
||||
* own date), a Monday with a weekend-dated row — the live session filed under
|
||||
* a weekend date, which would count it twice — and the first minutes after
|
||||
* the 5 PM roll. */
|
||||
function sessionDoubtful(rawTradeDate: string, cycleDailyPnL: { date: string; pnl: number }[], now: Date): boolean {
|
||||
const day = new Date(`${rawTradeDate}T00:00:00Z`);
|
||||
const dow = day.getUTCDay();
|
||||
if (dow === 0 || dow === 6) return true;
|
||||
if (dow === 1) {
|
||||
const sunday = fmtUTC(new Date(day.getTime() - 24 * 60 * 60 * 1000));
|
||||
const saturday = fmtUTC(new Date(day.getTime() - 2 * 24 * 60 * 60 * 1000));
|
||||
if (cycleDailyPnL.some((d) => d.date === saturday || d.date === sunday)) return true;
|
||||
}
|
||||
// Settle window: a few minutes ago it was still an earlier trade date.
|
||||
return currentTradeDateCT(new Date(now.getTime() - ROLL_SETTLE_MS)) < rawTradeDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Did today's trading finish the account's current cycle — profit target,
|
||||
* consistency and minimum days all met? projectPerfectPath's "already
|
||||
@@ -666,40 +708,116 @@ export function cycleCompletion(opts: {
|
||||
/** Defaults to the current time. */
|
||||
now?: Date;
|
||||
}): CycleCompletion {
|
||||
const { accountId, rawTradeDate, effective: eff } = opts;
|
||||
const { accountId, rawTradeDate, effective: eff, equityProfit, splitEnabled } = opts;
|
||||
const view = combineCmeSessions(opts.combineCmeHolidays, opts.cycleDailyPnL, rawTradeDate, opts.rawRealizedPnL);
|
||||
const prior = view.dailyPnL.filter((d) => d.date !== view.tradeDate);
|
||||
// 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.
|
||||
const owesNothing = (rows: { date: string; pnl: number }[], date: string, realized: number): boolean => {
|
||||
const plan = computeDayPlan(
|
||||
accountId, date, eff.profitTarget, eff.consistency, rows, eff.minDayPnL,
|
||||
eff.minTradingDays, opts.equityProfit, realized, opts.splitEnabled, eff.drain,
|
||||
);
|
||||
const qualifying = rows.filter((d) => d.date !== date
|
||||
&& (eff.minDayPnL > 0 ? d.pnl >= eff.minDayPnL : d.pnl !== 0)).length;
|
||||
return plan !== null && plan.dayTarget <= 0 && qualifying >= eff.minTradingDays;
|
||||
};
|
||||
|
||||
if (owesNothing(prior, view.tradeDate, view.realizedPnL)) return 'before';
|
||||
|
||||
const day = new Date(`${rawTradeDate}T00:00:00Z`);
|
||||
const dow = day.getUTCDay();
|
||||
if (dow === 0 || dow === 6) return 'none';
|
||||
if (dow === 1) {
|
||||
const sunday = fmtUTC(new Date(day.getTime() - 24 * 60 * 60 * 1000));
|
||||
const saturday = fmtUTC(new Date(day.getTime() - 2 * 24 * 60 * 60 * 1000));
|
||||
if (opts.cycleDailyPnL.some((d) => d.date === saturday || d.date === sunday)) return 'none';
|
||||
}
|
||||
// Settle window: a few minutes ago it was still an earlier trade date.
|
||||
const now = opts.now ?? new Date();
|
||||
if (currentTradeDateCT(new Date(now.getTime() - ROLL_SETTLE_MS)) < rawTradeDate) return 'none';
|
||||
if (cycleOwesNothing(accountId, eff, prior, view.tradeDate, equityProfit, view.realizedPnL, splitEnabled)) return 'before';
|
||||
if (sessionDoubtful(rawTradeDate, opts.cycleDailyPnL, opts.now ?? new Date())) return 'none';
|
||||
|
||||
const withToday = [...prior, { date: view.tradeDate, pnl: view.realizedPnL }];
|
||||
if (view.realizedPnL !== 0 && owesNothing(withToday, nextTradingDate(view.tradeDate), 0)) return 'today';
|
||||
const probe = nextTradingDate(view.tradeDate);
|
||||
if (view.realizedPnL !== 0 && cycleOwesNothing(accountId, eff, withToday, probe, equityProfit, 0, splitEnabled)) return 'today';
|
||||
return 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch-up for a missed final day: did the cycle the latest fund transaction
|
||||
* just ended stand complete — profit target, consistency and minimum days all
|
||||
* met? cycleCompletion reads 'today' on the final day only, so a completion
|
||||
* its guards held back (or that the instance was offline for) would vanish
|
||||
* once the payout, erasure or reset starts a new cycle and the days revert
|
||||
* to 0. Checked for a week after that fund transaction.
|
||||
*
|
||||
* The ended cycle is judged on its own rows — from the previous fund date
|
||||
* (its own row too when the client counted it in, as it does for a deposit),
|
||||
* up to the latest one, less any the current cycle holds — against the
|
||||
* equity it ended on: live equity less the current cycle's P&L and the fund
|
||||
* transaction itself. A failed account's reset (a positive adjustment after
|
||||
* a cycle that fell short) correctly reads null: the ended cycle never met
|
||||
* the rule. Null on a doubtful session too — on a weekend the live
|
||||
* realizedPnL is stale and would come off equity twice — and when this
|
||||
* instance never saw the ended cycle start (a new instance or a wipe, whose
|
||||
* fetch window cuts it short; TopstepX's first-check lump, which isn't a
|
||||
* cycle at all).
|
||||
*
|
||||
* `cycle` is the fund date that STARTED the ended cycle ('' for the
|
||||
* account's first), which is what client.lastFundDates held while it was
|
||||
* current — the same key the final-day path reported for this cycle, so the
|
||||
* master dedupes the two.
|
||||
*/
|
||||
export function endedCycleCompletion(opts: {
|
||||
accountId: number;
|
||||
/** currentTradeDateCT(). */
|
||||
rawTradeDate: string;
|
||||
/** Every cycle's daily P&L, raw (client.fullDailyPnL). */
|
||||
fullDailyPnL: { date: string; pnl: number }[];
|
||||
/** Current-cycle daily P&L, raw (client.dailyPnL). */
|
||||
cycleDailyPnL: { date: string; pnl: number }[];
|
||||
fundTransactions: { date: string; amount: number }[];
|
||||
/** client.lastFundDates — the fund transaction that started the current cycle. */
|
||||
lastFundDate: string | null;
|
||||
/** loadEndedCycleStart() — where the client saw the ended cycle begin,
|
||||
* null when it never did. */
|
||||
endedCycleStart: { fundDate: string; includesDay: boolean } | null;
|
||||
rawRealizedPnL: number;
|
||||
/** amount − accountSize — today's P&L included. */
|
||||
equityProfit: number;
|
||||
cfg: {
|
||||
profitTarget: number; consistency: number; minTradingDays: number; minDayPnL: number;
|
||||
targetSameEquity: boolean; withdrawalStages: WithdrawalStage[];
|
||||
combineCmeHolidays: boolean; splitEnabled: boolean;
|
||||
};
|
||||
/** Defaults to the current time. */
|
||||
now?: Date;
|
||||
}): { date: string; stage: number; drain: boolean; cycle: string } | null {
|
||||
const { accountId, rawTradeDate, lastFundDate, endedCycleStart: start, fundTransactions, cfg } = opts;
|
||||
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 (sessionDoubtful(rawTradeDate, opts.cycleDailyPnL, opts.now ?? new Date())) return null;
|
||||
|
||||
const latest = (a: string, b: string) => (b > a ? b : a);
|
||||
const earlierFunds = fundTransactions.filter((f) => f.date < lastFundDate);
|
||||
const prevFundDate = earlierFunds.length > 0 ? earlierFunds.map((f) => f.date).reduce(latest) : null;
|
||||
// Saved for another transition — two fund dates since the client last
|
||||
// looked — so the start it saw isn't this cycle's.
|
||||
if (start.fundDate !== (prevFundDate ?? '')) return null;
|
||||
const current = new Set(opts.cycleDailyPnL.map((d) => d.date));
|
||||
const endedRaw = opts.fullDailyPnL.filter((d) => d.date <= lastFundDate
|
||||
&& (prevFundDate === null || d.date > prevFundDate || (start.includesDay && d.date === prevFundDate))
|
||||
&& !current.has(d.date));
|
||||
if (!endedRaw.some((d) => d.pnl !== 0)) return null;
|
||||
|
||||
// Equity the ended cycle finished on, worked back from live equity: less
|
||||
// everything the current cycle booked (today's live P&L included) and
|
||||
// less the fund transaction between the two.
|
||||
const cur = combineCmeSessions(cfg.combineCmeHolidays, opts.cycleDailyPnL, rawTradeDate, opts.rawRealizedPnL);
|
||||
const curTotal = cur.dailyPnL.filter((d) => d.date !== cur.tradeDate).reduce((s, d) => s + d.pnl, 0)
|
||||
+ cur.realizedPnL;
|
||||
const fundAmt = fundTransactions.filter((f) => f.date === lastFundDate).reduce((s, f) => s + f.amount, 0);
|
||||
const equityBefore = round2(opts.equityProfit - curTotal - fundAmt);
|
||||
|
||||
// The stage the ended cycle traded on: only the withdrawals before it.
|
||||
const eff = resolveEffectiveConfig(
|
||||
cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity,
|
||||
cfg.withdrawalStages, 0, earlierFunds, cfg.minDayPnL,
|
||||
);
|
||||
const rows = combineCmeSessions(cfg.combineCmeHolidays, endedRaw, lastFundDate, 0).dailyPnL;
|
||||
// A combining firm's folded day can net to 0 — nothing traded, as the firm counts it.
|
||||
const traded = rows.filter((d) => d.pnl !== 0);
|
||||
if (traded.length === 0) return null;
|
||||
const probe = nextTradingDate(rows.map((d) => d.date).reduce(latest));
|
||||
if (!cycleOwesNothing(accountId, eff, rows, probe, equityBefore, 0, cfg.splitEnabled)) return null;
|
||||
|
||||
return {
|
||||
date: traded.map((d) => d.date).reduce(latest),
|
||||
stage: 1 + earlierFunds.filter((f) => f.amount < 0).length,
|
||||
drain: eff.drain,
|
||||
cycle: prevFundDate ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
/** Dollar-per-point value for common futures products. */
|
||||
export const POINT_VALUES: { [symbol: string]: number } = {
|
||||
NQ: 20, MNQ: 2, ES: 50, MES: 5,
|
||||
|
||||
Reference in New Issue
Block a user