Files
autofirmer-expanded/app/api/debug/route.ts
T
Brandon LiandClaude Opus 5 8bdc85080c 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>
2026-08-31 22:17:23 -05:00

203 lines
8.4 KiB
TypeScript

import { NextResponse } from 'next/server';
import { getClients, resetClients } from '@/lib/clients';
import { getTradableFirms } from '@/lib/firm-catalog';
import axios from 'axios';
export async function GET() {
try {
const clients = getClients();
const firms = getTradableFirms();
const debug = firms.map((firm) => {
const client = clients.get(firm.id);
if (!client) return { firmId: firm.id, firmName: firm.name, status: 'no_client' };
return {
firmId: firm.id,
firmName: firm.name,
status: 'connected',
accountCount: client.accountList.length,
fetchDaysComplete: (client as any).fetchDaysComplete ?? 'n/a (old instance)',
lastFetchErrors: (client as any).lastFetchErrors ?? {},
lastFetchRaw: (client as any).lastFetchRaw ?? {},
accounts: client.accountList.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] ?? [],
})),
recentEntityEvents: client.recentEntityEvents.slice(-10).map((e) => ({
ts: new Date(e.ts).toISOString(),
entityType: e.entityType,
eventType: e.eventType,
entity: e.entity,
})),
};
});
return NextResponse.json({ ok: true, ts: new Date().toISOString(), firms: debug });
} catch (err) {
console.error('[GET /api/debug]', err);
return NextResponse.json({ error: String(err) }, { status: 500 });
}
}
/** POST /api/debug — manually trigger fetchDaysTraded on all clients and return results */
export async function POST() {
try {
const clients = getClients();
const firms = getTradableFirms();
const results = await Promise.all(
firms.map(async (firm) => {
const client = clients.get(firm.id);
if (!client || client.accountList.length === 0) {
return { firmId: firm.id, firmName: firm.name, status: 'skipped' };
}
try {
await (client as any).fetchDaysTraded();
} catch (err) {
return { firmId: firm.id, firmName: firm.name, status: 'error', error: String(err) };
}
return {
firmId: firm.id,
firmName: firm.name,
status: 'done',
fetchDaysComplete: (client as any).fetchDaysComplete ?? 'n/a',
errors: (client as any).lastFetchErrors ?? {},
rawSamples: (client as any).lastFetchRaw ?? {},
daysTraded: client.daysTraded,
dailyPnLCounts: Object.fromEntries(
Object.entries(client.dailyPnL).map(([k, v]) => [k, (v as any[]).length])
),
};
})
);
return NextResponse.json({ ok: true, ts: new Date().toISOString(), results });
} catch (err) {
console.error('[POST /api/debug]', err);
return NextResponse.json({ error: String(err) }, { status: 500 });
}
}
/**
* PATCH /api/debug — probe Tradovate APIs using a firm's access token.
*
* Generic proxy: { firmId, path?, url?, method?, body? }
* Full report run: { firmId, action: 'report', account, startDate, endDate }
* → does requestreport + getreport polling in one server-side call, returns raw data string
*/
export async function PATCH(req: Request) {
try {
const payload = await req.json();
const { firmId } = payload;
const client = getClients().get(firmId) as any;
if (!client?.accessInfo?.accessToken) {
return NextResponse.json({ error: 'no token for firmId ' + firmId }, { status: 400 });
}
const headers = { Authorization: `Bearer ${client.accessInfo.accessToken}` };
// Full report cycle
if (payload.action === 'report') {
const { account, startDate, endDate, name: reportName = 'Fills' } = payload;
try {
let reportData = (await axios.post(
'https://rpt-demo.tradovateapi.com/v1/reports/requestreport',
{
name: reportName, representationType: 'json', timezone: -300,
params: [
{ name: 'startDate', value: startDate },
{ name: 'endDate', value: endDate },
{ name: 'startTime', value: '00:00:00' },
{ name: 'endTime', value: '00:00:00' },
...(account ? [{ name: 'account', value: account }] : []),
],
},
{ headers }
)).data;
let attempts = 0;
while (reportData?.['p-ticket'] && attempts < 30) {
const ticket: string = reportData['p-ticket'];
const wait: number = Math.max(1, reportData['p-time'] ?? 1);
await new Promise((r) => setTimeout(r, wait * 1000));
reportData = (await axios.get(
'https://rpt-demo.tradovateapi.com/v1/reports/getreport',
{ params: { 'p-ticket': ticket }, headers }
)).data;
attempts++;
}
const raw: string = typeof reportData?.data === 'string' ? reportData.data : JSON.stringify(reportData);
// Parse to count fills and unique tradeDates
let fillCount = 0;
let uniqueDates: string[] = [];
try {
const fixed = raw.replace(/"_tradeDate":\s*(\d{4}-\d{2}-\d{2})/g, '"_tradeDate": "$1"');
const parsed: any[] = JSON.parse(fixed);
fillCount = parsed.length;
uniqueDates = [...new Set(parsed.map((f) => f._tradeDate as string))].sort();
} catch { /* not parseable yet */ }
return NextResponse.json({ ok: true, attempts, rawLen: raw.length, fillCount, uniqueDates, preview: raw.slice(0, 400) });
} catch (err: any) {
return NextResponse.json({
ok: false,
status: err?.response?.status,
data: err?.response?.data,
message: err?.message,
});
}
}
// Generic proxy
const { path, url: rawUrl, method = 'GET', body: reqBody } = payload;
const url = rawUrl ?? `https://demo.tradovateapi.com/v1/${path}`;
try {
const res = method === 'POST'
? await axios.post(url, reqBody, { headers })
: await axios.get(url, { headers });
return NextResponse.json({ ok: true, status: res.status, data: res.data });
} catch (err: any) {
return NextResponse.json({
ok: false,
status: err?.response?.status,
data: err?.response?.data,
message: err?.message,
});
}
} catch (err) {
return NextResponse.json({ error: String(err) }, { status: 500 });
}
}
/**
* DELETE /api/debug — force-reinitialize all Tradovate clients with fresh instances
* Clears the global pool so getClients() recreates everything from DB on next call.
*/
export async function DELETE() {
try {
const g = global as any;
const oldCount = (g.__tradovateClients as Map<number, any> | undefined)?.size ?? 0;
// Disconnect all existing WebSocket connections before clearing
resetClients();
// Re-initialize immediately with fresh instances
getClients();
return NextResponse.json({
ok: true,
message: `Disconnected ${oldCount} old client(s) — fresh instances initializing`,
ts: new Date().toISOString(),
});
} catch (err) {
console.error('[DELETE /api/debug]', err);
return NextResponse.json({ error: String(err) }, { status: 500 });
}
}