phase-1: optimize code

This commit is contained in:
Kazuma
2026-06-04 01:36:28 -04:00
committed by Kazuma
parent 19fc052d14
commit b75e8bda72
89 changed files with 11189 additions and 845 deletions
+187
View File
@@ -0,0 +1,187 @@
import { MarketCallStore } from '../../calls/MarketCallStore.js';
import { ScreenerEngine } from '../../screener/ScreenerEngine.js';
import { YahooClient } from '../../market/YahooClient.js';
import { chunkArray } from '../../screener/Chunker.js';
const noopLogger = { write: () => {}, log: () => {}, warn: () => {} };
const store = new MarketCallStore();
// Takes a screener result entry and flattens it to a snapshot record
const toSnapshot = (r) => {
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,
};
};
export default async function callsRoutes(app) {
// GET /api/calls — list all market calls (newest first)
app.get('/api/calls', async () => {
return { calls: store.list() };
});
// GET /api/calls/:id — get one call + enrich with current prices for comparison
app.get('/api/calls/:id', async (req, reply) => {
const call = store.get(req.params.id);
if (!call) return reply.code(404).send({ error: 'Call not found' });
// Re-screen the tickers to get current prices for comparison
let current = {};
if (call.tickers.length > 0) {
try {
const engine = new ScreenerEngine({ logger: noopLogger });
const results = await engine.screenTickers(call.tickers);
const all = [...results.STOCK, ...results.ETF, ...results.BOND];
for (const r of all) {
current[r.asset.ticker] = toSnapshot(r);
}
} catch {
// Non-fatal — return call without current prices
}
}
return { ...call, current };
});
// POST /api/calls — create a new market call and snapshot current prices
app.post('/api/calls', {
schema: {
body: {
type: 'object',
required: ['title', 'quarter', 'thesis', 'tickers'],
properties: {
title: { type: 'string', minLength: 3 },
quarter: { type: 'string', minLength: 2 },
date: { type: 'string' },
thesis: { type: 'string', minLength: 10 },
tickers: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 30 },
},
},
},
handler: async (req, reply) => {
const { title, quarter, date, thesis, tickers } = req.body;
const upperTickers = tickers.map((t) => t.toUpperCase());
// Snapshot current screener data for each ticker
let snapshot = {};
try {
const engine = new ScreenerEngine({ logger: noopLogger });
const results = await engine.screenTickers(upperTickers);
const all = [...results.STOCK, ...results.ETF, ...results.BOND];
for (const r of all) {
snapshot[r.asset.ticker] = toSnapshot(r);
}
} catch (err) {
app.log.warn('Could not snapshot prices for market call:', err.message);
}
const call = store.create({ title, quarter, date, thesis, tickers: upperTickers, snapshot });
return reply.code(201).send(call);
},
});
// DELETE /api/calls/:id
app.delete('/api/calls/:id', async (req, reply) => {
const deleted = store.delete(req.params.id);
if (!deleted) return reply.code(404).send({ error: 'Call not found' });
return { ok: true };
});
// GET /api/calls/calendar?tickers=AAPL,MSFT (or omit to use all call tickers)
// Returns upcoming earnings dates, ex-dividend dates and dividend dates per ticker.
// Fetched in parallel batches of 5 with rate-limit delay.
app.get('/api/calls/calendar', async (req) => {
const client = new YahooClient();
// Resolve tickers: from query param, or aggregate all unique tickers across all calls
let tickers;
if (req.query.tickers) {
tickers = req.query.tickers
.split(',')
.map((t) => t.trim().toUpperCase())
.filter(Boolean);
} else {
const allCalls = store.list();
const set = new Set(allCalls.flatMap((c) => c.tickers));
tickers = [...set];
}
if (tickers.length === 0) return { events: [] };
// Fetch calendarEvents in parallel batches
const results = {};
for (const batch of chunkArray(tickers, 5)) {
await Promise.all(
batch.map(async (ticker) => {
const cal = await client.fetchCalendarEvents(ticker);
if (cal) results[ticker] = cal;
}),
);
await new Promise((r) => setTimeout(r, 500));
}
// Flatten into a sorted event list
const events = [];
const now = Date.now();
for (const [ticker, cal] of Object.entries(results)) {
// Upcoming earnings dates
for (const dateVal of cal.earnings?.earningsDate ?? []) {
const d = new Date(dateVal);
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,
});
}
// Ex-dividend date
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,
});
}
// Dividend payment date
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,
});
}
}
// Sort: upcoming first, then past
events.sort((a, b) => {
if (a.isPast !== b.isPast) return a.isPast ? 1 : -1;
return a.isPast
? new Date(b.date) - new Date(a.date) // most recent past first
: new Date(a.date) - new Date(b.date); // soonest upcoming first
});
return { events, tickers };
});
}