fixed up prices, symbols / exchanges

This commit is contained in:
Boki 2025-07-11 08:34:59 -04:00
parent d8e7449605
commit f196c5dcf4
9 changed files with 191 additions and 202 deletions

View file

@ -1,14 +1,9 @@
import type { BaseHandler } from '@stock-bot/handlers';
import type { DataIngestionServices } from '../../../types';
import type { EodHandler } from '../eod.handler'; import type { EodHandler } from '../eod.handler';
import { EOD_CONFIG } from '../shared'; import { EOD_CONFIG } from '../shared';
import { getEodExchangeSuffix } from '../shared/utils';
interface FetchCorporateActionsInput { interface FetchCorporateActionsInput {
symbol: string; eodSearchCode: string;
exchange: string;
actionType: 'dividends' | 'splits'; actionType: 'dividends' | 'splits';
country?: string;
} }
export async function scheduleFetchCorporateActions( export async function scheduleFetchCorporateActions(
@ -57,10 +52,8 @@ export async function scheduleFetchCorporateActions(
const { symbol } = staleSymbolsDividends[i]; const { symbol } = staleSymbolsDividends[i];
await this.scheduleOperation('fetch-corporate-actions', { await this.scheduleOperation('fetch-corporate-actions', {
symbol: symbol.Code, eodSearchCode: symbol.eodSearchCode,
exchange: symbol.eodExchange || symbol.Exchange, // Use eodExchange if available actionType: 'dividends'
actionType: 'dividends',
country: symbol.Country
}, { }, {
attempts: 3, attempts: 3,
backoff: { backoff: {
@ -77,10 +70,8 @@ export async function scheduleFetchCorporateActions(
const { symbol } = staleSymbolsSplits[i]; const { symbol } = staleSymbolsSplits[i];
await this.scheduleOperation('fetch-corporate-actions', { await this.scheduleOperation('fetch-corporate-actions', {
symbol: symbol.Code, eodSearchCode: symbol.eodSearchCode,
exchange: symbol.eodExchange || symbol.Exchange, // Use eodExchange if available actionType: 'splits'
actionType: 'splits',
country: symbol.Country
}, { }, {
attempts: 3, attempts: 3,
backoff: { backoff: {
@ -109,9 +100,27 @@ export async function fetchCorporateActions(
input: FetchCorporateActionsInput input: FetchCorporateActionsInput
): Promise<{ success: boolean; recordsCount: number }> { ): Promise<{ success: boolean; recordsCount: number }> {
const logger = this.logger; const logger = this.logger;
const { symbol, exchange, actionType, country } = input; const { eodSearchCode, actionType } = input;
// Declare variables for catch block
let symbol: string = '';
let exchange: string = '';
try { try {
// Lookup symbol using eodSearchCode
const symbolDoc = await this.mongodb.collection('eodSymbols').findOne({
eodSearchCode: eodSearchCode
});
if (!symbolDoc) {
logger.error(`Symbol not found for eodSearchCode: ${eodSearchCode}`);
throw new Error(`Symbol not found: ${eodSearchCode}`);
}
symbol = symbolDoc.Code;
exchange = symbolDoc.eodExchange || symbolDoc.Exchange;
const country = symbolDoc.Country;
logger.info(`Fetching ${actionType} for ${symbol}.${exchange}`); logger.info(`Fetching ${actionType} for ${symbol}.${exchange}`);
// Get API key // Get API key
@ -120,23 +129,9 @@ export async function fetchCorporateActions(
throw new Error('EOD API key not configured'); throw new Error('EOD API key not configured');
} }
// Get country if not provided
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;
}
// Build URL based on action type // Build URL based on action type
// Use utility function to handle US symbols and EUFUND special case // Use utility function to handle US symbols and EUFUND special case
const exchangeSuffix = getEodExchangeSuffix(exchange, symbolCountry); const exchangeSuffix = getEodExchangeSuffix(exchange, country);
const endpoint = actionType === 'dividends' ? 'div' : 'splits'; const endpoint = actionType === 'dividends' ? 'div' : 'splits';
const url = new URL(`https://eodhd.com/api/${endpoint}/${symbol}.${exchangeSuffix}`); const url = new URL(`https://eodhd.com/api/${endpoint}/${symbol}.${exchangeSuffix}`);
@ -174,7 +169,7 @@ export async function fetchCorporateActions(
if (data.length === 0) { if (data.length === 0) {
// Update symbol to indicate we checked but found no data // Update symbol to indicate we checked but found no data
await this.mongodb.collection('eodSymbols').updateOne( await this.mongodb.collection('eodSymbols').updateOne(
{ Code: symbol, Exchange: exchange }, { eodSearchCode: eodSearchCode },
{ {
$set: { $set: {
[`last${actionType.charAt(0).toUpperCase() + actionType.slice(1)}Update`]: new Date(), [`last${actionType.charAt(0).toUpperCase() + actionType.slice(1)}Update`]: new Date(),
@ -190,7 +185,7 @@ export async function fetchCorporateActions(
const recordsWithMetadata = data.map(record => ({ const recordsWithMetadata = data.map(record => ({
symbol, symbol,
exchange, exchange,
symbolExchange: `${symbol}.${exchange}`, eodSearchCode,
...record, ...record,
actionType, actionType,
updatedAt: new Date(), updatedAt: new Date(),
@ -200,16 +195,15 @@ export async function fetchCorporateActions(
// Determine collection name based on action type // Determine collection name based on action type
const collectionName = actionType === 'dividends' ? 'eodDividends' : 'eodSplits'; const collectionName = actionType === 'dividends' ? 'eodDividends' : 'eodSplits';
// Save to MongoDB - use date and symbol as unique identifier // Save to MongoDB - use date and eodSearchCode as unique identifier
const result = await this.mongodb.batchUpsert( const result = await this.mongodb.batchUpsert(
collectionName, collectionName,
recordsWithMetadata, recordsWithMetadata,
['date', 'symbolExchange'] ['date', 'eodSearchCode']
); );
// Update operation tracker based on action type // Update operation tracker based on action type
const operationName = actionType === 'dividends' ? 'dividends_update' : 'splits_update'; const operationName = actionType === 'dividends' ? 'dividends_update' : 'splits_update';
const eodSearchCode = `${symbol}.${exchange}`;
await this.operationRegistry.updateOperation('eod', eodSearchCode, operationName, { await this.operationRegistry.updateOperation('eod', eodSearchCode, operationName, {
status: 'success', status: 'success',
recordCount: result.insertedCount, recordCount: result.insertedCount,
@ -231,10 +225,9 @@ export async function fetchCorporateActions(
// Update operation tracker with failure // Update operation tracker with failure
const operationName = actionType === 'dividends' ? 'dividends_update' : 'splits_update'; const operationName = actionType === 'dividends' ? 'dividends_update' : 'splits_update';
const eodSearchCode = `${symbol}.${exchange}`;
await this.operationRegistry.updateOperation('eod', eodSearchCode, operationName, { await this.operationRegistry.updateOperation('eod', eodSearchCode, operationName, {
status: 'failure', status: 'failure',
error: error.message error: error instanceof Error ? error.message : String(error)
}); });
throw error; throw error;

View file

@ -1,17 +1,11 @@
import type { BaseHandler } from '@stock-bot/handlers';
import type { DataIngestionServices } from '../../../types';
import type { EodHandler } from '../eod.handler'; import type { EodHandler } from '../eod.handler';
import { EOD_CONFIG } from '../shared'; import { EOD_CONFIG } from '../shared';
import { getEodExchangeSuffix } from '../shared/utils';
interface BulkFundamentalsInput { interface BulkFundamentalsInput {
symbols: Array<{ symbol: string; exchange: string; country?: string }>; eodSearchCodes: string[];
} }
interface FetchSingleFundamentalsInput { interface FetchSingleFundamentalsInput {
symbol: string; eodSearchCode: string;
exchange: string;
country?: string;
} }
export async function scheduleFetchFundamentals( export async function scheduleFetchFundamentals(
@ -67,9 +61,7 @@ export async function scheduleFetchFundamentals(
for (let i = 0; i < etfs.length; i++) { for (let i = 0; i < etfs.length; i++) {
const { symbol: etf } = etfs[i]; const { symbol: etf } = etfs[i];
await this.scheduleOperation('fetch-single-fundamentals', { await this.scheduleOperation('fetch-single-fundamentals', {
symbol: etf.Code, eodSearchCode: etf.eodSearchCode
exchange: etf.eodExchange || etf.Exchange, // Use eodExchange if available
country: etf.Country
}, { }, {
attempts: 3, attempts: 3,
backoff: { backoff: {
@ -91,15 +83,11 @@ export async function scheduleFetchFundamentals(
for (let i = 0; i < nonEtfs.length; i += batchSize) { for (let i = 0; i < nonEtfs.length; i += batchSize) {
const batch = nonEtfs.slice(i, i + batchSize); const batch = nonEtfs.slice(i, i + batchSize);
// Convert to array of {symbol, exchange, country} objects // Convert to array of eodSearchCodes
const symbolBatch = batch.map(s => ({ const eodSearchCodes = batch.map(s => s.symbol.eodSearchCode);
symbol: s.symbol.Code,
exchange: s.symbol.eodExchange || s.symbol.Exchange, // Use eodExchange if available
country: s.symbol.Country
}));
await this.scheduleOperation('fetch-bulk-fundamentals', { await this.scheduleOperation('fetch-bulk-fundamentals', {
symbols: symbolBatch eodSearchCodes: eodSearchCodes
}, { }, {
attempts: 3, attempts: 3,
backoff: { backoff: {
@ -110,7 +98,7 @@ export async function scheduleFetchFundamentals(
}); });
jobsScheduled++; jobsScheduled++;
logger.info(`Scheduled fundamentals batch with ${symbolBatch.length} non-ETF symbols`); logger.info(`Scheduled fundamentals batch with ${eodSearchCodes.length} non-ETF symbols`);
} }
logger.info(`Successfully scheduled ${jobsScheduled} fundamentals fetch jobs`); logger.info(`Successfully scheduled ${jobsScheduled} fundamentals fetch jobs`);
@ -130,10 +118,20 @@ export async function fetchBulkFundamentals(
input: BulkFundamentalsInput input: BulkFundamentalsInput
): Promise<{ success: boolean; symbolsProcessed: number }> { ): Promise<{ success: boolean; symbolsProcessed: number }> {
const logger = this.logger; const logger = this.logger;
const { symbols } = input; const { eodSearchCodes } = input;
try { try {
logger.info('Fetching bulk fundamentals', { symbolCount: symbols.length }); logger.info('Fetching bulk fundamentals', { symbolCount: eodSearchCodes.length });
// Lookup all symbols
const symbolDocs = await this.mongodb.collection('eodSymbols').find({
eodSearchCode: { $in: eodSearchCodes }
}).toArray();
if (symbolDocs.length === 0) {
logger.error('No symbols found for provided eodSearchCodes');
return { success: true, symbolsProcessed: 0 };
}
// Get API key // Get API key
const apiKey = EOD_CONFIG.API_TOKEN; const apiKey = EOD_CONFIG.API_TOKEN;
@ -142,7 +140,11 @@ export async function fetchBulkFundamentals(
} }
// Group symbols by actual exchange for API endpoint, but use country for symbol suffix // Group symbols by actual exchange for API endpoint, but use country for symbol suffix
const exchangeGroups = symbols.reduce((acc, { symbol, exchange, country }) => { const exchangeGroups = symbolDocs.reduce((acc, symbolDoc) => {
const symbol = symbolDoc.Code;
const exchange = symbolDoc.eodExchange || symbolDoc.Exchange;
const country = symbolDoc.Country;
if (!acc[exchange]) { if (!acc[exchange]) {
acc[exchange] = []; acc[exchange] = [];
} }
@ -193,20 +195,34 @@ export async function fetchBulkFundamentals(
} }
// Extract symbol and exchange from the key // Extract symbol and exchange from the key
const [symbol, exc] = symbolExchange.split('.'); const [symbol, exchangeSuffix] = symbolExchange.split('.');
// Find the original symbol doc to get the actual exchange code
const symbolDoc = symbolDocs.find(doc =>
doc.Code === symbol &&
(doc.eodExchange === exchangeSuffix || doc.Exchange === exchangeSuffix)
);
if (!symbolDoc) {
logger.warn(`Could not find symbol doc for ${symbolExchange}`);
continue;
}
const actualExchange = symbolDoc.eodExchange || symbolDoc.Exchange;
const eodSearchCode = symbolDoc.eodSearchCode;
// Add metadata // Add metadata
const fundamentalsWithMetadata = { const fundamentalsWithMetadata = {
symbol, symbol,
exchange: exc, exchange: actualExchange,
symbolExchange, eodSearchCode,
...fundamentals, ...fundamentals,
updatedAt: new Date(), updatedAt: new Date(),
source: 'eod' source: 'eod'
}; };
fundamentalsToSave.push(fundamentalsWithMetadata); fundamentalsToSave.push(fundamentalsWithMetadata);
symbolsToUpdate.push({ symbol, exchange: exc }); symbolsToUpdate.push({ eodSearchCode });
} }
if (fundamentalsToSave.length > 0) { if (fundamentalsToSave.length > 0) {
@ -214,14 +230,13 @@ export async function fetchBulkFundamentals(
const result = await this.mongodb.batchUpsert( const result = await this.mongodb.batchUpsert(
'eodFundamentals', 'eodFundamentals',
fundamentalsToSave, fundamentalsToSave,
['symbolExchange'] ['eodSearchCode']
); );
logger.info(`Saved ${result.insertedCount} fundamentals records for ${exchange}`); logger.info(`Saved ${result.insertedCount} fundamentals records for ${exchange}`);
// Update operation tracker for each symbol // Update operation tracker for each symbol
const updatePromises = symbolsToUpdate.map(({ symbol, exchange }) => { const updatePromises = symbolsToUpdate.map(({ eodSearchCode }) => {
const eodSearchCode = `${symbol}.${exchange}`;
return this.operationRegistry.updateOperation('eod', eodSearchCode, 'fundamentals_update', { return this.operationRegistry.updateOperation('eod', eodSearchCode, 'fundamentals_update', {
status: 'success', status: 'success',
recordCount: 1, recordCount: 1,
@ -247,8 +262,7 @@ export async function fetchBulkFundamentals(
logger.error('Failed to fetch bulk fundamentals', { error }); logger.error('Failed to fetch bulk fundamentals', { error });
// Mark all symbols as failed // Mark all symbols as failed
const failPromises = input.symbols.map(({ symbol, exchange }) => { const failPromises = eodSearchCodes.map((eodSearchCode) => {
const eodSearchCode = `${symbol}.${exchange}`;
return this.operationRegistry.updateOperation('eod', eodSearchCode, 'fundamentals_update', { return this.operationRegistry.updateOperation('eod', eodSearchCode, 'fundamentals_update', {
status: 'failure', status: 'failure',
error: error.message error: error.message
@ -265,25 +279,29 @@ export async function fetchSingleFundamentals(
input: FetchSingleFundamentalsInput input: FetchSingleFundamentalsInput
): Promise<{ success: boolean; saved: boolean }> { ): Promise<{ success: boolean; saved: boolean }> {
const logger = this.logger; const logger = this.logger;
const { symbol, exchange, country } = input; const { eodSearchCode } = input;
// Declare variables for catch block
let symbol: string = '';
let exchange: string = '';
try { try {
logger.info(`Fetching single fundamentals for ${symbol}.${exchange}`); // Lookup symbol using eodSearchCode
const symbolDoc = await this.mongodb.collection('eodSymbols').findOne({
eodSearchCode: eodSearchCode
});
// Get country if not provided if (!symbolDoc) {
let symbolCountry = country; logger.error(`Symbol not found for eodSearchCode: ${eodSearchCode}`);
if (!symbolCountry) { throw new Error(`Symbol not found: ${eodSearchCode}`);
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;
} }
symbol = symbolDoc.Code;
exchange = symbolDoc.eodExchange || symbolDoc.Exchange;
const country = symbolDoc.Country;
logger.info(`Fetching single fundamentals for ${symbol}.${exchange}`);
// Get API key // Get API key
const apiKey = EOD_CONFIG.API_TOKEN; const apiKey = EOD_CONFIG.API_TOKEN;
if (!apiKey) { if (!apiKey) {
@ -292,7 +310,7 @@ export async function fetchSingleFundamentals(
// Build URL for single fundamentals endpoint // Build URL for single fundamentals endpoint
// Use utility function to handle US symbols and EUFUND special case // Use utility function to handle US symbols and EUFUND special case
const exchangeSuffix = getEodExchangeSuffix(exchange, symbolCountry); const exchangeSuffix = getEodExchangeSuffix(exchange, country);
const url = new URL(`https://eodhd.com/api/fundamentals/${symbol}.${exchangeSuffix}`); const url = new URL(`https://eodhd.com/api/fundamentals/${symbol}.${exchangeSuffix}`);
url.searchParams.append('api_token', apiKey); url.searchParams.append('api_token', apiKey);
@ -317,7 +335,7 @@ export async function fetchSingleFundamentals(
const fundamentalsWithMetadata = { const fundamentalsWithMetadata = {
symbol, symbol,
exchange, exchange,
symbolExchange: `${symbol}.${exchange}`, eodSearchCode,
...fundamentals, ...fundamentals,
updatedAt: new Date(), updatedAt: new Date(),
source: 'eod' source: 'eod'
@ -327,11 +345,10 @@ export async function fetchSingleFundamentals(
const result = await this.mongodb.batchUpsert( const result = await this.mongodb.batchUpsert(
'eodFundamentals', 'eodFundamentals',
[fundamentalsWithMetadata], [fundamentalsWithMetadata],
['symbolExchange'] ['eodSearchCode']
); );
// Update operation tracker // Update operation tracker
const eodSearchCode = `${symbol}.${exchange}`;
await this.operationRegistry.updateOperation('eod', eodSearchCode, 'fundamentals_update', { await this.operationRegistry.updateOperation('eod', eodSearchCode, 'fundamentals_update', {
status: 'success', status: 'success',
recordCount: result.insertedCount, recordCount: result.insertedCount,
@ -352,7 +369,6 @@ export async function fetchSingleFundamentals(
logger.error('Failed to fetch single fundamentals', { error, symbol, exchange }); logger.error('Failed to fetch single fundamentals', { error, symbol, exchange });
// Update operation tracker with failure // Update operation tracker with failure
const eodSearchCode = `${symbol}.${exchange}`;
await this.operationRegistry.updateOperation('eod', eodSearchCode, 'fundamentals_update', { await this.operationRegistry.updateOperation('eod', eodSearchCode, 'fundamentals_update', {
status: 'failure', status: 'failure',
error: error.message error: error.message

View file

@ -3,19 +3,15 @@ import type { EodHandler } from '../eod.handler';
import { EOD_CONFIG } from '../shared'; import { EOD_CONFIG } from '../shared';
interface FetchIntradayInput { interface FetchIntradayInput {
symbol: string; eodSearchCode: string;
exchange: string;
interval: '1m' | '5m' | '1h'; interval: '1m' | '5m' | '1h';
fromDate?: Date; fromDate?: Date;
toDate?: Date; toDate?: Date;
country?: string;
} }
interface CrawlIntradayInput { interface CrawlIntradayInput {
symbol: string; eodSearchCode: string;
exchange: string;
interval: '1m' | '5m' | '1h'; interval: '1m' | '5m' | '1h';
country?: string;
} }
@ -53,7 +49,7 @@ export async function scheduleIntradayCrawl(
}).toArray(); }).toArray();
// Add interval info to each symbol // Add interval info to each symbol
symbolsForInterval.forEach(symbol => { symbolsForInterval.forEach((symbol: any) => {
// Check if this interval needs processing (not finished or needs new data) // Check if this interval needs processing (not finished or needs new data)
const operationStatus = symbol.operations?.[operationName]; const operationStatus = symbol.operations?.[operationName];
const shouldProcess = !operationStatus || !operationStatus.finished || const shouldProcess = !operationStatus || !operationStatus.finished ||
@ -101,10 +97,8 @@ export async function scheduleIntradayCrawl(
const { symbol, interval } = item; const { symbol, interval } = item;
await this.scheduleOperation('crawl-intraday', { await this.scheduleOperation('crawl-intraday', {
symbol: symbol.Code, eodSearchCode: symbol.eodSearchCode,
exchange: symbol.eodExchange || symbol.Exchange, // Use eodExchange if available interval
interval,
country: symbol.Country
}, { }, {
priority: 5, // Initial crawl jobs get priority 5 (lower priority) priority: 5, // Initial crawl jobs get priority 5 (lower priority)
attempts: 3, attempts: 3,
@ -134,26 +128,31 @@ export async function crawlIntraday(
input: CrawlIntradayInput input: CrawlIntradayInput
): Promise<{ success: boolean; recordsProcessed: number; finished: boolean }> { ): Promise<{ success: boolean; recordsProcessed: number; finished: boolean }> {
const logger = this.logger; const logger = this.logger;
const { symbol, exchange, interval, country } = input; const { eodSearchCode, interval } = input;
try { try {
// Lookup symbol using eodSearchCode
const symbolDoc = await this.mongodb.collection('eodSymbols').findOne({
eodSearchCode: eodSearchCode
});
if (!symbolDoc) {
logger.error(`Symbol not found for eodSearchCode: ${eodSearchCode}`);
throw new Error(`Symbol not found: ${eodSearchCode}`);
}
const symbol = symbolDoc.Code;
const exchange = symbolDoc.eodExchange || symbolDoc.Exchange;
const country = symbolDoc.Country;
logger.info(`Starting intraday crawl for ${symbol}.${exchange} - ${interval}`, { logger.info(`Starting intraday crawl for ${symbol}.${exchange} - ${interval}`, {
symbol, symbol,
exchange, exchange,
interval, interval,
country country,
eodSearchCode
}); });
// Get symbol to check if it exists
const symbolDoc = await this.mongodb.collection('eodSymbols').findOne({
Code: symbol,
eodExchange: exchange
});
if (!symbolDoc) {
throw new Error(`Symbol ${symbol}.${exchange} not found`);
}
logger.debug('Found symbol document', { logger.debug('Found symbol document', {
symbol, symbol,
exchange, exchange,
@ -202,12 +201,10 @@ export async function crawlIntraday(
// Fetch data for this batch // Fetch data for this batch
const result = await fetchIntraday.call(this, { const result = await fetchIntraday.call(this, {
symbol, eodSearchCode,
exchange,
interval, interval,
fromDate, fromDate,
toDate, toDate
country
}); });
// Prepare update data // Prepare update data
@ -270,7 +267,6 @@ export async function crawlIntraday(
finished: updateData.finished finished: updateData.finished
}); });
const eodSearchCode = `${symbol}.${exchange}`;
await this.operationRegistry.updateOperation('eod', eodSearchCode, operationName, updateData); await this.operationRegistry.updateOperation('eod', eodSearchCode, operationName, updateData);
logger.info(`Operation tracker updated for ${symbol}.${exchange} - ${interval}`); logger.info(`Operation tracker updated for ${symbol}.${exchange} - ${interval}`);
@ -278,10 +274,8 @@ export async function crawlIntraday(
// If not finished, schedule next batch // If not finished, schedule next batch
if (!updateData.finished) { if (!updateData.finished) {
await this.scheduleOperation('crawl-intraday', { await this.scheduleOperation('crawl-intraday', {
symbol, eodSearchCode,
exchange, interval
interval,
country
}, { }, {
priority: 3, // Continuation jobs get higher priority (3) than initial jobs (5) priority: 3, // Continuation jobs get higher priority (3) than initial jobs (5)
attempts: 3, attempts: 3,
@ -319,9 +313,27 @@ export async function fetchIntraday(
input: FetchIntradayInput input: FetchIntradayInput
): Promise<{ success: boolean; recordsSaved: number; recordsFetched: number }> { ): Promise<{ success: boolean; recordsSaved: number; recordsFetched: number }> {
const logger = this.logger; const logger = this.logger;
const { symbol, exchange, interval, fromDate, toDate, country } = input; const { eodSearchCode, interval, fromDate, toDate } = input;
// Declare variables for catch block
let symbol: string = '';
let exchange: string = '';
try { try {
// Lookup symbol using eodSearchCode
const symbolDoc = await this.mongodb.collection('eodSymbols').findOne({
eodSearchCode: eodSearchCode
});
if (!symbolDoc) {
logger.error(`Symbol not found for eodSearchCode: ${eodSearchCode}`);
throw new Error(`Symbol not found: ${eodSearchCode}`);
}
symbol = symbolDoc.Code;
exchange = symbolDoc.eodExchange || symbolDoc.Exchange;
const country = symbolDoc.Country;
logger.info(`Fetching intraday data for ${symbol}.${exchange} - ${interval}`, { logger.info(`Fetching intraday data for ${symbol}.${exchange} - ${interval}`, {
symbol, symbol,
exchange, exchange,
@ -332,20 +344,6 @@ export async function fetchIntraday(
url: `https://eodhd.com/api/intraday/${symbol}.${exchange}` url: `https://eodhd.com/api/intraday/${symbol}.${exchange}`
}); });
// Get country if not provided
let symbolCountry = country;
if (!symbolCountry) {
const symbolDoc = await this.mongodb.collection('eodSymbols').findOne({
Code: symbol,
eodExchange: exchange
});
if (!symbolDoc) {
throw new Error(`Symbol ${symbol}.${exchange} not found in database`);
}
symbolCountry = symbolDoc.Country;
}
// Get API key // Get API key
const apiKey = EOD_CONFIG.API_TOKEN; const apiKey = EOD_CONFIG.API_TOKEN;
if (!apiKey) { if (!apiKey) {
@ -392,7 +390,7 @@ export async function fetchIntraday(
const recordsWithMetadata = data.map(bar => ({ const recordsWithMetadata = data.map(bar => ({
symbol, symbol,
exchange, exchange,
symbolExchange: `${symbol}.${exchange}`, eodSearchCode,
datetime: bar.datetime, datetime: bar.datetime,
timestamp: bar.timestamp, timestamp: bar.timestamp,
gmtoffset: bar.gmtoffset, gmtoffset: bar.gmtoffset,
@ -404,12 +402,12 @@ export async function fetchIntraday(
source: 'eod' source: 'eod'
})); }));
// Save to MongoDB - use timestamp and symbolExchange as unique identifier // Save to MongoDB - use timestamp and eodSearchCode as unique identifier
const collectionName = `eodIntraday${interval.toUpperCase()}`; const collectionName = `eodIntraday${interval.toUpperCase()}`;
const result = await this.mongodb.batchUpsert( const result = await this.mongodb.batchUpsert(
collectionName, collectionName,
recordsWithMetadata, recordsWithMetadata,
['timestamp', 'symbolExchange'] ['timestamp', 'eodSearchCode']
); );
logger.info(`Saved ${result.insertedCount} intraday records`, { logger.info(`Saved ${result.insertedCount} intraday records`, {

View file

@ -1,11 +1,8 @@
import type { EodHandler } from '../eod.handler'; import type { EodHandler } from '../eod.handler';
import { EOD_CONFIG } from '../shared'; import { EOD_CONFIG } from '../shared';
import { getEodExchangeSuffix } from '../shared/utils';
interface FetchPricesInput { interface FetchPricesInput {
symbol: string; eodSearchCode: string;
exchange: string;
country?: string; // Optional to maintain backward compatibility
} }
export async function scheduleFetchPrices( export async function scheduleFetchPrices(
@ -18,7 +15,7 @@ export async function scheduleFetchPrices(
// Use OperationTracker to find stale symbols // Use OperationTracker to find stale symbols
const staleSymbols = await this.operationRegistry.getStaleSymbols('eod', 'price_update', { const staleSymbols = await this.operationRegistry.getStaleSymbols('eod', 'price_update', {
limit: 50000 // Higher limit to process all symbols limit: 14000 // Higher limit to process all symbols
}); });
if (!staleSymbols || staleSymbols.length === 0) { if (!staleSymbols || staleSymbols.length === 0) {
@ -39,17 +36,20 @@ export async function scheduleFetchPrices(
// Schedule jobs with staggered delays // Schedule jobs with staggered delays
for (let i = 0; i < staleSymbols.length; i++) { for (let i = 0; i < staleSymbols.length; i++) {
const { symbol } = staleSymbols[i]; const staleSymbol = staleSymbols[i];
if (!staleSymbol || !staleSymbol.symbol) {
logger.warn(`Skipping invalid stale symbol at index ${i}`, { staleSymbol });
continue;
}
const { symbol } = staleSymbol;
logger.debug(`Scheduling price fetch for ${symbol.Code}.${symbol.Exchange}`, { logger.debug(`Scheduling price fetch for ${symbol.Code}.${symbol.Exchange}`, {
name: symbol.Name, name: symbol.Name,
lastUpdate: staleSymbols[i].lastRun, lastUpdate: staleSymbol.lastRun,
delay: i * 100 delay: i * 100
}); });
await this.scheduleOperation('fetch-prices', { await this.scheduleOperation('fetch-prices', {
symbol: symbol.Code, eodSearchCode: symbol.eodSearchCode
exchange: symbol.eodExchange || symbol.Exchange, // Use eodExchange if available
country: symbol.Country
}, { }, {
attempts: 3, attempts: 3,
backoff: { backoff: {
@ -78,36 +78,35 @@ export async function fetchPrices(
input: FetchPricesInput input: FetchPricesInput
): Promise<{ success: boolean; priceCount: number }> { ): Promise<{ success: boolean; priceCount: number }> {
const logger = this.logger; const logger = this.logger;
const { symbol, exchange, country } = input; const { eodSearchCode } = input;
// Declare variables that need to be accessible in catch block
let symbol: string = '';
let exchange: string = '';
try { try {
logger.info(`Fetching prices for ${symbol}.${exchange}`); // Lookup symbol using eodSearchCode
const symbolDoc = await this.mongodb.collection('eodSymbols').findOne({
eodSearchCode: eodSearchCode
});
// Use provided country or fetch from database if (!symbolDoc) {
let symbolCountry = country; logger.error(`Symbol not found for eodSearchCode: ${eodSearchCode}`);
if (!symbolCountry) { throw new Error(`Symbol not found: ${eodSearchCode}`);
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;
} }
symbol = symbolDoc.Code;
exchange = symbolDoc.Exchange;
logger.info(`Fetching prices for ${eodSearchCode}`);
// Get API key from config // Get API key from config
const apiKey = EOD_CONFIG.API_TOKEN; const apiKey = EOD_CONFIG.API_TOKEN;
if (!apiKey) { if (!apiKey) {
throw new Error('EOD API key not configured'); throw new Error('EOD API key not configured');
} }
// Build URL for EOD price data const url = new URL(`https://eodhd.com/api/eod/${eodSearchCode}`);
// 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('api_token', apiKey);
url.searchParams.append('fmt', 'json'); url.searchParams.append('fmt', 'json');
// Fetch price data from EOD API // Fetch price data from EOD API
@ -124,11 +123,11 @@ export async function fetchPrices(
throw new Error('Invalid response format from EOD API - expected array'); throw new Error('Invalid response format from EOD API - expected array');
} }
logger.info(`Fetched ${priceData.length} price records for ${symbol}.${exchange}`); logger.info(`Fetched ${priceData.length} price records for ${eodSearchCode}`);
// Log date range of prices // Log date range of prices
if (priceData.length > 0) { if (priceData.length > 0) {
logger.debug(`Price data range for ${symbol}.${exchange}:`, { logger.debug(`Price data range for ${eodSearchCode}:`, {
oldest: priceData[0].date, oldest: priceData[0].date,
newest: priceData[priceData.length - 1].date, newest: priceData[priceData.length - 1].date,
count: priceData.length count: priceData.length
@ -139,7 +138,7 @@ export async function fetchPrices(
const pricesWithMetadata = priceData.map(price => ({ const pricesWithMetadata = priceData.map(price => ({
symbol, symbol,
exchange, exchange,
symbolExchange: `${symbol}.${exchange}`, eodSearchCode,
date: price.date, date: price.date,
open: price.open, open: price.open,
high: price.high, high: price.high,
@ -149,15 +148,14 @@ export async function fetchPrices(
volume: price.volume, volume: price.volume,
})); }));
// Save to MongoDB - use date and symbol as unique identifier // Save to MongoDB - use date and eodSearchCode as unique identifier
const result = await this.mongodb.batchUpsert( const result = await this.mongodb.batchUpsert(
'eodPrices', 'eodPrices',
pricesWithMetadata, pricesWithMetadata,
['date', 'symbolExchange'] ['date', 'eodSearchCode']
); );
// Update operation tracker instead of directly updating the symbol // Update operation tracker instead of directly updating the symbol
const eodSearchCode = `${symbol}.${exchange}`;
await this.operationRegistry.updateOperation('eod', eodSearchCode, 'price_update', { await this.operationRegistry.updateOperation('eod', eodSearchCode, 'price_update', {
status: 'success', status: 'success',
lastRecordDate: priceData.length > 0 ? priceData[priceData.length - 1].date : null, lastRecordDate: priceData.length > 0 ? priceData[priceData.length - 1].date : null,
@ -168,7 +166,7 @@ export async function fetchPrices(
} }
}); });
logger.info(`Successfully saved ${result.insertedCount} price records for ${symbol}.${exchange}`); logger.info(`Successfully saved ${result.insertedCount} price records for ${eodSearchCode}`);
return { return {
success: true, success: true,
@ -178,10 +176,9 @@ export async function fetchPrices(
logger.error('Failed to fetch or save prices', { error, symbol, exchange }); logger.error('Failed to fetch or save prices', { error, symbol, exchange });
// Update operation tracker with failure // Update operation tracker with failure
const eodSearchCode = `${symbol}.${exchange}`;
await this.operationRegistry.updateOperation('eod', eodSearchCode, 'price_update', { await this.operationRegistry.updateOperation('eod', eodSearchCode, 'price_update', {
status: 'failure', status: 'failure',
error: error.message error: error instanceof Error ? error.message : String(error)
}); });
throw error; throw error;

View file

@ -25,7 +25,7 @@ export async function scheduleFetchSymbols(
} }
logger.info(`Found ${exchanges.length} exchanges to process`, { logger.info(`Found ${exchanges.length} exchanges to process`, {
exchanges: exchanges.map(e => ({ code: e.Code, name: e.Name, country: e.Country })) exchanges: exchanges.map((e: any) => ({ code: e.Code, name: e.Name, country: e.Country }))
}); });
let jobsCreated = 0; let jobsCreated = 0;
@ -118,7 +118,7 @@ export async function fetchSymbols(
logger.debug(`Sample ${delisted ? 'delisted' : 'active'} symbols for ${exchangeCode}:`, { logger.debug(`Sample ${delisted ? 'delisted' : 'active'} symbols for ${exchangeCode}:`, {
count: symbols.length, count: symbols.length,
samples: symbols.slice(0, 5).map(s => ({ samples: symbols.slice(0, 5).map(s => ({
code: s.Code, symbol: s.Code,
name: s.Name, name: s.Name,
type: s.Type type: s.Type
})) }))
@ -128,7 +128,6 @@ export async function fetchSymbols(
// Add metadata to each symbol // Add metadata to each symbol
const symbolsWithMetadata = symbols.map(symbol => ({ const symbolsWithMetadata = symbols.map(symbol => ({
...symbol, ...symbol,
Exchange: symbol.Exchange || exchangeCode, // Keep the original exchange (might be wrong)
eodExchange: exchangeCode, // Store the correct exchange code used to fetch this symbol eodExchange: exchangeCode, // Store the correct exchange code used to fetch this symbol
eodSearchCode: `${symbol.Code}.${exchangeCode}`, // Create unique search code like AAPL.US eodSearchCode: `${symbol.Code}.${exchangeCode}`, // Create unique search code like AAPL.US
delisted: delisted, delisted: delisted,
@ -138,7 +137,7 @@ export async function fetchSymbols(
const result = await this.mongodb.batchUpsert( const result = await this.mongodb.batchUpsert(
'eodSymbols', 'eodSymbols',
symbolsWithMetadata, symbolsWithMetadata,
['Code', 'Exchange'] ['eodSearchCode']
); );
logger.info(`Successfully saved ${result.insertedCount} ${delisted ? 'delisted' : 'active'} symbols for ${exchangeCode}`); logger.info(`Successfully saved ${result.insertedCount} ${delisted ? 'delisted' : 'active'} symbols for ${exchangeCode}`);

View file

@ -110,7 +110,7 @@ export class EodHandler extends BaseHandler<DataIngestionServices> {
*/ */
@Operation('schedule-intraday-crawl') @Operation('schedule-intraday-crawl')
@ScheduledOperation('schedule-intraday-crawl', '0 3 * * *', { @ScheduledOperation('schedule-intraday-crawl', '0 3 * * *', {
immediately: true, // immediately: true,
}) })
@RateLimit(1) // 1 point for scheduling @RateLimit(1) // 1 point for scheduling
scheduleIntradayCrawl = scheduleIntradayCrawl; scheduleIntradayCrawl = scheduleIntradayCrawl;

View file

@ -1,3 +1,3 @@
export * from './config'; export * from './config';
export * from './utils';
export * from './operation-provider'; export * from './operation-provider';

View file

@ -1,21 +0,0 @@
/**
* Get the exchange suffix for EOD API calls based on country and exchange
* US symbols use :US suffix, except EUFUND and GBOND which always use their own codes
* Others use their actual exchange code
*/
export function getEodExchangeSuffix(exchange: string, country?: string): string {
// Special cases that always use their own exchange code
if (exchange === 'EUFUND' || exchange === 'GBOND') {
return exchange;
}
// US symbols use :US suffix
return country === 'USA' ? 'US' : exchange;
}
/**
* Build symbol.exchange format for EOD API
*/
export function buildEodSymbol(symbol: string, exchange: string, country?: string): string {
const suffix = getEodExchangeSuffix(exchange, country);
return `${symbol}.${suffix}`;
}

7
docs/serverinfo.md Normal file
View file

@ -0,0 +1,7 @@
12900k
P-cores: 48
E-cores: 39
Vcore: -0.045V adaptive
LLC: 4
SVID: Auto
XMPII 5600 with 5200 override