2e7860637e
- 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
110 lines
3.6 KiB
TypeScript
110 lines
3.6 KiB
TypeScript
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);
|
|
}
|
|
}
|