189 lines
No EOL
6 KiB
TypeScript
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;
|
|
}
|
|
} |