import 'dotenv/config'; import { config, loadDynamicConfig } from '../src/config/index.js'; import { ConnectorFactory } from '../src/connectors/factory.js'; import { TradeExecutor } from '../src/services/TradeExecutor.js'; import { healthTracker } from '../src/services/healthTracker.js'; import { reconciliationExitBackfillService } from '../src/services/reconciliationExitBackfillService.js'; import { supabaseService } from '../src/services/SupabaseService.js'; type BackfillCliOptions = { apply: boolean; profileIds: Set; ignoreAllowlist: boolean; }; type ProfileSummary = { profileId: string; userId: string; attempted: boolean; skippedReason?: string; batchId?: string; dryRun: boolean; openTradeCandidates: number; proposedRows: number; insertedRows: number; noGoTrades: number; }; const parseOptions = (argv: string[]): BackfillCliOptions => { const options: BackfillCliOptions = { apply: false, profileIds: new Set(), ignoreAllowlist: false }; for (const arg of argv) { if (arg === '--apply') { options.apply = true; continue; } if (arg === '--ignore-allowlist') { options.ignoreAllowlist = true; continue; } if (arg.startsWith('--profile=')) { const value = String(arg.slice('--profile='.length) || '').trim(); if (value) options.profileIds.add(value); continue; } } return options; }; const isPlaceholder = (value: string | undefined): boolean => { const normalized = String(value || '').trim(); if (!normalized) return true; return normalized === 'your_key' || normalized === 'your_secret'; }; const normalizeProfileIds = (profileIds: Set): string[] => { return Array.from(profileIds) .map((value) => String(value || '').trim()) .filter(Boolean); }; const run = async (): Promise => { const options = parseOptions(process.argv.slice(2)); await loadDynamicConfig(); const originalDryRun = config.RECON_EXIT_BACKFILL_DRY_RUN; const originalAllowlist = [...config.RECON_EXIT_BACKFILL_PROFILE_ALLOWLIST]; if (!config.ENABLE_RECON_EXIT_BACKFILL) { throw new Error('ENABLE_RECON_EXIT_BACKFILL=false. Enable it before running reconciliation EXIT backfill.'); } config.RECON_EXIT_BACKFILL_DRY_RUN = !options.apply; if (options.ignoreAllowlist) { config.RECON_EXIT_BACKFILL_PROFILE_ALLOWLIST = []; } else if (options.profileIds.size > 0) { config.RECON_EXIT_BACKFILL_PROFILE_ALLOWLIST = normalizeProfileIds(options.profileIds); } healthTracker.recordTradingControl({ mode: 'PAUSED', lastChangedBy: 'maintenance-script', lastChangedAt: Date.now(), reason: 'Offline reconciliation EXIT backfill cycle' }); const [users, profiles] = await Promise.all([ supabaseService.getActiveUsers(), supabaseService.getActiveProfiles() ]); const userById = new Map(); for (const user of users || []) { const userId = String((user as any)?.user_id || '').trim(); if (!userId) continue; userById.set(userId, user); } const selectedProfiles = (profiles || []).filter((profile: any) => { const profileId = String(profile?.id || '').trim(); if (!profileId) return false; if (options.profileIds.size === 0) return true; return options.profileIds.has(profileId); }); const results: ProfileSummary[] = []; for (const profile of selectedProfiles) { const profileId = String(profile?.id || '').trim(); const userId = String(profile?.user_id || '').trim(); if (!profileId || !userId) continue; const user = userById.get(userId); if (!user) { results.push({ profileId, userId, attempted: false, skippedReason: 'user_not_found', dryRun: config.RECON_EXIT_BACKFILL_DRY_RUN, openTradeCandidates: 0, proposedRows: 0, insertedRows: 0, noGoTrades: 0 }); continue; } const apiKey = config.PAPER_TRADING ? user.ALPACA_API_KEY : user.REAL_ALPACA_API_KEY; const apiSecret = config.PAPER_TRADING ? user.ALPACA_SECRET_KEY : user.REAL_ALPACA_SECRET_KEY; if (isPlaceholder(apiKey) || isPlaceholder(apiSecret)) { results.push({ profileId, userId, attempted: false, skippedReason: 'missing_exchange_credentials', dryRun: config.RECON_EXIT_BACKFILL_DRY_RUN, openTradeCandidates: 0, proposedRows: 0, insertedRows: 0, noGoTrades: 0 }); continue; } const connector = ConnectorFactory.getCustomConnector(config.EXECUTION_PROVIDER, apiKey, apiSecret); const executor = new TradeExecutor(connector, undefined, userId, profileId); executor.setProfileSettings(profile); try { const result = await reconciliationExitBackfillService.runProfile({ profileId, userId, executor }); results.push({ profileId, userId, attempted: result.attempted, skippedReason: result.skippedReason, batchId: result.batchId, dryRun: result.dryRun, openTradeCandidates: result.openTradeCandidates, proposedRows: result.proposedRows, insertedRows: result.insertedRows, noGoTrades: result.noGoTrades }); } finally { executor.dispose(); } } config.RECON_EXIT_BACKFILL_DRY_RUN = originalDryRun; config.RECON_EXIT_BACKFILL_PROFILE_ALLOWLIST = originalAllowlist; const aggregate = results.reduce( (acc, row) => { if (row.attempted) acc.attemptedProfiles += 1; if (!row.attempted && row.skippedReason) { acc.skippedProfiles[row.skippedReason] = (acc.skippedProfiles[row.skippedReason] || 0) + 1; } acc.proposedRows += row.proposedRows; acc.insertedRows += row.insertedRows; acc.noGoTrades += row.noGoTrades; return acc; }, { attemptedProfiles: 0, skippedProfiles: {} as Record, proposedRows: 0, insertedRows: 0, noGoTrades: 0 } ); console.log(JSON.stringify({ mode: options.apply ? 'apply' : 'dry-run', profileFilter: normalizeProfileIds(options.profileIds), ignoreAllowlist: options.ignoreAllowlist, requirePause: config.RECON_EXIT_BACKFILL_REQUIRE_PAUSE, dryRunFlagUsed: !options.apply, aggregate, results }, null, 2)); }; run().catch((error) => { const message = error instanceof Error ? error.message : String(error); console.error(JSON.stringify({ error: message }, null, 2)); process.exit(1); });