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: [], + }; + } } diff --git a/server/worldmonitor/news/v1/_shared.ts b/server/worldmonitor/news/v1/_shared.ts index 02da69557..6ed4ae1d0 100644 --- a/server/worldmonitor/news/v1/_shared.ts +++ b/server/worldmonitor/news/v1/_shared.ts @@ -68,7 +68,7 @@ export function buildArticlePrompts( if (isTechVariant) { systemPrompt = `${dateContext} -Summarize the single most important tech/startup headline in 2-3 sentences. +Summarize the single most important tech/startup headline in 2 concise sentences MAX (under 60 words total). Rules: - Each numbered headline below is a SEPARATE, UNRELATED story - Pick the ONE most significant headline and summarize ONLY that story @@ -76,11 +76,11 @@ Rules: - Focus ONLY on technology, startups, AI, funding, product launches, or developer news - IGNORE political news, trade policy, tariffs, government actions unless directly about tech regulation - Lead with the company/product/technology name -- No bullet points, no meta-commentary${langInstruction}`; +- No bullet points, no meta-commentary, no elaboration beyond the core facts${langInstruction}`; } else { systemPrompt = `${dateContext} -Summarize the single most important headline in 2-3 sentences. +Summarize the single most important headline in 2 concise sentences MAX (under 60 words total). Rules: - Each numbered headline below is a SEPARATE, UNRELATED story - Pick the ONE most significant headline and summarize ONLY that story @@ -89,35 +89,33 @@ Rules: - NEVER start with "Breaking news", "Good evening", "Tonight", or TV-style openings - Start directly with the subject of the chosen headline - If intelligence context is provided, use it only if it relates to your chosen headline -- No bullet points, no meta-commentary${langInstruction}`; +- No bullet points, no meta-commentary, no elaboration beyond the core facts${langInstruction}`; } userPrompt = `Each headline below is a separate story. Pick the most important ONE and summarize only that story:\n${headlineText}${intelSection}`; } else if (opts.mode === 'analysis') { if (isTechVariant) { systemPrompt = `${dateContext} -Analyze the most significant tech/startup development in 2-3 sentences. +Analyze the most significant tech/startup development in 2 concise sentences MAX (under 60 words total). Rules: - Each numbered headline below is a SEPARATE, UNRELATED story - Pick the ONE most significant story and analyze ONLY that - NEVER combine facts from different headlines - Focus ONLY on technology implications: funding trends, AI developments, market shifts, product strategy - IGNORE political implications, trade wars, government unless directly about tech policy -- Lead with the insight for tech industry -- Connect to startup ecosystem, VC trends, or technical implications`; +- Lead with the insight, no filler or elaboration`; } else { systemPrompt = `${dateContext} -Provide analysis of the most significant development in 2-3 sentences. Be direct and specific. +Analyze the most significant development in 2 concise sentences MAX (under 60 words total). Be direct and specific. Rules: - Each numbered headline below is a SEPARATE, UNRELATED story - Pick the ONE most significant story and analyze ONLY that - NEVER combine or merge people, places, or facts from different headlines - Lead with the insight - what's significant and why - NEVER start with "Breaking news", "Tonight", "The key/dominant narrative is" -- Start with substance about your chosen headline -- If intelligence context is provided, use it only if it relates to your chosen headline -- Connect dots, be specific about implications`; +- Start with substance, no filler or elaboration +- If intelligence context is provided, use it only if it relates to your chosen headline`; } userPrompt = isTechVariant ? `Each headline is a separate story. What's the key tech trend?\n${headlineText}${intelSection}` @@ -133,8 +131,8 @@ Rules: userPrompt = `Translate to ${targetLang}:\n${headlines[0]}`; } else { systemPrompt = isTechVariant - ? `${dateContext}\n\nPick the most important tech headline and summarize it in 2 sentences. Each headline is a separate story - NEVER merge facts from different headlines. Focus on startups, AI, funding, products. Ignore politics unless directly about tech regulation.${langInstruction}` - : `${dateContext}\n\nPick the most important headline and summarize it in 2 sentences. Each headline is a separate, unrelated story - NEVER merge people or facts from different headlines. Lead with substance. NEVER start with "Breaking news" or "Tonight".${langInstruction}`; + ? `${dateContext}\n\nPick the most important tech headline and summarize it in 2 concise sentences (under 60 words). Each headline is a separate story - NEVER merge facts from different headlines. Focus on startups, AI, funding, products. Ignore politics unless directly about tech regulation.${langInstruction}` + : `${dateContext}\n\nPick the most important headline and summarize it in 2 concise sentences (under 60 words). Each headline is a separate, unrelated story - NEVER merge people or facts from different headlines. Lead with substance. NEVER start with "Breaking news" or "Tonight".${langInstruction}`; userPrompt = `Each headline is a separate story. Key takeaway from the most important one:\n${headlineText}${intelSection}`; } diff --git a/server/worldmonitor/news/v1/summarize-article.ts b/server/worldmonitor/news/v1/summarize-article.ts index 491bf767f..27d3fd5e0 100644 --- a/server/worldmonitor/news/v1/summarize-article.ts +++ b/server/worldmonitor/news/v1/summarize-article.ts @@ -114,7 +114,7 @@ export async function summarizeArticle( { role: 'user', content: userPrompt }, ], temperature: 0.3, - max_tokens: 150, + max_tokens: 100, top_p: 0.9, ...extraBody, }), diff --git a/src/services/summarization.ts b/src/services/summarization.ts index 95c830ad2..8786960d1 100644 --- a/src/services/summarization.ts +++ b/src/services/summarization.ts @@ -108,7 +108,7 @@ async function tryBrowserT5(headlines: string[], modelId?: string): Promise h.slice(0, 80)).join('. '); - const prompt = `Summarize the main themes from these news headlines in 2 sentences: ${combinedText}`; + const prompt = `Summarize the most important headline in 2 concise sentences (under 60 words): ${combinedText}`; const [summary] = await mlWorker.summarize([prompt], modelId);