Take firms from the master instead of defining them per machine

This box no longer decides what firms exist. It caches the master's catalog
and stores one thing of its own: the Tradovate login for each firm. Creating
firms locally is gone — app/add, POST /api/firms, the account-config routes
and the banned-symbol route with it.

The cache is the point, not an optimisation. The scheduler needs every
account's target and size on each 60s tick, so fetching them per tick would
stop trading with positions open the first time the master rebooted. It is
one JSON blob in settings, replaced only by a complete valid response, and a
response with zero firms is refused outright: a misconfigured FIRMS_DB serves
{"firms":[]} as a healthy 200, and mirroring that would idle the whole fleet
at once.

Accounts bind to an account type by prefix AND size, the size taken from the
opening funding transaction. That does not arrive with the first sync, so an
account sits Unassigned and the resolver tries again next sync rather than
guessing. A type picked by hand is never overwritten by an auto pass. This
replaces the longest-prefix helper that had been copy-pasted into five files,
which could not tell two types with the same prefix apart at all.

firm_credentials and account_assignments deliberately have no foreign key to
the cached catalog. foreign_keys is ON, so a cascade would erase the Tradovate
passwords the moment a firm was removed on the master — routine, not
exceptional. They survive it, and are re-keyed by name if the master's database
is ever rebuilt with fresh ids.

