0dac8128bd
- Restructured server layer with 5 domains: shared, screener, portfolio, calls, finance - Migrated 58 TypeScript files to domain-driven structure - Updated CLAUDE.md with new architecture documentation - Added .gitignore rules for .md files (except CLAUDE.md) - Removed unused CatalystAnalyst import from app.ts - Fixed lint errors: removed unused imports, fixed regex escape, added console suppressions - Verified no sensitive data in git history - Server code compiles cleanly with TypeScript strict mode
83 lines
2.5 KiB
TypeScript
83 lines
2.5 KiB
TypeScript
import { YahooFinanceClient, chunkArray } from '../../domains/shared';
|
|
import type { CalendarEvent } from '../../domains/shared';
|
|
|
|
export class CalendarService {
|
|
constructor(private readonly yahoo: YahooFinanceClient) {}
|
|
|
|
async getEvents(tickers: string[]): Promise<{ events: CalendarEvent[]; tickers: string[] }> {
|
|
if (tickers.length === 0) return { events: [], tickers: [] };
|
|
|
|
const raw: Record<string, any> = {};
|
|
for (const batch of chunkArray(tickers, 5)) {
|
|
await Promise.all(
|
|
batch.map(async (ticker) => {
|
|
const cal = await this.yahoo.fetchCalendarEvents(ticker);
|
|
if (cal) raw[ticker] = cal;
|
|
}),
|
|
);
|
|
await new Promise<void>((r) => setTimeout(r, 500));
|
|
}
|
|
|
|
const now = Date.now();
|
|
const events = CalendarService.buildEvents(raw, now);
|
|
CalendarService.sortEvents(events);
|
|
|
|
return { events, tickers };
|
|
}
|
|
|
|
private static buildEvents(raw: Record<string, any>, now: number): CalendarEvent[] {
|
|
const events: CalendarEvent[] = [];
|
|
|
|
for (const [ticker, cal] of Object.entries(raw)) {
|
|
for (const dateVal of cal.earnings?.earningsDate ?? []) {
|
|
const d = new Date(dateVal as string);
|
|
events.push({
|
|
ticker,
|
|
type: 'earnings',
|
|
date: d.toISOString().slice(0, 10),
|
|
label: 'Earnings',
|
|
detail: cal.earnings.isEarningsDateEstimate ? 'Estimated' : 'Confirmed',
|
|
epsEstimate: cal.earnings.earningsAverage ?? null,
|
|
revEstimate: cal.earnings.revenueAverage ?? null,
|
|
isPast: d.getTime() < now,
|
|
});
|
|
}
|
|
|
|
if (cal.exDividendDate) {
|
|
const d = new Date(cal.exDividendDate);
|
|
events.push({
|
|
ticker,
|
|
type: 'exdividend',
|
|
date: d.toISOString().slice(0, 10),
|
|
label: 'Ex-Dividend',
|
|
detail: null,
|
|
isPast: d.getTime() < now,
|
|
});
|
|
}
|
|
|
|
if (cal.dividendDate) {
|
|
const d = new Date(cal.dividendDate);
|
|
events.push({
|
|
ticker,
|
|
type: 'dividend',
|
|
date: d.toISOString().slice(0, 10),
|
|
label: 'Dividend',
|
|
detail: null,
|
|
isPast: d.getTime() < now,
|
|
});
|
|
}
|
|
}
|
|
|
|
return events;
|
|
}
|
|
|
|
private static sortEvents(events: CalendarEvent[]): void {
|
|
events.sort((a, b) => {
|
|
if (a.isPast !== b.isPast) return a.isPast ? 1 : -1;
|
|
return a.isPast
|
|
? new Date(b.date).getTime() - new Date(a.date).getTime()
|
|
: new Date(a.date).getTime() - new Date(b.date).getTime();
|
|
});
|
|
}
|
|
}
|