162 lines
4.5 KiB
TypeScript
162 lines
4.5 KiB
TypeScript
import { format } from 'date-fns';
|
|
import type { Filing, RecentDevelopmentItem, RecentDevelopments } from '@/lib/types';
|
|
|
|
type RecentDevelopmentSourceContext = {
|
|
filings: Filing[];
|
|
now?: Date;
|
|
};
|
|
|
|
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
|
|
}));
|
|
}
|
|
};
|
|
|
|
const yahooDevelopmentSource: RecentDevelopmentSource | null = null;
|
|
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
|
|
};
|