Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions server/worldmonitor/economic/v1/get-macro-signals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,22 @@ function buildFallbackResult(): GetMacroSignalsResponse {
async function computeMacroSignals(): Promise<GetMacroSignalsResponse> {
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);
Expand Down
31 changes: 17 additions & 14 deletions server/worldmonitor/market/v1/get-sector-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -22,24 +22,27 @@ export async function getSectorSummary(
_req: GetSectorSummaryRequest,
): Promise<GetSectorSummaryResponse> {
const apiKey = process.env.FINNHUB_API_KEY;
if (!apiKey) return { sectors: [] };

try {
const result = await cachedFetchJson<GetSectorSummaryResponse>(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 });
}
}

Expand Down
26 changes: 9 additions & 17 deletions server/worldmonitor/market/v1/list-commodity-quotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -30,22 +30,14 @@ export async function listCommodityQuotes(

try {
const result = await cachedFetchJson<ListCommodityQuotesResponse>(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;
});

Expand Down
26 changes: 18 additions & 8 deletions server/worldmonitor/market/v1/list-etf-flows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,13 @@ export async function listEtfFlows(
return etfCache;
}

try {
const result = await cachedFetchJson<ListEtfFlowsResponse>(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);
}
}
Expand Down Expand Up @@ -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: [],
};
}
}
24 changes: 11 additions & 13 deletions server/worldmonitor/news/v1/_shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,19 @@ 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
- NEVER combine or merge facts, names, or details from different headlines
- 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
Expand All @@ -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}`
Expand All @@ -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}`;
}

Expand Down
2 changes: 1 addition & 1 deletion server/worldmonitor/news/v1/summarize-article.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
Expand Down
2 changes: 1 addition & 1 deletion src/services/summarization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ async function tryBrowserT5(headlines: string[], modelId?: string): Promise<Summ
lastAttemptedProvider = 'browser';

const combinedText = headlines.slice(0, 5).map(h => 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);

Expand Down