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:
committed by
saikiranvella
parent
83116baa3c
commit
96a752ecf7
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Calls domain — market call tracking and calendar
|
||||
export { CallsController } from './calls.controller';
|
||||
export { CalendarService } from './CalendarService';
|
||||
Reference in New Issue
Block a user