import 'dotenv/config'; import { config, loadDynamicConfig } from '../src/config/index.js'; import { AlpacaConnector } from '../src/connectors/alpaca.js'; import { TradeExecutor } from '../src/services/TradeExecutor.js'; import { reconciliationOrderCoverageService } from '../src/services/reconciliationOrderCoverageService.js'; import { supabaseService } from '../src/services/SupabaseService.js'; type CliOptions = { apply: boolean; profileIds: Set; lookbackHours?: number; fetchLimitPerPage?: number; maxFetchPages?: number; maxInsertsPerProfile?: number; ignoreFeatureFlag: boolean; }; type ProfileSummary = { profileId: string; userId: string; attempted: boolean; skippedReason?: string; dryRun: boolean; scannedOrders: number; filledLikeOrders: number; botOwnedOrders: number; eligibleOrders: number; missingInDb: number; insertedRows: number; skippedNotBotOwned: number; skippedUnmappedTrade: number; skippedUnmappedAction: number; skippedMissingFillData: number; skippedMissingOrderId: number; skippedExisting: number; skippedMaxInsertLimit: number; }; const parseOptions = (argv: string[]): CliOptions => { const options: CliOptions = { apply: false, profileIds: new Set(), ignoreFeatureFlag: false }; for (const arg of argv) { if (arg === '--apply') { options.apply = true; continue; } if (arg === '--ignore-feature-flag') { options.ignoreFeatureFlag = true; continue; } if (arg.startsWith('--profile=')) { const profileId = String(arg.slice('--profile='.length) || '').trim(); if (profileId) options.profileIds.add(profileId); continue; } if (arg.startsWith('--lookback-hours=')) { const parsed = Number(arg.slice('--lookback-hours='.length)); if (Number.isFinite(parsed) && parsed > 0) options.lookbackHours = Math.floor(parsed); continue; } if (arg.startsWith('--max-inserts-per-profile=')) { const parsed = Number(arg.slice('--max-inserts-per-profile='.length)); if (Number.isFinite(parsed) && parsed > 0) options.maxInsertsPerProfile = Math.floor(parsed); continue; } if (arg.startsWith('--fetch-limit-per-page=')) { const parsed = Number(arg.slice('--fetch-limit-per-page='.length)); if (Number.isFinite(parsed) && parsed > 0) options.fetchLimitPerPage = Math.floor(parsed); continue; } if (arg.startsWith('--max-fetch-pages=')) { const parsed = Number(arg.slice('--max-fetch-pages='.length)); if (Number.isFinite(parsed) && parsed > 0) options.maxFetchPages = Math.floor(parsed); 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(supabaseService); const originalEnabled = config.ENABLE_RECON_ORDER_COVERAGE_SYNC; const originalDryRun = config.RECON_ORDER_COVERAGE_DRY_RUN; const originalLookback = config.RECON_ORDER_COVERAGE_LOOKBACK_HOURS; const originalFetchLimitPerPage = config.RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE; const originalMaxFetchPages = config.RECON_ORDER_COVERAGE_MAX_FETCH_PAGES; const originalMaxInserts = config.RECON_ORDER_COVERAGE_MAX_INSERTS_PER_PROFILE; if (!config.ENABLE_RECON_ORDER_COVERAGE_SYNC && !options.ignoreFeatureFlag) { throw new Error('ENABLE_RECON_ORDER_COVERAGE_SYNC=false. Enable it or pass --ignore-feature-flag for one-shot run.'); } if (options.ignoreFeatureFlag) { config.ENABLE_RECON_ORDER_COVERAGE_SYNC = true; } config.RECON_ORDER_COVERAGE_DRY_RUN = !options.apply; if (Number.isFinite(options.lookbackHours)) { config.RECON_ORDER_COVERAGE_LOOKBACK_HOURS = Number(options.lookbackHours); } if (Number.isFinite(options.fetchLimitPerPage)) { config.RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE = Number(options.fetchLimitPerPage); } if (Number.isFinite(options.maxFetchPages)) { config.RECON_ORDER_COVERAGE_MAX_FETCH_PAGES = Number(options.maxFetchPages); } if (Number.isFinite(options.maxInsertsPerProfile)) { config.RECON_ORDER_COVERAGE_MAX_INSERTS_PER_PROFILE = Number(options.maxInsertsPerProfile); } const effectiveLookbackHours = config.RECON_ORDER_COVERAGE_LOOKBACK_HOURS; const effectiveFetchLimitPerPage = config.RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE; const effectiveMaxFetchPages = config.RECON_ORDER_COVERAGE_MAX_FETCH_PAGES; const effectiveMaxInsertsPerProfile = config.RECON_ORDER_COVERAGE_MAX_INSERTS_PER_PROFILE; const effectiveDryRunFlag = config.RECON_ORDER_COVERAGE_DRY_RUN; 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_ORDER_COVERAGE_DRY_RUN, scannedOrders: 0, filledLikeOrders: 0, botOwnedOrders: 0, eligibleOrders: 0, missingInDb: 0, insertedRows: 0, skippedNotBotOwned: 0, skippedUnmappedTrade: 0, skippedUnmappedAction: 0, skippedMissingFillData: 0, skippedMissingOrderId: 0, skippedExisting: 0, skippedMaxInsertLimit: 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_ORDER_COVERAGE_DRY_RUN, scannedOrders: 0, filledLikeOrders: 0, botOwnedOrders: 0, eligibleOrders: 0, missingInDb: 0, insertedRows: 0, skippedNotBotOwned: 0, skippedUnmappedTrade: 0, skippedUnmappedAction: 0, skippedMissingFillData: 0, skippedMissingOrderId: 0, skippedExisting: 0, skippedMaxInsertLimit: 0 }); continue; } const connector = new AlpacaConnector(apiKey, apiSecret); const executor = new TradeExecutor(connector, undefined, userId, profileId); executor.setProfileSettings(profile); try { const result = await reconciliationOrderCoverageService.runProfile({ profileId, userId, executor }); results.push({ profileId, userId, attempted: result.attempted, skippedReason: result.skippedReason, dryRun: result.dryRun, scannedOrders: result.scannedOrders, filledLikeOrders: result.filledLikeOrders, botOwnedOrders: result.botOwnedOrders, eligibleOrders: result.eligibleOrders, missingInDb: result.missingInDb, insertedRows: result.insertedRows, skippedNotBotOwned: result.skippedNotBotOwned, skippedUnmappedTrade: result.skippedUnmappedTrade, skippedUnmappedAction: result.skippedUnmappedAction, skippedMissingFillData: result.skippedMissingFillData, skippedMissingOrderId: result.skippedMissingOrderId, skippedExisting: result.skippedExisting, skippedMaxInsertLimit: result.skippedMaxInsertLimit }); } finally { executor.dispose(); } } config.ENABLE_RECON_ORDER_COVERAGE_SYNC = originalEnabled; config.RECON_ORDER_COVERAGE_DRY_RUN = originalDryRun; config.RECON_ORDER_COVERAGE_LOOKBACK_HOURS = originalLookback; config.RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE = originalFetchLimitPerPage; config.RECON_ORDER_COVERAGE_MAX_FETCH_PAGES = originalMaxFetchPages; config.RECON_ORDER_COVERAGE_MAX_INSERTS_PER_PROFILE = originalMaxInserts; 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.scannedOrders += row.scannedOrders; acc.filledLikeOrders += row.filledLikeOrders; acc.botOwnedOrders += row.botOwnedOrders; acc.eligibleOrders += row.eligibleOrders; acc.missingInDb += row.missingInDb; acc.insertedRows += row.insertedRows; acc.skippedNotBotOwned += row.skippedNotBotOwned; acc.skippedUnmappedTrade += row.skippedUnmappedTrade; acc.skippedUnmappedAction += row.skippedUnmappedAction; acc.skippedMissingFillData += row.skippedMissingFillData; acc.skippedMissingOrderId += row.skippedMissingOrderId; acc.skippedExisting += row.skippedExisting; acc.skippedMaxInsertLimit += row.skippedMaxInsertLimit; return acc; }, { attemptedProfiles: 0, skippedProfiles: {} as Record, scannedOrders: 0, filledLikeOrders: 0, botOwnedOrders: 0, eligibleOrders: 0, missingInDb: 0, insertedRows: 0, skippedNotBotOwned: 0, skippedUnmappedTrade: 0, skippedUnmappedAction: 0, skippedMissingFillData: 0, skippedMissingOrderId: 0, skippedExisting: 0, skippedMaxInsertLimit: 0 }); console.log(JSON.stringify({ mode: options.apply ? 'apply' : 'dry-run', profileFilter: normalizeProfileIds(options.profileIds), ignoreFeatureFlag: options.ignoreFeatureFlag, configuredLookbackHours: effectiveLookbackHours, configuredFetchLimitPerPage: effectiveFetchLimitPerPage, configuredMaxFetchPages: effectiveMaxFetchPages, configuredMaxInsertsPerProfile: effectiveMaxInsertsPerProfile, dryRunFlagUsed: effectiveDryRunFlag, 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); });