import { sanitizeListingFinancialHistory } from "../listing-history"; import { withdrawKnownProviderStatements } from "../../utils/statement-observations"; import { selectCachedResource } from "./cache"; import { financialHistoryVariants, hasReusableExtendedHistory } from "./statement-history"; import type { MarketDataRequestContext } from "../../types/data-provider"; import type { Quote, TickerFinancials } from "../../types/financials"; import { normalizeTickerFinancialsPriceHistory } from "../../utils/price-history"; import { resolveTickerFinancialsQuoteState } from "../../market-data/quotes/resolution"; import { shouldLogProviderError } from "../provider-errors"; import { sanitizeShellFinancialHistory } from "../history-coverage"; import { quoteMetadataFromQuote } from "../../market-data/quotes/metadata"; import { dropUnusableProviderQuote, hasDetailedStatementRows, hasDeepStatementHistory, hasStatementRows, needsFinancialProfile, profileForSameListing, isProviderQuoteUsableForCurrentSession, providerFinancialsMatchTarget, mergeMissingStatementArrays, mergeFinancials, } from "./financials"; import type { ProviderRouterCoreDeps, SourceResult } from "./route-types"; export class ProviderRouterPrimaryRoutes { constructor(private readonly options: ProviderRouterCoreDeps) {} async fetchBrokerFinancials( ticker: string, exchange?: string, context?: MarketDataRequestContext, ): Promise | null> { const entityKey = this.options.getEntityKey(ticker, context?.instrument); const variantKey = this.options.getTickerVariantCandidates(exchange)[0] ?? ""; for (const candidate of this.options.getBrokerCandidatesForContext(context, false)) { if (!candidate.broker.getTickerFinancials) continue; try { const rawResult = await candidate.broker.getTickerFinancials( ticker, candidate.instance, exchange, context?.instrument ?? null, ); const result = resolveTickerFinancialsQuoteState(normalizeTickerFinancialsPriceHistory(rawResult)); if (!result) continue; this.options.cacheResource( "financials", entityKey, variantKey, this.options.brokerSourceKey(candidate), result, this.options.resolveBrokerPolicy("financials", candidate.broker), ); return { sourceKey: this.options.brokerSourceKey(candidate), value: result }; } catch { // continue } } return null; } async fetchProviderFinancials( ticker: string, exchange?: string, context?: MarketDataRequestContext, ): Promise | null> { const entityKey = this.options.getEntityKey(ticker, context?.instrument); const variantKey = financialHistoryVariants(this.options.getTickerVariantCandidates(exchange), context)[0] ?? ""; let primaryResult: SourceResult | null = null; let primaryIdentity: TickerFinancials | null = null; for (const provider of this.options.providersInPriorityOrder()) { try { const rawValue = await provider.getTickerFinancials(ticker, exchange, context); if (rawValue && !context?.instrument && !providerFinancialsMatchTarget(rawValue, ticker, exchange)) continue; const resolvedValue = resolveTickerFinancialsQuoteState(normalizeTickerFinancialsPriceHistory(rawValue)); if (resolvedValue && !context?.instrument && !providerFinancialsMatchTarget(resolvedValue, ticker, exchange)) continue; let value = resolvedValue ? dropUnusableProviderQuote(resolvedValue, exchange) : null; if (!value) continue; const sourceKey = this.options.providerSourceKey(provider); if (context?.statementHistory === "extended" && !hasReusableExtendedHistory(value)) { const previous = selectCachedResource(this.options.resources, "financials", entityKey, [variantKey], [sourceKey], true); const attempt = value.statementHistory; value = { ...mergeFinancials(value, previous?.value ?? null)!, statementHistory: attempt }; } value = sanitizeShellFinancialHistory(value, { symbol: ticker, exchange }, sourceKey); value = sanitizeListingFinancialHistory(value, { symbol: ticker, exchange }, sourceKey); value = withdrawKnownProviderStatements(value, { symbol: ticker, exchange }, sourceKey); // Validate the raw identities before quote normalization can choose one // of conflicting quote/metadata contributions, including contract routes. const fallbackProfile = primaryIdentity && rawValue ? profileForSameListing({ ...rawValue, profile: value.profile }, primaryIdentity, { symbol: ticker, exchange }) : undefined; const cacheValue = primaryResult ? { profile: fallbackProfile, quoteMetadata: value.quoteMetadata ?? (value.quote ? quoteMetadataFromQuote(value.quote) : undefined), financialCurrency: value.financialCurrency, statementHistory: value.statementHistory, operatingHistoryRetryAt: value.operatingHistoryRetryAt, earningsHistoryRetryAt: value.earningsHistoryRetryAt, annualStatements: value.annualStatements, quarterlyStatements: value.quarterlyStatements, priceHistory: [], } : value; this.options.cacheResource( "financials", entityKey, variantKey, sourceKey, cacheValue, context?.statementHistory === "extended" && !hasReusableExtendedHistory(value) ? { staleMs: 60_000, expireMs: 60_000 } : this.options.resolveProviderPolicy("financials", provider), ); if (!primaryResult) { primaryResult = { sourceKey, value }; primaryIdentity = rawValue; if (!needsFinancialProfile(value) && (context?.statementHistory === "extended" ? value.statementHistory?.status === "available" : hasDetailedStatementRows(value) && hasDeepStatementHistory(value))) return primaryResult; continue; } if (!primaryResult.value.quote && value.quote) { primaryResult = { sourceKey: primaryResult.sourceKey, value: mergeFinancials(primaryResult.value, { annualStatements: [], quarterlyStatements: [], priceHistory: [], quote: value.quote, }) ?? primaryResult.value, }; } if (hasStatementRows(value) || fallbackProfile) { primaryResult = { sourceKey: primaryResult.sourceKey, value: mergeMissingStatementArrays(primaryResult.value, { ...value, profile: fallbackProfile }), }; if (!needsFinancialProfile(primaryResult.value) && (context?.statementHistory === "extended" ? primaryResult.value.statementHistory?.status === "available" : hasDetailedStatementRows(primaryResult.value) && hasDeepStatementHistory(primaryResult.value))) return primaryResult; } } catch (error) { if (shouldLogProviderError(error)) { this.options.logProviderError(`${provider.id} failed: ${error}`); } } } if (needsFinancialProfile(primaryResult?.value)) { this.options.cacheResource("financial-profile-attempt", entityKey, variantKey, "router", { sourceKeys: this.options.providersInPriorityOrder().map(provider => this.options.providerSourceKey(provider)) }, { staleMs: 60_000, expireMs: 60_000 }); } return primaryResult; } async fetchBrokerQuote( ticker: string, exchange?: string, context?: MarketDataRequestContext, ): Promise | null> { const entityKey = this.options.getEntityKey(ticker, context?.instrument); const variantKey = this.options.getTickerVariantCandidates(exchange)[0] ?? ""; for (const candidate of this.options.getBrokerCandidatesForContext(context, false)) { if (!candidate.broker.getQuote) continue; try { const result = await candidate.broker.getQuote( ticker, candidate.instance, exchange, context?.instrument ?? null, ); this.options.cacheResource( "quote", entityKey, variantKey, this.options.brokerSourceKey(candidate), result, this.options.resolveBrokerPolicy("brokerQuote", candidate.broker), ); return { sourceKey: this.options.brokerSourceKey(candidate), value: result }; } catch { // continue } } return null; } async fetchProviderQuote( ticker: string, exchange?: string, context?: MarketDataRequestContext, ): Promise | null> { const entityKey = this.options.getEntityKey(ticker, context?.instrument); const variantKey = this.options.getTickerVariantCandidates(exchange)[0] ?? ""; for (const provider of this.options.providersInPriorityOrder()) { try { const quote = await provider.getQuote(ticker, exchange, context); if (!isProviderQuoteUsableForCurrentSession(quote, exchange, ticker)) continue; const sourceKey = this.options.providerSourceKey(provider); this.options.cacheResource( "quote", entityKey, variantKey, sourceKey, quote, this.options.resolveProviderPolicy("quote", provider), ); return { sourceKey, value: quote }; } catch (error) { if (shouldLogProviderError(error)) { this.options.logProviderError(`${provider.id} failed: ${error}`); } } } return null; } }