added initial price eod, still need to test
This commit is contained in:
parent
960edbaa47
commit
0b63690500
7 changed files with 198 additions and 7 deletions
|
|
@ -150,7 +150,7 @@
|
||||||
"name": "eod",
|
"name": "eod",
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"priority": 4,
|
"priority": 4,
|
||||||
"apiKey": "",
|
"apiKey": "657fe003583a32.85708911",
|
||||||
"baseUrl": "https://eodhistoricaldata.com/api",
|
"baseUrl": "https://eodhistoricaldata.com/api",
|
||||||
"tier": "free"
|
"tier": "free"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { BaseHandler } from '@stock-bot/handlers';
|
import type { BaseHandler } from '@stock-bot/handlers';
|
||||||
import type { DataIngestionServices } from '../../../types';
|
import type { DataIngestionServices } from '../../../types';
|
||||||
|
import { EOD_CONFIG } from '../shared';
|
||||||
|
|
||||||
export async function fetchExchanges(
|
export async function fetchExchanges(
|
||||||
this: BaseHandler<DataIngestionServices>
|
this: BaseHandler<DataIngestionServices>
|
||||||
|
|
@ -9,9 +10,15 @@ export async function fetchExchanges(
|
||||||
try {
|
try {
|
||||||
logger.info('Fetching EOD exchanges list');
|
logger.info('Fetching EOD exchanges list');
|
||||||
|
|
||||||
|
// Get API key from config
|
||||||
|
const apiKey = EOD_CONFIG.API_TOKEN;
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error('EOD API key not configured');
|
||||||
|
}
|
||||||
|
|
||||||
// Build URL with query parameters
|
// Build URL with query parameters
|
||||||
const url = new URL('https://eodhd.com/api/exchanges-list/');
|
const url = new URL('https://eodhd.com/api/exchanges-list/');
|
||||||
url.searchParams.append('api_token', '657fe003583a32.85708911');
|
url.searchParams.append('api_token', apiKey);
|
||||||
url.searchParams.append('fmt', 'json');
|
url.searchParams.append('fmt', 'json');
|
||||||
|
|
||||||
// Fetch exchanges from EOD API using Bun fetch
|
// Fetch exchanges from EOD API using Bun fetch
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export { fetchExchanges } from './fetch-exchanges';
|
export { fetchExchanges } from './exchanges';
|
||||||
export { fetchSymbols, scheduleFetchSymbols } from './fetch-symbols';
|
export { fetchSymbols, scheduleFetchSymbols } from './symbols';
|
||||||
|
export { fetchPrices, scheduleFetchPrices } from './prices';
|
||||||
|
|
|
||||||
152
apps/stock/data-ingestion/src/handlers/eod/actions/prices.ts
Normal file
152
apps/stock/data-ingestion/src/handlers/eod/actions/prices.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
import type { BaseHandler } from '@stock-bot/handlers';
|
||||||
|
import type { DataIngestionServices } from '../../../types';
|
||||||
|
import { EOD_CONFIG } from '../shared';
|
||||||
|
|
||||||
|
interface FetchPricesInput {
|
||||||
|
symbol: string;
|
||||||
|
exchange: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function scheduleFetchPrices(
|
||||||
|
this: BaseHandler<DataIngestionServices>
|
||||||
|
): Promise<{ success: boolean; jobsScheduled: number }> {
|
||||||
|
const logger = this.logger;
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info('Scheduling price fetch jobs for Canadian symbols');
|
||||||
|
|
||||||
|
// Calculate date one week ago
|
||||||
|
const oneWeekAgo = new Date();
|
||||||
|
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
||||||
|
|
||||||
|
// Get Canadian exchanges (TSX, TSV, CNQ, NEO)
|
||||||
|
const canadianExchanges = ['TO', 'V', 'CN', 'NEO'];
|
||||||
|
|
||||||
|
// Find symbols that haven't been updated in the last week
|
||||||
|
const symbols = await this.mongodb.collection('eodSymbols').find({
|
||||||
|
Exchange: { $in: canadianExchanges },
|
||||||
|
delisted: false,
|
||||||
|
$or: [
|
||||||
|
{ lastPriceUpdate: { $lt: oneWeekAgo } },
|
||||||
|
{ lastPriceUpdate: { $exists: false } }
|
||||||
|
]
|
||||||
|
}).toArray();
|
||||||
|
|
||||||
|
if (!symbols || symbols.length === 0) {
|
||||||
|
logger.info('No Canadian symbols need price updates');
|
||||||
|
return { success: true, jobsScheduled: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Found ${symbols.length} Canadian symbols needing price updates`);
|
||||||
|
|
||||||
|
let jobsScheduled = 0;
|
||||||
|
|
||||||
|
// Schedule jobs with staggered delays
|
||||||
|
for (let i = 0; i < symbols.length; i++) {
|
||||||
|
const symbol = symbols[i];
|
||||||
|
await this.scheduleOperation('fetch-prices', {
|
||||||
|
symbol: symbol.Code,
|
||||||
|
exchange: symbol.Exchange
|
||||||
|
}, {
|
||||||
|
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: BaseHandler<DataIngestionServices>,
|
||||||
|
input: FetchPricesInput
|
||||||
|
): Promise<{ success: boolean; priceCount: number }> {
|
||||||
|
const logger = this.logger;
|
||||||
|
const { symbol, exchange } = input;
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info('Fetching prices', { symbol, exchange });
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const url = new URL(`https://eodhd.com/api/eod/${symbol}.${exchange}`);
|
||||||
|
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}`);
|
||||||
|
|
||||||
|
// 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 the symbol's last price update timestamp
|
||||||
|
await this.mongodb.collection('eodSymbols').updateOne(
|
||||||
|
{ Code: symbol, Exchange: exchange },
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
lastPriceUpdate: new Date(),
|
||||||
|
lastPriceDate: priceData.length > 0 ? priceData[priceData.length - 1].date : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Successfully saved ${result.insertedCount} price records for ${symbol}.${exchange}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
priceCount: result.insertedCount
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to fetch or save prices', { error, symbol, exchange });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { BaseHandler } from '@stock-bot/handlers';
|
import type { BaseHandler } from '@stock-bot/handlers';
|
||||||
import type { DataIngestionServices } from '../../../types';
|
import type { DataIngestionServices } from '../../../types';
|
||||||
|
import { EOD_CONFIG } from '../shared';
|
||||||
|
|
||||||
interface FetchSymbolsInput {
|
interface FetchSymbolsInput {
|
||||||
exchangeCode: string;
|
exchangeCode: string;
|
||||||
|
|
@ -80,9 +81,15 @@ export async function fetchSymbols(
|
||||||
try {
|
try {
|
||||||
logger.info('Fetching symbols for exchange', { exchangeCode, delisted });
|
logger.info('Fetching symbols for exchange', { exchangeCode, delisted });
|
||||||
|
|
||||||
|
// Get API key from config
|
||||||
|
const apiKey = EOD_CONFIG.API_TOKEN;
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error('EOD API key not configured');
|
||||||
|
}
|
||||||
|
|
||||||
// Build URL with query parameters
|
// Build URL with query parameters
|
||||||
const url = new URL(`https://eodhd.com/api/exchange-symbol-list/${exchangeCode}`);
|
const url = new URL(`https://eodhd.com/api/exchange-symbol-list/${exchangeCode}`);
|
||||||
url.searchParams.append('api_token', '657fe003583a32.85708911');
|
url.searchParams.append('api_token', apiKey);
|
||||||
url.searchParams.append('fmt', 'json');
|
url.searchParams.append('fmt', 'json');
|
||||||
|
|
||||||
if (delisted) {
|
if (delisted) {
|
||||||
|
|
@ -6,7 +6,13 @@ import {
|
||||||
ScheduledOperation
|
ScheduledOperation
|
||||||
} from '@stock-bot/handlers';
|
} from '@stock-bot/handlers';
|
||||||
import type { DataIngestionServices } from '../../types';
|
import type { DataIngestionServices } from '../../types';
|
||||||
import { fetchExchanges, fetchSymbols, scheduleFetchSymbols } from './actions';
|
import {
|
||||||
|
fetchExchanges,
|
||||||
|
fetchSymbols,
|
||||||
|
scheduleFetchSymbols,
|
||||||
|
fetchPrices,
|
||||||
|
scheduleFetchPrices
|
||||||
|
} from './actions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EOD (End of Day) Handler demonstrating advanced rate limiting
|
* EOD (End of Day) Handler demonstrating advanced rate limiting
|
||||||
|
|
@ -49,6 +55,23 @@ export class EodHandler extends BaseHandler<DataIngestionServices> {
|
||||||
* Called by schedule-fetch-symbols for each exchange
|
* Called by schedule-fetch-symbols for each exchange
|
||||||
*/
|
*/
|
||||||
@Operation('fetch-symbols')
|
@Operation('fetch-symbols')
|
||||||
@RateLimit(1) // 10 points per exchange
|
@RateLimit(10) // 10 points per exchange
|
||||||
fetchSymbols = fetchSymbols;
|
fetchSymbols = fetchSymbols;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule price fetching for Canadian symbols not updated in last week
|
||||||
|
* Runs daily at 2 AM
|
||||||
|
*/
|
||||||
|
@Operation('schedule-fetch-prices')
|
||||||
|
@ScheduledOperation('schedule-fetch-prices', '0 2 * * *')
|
||||||
|
@RateLimit(1) // 1 point for scheduling
|
||||||
|
scheduleFetchPrices = scheduleFetchPrices;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch historical prices for a specific symbol
|
||||||
|
* Called by schedule-fetch-prices for each symbol
|
||||||
|
*/
|
||||||
|
@Operation('fetch-prices')
|
||||||
|
@RateLimit(10) // 10 points per price fetch
|
||||||
|
fetchPrices = fetchPrices;
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export const EOD_CONFIG = {
|
export const EOD_CONFIG = {
|
||||||
// API configuration
|
// API configuration
|
||||||
API_BASE_URL: 'https://eodhd.com/api/',
|
API_BASE_URL: 'https://eodhd.com/api/',
|
||||||
|
API_TOKEN: '657fe003583a32.85708911"',
|
||||||
};
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue