Add day-plan double-count diagnostics

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=<substring>)
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 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-08 21:28:39 -05:00
co-authored by Claude Opus 4.8
parent 5a69ea2f64
commit ef476ccbb7
2 changed files with 95 additions and 2 deletions
+52 -2
View File
@@ -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=<substring> (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(),
+43
View File
@@ -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) {