From ef476ccbb7a24ae8718f8418c5584c3236c2bb4b Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Tue, 8 Sep 2026 21:28:39 -0500 Subject: [PATCH] Add day-plan double-count diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The daily target sometimes inflates to base + consistency×todayPnL (e.g. 1200 + 0.4×1218.48 = 1687.39), which happens when a session's cached daily-P&L row is dated something other than the current tradeDate: it then lands in BOTH the prior-day list and realizedPnL, so day-start equity reads too low while the prior-day sum stays high. Detect it with a book-balance invariant that is coincidence-proof (it does not care whether prior and today P&L happen to be equal): the reconstructed day-start equity (equityProfit − realizedPnL) must equal priorProfit + sum(prior days). The state route now logs the full account state whenever those disagree by more than $50, and GET /api/debug reports the same per-account breakdown on demand (?account=) so the exact inputs can be captured the moment a number looks wrong. Read-only; no behavior change. Remove once the trigger is fixed. Co-Authored-By: Claude Opus 4.8 --- app/api/debug/route.ts | 54 ++++++++++++++++++++++++++++++++++++++++-- app/api/state/route.ts | 43 +++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/app/api/debug/route.ts b/app/api/debug/route.ts index 378393e..185c249 100644 --- a/app/api/debug/route.ts +++ b/app/api/debug/route.ts @@ -1,12 +1,59 @@ import { NextResponse } from 'next/server'; import { getClients, resetClients } from '@/lib/clients'; import { getTradableFirms } from '@/lib/firm-catalog'; +import { getAccountConfig } from '@/lib/account-type-resolver'; +import { combineCmeSessions, computeDayPlan, resolveEffectiveConfig, currentTradeDateCT } from '@/lib/trading-logic'; import axios from 'axios'; -export async function GET() { +/** + * Per-account snapshot of everything computeDayPlan sees, plus the book-balance + * check the state route warns on: day-start equity (equityProfit − realizedPnL) + * must equal priorProfit + sum(prior days). A large `drift` means a session is + * counted in both the prior-day list and realizedPnL — the daily-target inflation + * bug. Filter with ?account= (case-insensitive). Read-only. + */ +function dayPlanDiag(client: any, acc: { id: number; name: string }, tradeDate: string) { + const cfg = getAccountConfig(acc.id).accountType; + const cash = client.accountCashBalances[acc.id] ?? { amount: 0, realizedPnL: 0 }; + if (!cfg) return { tradeDate, note: 'no account type assigned', cash, rawDailyPnL: client.dailyPnL[acc.id] ?? [] }; + + const priorProfit = client.priorProfit?.[acc.id] ?? 0; + const allFundTxns = client.fundTransactions?.[acc.id] ?? []; + const combined = combineCmeSessions(cfg.combineCmeHolidays === true, client.dailyPnL[acc.id] ?? [], tradeDate, cash.realizedPnL); + const effective = resolveEffectiveConfig(cfg.profitTarget, cfg.consistency, cfg.minTradingDays, cfg.targetSameEquity, cfg.withdrawalStages, priorProfit, allFundTxns); + const equityProfit = cash.amount - cfg.accountSize; + const plan = computeDayPlan(acc.id, combined.tradeDate, effective.profitTarget, effective.consistency, combined.dailyPnL, cfg.minDayPnL, effective.minTradingDays, equityProfit, combined.realizedPnL, cfg.splitEnabled); + + const priorDaysSum = combined.dailyPnL.filter((d) => d.date !== combined.tradeDate).reduce((s, d) => s + d.pnl, 0); + const dayStartEquity = Math.round((equityProfit - combined.realizedPnL) * 100) / 100; + const expectedDayStart = Math.round((priorProfit + priorDaysSum) * 100) / 100; + + return { + tradeDate: combined.tradeDate, + drift: Math.round((dayStartEquity - expectedDayStart) * 100) / 100, + dayStartEquity, + expectedDayStart, + equityProfit, + realizedPnL: combined.realizedPnL, + priorProfit, + priorDaysSum: Math.round(priorDaysSum * 100) / 100, + dayTarget: plan?.dayTarget ?? null, + remainingToday: plan?.remainingToday ?? null, + path: plan?.path ?? null, + consistency: effective.consistency, + hasTodayRow: combined.dailyPnL.some((d) => d.date === combined.tradeDate), + combineCmeHolidays: cfg.combineCmeHolidays === true, + combinedDailyPnL: combined.dailyPnL, + rawDailyPnL: client.dailyPnL[acc.id] ?? [], + }; +} + +export async function GET(req: Request) { try { const clients = getClients(); const firms = getTradableFirms(); + const tradeDate = currentTradeDateCT(); + const nameFilter = (new URL(req.url).searchParams.get('account') ?? '').toLowerCase(); const debug = firms.map((firm) => { const client = clients.get(firm.id); @@ -20,13 +67,16 @@ export async function GET() { fetchDaysComplete: (client as any).fetchDaysComplete ?? 'n/a (old instance)', lastFetchErrors: (client as any).lastFetchErrors ?? {}, lastFetchRaw: (client as any).lastFetchRaw ?? {}, - accounts: client.accountList.map((acc) => ({ + accounts: client.accountList + .filter((acc) => !nameFilter || acc.name.toLowerCase().includes(nameFilter)) + .map((acc) => ({ id: acc.id, name: acc.name, cash: client.accountCashBalances[acc.id] ?? null, daysTraded: client.daysTraded[acc.id] ?? '(not set)', dailyPnLEntries: (client.dailyPnL[acc.id] ?? []).length, dailyPnL: client.dailyPnL[acc.id] ?? [], + dayPlan: dayPlanDiag(client, acc, tradeDate), })), recentEntityEvents: client.recentEntityEvents.slice(-10).map((e) => ({ ts: new Date(e.ts).toISOString(), diff --git a/app/api/state/route.ts b/app/api/state/route.ts index 0eb6fe8..6364568 100644 --- a/app/api/state/route.ts +++ b/app/api/state/route.ts @@ -93,6 +93,49 @@ export async function GET() { const consistencyReal = (effective.consistency > 0 && effective.consistency < 1 && maxDay > 0) ? maxDay / effective.consistency : 0; effectiveProfitTarget = Math.max(effective.profitTarget, consistencyReal); + + // ── Double-count diagnostic ─────────────────────────────── + // computeDayPlan reconstructs day-start equity as + // equityProfit − realizedPnL, then treats every dailyPnL row + // NOT dated today as a prior day. Those two views must agree: + // day-start equity should equal priorProfit + sum(prior days). + // When a session's cached row is dated something other than + // today's tradeDate, that session lands in BOTH the prior-day + // list and realizedPnL — day-start equity reads too low while + // the prior sum stays high, and the daily target inflates to + // base + consistency×todayPnL (the $1,687.39 symptom). Fires + // only when the books disagree by > $50, logging the exact + // state needed to diagnose the trigger. Remove once resolved. + const priorDaysSum = dailyPnL + .filter((d) => d.date !== combined.tradeDate) + .reduce((s, d) => s + d.pnl, 0); + const dayStartEquity = equityProfit - combined.realizedPnL; + const expectedDayStart = priorProfit + priorDaysSum; + const drift = Math.round((dayStartEquity - expectedDayStart) * 100) / 100; + if (Math.abs(drift) > 50) { + console.warn('[day-plan][equity-drift] ' + JSON.stringify({ + account: acc.name, + id: acc.id, + serverNow: new Date().toISOString(), + tradeDate: combined.tradeDate, + drift, + dayStartEquity, + expectedDayStart, + equityProfit, + realizedPnL: combined.realizedPnL, + priorProfit, + priorDaysSum, + dayTarget: plan?.dayTarget ?? null, + remainingToday: plan?.remainingToday ?? null, + consistency: effective.consistency, + effectiveProfitTarget, + hasTodayRow: dailyPnL.some((d) => d.date === combined.tradeDate), + combineCmeHolidays: cfg.combineCmeHolidays === true, + combinedDailyPnL: dailyPnL, + rawDailyPnL: client.dailyPnL[acc.id] ?? [], + })); + } + // Condition 1: profit target already exceeded (target=0), still need days → any activity counts // Condition 2: target > 0 → all legs done (day's remaining target used up) if (plan) {