The legacy firms/account_configs/firm_banned_symbols tables are left on disk,
unreferenced. firms is the only table that ever held plaintext credentials and
dropping it would make a rollback unrecoverable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-08-31 22:17:23 -05:00
co-authored by Claude Opus 5
parent 8b2e78d766
commit 8bdc85080c
27 changed files with 1489 additions and 1311 deletions
-87
View File
@@ -1,87 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { updateAccountConfig, deleteAccountConfig } from '@/lib/db';
export async function PUT(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: idStr } = await params;
const id = parseInt(idStr, 10);
if (isNaN(id)) {
return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
}
const body = await req.json() as {
prefix?: string;
profitTarget?: number;
consistency?: number;
minDayPnL?: number;
minTradingDays?: number;
accountSize?: number;
maxLoss?: number;
maxPositionSize?: number;
targetSameEquity?: boolean;
withdrawalStages?: { profit: number; consistency: number; minTradingDays: number }[];
};
const { prefix, profitTarget, consistency, minDayPnL, minTradingDays, accountSize, maxLoss, maxPositionSize, targetSameEquity, withdrawalStages } = body;
if (
typeof prefix !== 'string' || !prefix.trim() ||
typeof profitTarget !== 'number' ||
typeof consistency !== 'number' ||
typeof minDayPnL !== 'number' ||
typeof minTradingDays !== 'number' ||
typeof accountSize !== 'number'
) {
return NextResponse.json({ error: 'Invalid body' }, { status: 400 });
}
try {
const updated = updateAccountConfig(id, {
prefix: prefix.trim(),
profitTarget,
consistency,
minDayPnL,
minTradingDays,
accountSize,
maxLoss: maxLoss ?? 0,
maxPositionSize: maxPositionSize ?? 0,
targetSameEquity: targetSameEquity ?? false,
withdrawalStages: withdrawalStages ?? [],
});
if (!updated) {
return NextResponse.json({ error: 'Account config not found' }, { status: 404 });
}
return NextResponse.json({ success: true });
} catch (err) {
console.error('[PUT /api/account-configs/:id]', err);
return NextResponse.json({ error: 'Failed to update' }, { status: 500 });
}
}
export async function DELETE(
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: idStr } = await params;
const id = parseInt(idStr, 10);
if (isNaN(id)) {
return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
}
try {
const deleted = deleteAccountConfig(id);
if (!deleted) {
return NextResponse.json({ error: 'Account config not found' }, { status: 404 });
}
return NextResponse.json({ success: true });
} catch (err) {
console.error('[DELETE /api/account-configs/:id]', err);
return NextResponse.json({ error: 'Failed to delete' }, { status: 500 });
}
}
@@ -0,0 +1,68 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAccountTypeById } from '@/lib/firm-catalog';
import { getAssignment } from '@/lib/db';
import { getClients } from '@/lib/clients';
import {
clearManualAssignment,
resolveFirmAssignments,
setManualAssignment,
} from '@/lib/account-type-resolver';
type Params = { params: Promise<{ id: string }> };
function parseAccountId(raw: string): number | null {
const id = parseInt(raw, 10);
return Number.isNaN(id) ? null : id;
}
/** Pin an account to one of its firm's account types. A manual pick is never
* overwritten by the auto-resolver — that is the point of it. */
export async function PUT(req: NextRequest, { params }: Params) {
const { id: idStr } = await params;
const accountId = parseAccountId(idStr);
if (accountId === null) return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
const { accountTypeId } = await req.json() as { accountTypeId?: string | null };
// The firm comes from the existing assignment, or from whichever client is
// holding this account — an account the machine has never seen has nothing
// to pin to.
let firmId = getAssignment(accountId)?.firm_id ?? null;
if (!firmId) {
for (const [candidateFirmId, client] of getClients()) {
if (client.accountList.some((a) => a.id === accountId)) {
firmId = candidateFirmId;
break;
}
}
}
if (!firmId) return NextResponse.json({ error: 'Unknown account' }, { status: 404 });
if (accountTypeId) {
const type = getAccountTypeById(accountTypeId);
if (!type) return NextResponse.json({ error: 'Unknown account type' }, { status: 400 });
if (type.firmId !== firmId) {
return NextResponse.json({ error: 'That account type belongs to another firm' }, { status: 400 });
}
}
setManualAssignment(accountId, firmId, accountTypeId ?? null);
return NextResponse.json({ ok: true });
}
/** Drop the override and hand the account back to the auto-resolver. */
export async function DELETE(_req: NextRequest, { params }: Params) {
const { id: idStr } = await params;
const accountId = parseAccountId(idStr);
if (accountId === null) return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
const firmId = getAssignment(accountId)?.firm_id ?? null;
clearManualAssignment(accountId);
if (firmId) {
const client = getClients().get(firmId);
if (client) resolveFirmAssignments(firmId, client);
}
return NextResponse.json({ ok: true });
}
+3 -3
View File
@@ -1,12 +1,12 @@
import { NextResponse } from 'next/server';
import { getClients, resetClients } from '@/lib/clients';
import { getFirms } from '@/lib/db';
import { getTradableFirms } from '@/lib/firm-catalog';
import axios from 'axios';
export async function GET() {
try {
const clients = getClients();
const firms = getFirms();
const firms = getTradableFirms();
const debug = firms.map((firm) => {
const client = clients.get(firm.id);
@@ -48,7 +48,7 @@ export async function GET() {
export async function POST() {
try {
const clients = getClients();
const firms = getFirms();
const firms = getTradableFirms();
const results = await Promise.all(
firms.map(async (firm) => {
+31
View File
@@ -0,0 +1,31 @@
import { NextResponse } from 'next/server';
import { getCatalogMeta, syncCatalog } from '@/lib/firm-catalog';
import { getClients, reconcileClients } from '@/lib/clients';
import { resolveFirmAssignments } from '@/lib/account-type-resolver';
import { getSetting } from '@/lib/db';
/** Sync status for the Settings page. */
export async function GET() {
return NextResponse.json({
...getCatalogMeta(),
masterUrl: getSetting('master_dashboard_url'),
});
}
/** Force a pull now, instead of waiting for the poll. */
export async function POST() {
const result = await syncCatalog();
if (result.changed) {
reconcileClients();
for (const [firmId, client] of getClients()) {
try {
resolveFirmAssignments(firmId, client);
} catch (err) {
console.error(`[resolver] re-resolve failed for ${firmId}`, err);
}
}
}
return NextResponse.json({ ...result, ...getCatalogMeta() });
}
-76
View File
@@ -1,76 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { getFirmById, createAccountConfig } from '@/lib/db';
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: idStr } = await params;
const firmId = parseInt(idStr, 10);
if (isNaN(firmId)) {
return NextResponse.json({ error: 'Invalid firm id' }, { status: 400 });
}
if (!getFirmById(firmId)) {
return NextResponse.json({ error: 'Firm not found' }, { status: 404 });
}
const body = await req.json() as {
prefix?: string;
profitTarget?: number;
consistency?: number;
minDayPnL?: number;
minTradingDays?: number;
accountSize?: number;
maxLoss?: number;
maxPositionSize?: number;
targetSameEquity?: boolean;
withdrawalStages?: { profit: number; consistency: number; minTradingDays: number }[];
};
const { prefix, profitTarget, consistency, minDayPnL, minTradingDays, accountSize, maxLoss, maxPositionSize, targetSameEquity, withdrawalStages } = body;
if (
typeof prefix !== 'string' || !prefix.trim() ||
typeof profitTarget !== 'number' ||
typeof consistency !== 'number' ||
typeof minDayPnL !== 'number' ||
typeof minTradingDays !== 'number' ||
typeof accountSize !== 'number'
) {
return NextResponse.json({ error: 'Invalid body' }, { status: 400 });
}
try {
const row = createAccountConfig(firmId, {
prefix: prefix.trim(),
profitTarget,
consistency,
minDayPnL,
minTradingDays,
accountSize,
maxLoss: maxLoss ?? 0,
maxPositionSize: maxPositionSize ?? 0,
targetSameEquity: targetSameEquity ?? false,
withdrawalStages: withdrawalStages ?? [],
});
return NextResponse.json({
id: row.id,
prefix: row.prefix,
profitTarget: row.profit_target,
consistency: row.consistency,
minDayPnL: row.min_day_pnl,
minTradingDays: row.min_trading_days,
accountSize: row.account_size,
maxLoss: row.max_loss,
maxPositionSize: row.max_position_size,
targetSameEquity: row.target_same_equity === 1,
withdrawalStages: (() => { try { return JSON.parse(row.withdrawal_stages ?? '[]') as { profit: number; consistency: number; minTradingDays: number }[]; } catch { return []; } })(),
}, { status: 201 });
} catch (err) {
console.error('[POST /api/firms/:id/accounts]', err);
return NextResponse.json({ error: 'Failed to create account type' }, { status: 500 });
}
}
@@ -1,25 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { getBannedSymbols, setBannedSymbol } from '@/lib/db';
type Params = { params: Promise<{ id: string }> };
/** GET /api/firms/[id]/banned-symbols — list banned symbols for a firm */
export async function GET(_req: NextRequest, { params }: Params) {
const { id: idStr } = await params;
const firmId = parseInt(idStr, 10);
if (isNaN(firmId)) return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
return NextResponse.json(getBannedSymbols(firmId));
}
/** PATCH /api/firms/[id]/banned-symbols — { symbol, banned } to ban or unban */
export async function PATCH(req: NextRequest, { params }: Params) {
const { id: idStr } = await params;
const firmId = parseInt(idStr, 10);
if (isNaN(firmId)) return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
const { symbol, banned } = await req.json() as { symbol: string; banned: boolean };
if (!symbol) return NextResponse.json({ error: 'symbol required' }, { status: 400 });
setBannedSymbol(firmId, symbol, banned);
return NextResponse.json({ firmId, symbol, banned });
}
+40
View File
@@ -0,0 +1,40 @@
import { NextRequest, NextResponse } from 'next/server';
import { getCachedFirm } from '@/lib/firm-catalog';
import { setCredentials, clearCredentials } from '@/lib/db';
import { reconcileClients, removeClient } from '@/lib/clients';
type Params = { params: Promise<{ id: string }> };
/** The Tradovate login for one firm. This is the only firm data this machine
* owns — everything else comes from the master catalog, and these credentials
* never go the other way. */
export async function PUT(req: NextRequest, { params }: Params) {
const { id } = await params;
const firm = getCachedFirm(id);
if (!firm) return NextResponse.json({ error: 'Firm not found' }, { status: 404 });
const body = await req.json() as { username?: string; password?: string };
const username = body.username?.trim() ?? '';
const password = body.password?.trim() ?? '';
if (!username || !password) {
return NextResponse.json({ error: 'username and password are required' }, { status: 400 });
}
try {
setCredentials(id, firm.name, username, password);
// Picks up the new login immediately rather than at the next restart.
reconcileClients();
return NextResponse.json({ ok: true });
} catch (err) {
console.error('[PUT /api/firms/:id/credentials]', err);
return NextResponse.json({ error: 'Failed to save credentials' }, { status: 500 });
}
}
export async function DELETE(_req: NextRequest, { params }: Params) {
const { id } = await params;
const cleared = clearCredentials(id);
removeClient(id);
return NextResponse.json({ ok: cleared });
}
+1 -2
View File
@@ -7,8 +7,7 @@ export async function GET(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const firmId = parseInt(id, 10);
const { id: firmId } = await params;
const clients = getClients();
const client = clients.get(firmId) as any;
if (!client) return NextResponse.json({ error: 'Client not found' }, { status: 404 });
+1 -3
View File
@@ -6,9 +6,7 @@ export async function POST(
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const firmId = parseInt(id, 10);
if (isNaN(firmId)) return NextResponse.json({ error: 'Invalid firm ID' }, { status: 400 });
const { id: firmId } = await params;
const body = await req.json() as {
accountId: number;
+13 -48
View File
@@ -1,64 +1,29 @@
import { NextRequest, NextResponse } from 'next/server';
import { getFirmById, deleteFirm } from '@/lib/db';
import { removeClient } from '@/lib/clients';
import { getCachedFirm } from '@/lib/firm-catalog';
import { getCredentials } from '@/lib/db';
/** One firm from the cached catalog. Read-only: firms and their account types
* are owned by the master dashboard, and there is no local DELETE — a firm
* goes away here when it goes away there. */
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: idStr } = await params;
const id = parseInt(idStr, 10);
const { id } = await params;
if (isNaN(id)) {
return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
}
const firm = getFirmById(id);
const firm = getCachedFirm(id);
if (!firm) {
return NextResponse.json({ error: 'Firm not found' }, { status: 404 });
}
const creds = getCredentials(id);
return NextResponse.json({
id: firm.id,
firm: firm.name,
username: firm.username,
password: firm.password,
accounts: firm.accounts.map((a) => ({
id: a.id,
prefix: a.prefix,
profitTarget: a.profit_target,
consistency: a.consistency,
minDayPnL: a.min_day_pnl,
minTradingDays: a.min_trading_days,
accountSize: a.account_size,
maxLoss: a.max_loss,
maxPositionSize: a.max_position_size,
targetSameEquity: a.target_same_equity === 1,
withdrawalStages: (() => { try { return JSON.parse(a.withdrawal_stages ?? '[]') as { profit: number; consistency: number; minTradingDays: number }[]; } catch { return []; } })(),
})),
username: creds?.username ?? '',
hasCredentials: !!creds,
bannedSymbols: firm.bannedSymbols,
accounts: firm.accountTypes,
catalogUpdatedAt: firm.updatedAt,
});
}
export async function DELETE(
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: idStr } = await params;
const id = parseInt(idStr, 10);
if (isNaN(id)) {
return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
}
try {
const deleted = deleteFirm(id);
if (!deleted) {
return NextResponse.json({ error: 'Firm not found' }, { status: 404 });
}
removeClient(id);
return NextResponse.json({ success: true });
} catch (err) {
console.error('[DELETE /api/firms/:id]', err);
return NextResponse.json({ error: 'Failed to delete firm' }, { status: 500 });
}
}
+40 -48
View File
@@ -1,57 +1,49 @@
import { NextRequest, NextResponse } from 'next/server';
import { getFirms, createFirm, getBannedSymbols } from '@/lib/db';
import { initClient } from '@/lib/clients';
import { NextResponse } from 'next/server';
import { getCachedCatalog } from '@/lib/firm-catalog';
import { getCredentials } from '@/lib/db';
import type { FirmConfig } from '@/types';
/**
* The firms this machine knows about — the master's catalog, annotated with
* whether credentials for each are stored here.
*
* Firms are created and edited on the master dashboard; there is no POST. The
* only writable thing per firm is its credentials, at
* PUT /api/firms/[id]/credentials.
*
* The stored password is deliberately not returned. It used to be, and nothing
* ever read it.
*/
export async function GET() {
try {
const firms = getFirms();
const result = firms.map((f) => ({
id: f.id,
firm: f.name,
username: f.username,
password: f.password,
bannedSymbols: getBannedSymbols(f.id),
accounts: f.accounts.map((a) => ({
prefix: a.prefix,
profitTarget: a.profit_target,
consistency: a.consistency,
minDayPnL: a.min_day_pnl,
minTradingDays: a.min_trading_days,
accountSize: a.account_size,
maxLoss: a.max_loss,
})),
}));
const result: FirmConfig[] = getCachedCatalog().map((firm) => {
const creds = getCredentials(firm.id);
return {
id: firm.id,
firm: firm.name,
username: creds?.username ?? '',
hasCredentials: !!creds,
bannedSymbols: firm.bannedSymbols,
accounts: firm.accountTypes.map((a) => ({
id: a.id,
label: a.label,
prefix: a.prefix,
profitTarget: a.profitTarget,
consistency: a.consistency,
minDayPnL: a.minDayPnL,
minTradingDays: a.minTradingDays,
accountSize: a.accountSize,
maxLoss: a.maxLoss,
maxPositionSize: a.maxPositionSize,
targetSameEquity: a.targetSameEquity,
withdrawalStages: a.withdrawalStages,
})),
catalogUpdatedAt: firm.updatedAt,
};
});
return NextResponse.json(result);
} catch (err) {
console.error('[GET /api/firms]', err);
return NextResponse.json({ error: 'Failed to fetch firms' }, { status: 500 });
}
}
export async function POST(req: NextRequest) {
const body = await req.json() as { name?: string; username?: string; password?: string };
const { name, username, password } = body;
if (!name?.trim() || !username?.trim() || !password?.trim()) {
return NextResponse.json({ error: 'name, username, and password are required' }, { status: 400 });
}
try {
const firm = createFirm(name.trim(), username.trim(), password.trim());
initClient(firm.id, firm.username, firm.password, firm.name);
return NextResponse.json({
id: firm.id,
firm: firm.name,
username: firm.username,
password: firm.password,
accounts: [],
}, { status: 201 });
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
if (msg.includes('UNIQUE')) {
return NextResponse.json({ error: 'A firm with that name already exists' }, { status: 409 });
}
console.error('[POST /api/firms]', err);
return NextResponse.json({ error: 'Failed to create firm' }, { status: 500 });
}
}
+39 -25
View File
@@ -1,36 +1,39 @@
import { NextResponse } from 'next/server';
import { getFirms, loadDailyPnL } from '@/lib/db';
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 { computeDailyTarget, resolveEffectiveConfig } from '@/lib/trading-logic';
import type { AccountConfigRow } from '@/lib/db';
function getAccountConfig(name: string, accounts: AccountConfigRow[]): AccountConfigRow | undefined {
return [...accounts]
.sort((a, b) => b.prefix.length - a.prefix.length)
.find((a) => name.startsWith(a.prefix));
}
export async function GET() {
try {
const firms = getFirms();
const firms = getCachedCatalog();
const clients = getClients();
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 { firm: f.name, connected: false, accounts: [] };
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 };
const dailyPnL: { date: string; pnl: number }[] = client.dailyPnL[acc.id] ?? [];
const totalProfit = dailyPnL.reduce((sum, d) => sum + d.pnl, 0);
// Compute daily target and targetHit in one place — the single source of truth.
const cfg = getAccountConfig(acc.name, f.accounts);
// 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);
// 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 = cfg && cfg.min_day_pnl > 0
? dailyPnL.filter((d) => d.pnl >= cfg.min_day_pnl).length
const daysTraded: number = cfg && cfg.minDayPnL > 0
? dailyPnL.filter((d) => d.pnl >= cfg.minDayPnL).length
: (client.daysTraded[acc.id] ?? 0);
const autoLiqThreshold = client.autoLiqThresholds[acc.id] ?? 0;
const isDead = autoLiqThreshold > 0 && cash.amount <= autoLiqThreshold;
@@ -39,31 +42,36 @@ export async function GET() {
// Stage = 1 + number of withdrawals (negative fund transactions)
const stage = 1 + allFundTxns.filter((f) => f.amount < 0).length;
// Hide initial funding (amount === accountSize) from display
const displayFundTxns = cfg
? allFundTxns.filter((f) => f.amount !== cfg.account_size)
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 effectiveProfitTarget: number | null = null;
if (cfg && !isDead) {
const withdrawalStages: { profit: number; consistency: number; minTradingDays: number }[] = (() => { try { return JSON.parse(cfg.withdrawal_stages ?? '[]'); } catch { return []; } })();
const effective = resolveEffectiveConfig(
cfg.profit_target,
cfg.profitTarget,
cfg.consistency,
cfg.min_trading_days,
cfg.target_same_equity === 1,
withdrawalStages,
cfg.minTradingDays,
cfg.targetSameEquity,
cfg.withdrawalStages,
priorProfit,
allFundTxns
);
const equityProfit = cash.amount - cfg.account_size;
const target = computeDailyTarget(effective.profitTarget, effective.consistency, dailyPnL, cfg.min_day_pnl, effective.minTradingDays, equityProfit);
const equityProfit = cash.amount - cfg.accountSize;
const target = computeDailyTarget(effective.profitTarget, effective.consistency, dailyPnL, cfg.minDayPnL, effective.minTradingDays, equityProfit);
dailyTarget = target;
// 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 = cfg.min_day_pnl === 0 ? dailyPnL : dailyPnL.filter((d) => d.pnl >= cfg.min_day_pnl);
const qualifying = cfg.minDayPnL === 0 ? dailyPnL : dailyPnL.filter((d) => d.pnl >= cfg.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;
@@ -96,9 +104,15 @@ export async function GET() {
dailyPnL,
fullDailyPnL: client.fullDailyPnL?.[acc.id] ?? loadDailyPnL(acc.id),
fundTransactions: displayFundTxns,
accountTypeId: resolved.accountTypeId,
accountTypeLabel: cfg?.label ?? null,
assignmentSource: resolved.source,
accountType: cfg,
inferredAccountSize,
accountTypeAmbiguous: ambiguous,
};
});
return { firm: f.name, connected: true, accounts };
return { firmId: f.id, firm: f.name, connected, accounts };
});
return NextResponse.json(state);