stock-bot/apps/stock/data-ingestion/src/handlers/eod/actions/prices.ts

189 lines
No EOL
6 KiB
TypeScript

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;
}
}