254 lines
11 KiB
TypeScript
254 lines
11 KiB
TypeScript
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 '@/lib/http-client';
|
||
|
||
/**
|
||
* 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, cfg.minDayPnL);
|
||
const equityProfit = cash.amount - cfg.accountSize;
|
||
const plan = computeDayPlan(acc.id, combined.tradeDate, effective.profitTarget, effective.consistency, combined.dailyPnL, effective.minDayPnL, effective.minTradingDays, equityProfit, combined.realizedPnL, cfg.splitEnabled, effective.drain);
|
||
|
||
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,
|
||
drain: effective.drain,
|
||
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);
|
||
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
|
||
.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(),
|
||
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 });
|
||
}
|
||
}
|