Count CME holiday sessions as one combined trading day when the type opts in

Account types with the catalog's new combineCmeHolidays flag are read
through a combined-day view: holiday-session P&L entries fold into the
next full trading day (computed from the federal-holiday rules, so any
year works), the day count matches how the firm records it (Apex: Sun/
Mon/Tue around a Monday holiday is ONE day), and on the later sessions
of the stretch the earlier sessions' booked P&L carries into realized —
so day targets, already-traded gates, target-hit status and the panel's
day P&L all treat the stretch as a single day. Off by default; older
masters that don't send the flag behave exactly as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-08 21:12:37 -05:00
co-authored by Claude Fable 5
parent 57846d2b5f
commit f07721019b
9 changed files with 212 additions and 31 deletions
+12 -8
View File
@@ -17,7 +17,7 @@ import { getClients } from './clients';
import { getTradableFirms } from './firm-catalog';
import { getAccountConfig } from './account-type-resolver';
import { getSchedulerStatus } from './auto-trade';
import { computeDayPlan, currentTradeDateCT, LEG_COMPLETION_EPSILON } from './trading-logic';
import { combineCmeSessions, computeDayPlan, currentTradeDateCT, LEG_COMPLETION_EPSILON } from './trading-logic';
import type { AccountTypeStats } from './master-protocol';
function collectFirmStats() {
@@ -90,23 +90,27 @@ function collectFirmStats() {
// Check if target was hit today
if (cfg) {
const dailyPnL = client.dailyPnL[acc.id] ?? [];
// Same combined-day view as the trade path: for a combining
// firm, "today" spans the holiday stretch.
const combined = combineCmeSessions(
cfg.combineCmeHolidays, client.dailyPnL[acc.id] ?? [], currentTradeDateCT(), cash.realizedPnL,
);
const equityProfit = cash.amount - cfg.accountSize;
const plan = computeDayPlan(
acc.id, currentTradeDateCT(), cfg.profitTarget, cfg.consistency,
dailyPnL, cfg.minDayPnL, cfg.minTradingDays, equityProfit, cash.realizedPnL,
acc.id, combined.tradeDate, cfg.profitTarget, cfg.consistency,
combined.dailyPnL, cfg.minDayPnL, cfg.minTradingDays, equityProfit, combined.realizedPnL,
cfg.splitEnabled
);
const targetHit = !plan ? false :
((plan.dayTarget === 0 && Math.abs(cash.realizedPnL) > 0 && (client.daysTraded[acc.id] ?? 0) <= cfg.minTradingDays) ||
(cash.realizedPnL > 0 && plan.remainingToday <= LEG_COMPLETION_EPSILON));
((plan.dayTarget === 0 && Math.abs(combined.realizedPnL) > 0 && combined.daysTraded <= cfg.minTradingDays) ||
(combined.realizedPnL > 0 && plan.remainingToday <= LEG_COMPLETION_EPSILON));
if (targetHit || cash.realizedPnL !== 0) {
if (targetHit || combined.realizedPnL !== 0) {
accountsTraded++;
slice.accountsTraded++;
// A grind-day fill (dayTarget 0) can flag targetHit on a
// negative day — a loss is still a loss, so red wins.
if (cash.realizedPnL < 0) {
if (combined.realizedPnL < 0) {
lossCount++;
slice.loss = (slice.loss ?? 0) + 1;
} else if (targetHit) {