Rebuild company overview analysis page
This commit is contained in:
161
lib/server/recent-developments.ts
Normal file
161
lib/server/recent-developments.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user