import type { EodHandler } from '../eod.handler'; import { EOD_CONFIG } from '../shared'; import { getEodExchangeSuffix } from '../shared/utils'; interface FetchPricesInput { symbol: string; exchange: string; country?: string; // Optional to maintain backward compatibility } export async function scheduleFetchPrices( this: EodHandler ): Promise<{ success: boolean; jobsScheduled: number }> { const logger = this.logger; try { logger.info('Scheduling price fetch jobs for all symbols'); // Use OperationTracker to find stale symbols const staleSymbols = await this.operationRegistry.getStaleSymbols('eod', 'price_update', { limit: 50000 // Higher limit to process all symbols }); if (!staleSymbols || staleSymbols.length === 0) { logger.info('No symbols need price updates'); return { success: true, jobsScheduled: 0 }; } logger.info(`Found ${staleSymbols.length} symbols needing price updates`, { symbols: staleSymbols.slice(0, 10).map(s => ({ symbol: s.symbol.Code, exchange: s.symbol.Exchange, name: s.symbol.Name, lastUpdate: s.lastRun })) }); let jobsScheduled = 0; // Schedule jobs with staggered delays for (let i = 0; i < staleSymbols.length; i++) { const { symbol } = staleSymbols[i]; logger.debug(`Scheduling price fetch for ${symbol.Code}.${symbol.Exchange}`, { name: symbol.Name, lastUpdate: staleSymbols[i].lastRun, delay: i * 100 }); await this.scheduleOperation('fetch-prices', { symbol: symbol.Code, exchange: symbol.eodExchange || symbol.Exchange, // Use eodExchange if available country: symbol.Country }, { attempts: 3, backoff: { type: 'exponential', delay: 5000 }, delay: i * 100 // Stagger jobs by 100ms per symbol to avoid rate limit spikes }); jobsScheduled++; } logger.info(`Successfully scheduled ${jobsScheduled} price fetch jobs`); return { success: true, jobsScheduled }; } catch (error) { logger.error('Failed to schedule price fetch jobs', { error }); throw error; } } export async function fetchPrices( this: EodHandler, input: FetchPricesInput ): Promise<{ success: boolean; priceCount: number }> { const logger = this.logger; const { symbol, exchange, country } = input; try { logger.info(`Fetching prices for ${symbol}.${exchange}`); // Use provided country or fetch from database let symbolCountry = country; if (!symbolCountry) { const symbolDoc = await this.mongodb.collection('eodSymbols').findOne({ Code: symbol, Exchange: exchange }); if (!symbolDoc) { throw new Error(`Symbol ${symbol}.${exchange} not found in database`); } symbolCountry = symbolDoc.Country; } // Get API key from config const apiKey = EOD_CONFIG.API_TOKEN; if (!apiKey) { throw new Error('EOD API key not configured'); } // Build URL for EOD price data // Use utility function to handle US symbols and EUFUND special case const exchangeSuffix = getEodExchangeSuffix(exchange, symbolCountry); const url = new URL(`https://eodhd.com/api/eod/${symbol}.${exchangeSuffix}`); url.searchParams.append('api_token', apiKey); url.searchParams.append('fmt', 'json'); // Fetch price data from EOD API const response = await fetch(url.toString()); if (!response.ok) { throw new Error(`EOD Prices API returned ${response.status}: ${response.statusText}`); } const priceData = await response.json(); // EOD returns an array of historical prices if (!Array.isArray(priceData)) { throw new Error('Invalid response format from EOD API - expected array'); } logger.info(`Fetched ${priceData.length} price records for ${symbol}.${exchange}`); // Log date range of prices if (priceData.length > 0) { logger.debug(`Price data range for ${symbol}.${exchange}:`, { oldest: priceData[0].date, newest: priceData[priceData.length - 1].date, count: priceData.length }); } // Add metadata to each price record const pricesWithMetadata = priceData.map(price => ({ symbol, exchange, symbolExchange: `${symbol}.${exchange}`, date: price.date, open: price.open, high: price.high, low: price.low, close: price.close, adjustedClose: price.adjusted_close, volume: price.volume, })); // Save to MongoDB - use date and symbol as unique identifier const result = await this.mongodb.batchUpsert( 'eodPrices', pricesWithMetadata, ['date', 'symbolExchange'] ); // Update operation tracker instead of directly updating the symbol const eodSearchCode = `${symbol}.${exchange}`; await this.operationRegistry.updateOperation('eod', eodSearchCode, 'price_update', { status: 'success', lastRecordDate: priceData.length > 0 ? priceData[priceData.length - 1].date : null, recordCount: priceData.length, metadata: { insertedCount: result.insertedCount, updatedCount: priceData.length - result.insertedCount } }); logger.info(`Successfully saved ${result.insertedCount} price records for ${symbol}.${exchange}`); return { success: true, priceCount: result.insertedCount }; } catch (error: unknown) { logger.error('Failed to fetch or save prices', { error, symbol, exchange }); // Update operation tracker with failure const eodSearchCode = `${symbol}.${exchange}`; await this.operationRegistry.updateOperation('eod', eodSearchCode, 'price_update', { status: 'failure', error: error.message }); throw error; } }