Files
autofirmer-expanded/app/api/state/route.ts
T
Brandon LiandClaude Opus 5.5 acb8879575 Reporter: name the accounts whose final day completed their cycle
cycleCompletion() says whether today's P&L finished the current cycle
(profit target, consistency and minimum days all met) — projectPerfectPath's
completion probe run without and with today. The reporter counts those as
`completed` (a subset of targetHit) and sends their names, so the master
can keep an account a firm erases after it passes. Checked ahead of the
lockout branch: a firm locking a passed account is no DLL loss. The local
dashboard shows them with a purple Completed pill.

Because the master keeps a completion for good, doubtful reads stay
'none': weekends, a Monday with a weekend-dated row, the first minutes
after the 5 PM roll, and history that hasn't freshly loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 12:28:41 -05:00

250 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextResponse } from 'next/server';
import { loadDailyPnL } from '@/lib/db';
import { getClients } from '@/lib/clients';
import { getCachedCatalog } from '@/lib/firm-catalog';
import { getAccountConfig, getInferredSize, matchAccountType } from '@/lib/account-type-resolver';
import { historyFresh } from '@/lib/platform-client';
import {
combineCmeSessions, computeDayPlan, cycleCompletion, resolveEffectiveConfig, currentTradeDateCT,
BIG_DAY_THRESHOLD, FIRST_DAY_STOP, LEG_COMPLETION_EPSILON, MIN_DAY_STOP_MULT,
} from '@/lib/trading-logic';
export async function GET() {
try {
const firms = getCachedCatalog();
const clients = getClients();
const tradeDate = currentTradeDateCT();
const state = firms.map((f) => {
const client = clients.get(f.id);
// connected == the Tradovate sync has come back, NOT "has accounts".
// A firm with no accounts is a real, settled state and must not be
// reported the same way as one that is still authenticating.
const connected = !!client && client.syncComplete;
if (!client || client.accountList.length === 0) {
return { firmId: f.id, firm: f.name, connected, accounts: [] };
}
const accounts = client.accountList.map((acc) => {
const cash = client.accountCashBalances[acc.id] ?? { amount: 0, realizedPnL: 0 };
// One lookup against the assignment table — no prefix matching
// here any more. An unassigned account gets no targets, which
// is also what stops it being traded.
const resolved = getAccountConfig(acc.id);
const cfg = resolved.accountType;
const inferredAccountSize = getInferredSize(acc.id, client);
// A combining firm records holiday sessions with the next full
// trading day — everything below reads the account through that
// view (the identity view when the account type doesn't combine).
const combined = combineCmeSessions(
cfg?.combineCmeHolidays === true,
client.dailyPnL[acc.id] ?? [],
tradeDate,
cash.realizedPnL,
);
const dailyPnL = combined.dailyPnL;
const totalProfit = dailyPnL.reduce((sum, d) => sum + d.pnl, 0);
const autoLiqThreshold = client.autoLiqThresholds[acc.id] ?? 0;
// 0 = no floor; negative floors are reconstructed TopstepX MLLs.
const isDead = autoLiqThreshold !== 0 && cash.amount <= autoLiqThreshold;
const allFundTxns = client.fundTransactions?.[acc.id] ?? [];
const priorProfit = client.priorProfit?.[acc.id] ?? 0;
// Stage = 1 + number of withdrawals (negative fund transactions)
const stage = 1 + allFundTxns.filter((f) => f.amount < 0).length;
// Stage-resolved config: a drain stage swaps in its own min-day
// (default $5) and no profit target.
const effective = cfg ? resolveEffectiveConfig(
cfg.profitTarget,
cfg.consistency,
cfg.minTradingDays,
cfg.targetSameEquity,
cfg.withdrawalStages,
priorProfit,
allFundTxns,
cfg.minDayPnL
) : null;
// Days traded counts only days that hit minDayPnL (when set) — that's what
// the firm requires toward the min trading day rule.
const daysTraded: number = effective && effective.minDayPnL > 0
? dailyPnL.filter((d) => d.pnl >= effective.minDayPnL).length
: combined.daysTraded;
// Hide initial funding (amount === accountSize) from display
const fundingAmount = cfg?.accountSize ?? inferredAccountSize;
const displayFundTxns = fundingAmount !== null
? allFundTxns.filter((f) => f.amount !== fundingAmount)
: allFundTxns;
// Surfaced so the dashboard can flag an account whose type was
// picked out of several equally valid matches.
const ambiguous = cfg
? matchAccountType(acc.name, inferredAccountSize, f.accountTypes).ambiguous
: false;
let targetHit = false;
let dailyTarget: { amount: number; path: 'first_day' | 'normal_day' | 'reduced_day' } | null = null;
let remainingToday: number | null = null;
let effectiveProfitTarget: number | null = null;
let nextStop: number | null = null;
if (cfg && effective && !isDead) {
const equityProfit = cash.amount - cfg.accountSize;
const plan = computeDayPlan(
acc.id, combined.tradeDate, effective.profitTarget, effective.consistency,
dailyPnL, effective.minDayPnL, effective.minTradingDays, equityProfit, combined.realizedPnL,
cfg.splitEnabled, effective.drain
);
dailyTarget = plan ? { amount: plan.dayTarget, path: plan.path } : null;
remainingToday = plan?.remainingToday ?? null;
// The stop the next trade would carry under the obfuscation
// options, mirroring obfuscationStopDollars in auto-trade:
// day 1 keeps the day's net floor at −$1,000, min days stop
// at 2× the target, and the day after a loss rides stopless.
if (plan && (cfg.firstDayStop || cfg.minDayStop)) {
const prior = dailyPnL
.filter((d) => d.date < combined.tradeDate && d.pnl !== 0)
.sort((a, b) => a.date.localeCompare(b.date));
const recoveryDay = prior.length > 0 && prior[prior.length - 1].pnl < 0;
if (!recoveryDay) {
let stop = Infinity;
if (cfg.firstDayStop && prior.length === 0) {
stop = Math.max(0, Math.round((FIRST_DAY_STOP + combined.realizedPnL) * 100) / 100);
}
if (cfg.minDayStop && plan.dayTarget <= BIG_DAY_THRESHOLD) {
stop = Math.min(stop, MIN_DAY_STOP_MULT * plan.dayTarget);
}
if (Number.isFinite(stop) && stop > 0) nextStop = Math.round(stop * 100) / 100;
}
}
// Effective profit target = max(stage target, consistency realTarget).
// If a big day forces the consistency rule, the account must reach maxDay/consistency
// in total trading profit for the stage, not just profitTarget.
const qualifying = effective.minDayPnL === 0 ? dailyPnL : dailyPnL.filter((d) => d.pnl >= effective.minDayPnL);
const maxDay = qualifying.length > 0 ? Math.max(...qualifying.map((d) => d.pnl)) : 0;
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) {
targetHit =
(plan.dayTarget === 0 && Math.abs(combined.realizedPnL) > 0 && combined.daysTraded <= effective.minTradingDays) ||
(combined.realizedPnL > 0 && plan.remainingToday <= LEG_COMPLETION_EPSILON);
}
}
// Today finished the cycle — the reporter's check and guards,
// so this pill and the master's purple count agree.
let completed = false;
const historyLoaded = client.dailyPnL[acc.id] !== undefined
&& client.fundTransactions?.[acc.id] !== undefined;
if (cfg && effective && historyLoaded && historyFresh(client, acc) && !client.positions[acc.id]) {
completed = cycleCompletion({
accountId: acc.id,
rawTradeDate: tradeDate,
cycleDailyPnL: client.dailyPnL[acc.id],
rawRealizedPnL: cash.realizedPnL,
combineCmeHolidays: cfg.combineCmeHolidays,
effective,
equityProfit: cash.amount - cfg.accountSize,
splitEnabled: cfg.splitEnabled,
}) === 'today';
}
return {
id: acc.id,
name: acc.name,
active: acc.active,
amount: cash.amount,
// The combined day's P&L, not just the current session's —
// on the Tuesday after a holiday this includes Sun/Mon.
realizedPnL: combined.realizedPnL,
daysTraded,
// A stop-enabled account whose day booked red via its own
// stop — the personal DLL. Firm DLL lockouts show via
// !active instead.
personalDll: !!cfg && (cfg.firstDayStop || cfg.minDayStop)
&& acc.active && !client.positions[acc.id]
&& combined.realizedPnL <= -25,
positionDirection: client.positions[acc.id]
? (client.positions[acc.id].netPos > 0 ? 'long' as const : 'short' as const)
: null,
autoLiqThreshold,
totalProfit,
targetHit,
completed,
stage,
// Current stage is a drain stage — dashboards label it "Stage N – Drain".
stageDrain: effective?.drain ?? false,
dailyTarget,
nextStop,
remainingToday,
effectiveProfitTarget,
dailyPnL,
fullDailyPnL: combineCmeSessions(
cfg?.combineCmeHolidays === true,
client.fullDailyPnL?.[acc.id] ?? loadDailyPnL(acc.id),
tradeDate,
0,
).dailyPnL,
fundTransactions: displayFundTxns,
accountTypeId: resolved.accountTypeId,
accountTypeLabel: cfg?.label ?? null,
assignmentSource: resolved.source,
accountType: cfg,
inferredAccountSize,
accountTypeAmbiguous: ambiguous,
};
});
return { firmId: f.id, firm: f.name, connected, accounts };
});
return NextResponse.json(state);
} catch (err) {
console.error('[GET /api/state]', err);
return NextResponse.json({ error: 'Failed to fetch state' }, { status: 500 });
}
}