From 6b09d320e582c42fa0bf6be2602ddaec121023bc Mon Sep 17 00:00:00 2001 From: Elie Habib Date: Thu, 26 Feb 2026 13:08:22 +0400 Subject: [PATCH] fix: sequential Yahoo calls, sector fallback, and missing try-catch guards - list-commodity-quotes: replace Promise.all with fetchYahooQuotesBatch to prevent Yahoo 429 - get-sector-summary: add Yahoo Finance fallback when FINNHUB_API_KEY is missing - list-etf-flows: sequential fetch loop + add missing try-catch around cachedFetchJson - get-macro-signals: replace unnecessary Promise.allSettled([single]) with direct await --- .../economic/v1/get-macro-signals.ts | 20 ++++++------ .../market/v1/get-sector-summary.ts | 31 ++++++++++--------- .../market/v1/list-commodity-quotes.ts | 26 ++++++---------- .../worldmonitor/market/v1/list-etf-flows.ts | 26 +++++++++++----- 4 files changed, 54 insertions(+), 49 deletions(-) diff --git a/server/worldmonitor/economic/v1/get-macro-signals.ts b/server/worldmonitor/economic/v1/get-macro-signals.ts index 91669033b..1a258a56f 100644 --- a/server/worldmonitor/economic/v1/get-macro-signals.ts +++ b/server/worldmonitor/economic/v1/get-macro-signals.ts @@ -51,22 +51,22 @@ function buildFallbackResult(): GetMacroSignalsResponse { async function computeMacroSignals(): Promise { const yahooBase = 'https://query1.finance.yahoo.com/v8/finance/chart'; - // Yahoo calls go through global yahooGate() in fetchJSON - const jpyChart = await Promise.allSettled([fetchJSON(`${yahooBase}/JPY=X?range=1y&interval=1d`)]).then(r => r[0]!); - const btcChart = await Promise.allSettled([fetchJSON(`${yahooBase}/BTC-USD?range=1y&interval=1d`)]).then(r => r[0]!); - const qqqChart = await Promise.allSettled([fetchJSON(`${yahooBase}/QQQ?range=1y&interval=1d`)]).then(r => r[0]!); - const xlpChart = await Promise.allSettled([fetchJSON(`${yahooBase}/XLP?range=1y&interval=1d`)]).then(r => r[0]!); + // Yahoo calls go through global yahooGate() in fetchJSON — sequential to avoid 429 + const jpyChart = await fetchJSON(`${yahooBase}/JPY=X?range=1y&interval=1d`).catch(() => null); + const btcChart = await fetchJSON(`${yahooBase}/BTC-USD?range=1y&interval=1d`).catch(() => null); + const qqqChart = await fetchJSON(`${yahooBase}/QQQ?range=1y&interval=1d`).catch(() => null); + const xlpChart = await fetchJSON(`${yahooBase}/XLP?range=1y&interval=1d`).catch(() => null); // Non-Yahoo calls can go in parallel const [fearGreed, mempoolHash] = await Promise.allSettled([ fetchJSON('https://api.alternative.me/fng/?limit=30&format=json'), fetchJSON('https://mempool.space/api/v1/mining/hashrate/1m'), ]); - const jpyPrices = jpyChart.status === 'fulfilled' ? extractClosePrices(jpyChart.value) : []; - const btcPrices = btcChart.status === 'fulfilled' ? extractClosePrices(btcChart.value) : []; - const btcAligned = btcChart.status === 'fulfilled' ? extractAlignedPriceVolume(btcChart.value) : []; - const qqqPrices = qqqChart.status === 'fulfilled' ? extractClosePrices(qqqChart.value) : []; - const xlpPrices = xlpChart.status === 'fulfilled' ? extractClosePrices(xlpChart.value) : []; + const jpyPrices = jpyChart ? extractClosePrices(jpyChart) : []; + const btcPrices = btcChart ? extractClosePrices(btcChart) : []; + const btcAligned = btcChart ? extractAlignedPriceVolume(btcChart) : []; + const qqqPrices = qqqChart ? extractClosePrices(qqqChart) : []; + const xlpPrices = xlpChart ? extractClosePrices(xlpChart) : []; // 1. Liquidity Signal (JPY 30d ROC) const jpyRoc30 = rateOfChange(jpyPrices, 30); diff --git a/server/worldmonitor/market/v1/get-sector-summary.ts b/server/worldmonitor/market/v1/get-sector-summary.ts index 57ad9de0f..a657c1beb 100644 --- a/server/worldmonitor/market/v1/get-sector-summary.ts +++ b/server/worldmonitor/market/v1/get-sector-summary.ts @@ -11,7 +11,7 @@ import type { GetSectorSummaryResponse, SectorPerformance, } from '../../../../src/generated/server/worldmonitor/market/v1/service_server'; -import { fetchFinnhubQuote } from './_shared'; +import { fetchFinnhubQuote, fetchYahooQuotesBatch } from './_shared'; import { cachedFetchJson } from '../../../_shared/redis'; const REDIS_CACHE_KEY = 'market:sectors:v1'; @@ -22,24 +22,27 @@ export async function getSectorSummary( _req: GetSectorSummaryRequest, ): Promise { const apiKey = process.env.FINNHUB_API_KEY; - if (!apiKey) return { sectors: [] }; try { const result = await cachedFetchJson(REDIS_CACHE_KEY, REDIS_CACHE_TTL, async () => { - // Sector ETF symbols const sectorSymbols = ['XLK', 'XLF', 'XLE', 'XLV', 'XLY', 'XLI', 'XLP', 'XLU', 'XLB', 'XLRE', 'XLC', 'SMH']; - const results = await Promise.all( - sectorSymbols.map((s) => fetchFinnhubQuote(s, apiKey)), - ); - const sectors: SectorPerformance[] = []; - for (const r of results) { - if (r) { - sectors.push({ - symbol: r.symbol, - name: r.symbol, - change: r.changePercent, - }); + + if (apiKey) { + const results = await Promise.all( + sectorSymbols.map((s) => fetchFinnhubQuote(s, apiKey)), + ); + for (const r of results) { + if (r) sectors.push({ symbol: r.symbol, name: r.symbol, change: r.changePercent }); + } + } + + // Fallback to Yahoo Finance when Finnhub key is missing or returned nothing + if (sectors.length === 0) { + const batch = await fetchYahooQuotesBatch(sectorSymbols); + for (const s of sectorSymbols) { + const yahoo = batch.get(s); + if (yahoo) sectors.push({ symbol: s, name: s, change: yahoo.change }); } } diff --git a/server/worldmonitor/market/v1/list-commodity-quotes.ts b/server/worldmonitor/market/v1/list-commodity-quotes.ts index 610c5c44f..12eb0c180 100644 --- a/server/worldmonitor/market/v1/list-commodity-quotes.ts +++ b/server/worldmonitor/market/v1/list-commodity-quotes.ts @@ -9,7 +9,7 @@ import type { ListCommodityQuotesResponse, CommodityQuote, } from '../../../../src/generated/server/worldmonitor/market/v1/service_server'; -import { fetchYahooQuote } from './_shared'; +import { fetchYahooQuotesBatch } from './_shared'; import { cachedFetchJson } from '../../../_shared/redis'; const REDIS_CACHE_KEY = 'market:commodities:v1'; @@ -30,22 +30,14 @@ export async function listCommodityQuotes( try { const result = await cachedFetchJson(redisKey, REDIS_CACHE_TTL, async () => { - const results = await Promise.all( - symbols.map(async (s) => { - const yahoo = await fetchYahooQuote(s); - if (!yahoo) return null; - return { - symbol: s, - name: s, - display: s, - price: yahoo.price, - change: yahoo.change, - sparkline: yahoo.sparkline, - } satisfies CommodityQuote; - }), - ); - - const quotes = results.filter((r): r is CommodityQuote => r !== null); + const batch = await fetchYahooQuotesBatch(symbols); + const quotes: CommodityQuote[] = []; + for (const s of symbols) { + const yahoo = batch.get(s); + if (yahoo) { + quotes.push({ symbol: s, name: s, display: s, price: yahoo.price, change: yahoo.change, sparkline: yahoo.sparkline }); + } + } return quotes.length > 0 ? { quotes } : null; }); diff --git a/server/worldmonitor/market/v1/list-etf-flows.ts b/server/worldmonitor/market/v1/list-etf-flows.ts index 742064027..4ab02a717 100644 --- a/server/worldmonitor/market/v1/list-etf-flows.ts +++ b/server/worldmonitor/market/v1/list-etf-flows.ts @@ -114,17 +114,13 @@ export async function listEtfFlows( return etfCache; } + try { const result = await cachedFetchJson(REDIS_CACHE_KEY, REDIS_CACHE_TTL, async () => { - const charts = await Promise.allSettled( - ETF_LIST.map((etf) => fetchEtfChart(etf.ticker)), - ); - const etfs: EtfFlow[] = []; - for (let i = 0; i < ETF_LIST.length; i++) { - const settled = charts[i]!; - const chart = settled.status === 'fulfilled' ? settled.value : null; + for (const etf of ETF_LIST) { + const chart = await fetchEtfChart(etf.ticker); if (chart) { - const parsed = parseEtfChartData(chart, ETF_LIST[i]!.ticker, ETF_LIST[i]!.issuer); + const parsed = parseEtfChartData(chart, etf.ticker, etf.issuer); if (parsed) etfs.push(parsed); } } @@ -174,4 +170,18 @@ export async function listEtfFlows( }, etfs: [], }; + } catch { + return etfCache || { + timestamp: new Date().toISOString(), + summary: { + etfCount: 0, + totalVolume: 0, + totalEstFlow: 0, + netDirection: 'UNAVAILABLE', + inflowCount: 0, + outflowCount: 0, + }, + etfs: [], + }; + } }