Add onvista fallback for EU ETF/stock quotes in stonks script

This commit is contained in:
Hoid 2026-02-13 12:04:05 +00:00
parent f6f52ba1b9
commit 954ffebaad
3 changed files with 126 additions and 18 deletions

View file

@ -56,10 +56,10 @@ function savePortfolio(p) {
fs.writeFileSync(PORTFOLIO_PATH, JSON.stringify(p, null, 2));
}
function finnhub(endpoint) {
function httpGet(url) {
const mod = url.startsWith('https') ? https : require('http');
return new Promise((resolve, reject) => {
const url = `https://finnhub.io/api/v1${endpoint}&token=${API_KEY}`;
https.get(url, (res) => {
mod.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, (res) => {
let data = '';
res.on('data', (c) => data += c);
res.on('end', () => {
@ -70,16 +70,84 @@ function finnhub(endpoint) {
});
}
async function quote(symbol) {
const q = await finnhub(`/quote?symbol=${encodeURIComponent(symbol)}`);
if (!q || q.c === 0) {
console.error(`No data for ${symbol}. Try search to find the right symbol.`);
return null;
function finnhub(endpoint) {
return httpGet(`https://finnhub.io/api/v1${endpoint}&token=${API_KEY}`);
}
// Onvista fallback for EU ETFs/stocks not covered by Finnhub
async function onvistaSearch(query) {
const r = await httpGet(`https://api.onvista.de/api/v1/instruments/search?searchValue=${encodeURIComponent(query)}`);
return r && r.list ? r.list : [];
}
async function onvistaQuote(entityValue) {
const r = await httpGet(`https://api.onvista.de/api/v1/instruments/FUND/${entityValue}/snapshot`);
if (r && r.quote) return r.quote;
// Try STOCK type
const r2 = await httpGet(`https://api.onvista.de/api/v1/instruments/STOCK/${entityValue}/snapshot`);
return r2 && r2.quote ? r2.quote : null;
}
// ISIN-to-onvista entity cache
const ONVISTA_CACHE_PATH = path.join(__dirname, '..', 'memory', 'onvista-cache.json');
function loadOnvistaCache() {
try { return JSON.parse(fs.readFileSync(ONVISTA_CACHE_PATH, 'utf8')); } catch { return {}; }
}
function saveOnvistaCache(c) {
fs.writeFileSync(ONVISTA_CACHE_PATH, JSON.stringify(c, null, 2));
}
async function onvistaQuoteByISIN(isin, name) {
const cache = loadOnvistaCache();
let entityValue = cache[isin];
if (!entityValue) {
const results = await onvistaSearch(name || isin);
const match = results.find(r => r.isin === isin);
if (!match) return null;
entityValue = match.entityValue;
cache[isin] = entityValue;
saveOnvistaCache(cache);
}
const change = q.d || 0;
const changePct = q.dp || 0;
console.log(`${symbol}\t${q.c}\t${change >= 0 ? '+' : ''}${change}\t${changePct >= 0 ? '+' : ''}${changePct}%\tH:${q.h}\tL:${q.l}\tO:${q.o}\tPC:${q.pc}`);
return q;
return onvistaQuote(entityValue);
}
async function quote(symbol) {
// First check if we have this in portfolio with ISIN — prefer onvista for EU instruments
const p = loadPortfolio();
const pos = p.positions.find(pos => pos.symbol === symbol || pos.symbol + '.DE' === symbol);
if (pos && pos.isin) {
try {
const oq = await onvistaQuoteByISIN(pos.isin, pos.name);
if (oq && oq.last) {
const change = oq.changeAbsolute || 0;
const changePct = oq.changePrevDay || 0;
console.log(`${symbol}\t${oq.last}\t${change >= 0 ? '+' : ''}${change.toFixed(2)}\t${changePct >= 0 ? '+' : ''}${changePct.toFixed(2)}%\tH:${oq.high || 'N/A'}\tL:${oq.low || 'N/A'}\tO:${oq.open || 'N/A'}\tPC:${oq.previousLast || 'N/A'}\t(onvista)`);
return { c: oq.last, d: change, dp: changePct, h: oq.high, l: oq.low, o: oq.open, pc: oq.previousLast };
}
} catch (e) {}
}
// Try Finnhub
const q = await finnhub(`/quote?symbol=${encodeURIComponent(symbol)}`);
if (q && q.c && q.c !== 0) {
const change = q.d || 0;
const changePct = q.dp || 0;
console.log(`${symbol}\t${q.c}\t${change >= 0 ? '+' : ''}${change}\t${changePct >= 0 ? '+' : ''}${changePct}%\tH:${q.h}\tL:${q.l}\tO:${q.o}\tPC:${q.pc}`);
return q;
}
// Last resort: check portfolio for ISIN without match above
if (pos && pos.isin) {
try {
const oq = await onvistaQuoteByISIN(pos.isin, pos.name);
if (oq && oq.last) {
const change = oq.changeAbsolute || 0;
const changePct = oq.changePrevDay || 0;
console.log(`${symbol}\t${oq.last}\t${change >= 0 ? '+' : ''}${change.toFixed(2)}\t${changePct >= 0 ? '+' : ''}${changePct.toFixed(2)}%\tH:${oq.high || 'N/A'}\tL:${oq.low || 'N/A'}\tO:${oq.open || 'N/A'}\tPC:${oq.previousLast || 'N/A'}\t(onvista)`);
return { c: oq.last, d: change, dp: changePct, h: oq.high, l: oq.low, o: oq.open, pc: oq.previousLast };
}
} catch (e) {}
}
console.error(`No data for ${symbol}. Try search to find the right symbol.`);
return null;
}
async function search(query) {
@ -151,8 +219,20 @@ async function showPortfolio() {
console.log('Symbol\tShares\tAvg Price\tCurrent\tValue\tP&L\tP&L%');
for (const pos of p.positions) {
try {
const q = await finnhub(`/quote?symbol=${encodeURIComponent(pos.symbol)}`);
const currentPrice = q.c || pos.avgPrice;
let currentPrice = null;
// Try onvista first for positions with ISIN (EU instruments)
if (pos.isin) {
try {
const oq = await onvistaQuoteByISIN(pos.isin, pos.name);
if (oq && oq.last) currentPrice = oq.last;
} catch (e) {}
}
// Fallback to Finnhub
if (!currentPrice) {
const q = await finnhub(`/quote?symbol=${encodeURIComponent(pos.symbol)}`);
if (q && q.c && q.c > 0) currentPrice = q.c;
}
currentPrice = currentPrice || pos.avgPrice;
const value = pos.shares * currentPrice;
const cost = pos.shares * pos.avgPrice;
const pnl = value - cost;