phase-9: domain-driven architecture complete

- 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
This commit is contained in:
Sai Kiran Vella
2026-06-06 13:21:24 -04:00
committed by saikiranvella
parent 83116baa3c
commit 96a752ecf7
88 changed files with 3576 additions and 3493 deletions
+82
View File
@@ -0,0 +1,82 @@
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();
});
}
}
+109
View File
@@ -0,0 +1,109 @@
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { MarketCallRepository } from '../../domains/shared';
import { CalendarService } from './CalendarService';
import { ScreenerEngine } from '../screener';
import type { SnapshotEntry } from '../../domains/shared';
import { callSchema } from '../../domains/shared/types/schemas';
export class CallsController {
constructor(
private readonly repo: MarketCallRepository,
private readonly engine: ScreenerEngine,
private readonly calendar: CalendarService,
) {}
private static toSnapshot(r: any): SnapshotEntry | null {
if (!r) return null;
const m = r.asset?.displayMetrics ?? r.asset?.getDisplayMetrics?.() ?? {};
return {
price: r.asset?.currentPrice ?? null,
signal: r.signal ?? null,
inflatedVerdict: r.inflated?.label ?? null,
fundamentalVerdict: r.fundamental?.label ?? null,
pe: m['P/E'] ?? null,
roe: m['ROE%'] ?? null,
fcf: m['FCF Yld%'] ?? null,
};
}
register(app: FastifyInstance): void {
app.get('/api/calls', this.list.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));
}
private async list() {
return { calls: this.repo.list() };
}
private async get(req: FastifyRequest, reply: FastifyReply) {
const call = this.repo.get((req.params as { id: string }).id);
if (!call) return reply.code(404).send({ error: 'Call not found' });
const current: Record<string, SnapshotEntry | null> = {};
if (call.tickers.length > 0) {
try {
const results = await this.engine.screenTickers(call.tickers);
for (const r of [...results.STOCK, ...results.ETF, ...results.BOND]) {
current[r.asset.ticker] = CallsController.toSnapshot(r);
}
} catch {
/* non-fatal */
}
}
return { ...call, current };
}
private async create(req: FastifyRequest, reply: FastifyReply) {
const { title, quarter, date, thesis, tickers } = req.body as {
title: string;
quarter: string;
date?: string;
thesis: string;
tickers: string[];
};
const upperTickers = tickers.map((t) => t.toUpperCase());
const snapshot: Record<string, SnapshotEntry | null> = {};
try {
const results = await this.engine.screenTickers(upperTickers);
for (const r of [...results.STOCK, ...results.ETF, ...results.BOND]) {
snapshot[r.asset.ticker] = CallsController.toSnapshot(r);
}
} catch (err) {
req.log.warn(`Could not snapshot prices for market call: ${(err as Error).message}`);
}
const call = this.repo.create({
title,
quarter,
date,
thesis,
tickers: upperTickers,
snapshot: snapshot as any,
});
return reply.code(201).send(call);
}
private async remove(req: FastifyRequest, reply: FastifyReply) {
const deleted = this.repo.delete((req.params as { id: string }).id);
if (!deleted) return reply.code(404).send({ error: 'Call not found' });
return { ok: true };
}
private async handleCalendar(req: FastifyRequest) {
let tickers: string[];
if ((req.query as any).tickers) {
tickers = String((req.query as any).tickers)
.split(',')
.map((t) => t.trim().toUpperCase())
.filter(Boolean);
} else {
tickers = [...new Set(this.repo.list().flatMap((c) => c.tickers))];
}
return this.calendar.getEvents(tickers);
}
}
+3
View File
@@ -0,0 +1,3 @@
// Calls domain — market call tracking and calendar
export { CallsController } from './calls.controller';
export { CalendarService } from './CalendarService';