/** * Reporter module — pushes aggregated state to the master dashboard. * * Every 30 seconds, collects firm-level stats + scheduler state and POSTs * to the configured master_dashboard_url. Silently skips if not configured. */ import { getSetting } from './db'; import { getFirms } from './db'; import { getClients } from './clients'; import { getSchedulerStatus } from './auto-trade'; import { computeDailyTarget } from './trading-logic'; import type { AccountConfigRow } from './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)); } function collectFirmStats() { const firms = getFirms(); const clients = getClients(); return firms.map((f) => { const client = clients.get(f.id); if (!client || client.accountList.length === 0) { return { firm: f.name, totalAccounts: 0, accountsTraded: 0, inTrade: 0 }; } let totalAccounts = 0; let accountsTraded = 0; let inTrade = 0; for (const acc of client.accountList) { const cash = client.accountCashBalances[acc.id] ?? { amount: 0, realizedPnL: 0 }; const autoLiqThreshold = client.autoLiqThresholds[acc.id] ?? 0; const isDead = autoLiqThreshold > 0 && cash.amount <= autoLiqThreshold; if (isDead) continue; totalAccounts++; if (client.positions[acc.id]) { inTrade++; accountsTraded++; continue; } // Check if target was hit today const cfg = getAccountConfig(acc.name, f.accounts); if (cfg) { const dailyPnL = client.dailyPnL[acc.id] ?? []; const equityProfit = cash.amount - cfg.account_size; const target = computeDailyTarget(cfg.profit_target, cfg.consistency, dailyPnL, cfg.min_day_pnl, cfg.min_trading_days, equityProfit); const targetHit = !target ? false : ((target.amount === 0 && Math.abs(cash.realizedPnL) > 0 && (client.daysTraded[acc.id] ?? 0) <= cfg.min_trading_days) || (cash.realizedPnL >= target.amount)); if (targetHit || cash.realizedPnL !== 0) { accountsTraded++; } } } return { firm: f.name, totalAccounts, accountsTraded, inTrade }; }); } // HMR-safe global to avoid duplicate intervals const _g = globalThis as typeof globalThis & { __reporterInterval?: ReturnType }; export function startReporter() { if (_g.__reporterInterval) { clearInterval(_g.__reporterInterval); } const report = async () => { const url = getSetting('master_dashboard_url'); const instanceName = getSetting('instance_name'); if (!url || !instanceName) return; try { const firms = collectFirmStats(); const scheduler = getSchedulerStatus(); await fetch(`${url}/api/report`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ instance: instanceName, firms, scheduler }), }); } catch { // Master might be down — silently continue } }; _g.__reporterInterval = setInterval(report, 30_000); // Fire first report after a short delay to let clients sync setTimeout(report, 10_000); console.log('[reporter] started — reporting every 30s'); }