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: [],
};
}
}