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:
co-authored by
Claude Opus 4.8
parent
5a69ea2f64
commit
ef476ccbb7
+52
-2
@@ -1,12 +1,59 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { getClients, resetClients } from '@/lib/clients';
|
import { getClients, resetClients } from '@/lib/clients';
|
||||||
import { getTradableFirms } from '@/lib/firm-catalog';
|
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';
|
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 {
|
try {
|
||||||
const clients = getClients();
|
const clients = getClients();
|
||||||
const firms = getTradableFirms();
|
const firms = getTradableFirms();
|
||||||
|
const tradeDate = currentTradeDateCT();
|
||||||
|
const nameFilter = (new URL(req.url).searchParams.get('account') ?? '').toLowerCase();
|
||||||
|
|
||||||
const debug = firms.map((firm) => {
|
const debug = firms.map((firm) => {
|
||||||
const client = clients.get(firm.id);
|
const client = clients.get(firm.id);
|
||||||
@@ -20,13 +67,16 @@ export async function GET() {
|
|||||||
fetchDaysComplete: (client as any).fetchDaysComplete ?? 'n/a (old instance)',
|
fetchDaysComplete: (client as any).fetchDaysComplete ?? 'n/a (old instance)',
|
||||||
lastFetchErrors: (client as any).lastFetchErrors ?? {},
|
lastFetchErrors: (client as any).lastFetchErrors ?? {},
|
||||||
lastFetchRaw: (client as any).lastFetchRaw ?? {},
|
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,
|
id: acc.id,
|
||||||
name: acc.name,
|
name: acc.name,
|
||||||
cash: client.accountCashBalances[acc.id] ?? null,
|
cash: client.accountCashBalances[acc.id] ?? null,
|
||||||
daysTraded: client.daysTraded[acc.id] ?? '(not set)',
|
daysTraded: client.daysTraded[acc.id] ?? '(not set)',
|
||||||
dailyPnLEntries: (client.dailyPnL[acc.id] ?? []).length,
|
dailyPnLEntries: (client.dailyPnL[acc.id] ?? []).length,
|
||||||
dailyPnL: client.dailyPnL[acc.id] ?? [],
|
dailyPnL: client.dailyPnL[acc.id] ?? [],
|
||||||
|
dayPlan: dayPlanDiag(client, acc, tradeDate),
|
||||||
})),
|
})),
|
||||||
recentEntityEvents: client.recentEntityEvents.slice(-10).map((e) => ({
|
recentEntityEvents: client.recentEntityEvents.slice(-10).map((e) => ({
|
||||||
ts: new Date(e.ts).toISOString(),
|
ts: new Date(e.ts).toISOString(),
|
||||||
|
|||||||
@@ -93,6 +93,49 @@ export async function GET() {
|
|||||||
const consistencyReal = (effective.consistency > 0 && effective.consistency < 1 && maxDay > 0)
|
const consistencyReal = (effective.consistency > 0 && effective.consistency < 1 && maxDay > 0)
|
||||||
? maxDay / effective.consistency : 0;
|
? maxDay / effective.consistency : 0;
|
||||||
effectiveProfitTarget = Math.max(effective.profitTarget, consistencyReal);
|
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 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)
|
// Condition 2: target > 0 → all legs done (day's remaining target used up)
|
||||||
if (plan) {
|
if (plan) {
|
||||||
|
|||||||
Reference in New Issue
Block a user