phase-1: optimize code
This commit is contained in:
@@ -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 };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user