Files
intelligence-terminal/apis/sources/yfinance.mjs

135 lines
3.9 KiB
JavaScript

// Yahoo Finance — Live market quotes (no API key required)
// Provides real-time prices for stocks, ETFs, crypto, commodities
// Replaces the need for Alpaca or any paid market data provider
import { safeFetch } from '../utils/fetch.mjs';
const BASE = 'https://query1.finance.yahoo.com/v8/finance/chart';
// Symbols to track — covers broad market, rates, commodities, crypto, volatility
const SYMBOLS = {
// Indexes / ETFs
'^GSPC': 'S&P 500',
'^IXIC': 'Nasdaq Composite',
'^DJI': 'Dow Jones',
'^RUT': 'Russell 2000',
// Rates / Credit
TLT: '20Y+ Treasury',
HYG: 'High Yield Corp',
LQD: 'IG Corporate',
// Commodities
'GC=F': 'Gold',
'SI=F': 'Silver',
'CL=F': 'WTI Crude',
'BZ=F': 'Brent Crude',
'NG=F': 'Natural Gas',
// Crypto
'BTC-USD': 'Bitcoin',
'ETH-USD': 'Ethereum',
// Volatility
'^VIX': 'VIX',
};
async function fetchQuote(symbol) {
try {
const url = `${BASE}/${encodeURIComponent(symbol)}?range=5d&interval=1d&includePrePost=false`;
const data = await safeFetch(url, {
timeout: 8000,
retries: 2,
source: `YFinance:${symbol}`,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'application/json,text/plain,*/*',
},
});
if (data?.error) throw new Error(data.error);
const result = data?.chart?.result?.[0];
if (!result) throw new Error(data?.chart?.error?.description || 'Yahoo response missing chart result');
const meta = result.meta || {};
const quotes = result.indicators?.quote?.[0] || {};
const closes = quotes.close || [];
const timestamps = result.timestamp || [];
// Get current price and previous close
const price = meta.regularMarketPrice ?? closes[closes.length - 1];
const prevClose = meta.chartPreviousClose ?? meta.previousClose ?? closes[closes.length - 2];
const change = price && prevClose ? price - prevClose : 0;
const changePct = prevClose ? (change / prevClose) * 100 : 0;
// Build 5-day history
const history = [];
for (let i = 0; i < timestamps.length; i++) {
if (closes[i] != null) {
history.push({
date: new Date(timestamps[i] * 1000).toISOString().split('T')[0],
close: Math.round(closes[i] * 100) / 100,
});
}
}
return {
symbol,
name: SYMBOLS[symbol] || meta.shortName || symbol,
price: Math.round(price * 100) / 100,
prevClose: Math.round((prevClose || 0) * 100) / 100,
change: Math.round(change * 100) / 100,
changePct: Math.round(changePct * 100) / 100,
currency: meta.currency || 'USD',
exchange: meta.exchangeName || '',
marketState: meta.marketState || 'UNKNOWN',
history,
};
} catch (e) {
return { symbol, name: SYMBOLS[symbol] || symbol, error: e.message };
}
}
export async function briefing() {
return collect();
}
export async function collect() {
const symbols = Object.keys(SYMBOLS);
const results = await Promise.allSettled(
symbols.map(s => fetchQuote(s))
);
const quotes = {};
let ok = 0;
let failed = 0;
for (const r of results) {
const q = r.status === 'fulfilled' ? r.value : null;
if (q && !q.error) {
quotes[q.symbol] = q;
ok++;
} else {
failed++;
const sym = q?.symbol || 'unknown';
quotes[sym] = q || { symbol: sym, error: 'fetch failed' };
}
}
// Categorize for easy dashboard consumption
return {
quotes,
summary: {
totalSymbols: symbols.length,
ok,
failed,
timestamp: new Date().toISOString(),
},
indexes: pickGroup(quotes, ['^GSPC', '^IXIC', '^DJI', '^RUT']),
rates: pickGroup(quotes, ['TLT', 'HYG', 'LQD']),
commodities: pickGroup(quotes, ['GC=F', 'SI=F', 'CL=F', 'BZ=F', 'NG=F']),
crypto: pickGroup(quotes, ['BTC-USD', 'ETH-USD']),
volatility: pickGroup(quotes, ['^VIX']),
};
}
function pickGroup(quotes, symbols) {
return symbols.map(s => quotes[s]).filter(Boolean);
}