Files
Neon-Desk/lib/server/recent-developments.ts

162 lines
4.6 KiB
TypeScript

import { format } from 'date-fns';
import type { Filing, RecentDevelopmentItem, RecentDevelopments } from '@/lib/types';
export type RecentDevelopmentSourceContext = {
filings: Filing[];
now?: Date;
};
export type RecentDevelopmentSource = {
name: string;
fetch: (ticker: string, context: RecentDevelopmentSourceContext) => Promise<RecentDevelopmentItem[]>;
};
type CacheEntry<T> = {
expiresAt: number;
value: T;
};
const RECENT_DEVELOPMENTS_CACHE_TTL_MS = 1000 * 60 * 10;
const recentDevelopmentsCache = new Map<string, CacheEntry<RecentDevelopments>>();
function filingPriority(filing: Filing) {
switch (filing.filing_type) {
case '8-K':
return 0;
case '10-Q':
return 1;
case '10-K':
return 2;
default:
return 3;
}
}
function sortFilings(filings: Filing[]) {
return [...filings].sort((left, right) => {
const dateDelta = Date.parse(right.filing_date) - Date.parse(left.filing_date);
if (dateDelta !== 0) {
return dateDelta;
}
return filingPriority(left) - filingPriority(right);
});
}
function buildTitle(filing: Filing) {
switch (filing.filing_type) {
case '8-K':
return `${filing.company_name} filed an 8-K`;
case '10-K':
return `${filing.company_name} annual filing`;
case '10-Q':
return `${filing.company_name} quarterly filing`;
default:
return `${filing.company_name} filing update`;
}
}
function buildSummary(filing: Filing) {
const analysisSummary = filing.analysis?.text ?? filing.analysis?.legacyInsights ?? null;
if (analysisSummary) {
return analysisSummary;
}
const formattedDate = format(new Date(filing.filing_date), 'MMM dd, yyyy');
if (filing.filing_type === '8-K') {
return `The company disclosed a current report on ${formattedDate}. Review the filing for event-specific detail and attached exhibits.`;
}
return `The company published a ${filing.filing_type} on ${formattedDate}. Review the filing for the latest reported business and financial changes.`;
}
export const secFilingsDevelopmentSource: RecentDevelopmentSource = {
name: 'SEC filings',
async fetch(_ticker, context) {
const now = context.now ?? new Date();
const nowEpoch = now.getTime();
const recentFilings = sortFilings(context.filings)
.filter((filing) => {
const filedAt = Date.parse(filing.filing_date);
if (!Number.isFinite(filedAt)) {
return false;
}
const ageInDays = (nowEpoch - filedAt) / (1000 * 60 * 60 * 24);
if (ageInDays > 14) {
return false;
}
return filing.filing_type === '8-K' || filing.filing_type === '10-K' || filing.filing_type === '10-Q';
})
.slice(0, 8);
return recentFilings.map((filing) => ({
id: `${filing.ticker}-${filing.accession_number}`,
kind: filing.filing_type,
title: buildTitle(filing),
url: filing.filing_url,
source: 'SEC filings',
publishedAt: filing.filing_date,
summary: buildSummary(filing),
accessionNumber: filing.accession_number
}));
}
};
export const yahooDevelopmentSource: RecentDevelopmentSource | null = null;
export const investorRelationsRssSource: RecentDevelopmentSource | null = null;
export async function getRecentDevelopments(
ticker: string,
context: RecentDevelopmentSourceContext,
options?: {
sources?: RecentDevelopmentSource[];
limit?: number;
}
): Promise<RecentDevelopments> {
const normalizedTicker = ticker.trim().toUpperCase();
const limit = options?.limit ?? 6;
const cacheKey = `${normalizedTicker}:${context.filings.map((filing) => filing.accession_number).join(',')}`;
const cached = recentDevelopmentsCache.get(cacheKey);
if (cached && cached.expiresAt > Date.now()) {
return cached.value;
}
const sources = options?.sources ?? [secFilingsDevelopmentSource];
const itemCollections = await Promise.all(
sources.map(async (source) => {
try {
return await source.fetch(normalizedTicker, context);
} catch {
return [] satisfies RecentDevelopmentItem[];
}
})
);
const items = itemCollections
.flat()
.sort((left, right) => Date.parse(right.publishedAt) - Date.parse(left.publishedAt))
.slice(0, limit);
const result: RecentDevelopments = {
status: items.length > 0 ? 'ready' : 'unavailable',
items,
weeklySnapshot: null
};
recentDevelopmentsCache.set(cacheKey, {
value: result,
expiresAt: Date.now() + RECENT_DEVELOPMENTS_CACHE_TTL_MS
});
return result;
}
export const __recentDevelopmentsInternals = {
buildSummary,
buildTitle,
sortFilings
};