import { config } from '../config/index.js'; export interface TradingControlSnapshot { mode: 'RUNNING' | 'PAUSED'; lastChangedBy: string; lastChangedAt: number; reason?: string; } export interface ReconciliationNoGoSample { profileId: string; symbol: string; tradeId: string; reason: string; } export interface HealthSnapshot { tradingLoopHealthy: boolean; tradingLoopLastRun: number | null; monitorLoopHealthy: boolean; monitorLoopLastRun: number | null; orderSyncHealthy: boolean; orderSyncLastRun: number | null; lockContentionCount: number; reconciliationLoopHealthy: boolean; reconciliationLoopLastRun: number | null; reconciliationMismatchCount: number; reconciliationMissingFromExchange: number; reconciliationMissingInDb: number; reconciliationNoGoTrades: number; reconciliationNoGoReasonCounts: Record; reconciliationNoGoSamples: ReconciliationNoGoSample[]; reconciliationIntegrityWatchdogTriggered: boolean; reconciliationLockContentionCount: number; tradingControl: TradingControlSnapshot; } export class HealthTracker { private tradingLoopLastRun: number | null = null; private tradingLoopLastSuccess = true; private monitorLoopLastRun: number | null = null; private orderSyncLastRun: number | null = null; private lockContention = 0; private reconciliationLockContention = 0; private reconciliationLastRun: number | null = null; private reconciliationLastSuccess = true; private reconciliationMismatchCount = 0; private reconciliationMissingFromExchange = 0; private reconciliationMissingInDb = 0; private reconciliationNoGoTrades = 0; private reconciliationNoGoReasonCounts: Record = {}; private reconciliationNoGoSamples: ReconciliationNoGoSample[] = []; private reconciliationIntegrityWatchdogTriggered = false; private tradingControlListeners = new Set<(update: TradingControlSnapshot) => void>(); private tradingControl: TradingControlSnapshot = { mode: 'RUNNING', lastChangedBy: 'system', lastChangedAt: Date.now() }; public recordTradingLoop(healthy: boolean) { this.tradingLoopLastRun = Date.now(); this.tradingLoopLastSuccess = healthy; } public recordMonitorLoop() { this.monitorLoopLastRun = Date.now(); } public recordOrderSyncLoop() { this.orderSyncLastRun = Date.now(); } public incrementLockContention() { this.lockContention += 1; } public incrementReconciliationLockContention() { this.reconciliationLockContention += 1; } public recordTradingControl(update: TradingControlSnapshot) { this.tradingControl = update; for (const listener of this.tradingControlListeners) { try { listener(update); } catch { // Observers are best-effort. Health state remains authoritative. } } } public subscribeTradingControl(listener: (update: TradingControlSnapshot) => void): () => void { this.tradingControlListeners.add(listener); return () => { this.tradingControlListeners.delete(listener); }; } public isPaused(): boolean { return this.tradingControl.mode === 'PAUSED'; } public isTradingPaused(): boolean { return this.isPaused(); } private isFresh(lastRun: number | null, intervalMs: number): boolean { if (!lastRun) return false; const now = Date.now(); const threshold = Math.max(intervalMs * 2, 120_000); return (now - lastRun) <= threshold; } public getSnapshot(): HealthSnapshot { return { tradingLoopHealthy: this.tradingLoopLastSuccess && this.isFresh(this.tradingLoopLastRun, Math.max(config.POLLING_INTERVAL, 1)), tradingLoopLastRun: this.tradingLoopLastRun, monitorLoopHealthy: this.isFresh(this.monitorLoopLastRun, Math.max(config.MONITOR_INTERVAL_MS, 1)), monitorLoopLastRun: this.monitorLoopLastRun, orderSyncHealthy: this.isFresh(this.orderSyncLastRun, Math.max(config.ORDER_SYNC_INTERVAL_MS, 1)), orderSyncLastRun: this.orderSyncLastRun, lockContentionCount: this.lockContention, reconciliationLoopHealthy: this.reconciliationLastSuccess && this.isFresh(this.reconciliationLastRun, Math.max(config.MONITOR_INTERVAL_MS, 1)), reconciliationLoopLastRun: this.reconciliationLastRun, reconciliationMismatchCount: this.reconciliationMismatchCount, reconciliationMissingFromExchange: this.reconciliationMissingFromExchange, reconciliationMissingInDb: this.reconciliationMissingInDb, reconciliationNoGoTrades: this.reconciliationNoGoTrades, reconciliationNoGoReasonCounts: this.reconciliationNoGoReasonCounts, reconciliationNoGoSamples: this.reconciliationNoGoSamples, reconciliationIntegrityWatchdogTriggered: this.reconciliationIntegrityWatchdogTriggered, reconciliationLockContentionCount: this.reconciliationLockContention, tradingControl: this.tradingControl }; } public recordReconciliationLoop(success: boolean, metrics?: { mismatchCount?: number; missingFromExchange?: number; missingInDb?: number; noGoTrades?: number; noGoReasonCounts?: Record; noGoSamples?: ReconciliationNoGoSample[]; integrityWatchdogTriggered?: boolean; }) { this.reconciliationLastRun = Date.now(); this.reconciliationLastSuccess = success; if (metrics?.mismatchCount !== undefined) { this.reconciliationMismatchCount = metrics.mismatchCount; } if (metrics?.missingFromExchange !== undefined) { this.reconciliationMissingFromExchange = metrics.missingFromExchange; } if (metrics?.missingInDb !== undefined) { this.reconciliationMissingInDb = metrics.missingInDb; } if (metrics?.noGoTrades !== undefined) { this.reconciliationNoGoTrades = metrics.noGoTrades; } if (metrics?.noGoReasonCounts !== undefined) { this.reconciliationNoGoReasonCounts = { ...(metrics.noGoReasonCounts || {}) }; } else if (this.reconciliationNoGoTrades === 0) { this.reconciliationNoGoReasonCounts = {}; } if (metrics?.noGoSamples !== undefined) { this.reconciliationNoGoSamples = (metrics.noGoSamples || []).slice(0, 10).map((sample) => ({ profileId: String(sample?.profileId || '').trim(), symbol: String(sample?.symbol || '').trim(), tradeId: String(sample?.tradeId || '').trim(), reason: String(sample?.reason || '').trim() || 'unknown' })); } else if (this.reconciliationNoGoTrades === 0) { this.reconciliationNoGoSamples = []; } if (metrics?.integrityWatchdogTriggered !== undefined) { this.reconciliationIntegrityWatchdogTriggered = metrics.integrityWatchdogTriggered; } else if (this.reconciliationMismatchCount === 0 && this.reconciliationMissingInDb === 0 && this.reconciliationNoGoTrades === 0) { this.reconciliationIntegrityWatchdogTriggered = false; } } } export const healthTracker = new HealthTracker();