import { getLogger } from '@stock-bot/logger'; import type { MasterExchange } from '@stock-bot/mongodb'; import type { IServiceContainer } from '@stock-bot/handlers'; import type { JobPayload } from '../../../types/job-payloads'; const logger = getLogger('sync-ib-exchanges'); interface IBExchange { id?: string; _id?: any; name?: string; code?: string; country_code?: string; currency?: string; } export async function syncIBExchanges( payload: JobPayload, container: IServiceContainer ): Promise<{ syncedCount: number; totalExchanges: number }> { logger.info('Syncing IB exchanges from database...'); try { const mongoClient = container.mongodb; const db = mongoClient.getDatabase(); // Filter by country code US and CA const ibExchanges = await db .collection('ibExchanges') .find({ country_code: { $in: ['US', 'CA'] }, }) .toArray(); logger.info('Found IB exchanges in database', { count: ibExchanges.length }); let syncedCount = 0; for (const exchange of ibExchanges) { try { await createOrUpdateMasterExchange(exchange, container); syncedCount++; logger.debug('Synced IB exchange', { ibId: exchange.id, country: exchange.country_code, }); } catch (error) { logger.error('Failed to sync IB exchange', { exchange: exchange.id, error }); } } logger.info('IB exchange sync completed', { syncedCount, totalExchanges: ibExchanges.length, }); return { syncedCount, totalExchanges: ibExchanges.length }; } catch (error) { logger.error('Failed to fetch IB exchanges from database', { error }); return { syncedCount: 0, totalExchanges: 0 }; } } /** * Create or update master exchange record 1:1 from IB exchange */ async function createOrUpdateMasterExchange(ibExchange: IBExchange, container: IServiceContainer): Promise { const mongoClient = container.mongodb; const db = mongoClient.getDatabase(); const collection = db.collection('masterExchanges'); const masterExchangeId = generateMasterExchangeId(ibExchange); const now = new Date(); // Check if master exchange already exists const existing = await collection.findOne({ masterExchangeId }); if (existing) { // Update existing record await collection.updateOne( { masterExchangeId }, { $set: { officialName: ibExchange.name || `Exchange ${ibExchange.id}`, country: ibExchange.country_code || 'UNKNOWN', currency: ibExchange.currency || 'USD', timezone: inferTimezone(ibExchange), updated_at: now, }, } ); logger.debug('Updated existing master exchange', { masterExchangeId }); } else { // Create new master exchange const masterExchange: MasterExchange = { masterExchangeId, shortName: masterExchangeId, // Set shortName to masterExchangeId on creation officialName: ibExchange.name || `Exchange ${ibExchange.id}`, country: ibExchange.country_code || 'UNKNOWN', currency: ibExchange.currency || 'USD', timezone: inferTimezone(ibExchange), active: false, // Set active to false only on creation sourceMappings: { ib: { id: ibExchange.id || ibExchange._id?.toString() || 'unknown', name: ibExchange.name || `Exchange ${ibExchange.id}`, code: ibExchange.code || ibExchange.id || '', aliases: generateAliases(ibExchange), lastUpdated: now, }, }, confidence: 1.0, // High confidence for direct IB mapping verified: true, // Mark as verified since it's direct from IB // DocumentBase fields source: 'ib-exchange-sync', created_at: now, updated_at: now, }; await collection.insertOne(masterExchange); logger.debug('Created new master exchange', { masterExchangeId }); } } /** * Generate master exchange ID from IB exchange */ function generateMasterExchangeId(ibExchange: IBExchange): string { // Use code if available, otherwise use ID, otherwise generate from name if (ibExchange.code) { return ibExchange.code.toUpperCase().replace(/[^A-Z0-9]/g, ''); } if (ibExchange.id) { return ibExchange.id.toUpperCase().replace(/[^A-Z0-9]/g, ''); } if (ibExchange.name) { return ibExchange.name .toUpperCase() .split(' ') .slice(0, 2) .join('_') .replace(/[^A-Z0-9_]/g, ''); } return 'UNKNOWN_EXCHANGE'; } /** * Generate aliases for the exchange */ function generateAliases(ibExchange: IBExchange): string[] { const aliases: string[] = []; if (ibExchange.name && ibExchange.name.includes(' ')) { // Add abbreviated version aliases.push( ibExchange.name .split(' ') .map(w => w[0]) .join('') .toUpperCase() ); } if (ibExchange.code) { aliases.push(ibExchange.code.toUpperCase()); } return aliases; } /** * Infer timezone from exchange name/location */ function inferTimezone(ibExchange: IBExchange): string { if (!ibExchange.name) { return 'UTC'; } const name = ibExchange.name.toUpperCase(); if (name.includes('NEW YORK') || name.includes('NYSE') || name.includes('NASDAQ')) { return 'America/New_York'; } if (name.includes('LONDON')) { return 'Europe/London'; } if (name.includes('TOKYO')) { return 'Asia/Tokyo'; } if (name.includes('SHANGHAI')) { return 'Asia/Shanghai'; } if (name.includes('TORONTO')) { return 'America/Toronto'; } if (name.includes('FRANKFURT')) { return 'Europe/Berlin'; } return 'UTC'; // Default }