phase-8g: add sqllite.

This commit is contained in:
Kazuma
2026-06-05 23:34:25 -04:00
committed by Kazuma
parent ca449b4300
commit 09f2444157
20 changed files with 2514 additions and 239 deletions
+6 -70
View File
@@ -1,16 +1,14 @@
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { YahooFinanceClient } from '../clients/YahooFinanceClient';
import { MarketCallRepository } from '../repositories/MarketCallRepository';
import { ScreenerEngine } from '../services/index';
import { CalendarService, ScreenerEngine } from '../services/index';
import type { SnapshotEntry } from '../types';
import { callSchema } from '../types/schemas';
import { chunkArray } from '../utils/Chunker';
export class CallsController {
constructor(
private readonly repo: MarketCallRepository,
private readonly engine: ScreenerEngine,
private readonly yahoo: YahooFinanceClient,
private readonly calendar: CalendarService,
) {}
private static toSnapshot(r: any): SnapshotEntry | null {
@@ -29,7 +27,7 @@ export class CallsController {
register(app: FastifyInstance): void {
app.get('/api/calls', this.list.bind(this));
app.get('/api/calls/calendar', this.calendar.bind(this));
app.get('/api/calls/calendar', this.handleCalendar.bind(this));
app.get('/api/calls/:id', this.get.bind(this));
app.post('/api/calls', { schema: callSchema }, this.create.bind(this));
app.delete('/api/calls/:id', this.remove.bind(this));
@@ -94,7 +92,7 @@ export class CallsController {
return { ok: true };
}
private async calendar(req: FastifyRequest) {
private async handleCalendar(req: FastifyRequest) {
let tickers: string[];
if ((req.query as any).tickers) {
tickers = String((req.query as any).tickers)
@@ -102,71 +100,9 @@ export class CallsController {
.map((t) => t.trim().toUpperCase())
.filter(Boolean);
} else {
const set = new Set(this.repo.list().flatMap((c) => c.tickers));
tickers = [...set];
tickers = [...new Set(this.repo.list().flatMap((c) => c.tickers))];
}
if (tickers.length === 0) return { events: [] };
const results: 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) results[ticker] = cal;
}),
);
await new Promise<void>((r) => setTimeout(r, 500));
}
const events: any[] = [];
const now = Date.now();
for (const [ticker, cal] of Object.entries(results)) {
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,
});
}
}
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();
});
return { events, tickers };
return this.calendar.getEvents(tickers);
}
}