import { TradovateClient } from './tradovate-class'; import { getInstruments } from './db'; import { getTradableFirms, startCatalogSync } from './firm-catalog'; import { resolveFirmAssignments } from './account-type-resolver'; import { resolveContracts } from './contract-resolver'; import { startReporter, sendReport } from './reporter'; import { startMasterLink } from './master-link'; import { resumeSchedulerIfPersisted } from './auto-trade'; // Use global to persist the client pool across HMR reloads in dev mode. // Keyed by the master's firm id, which is a uuid string. const g = global as typeof globalThis & { __tradovateClients?: Map; __tradovateClientsInitialized?: boolean; __tradovateClientCreds?: Map; __contractResolverTimer?: ReturnType; }; function ensureMap(): Map { if (!g.__tradovateClients) { g.__tradovateClients = new Map(); } return g.__tradovateClients; } /** Tracks which credentials each live client was built with, so a password * change can be noticed and the client rebuilt. */ function credsMap(): Map { if (!g.__tradovateClientCreds) g.__tradovateClientCreds = new Map(); return g.__tradovateClientCreds; } export function initClient(id: string, username: string, password: string, firmName: string): TradovateClient { const map = ensureMap(); const client = new TradovateClient(username, password, async () => { console.log(`[${firmName}] sync complete — ${client.accountList.length} account(s)`); // Every sync is a chance to bind accounts whose funding history has // finally arrived. The first pass usually resolves nothing, because the // reports fetch that produces fund transactions runs separately. try { const counts = resolveFirmAssignments(id, client); console.log( `[resolver] ${firmName}: ${counts.assigned} assigned, ` + `${counts.unassigned} unassigned, ${counts.manual} manual`, ); } catch (err) { console.error(`[resolver] ${firmName} failed`, err); } }); map.set(id, client); credsMap().set(id, `${username}${password}`); return client; } export function removeClient(id: string): void { const client = ensureMap().get(id); client?.disconnect(); ensureMap().delete(id); credsMap().delete(id); } /** Disconnect all clients and clear the pool so they are recreated on next getClients() call. */ export function resetClients(): void { const map = g.__tradovateClients; if (map) { for (const client of map.values()) { try { client.disconnect(); } catch { /* ignore */ } } } g.__tradovateClients = undefined; g.__tradovateClientCreds = undefined; g.__tradovateClientsInitialized = false; } /** * Brings the client pool in line with the catalog and the stored credentials. * * Called at boot, whenever credentials are saved, and after the catalog * changes — a firm can appear, disappear, or have its password corrected long * after startup, none of which the old one-shot initialization could handle. */ export function reconcileClients(): void { const map = ensureMap(); const firms = getTradableFirms(); const wanted = new Set(firms.map((f) => f.id)); // Firms removed on the master, or whose credentials were cleared. for (const id of [...map.keys()]) { if (!wanted.has(id)) { console.log(`[clients] dropping client for firm ${id} — no longer tradable`); removeClient(id); } } for (const firm of firms) { const signature = `${firm.username}${firm.password}`; const existing = map.get(firm.id); if (existing) { if (credsMap().get(firm.id) === signature) continue; console.log(`[clients] credentials changed for ${firm.name} — reconnecting`); removeClient(firm.id); } console.log(`[clients] starting client for ${firm.name}`); initClient(firm.id, firm.username, firm.password, firm.name); } } export function getClients(): Map { const map = ensureMap(); if (!g.__tradovateClientsInitialized) { g.__tradovateClientsInitialized = true; try { // Pull the catalog before anything else — without it there are no // firms to start clients for. Subsequent changes re-reconcile and // re-resolve, so a new account type picks up accounts that were // sitting Unassigned without waiting for a Tradovate sync. startCatalogSync(() => { reconcileClients(); for (const [firmId, client] of ensureMap()) { try { resolveFirmAssignments(firmId, client); } catch (err) { console.error(`[resolver] re-resolve failed for ${firmId}`, err); } } }); reconcileClients(); console.log(`[clients] Initialized ${map.size} Tradovate client(s)`); // Auto-resolve contracts after clients have time to authenticate setTimeout(() => triggerContractResolve(), 15_000); // Schedule daily resolve at midnight scheduleDailyResolve(); // Start master dashboard reporter startReporter(); // ...and the socket it prefers to report over, which is also how // the master triggers trades and AutoBuyer runs here. startMasterLink(() => { void sendReport(); }); // Pick the auto-trade schedule back up if it was running before restart resumeSchedulerIfPersisted(); } catch (err) { console.error('[clients] Failed to initialize clients', err); } } return map; } // ── Contract auto-resolve ──────────────────────────────────────────────────── function triggerContractResolve(): void { const map = ensureMap(); // Find first client with a valid access token let accessToken: string | null = null; for (const [, c] of map) { const token = (c as any).accessInfo?.accessToken; if (token) { accessToken = token; break; } } if (!accessToken) { console.log('[contract-resolver] No authenticated client yet — skipping resolve'); return; } const symbols = getInstruments().filter((i) => i.enabled).map((i) => i.symbol); console.log(`[contract-resolver] Resolving ${symbols.length} instruments...`); resolveContracts(symbols, accessToken) .then((results) => { const rolled = Object.entries(results).filter(([, v]) => v?.alternative); console.log(`[contract-resolver] Done — ${rolled.length} contract(s) rolled via price match`); }) .catch((err) => console.error('[contract-resolver] Resolve failed:', err)); } function scheduleDailyResolve(): void { // Clear any existing timer (HMR safety) if (g.__contractResolverTimer) clearInterval(g.__contractResolverTimer); // Check every minute if it's midnight (00:00) g.__contractResolverTimer = setInterval(() => { const now = new Date(); if (now.getHours() === 0 && now.getMinutes() === 0) { console.log('[contract-resolver] Midnight resolve triggered'); triggerContractResolve(); } }, 60_000); }