import { getLogger } from '@stock-bot/logger'; import { getPostgreSQLClient, getMongoDBClient } from '../clients'; import { Exchange, ExchangeWithMappings, ProviderMapping, CreateExchangeRequest, UpdateExchangeRequest, CreateProviderMappingRequest, UpdateProviderMappingRequest, ProviderExchange, ExchangeStats, } from '../types/exchange.types'; const logger = getLogger('exchange-service'); export class ExchangeService { private get postgresClient() { return getPostgreSQLClient(); } private get mongoClient() { return getMongoDBClient(); } // Exchanges async getAllExchanges(): Promise { const exchangesQuery = ` SELECT e.id, e.code, e.name, e.country, e.currency, e.active, e.visible, e.created_at, e.updated_at, COUNT(pem.id) as provider_mapping_count, COUNT(CASE WHEN pem.active = true THEN 1 END) as active_mapping_count, COUNT(CASE WHEN pem.verified = true THEN 1 END) as verified_mapping_count, STRING_AGG(DISTINCT pem.provider, ', ') as providers FROM exchanges e LEFT JOIN provider_exchange_mappings pem ON e.id = pem.master_exchange_id WHERE e.visible = true GROUP BY e.id, e.code, e.name, e.country, e.currency, e.active, e.visible, e.created_at, e.updated_at ORDER BY e.code `; const exchangesResult = await this.postgresClient.query(exchangesQuery); // Get all provider mappings const mappingsQuery = ` SELECT pem.*, e.code as master_exchange_code, e.name as master_exchange_name FROM provider_exchange_mappings pem JOIN exchanges e ON pem.master_exchange_id = e.id WHERE e.visible = true ORDER BY pem.master_exchange_id, pem.provider, pem.provider_exchange_code `; const mappingsResult = await this.postgresClient.query(mappingsQuery); // Group mappings by exchange ID const mappingsByExchange = mappingsResult.rows.reduce((acc, mapping) => { const exchangeId = mapping.master_exchange_id; if (!acc[exchangeId]) { acc[exchangeId] = []; } acc[exchangeId].push(mapping); return acc; }, {} as Record); // Attach mappings to exchanges return exchangesResult.rows.map(exchange => ({ ...exchange, provider_mappings: mappingsByExchange[exchange.id] || [], })); } async getExchangeById(id: string): Promise<{ exchange: Exchange; provider_mappings: ProviderMapping[] } | null> { const exchangeQuery = 'SELECT * FROM exchanges WHERE id = $1 AND visible = true'; const exchangeResult = await this.postgresClient.query(exchangeQuery, [id]); if (exchangeResult.rows.length === 0) { return null; } const mappingsQuery = ` SELECT pem.*, e.code as master_exchange_code, e.name as master_exchange_name FROM provider_exchange_mappings pem JOIN exchanges e ON pem.master_exchange_id = e.id WHERE pem.master_exchange_id = $1 ORDER BY pem.provider, pem.provider_exchange_code `; const mappingsResult = await this.postgresClient.query(mappingsQuery, [id]); return { exchange: exchangeResult.rows[0], provider_mappings: mappingsResult.rows, }; } async createExchange(data: CreateExchangeRequest): Promise { const query = ` INSERT INTO exchanges (code, name, country, currency, active, visible) VALUES ($1, $2, $3, $4, $5, true) RETURNING * `; const result = await this.postgresClient.query(query, [ data.code, data.name, data.country, data.currency, data.active, ]); logger.info('Exchange created', { exchangeId: result.rows[0].id, code: data.code, name: data.name, }); return result.rows[0]; } async updateExchange(id: string, updates: UpdateExchangeRequest): Promise { const updateFields = []; const values = []; let paramIndex = 1; Object.entries(updates).forEach(([key, value]) => { updateFields.push(`${key} = $${paramIndex++}`); values.push(value); }); updateFields.push(`updated_at = NOW()`); values.push(id); const query = ` UPDATE exchanges SET ${updateFields.join(', ')} WHERE id = $${paramIndex} RETURNING * `; const result = await this.postgresClient.query(query, values); if (result.rows.length === 0) { return null; } // If hiding an exchange, delete its provider mappings if (updates.visible === false) { await this.deleteProviderMappingsByExchangeId(id); } logger.info('Exchange updated', { exchangeId: id, updates }); return result.rows[0]; } // Provider Mappings async getAllProviderMappings(): Promise { const query = ` SELECT pem.*, e.code as master_exchange_code, e.name as master_exchange_name, e.active as master_exchange_active FROM provider_exchange_mappings pem JOIN exchanges e ON pem.master_exchange_id = e.id WHERE e.visible = true ORDER BY pem.provider, pem.provider_exchange_code `; const result = await this.postgresClient.query(query); return result.rows; } async getProviderMappingsByProvider(provider: string): Promise { const query = ` SELECT pem.*, e.code as master_exchange_code, e.name as master_exchange_name, e.active as master_exchange_active FROM provider_exchange_mappings pem JOIN exchanges e ON pem.master_exchange_id = e.id WHERE pem.provider = $1 AND e.visible = true ORDER BY pem.provider_exchange_code `; const result = await this.postgresClient.query(query, [provider]); return result.rows; } async createProviderMapping(data: CreateProviderMappingRequest): Promise { const query = ` INSERT INTO provider_exchange_mappings (provider, provider_exchange_code, provider_exchange_name, master_exchange_id, country_code, currency, confidence, active, verified, auto_mapped) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, false) RETURNING * `; const result = await this.postgresClient.query(query, [ data.provider, data.provider_exchange_code, data.provider_exchange_name, data.master_exchange_id, data.country_code, data.currency, data.confidence, data.active, data.verified, ]); logger.info('Provider mapping created', { provider: data.provider, provider_exchange_code: data.provider_exchange_code, master_exchange_id: data.master_exchange_id, }); return result.rows[0]; } async updateProviderMapping(id: string, updates: UpdateProviderMappingRequest): Promise { const updateFields = []; const values = []; let paramIndex = 1; Object.entries(updates).forEach(([key, value]) => { updateFields.push(`${key} = $${paramIndex++}`); values.push(value); }); updateFields.push(`updated_at = NOW()`); updateFields.push(`auto_mapped = false`); // Mark as manually managed values.push(id); const query = ` UPDATE provider_exchange_mappings SET ${updateFields.join(', ')} WHERE id = $${paramIndex} RETURNING * `; const result = await this.postgresClient.query(query, values); if (result.rows.length === 0) { return null; } logger.info('Provider mapping updated', { mappingId: id, updates }); return result.rows[0]; } async deleteProviderMappingsByExchangeId(exchangeId: string): Promise { const query = 'DELETE FROM provider_exchange_mappings WHERE master_exchange_id = $1'; const result = await this.postgresClient.query(query, [exchangeId]); logger.info('Deleted provider mappings for hidden exchange', { exchangeId, deletedMappings: result.rowCount, }); return result.rowCount || 0; } // Providers and Statistics async getProviders(): Promise { const query = ` SELECT DISTINCT provider FROM provider_exchange_mappings ORDER BY provider `; const result = await this.postgresClient.query(query); return result.rows.map(row => row.provider); } async getExchangeStats(): Promise { const query = ` SELECT (SELECT COUNT(*) FROM exchanges WHERE visible = true) as total_exchanges, (SELECT COUNT(*) FROM exchanges WHERE active = true AND visible = true) as active_exchanges, (SELECT COUNT(DISTINCT country) FROM exchanges WHERE visible = true) as countries, (SELECT COUNT(DISTINCT currency) FROM exchanges WHERE visible = true) as currencies, (SELECT COUNT(*) FROM provider_exchange_mappings pem JOIN exchanges e ON pem.master_exchange_id = e.id WHERE e.visible = true) as total_provider_mappings, (SELECT COUNT(*) FROM provider_exchange_mappings pem JOIN exchanges e ON pem.master_exchange_id = e.id WHERE pem.active = true AND e.visible = true) as active_provider_mappings, (SELECT COUNT(*) FROM provider_exchange_mappings pem JOIN exchanges e ON pem.master_exchange_id = e.id WHERE pem.verified = true AND e.visible = true) as verified_provider_mappings, (SELECT COUNT(DISTINCT provider) FROM provider_exchange_mappings pem JOIN exchanges e ON pem.master_exchange_id = e.id WHERE e.visible = true) as providers `; const result = await this.postgresClient.query(query); return result.rows[0]; } async getUnmappedProviderExchanges(provider: string): Promise { // Get existing mappings for this provider const existingMappingsQuery = ` SELECT provider_exchange_code FROM provider_exchange_mappings WHERE provider = $1 `; const existingMappings = await this.postgresClient.query(existingMappingsQuery, [provider]); const mappedCodes = new Set(existingMappings.rows.map(row => row.provider_exchange_code)); const db = this.mongoClient.getDatabase(); let providerExchanges: ProviderExchange[] = []; switch (provider) { case 'eod': { const eodExchanges = await db.collection('eodExchanges').find({ active: true }).toArray(); providerExchanges = eodExchanges .filter(exchange => !mappedCodes.has(exchange.Code)) .map(exchange => ({ provider_exchange_code: exchange.Code, provider_exchange_name: exchange.Name, country_code: exchange.CountryISO2, currency: exchange.Currency, symbol_count: null, })); break; } case 'ib': { const ibExchanges = await db.collection('ibExchanges').find({}).toArray(); providerExchanges = ibExchanges .filter(exchange => !mappedCodes.has(exchange.exchange_id)) .map(exchange => ({ provider_exchange_code: exchange.exchange_id, provider_exchange_name: exchange.name, country_code: exchange.country_code, currency: null, symbol_count: null, })); break; } case 'qm': { const qmExchanges = await db.collection('qmExchanges').find({}).toArray(); providerExchanges = qmExchanges .filter(exchange => !mappedCodes.has(exchange.exchangeCode)) .map(exchange => ({ provider_exchange_code: exchange.exchangeCode, provider_exchange_name: exchange.name, country_code: exchange.countryCode, currency: exchange.countryCode === 'CA' ? 'CAD' : 'USD', symbol_count: null, })); break; } default: throw new Error(`Unknown provider: ${provider}`); } return providerExchanges; } } // Export singleton instance export const exchangeService = new ExchangeService();