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);
}
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 {
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)}`);
const currentPrice = q.c || pos.avgPrice;
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;

View file

@ -0,0 +1,3 @@
{
"IE000YYE6WK5": "241121751"
}

View file

@ -1,10 +1,35 @@
{
"startingCapital": 1000,
"cash": 1000,
"cash": 300,
"currency": "EUR",
"positions": [],
"positions": [
{
"symbol": "DFNS",
"name": "VanEck Defense UCITS ETF A",
"isin": "IE000YYE6WK5",
"buyDate": "2026-02-13",
"avgPrice": 55.93,
"shares": 12.516,
"amount": 700
}
],
"limitOrders": [],
"history": [],
"watchlist": [
{ "symbol": "RHM.DE", "isin": "DE0007030009", "name": "Rheinmetall", "note": "Buy if major dip >10%" },
{ "symbol": "XDWD.DE", "isin": "IE00BJ0KDQ92", "name": "Xtrackers MSCI World", "note": "Safe diversification option" }
],
"history": [
{
"date": "2026-02-13",
"action": "BUY",
"symbol": "DFNS",
"isin": "IE000YYE6WK5",
"amount": 700,
"price": 55.93,
"reason": "European defense rearmament thesis. Diversified defense exposure via ETF. Strong momentum, geopolitical tailwinds."
}
],
"notes": "N26 uses Xetra tickers. Always provide ISIN for orders. Fractional shares by EUR amount supported.",
"created": "2026-02-12T20:00:00Z"
"created": "2026-02-12T20:00:00Z",
"lastUpdated": "2026-02-13T08:52:00Z"
}