import { YahooFinanceClient } from '../clients/YahooFinanceClient'; import type { Logger, CatalystResult, Story, YahooNewsItem } from '../types'; export class CatalystAnalyst { private static readonly NEWS_QUERIES = [ 'stock market today', 'earnings report today', 'market news catalyst', 'federal reserve interest rates', 'stock upgrade downgrade analyst', ]; private static readonly MAX_STORIES = 20; private static readonly TICKER_REGEX = /^[A-Z]{1,6}$/; private client: YahooFinanceClient; private logger: Pick; constructor({ logger }: { logger?: Pick } = {}) { this.client = new YahooFinanceClient(); this.logger = logger ?? { write: (msg: string) => process.stdout.write(msg) }; } async run(): Promise { this.logger.write('🔍 Fetching market news...'); const rawStories = await this.fetchNews(); if (!rawStories.length) { this.logger.write(' ⚠ all news queries failed — check network or Yahoo rate limit\n'); return { tickers: [], tickerFrequency: {}, stories: [] }; } const stories = rawStories.map((s) => ({ title: s.title, link: s.link ?? '', source: s.publisher ?? 'unknown', tickers: (s.relatedTickers ?? []) .map((t) => t.split(':')[0].toUpperCase()) .filter((t) => CatalystAnalyst.TICKER_REGEX.test(t)), })); const { tickers, tickerFrequency } = CatalystAnalyst.rankTickers(stories); this.logger.write(` ${stories.length} stories, ${tickers.length} tickers\n`); return { tickers, tickerFrequency, stories }; } // Search by specific ticker for the /api/analyze endpoint. async fetchStoriesForTickers(tickers: string[]): Promise { const seen = new Map(); await Promise.all( tickers.slice(0, 10).map(async (ticker) => { try { const news = await this.client.search(ticker, { newsCount: 3, quotesCount: 0 }); for (const item of news) { if (!seen.has(item.title)) seen.set(item.title, item); } } catch { /* skip tickers Yahoo can't resolve */ } }), ); return [...seen.values()].slice(0, 15).map((s) => ({ title: s.title, link: s.link ?? '', source: s.publisher ?? 'unknown', tickers: (s.relatedTickers ?? []) .map((t) => t.split(':')[0].toUpperCase()) .filter((t) => CatalystAnalyst.TICKER_REGEX.test(t)), })); } private async fetchNews(): Promise { const seen = new Map(); let successCount = 0; for (const query of CatalystAnalyst.NEWS_QUERIES) { try { const news = await this.client.search(query, { newsCount: 8, quotesCount: 0 }); successCount++; for (const s of news) { if (!seen.has(s.title)) { seen.set(s.title, { title: s.title, publisher: s.publisher, link: s.link, relatedTickers: s.relatedTickers ?? [], }); } } } catch { /* skip failed query — tracked via successCount */ } } if (successCount === 0) return []; return [...seen.values()].slice(0, CatalystAnalyst.MAX_STORIES); } static rankTickers(stories: Story[]): { tickers: string[]; tickerFrequency: Record; } { const freq: Record = {}; for (const { tickers } of stories) { for (const t of tickers) { freq[t] = (freq[t] ?? 0) + 1; } } const tickers = Object.keys(freq).sort((a, b) => freq[b] - freq[a]); return { tickers, tickerFrequency: freq }; } }