Add onvista fallback for EU ETF/stock quotes in stonks script
This commit is contained in:
parent
f6f52ba1b9
commit
954ffebaad
3 changed files with 126 additions and 18 deletions
98
bin/stonks
98
bin/stonks
|
|
@ -56,10 +56,10 @@ function savePortfolio(p) {
|
||||||
fs.writeFileSync(PORTFOLIO_PATH, JSON.stringify(p, null, 2));
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const url = `https://finnhub.io/api/v1${endpoint}&token=${API_KEY}`;
|
mod.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, (res) => {
|
||||||
https.get(url, (res) => {
|
|
||||||
let data = '';
|
let data = '';
|
||||||
res.on('data', (c) => data += c);
|
res.on('data', (c) => data += c);
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
|
|
@ -70,16 +70,84 @@ function finnhub(endpoint) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function quote(symbol) {
|
function finnhub(endpoint) {
|
||||||
const q = await finnhub(`/quote?symbol=${encodeURIComponent(symbol)}`);
|
return httpGet(`https://finnhub.io/api/v1${endpoint}&token=${API_KEY}`);
|
||||||
if (!q || q.c === 0) {
|
}
|
||||||
console.error(`No data for ${symbol}. Try search to find the right symbol.`);
|
|
||||||
return null;
|
// 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 change = q.d || 0;
|
||||||
const changePct = q.dp || 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}`);
|
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 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) {
|
async function search(query) {
|
||||||
|
|
@ -151,8 +219,20 @@ async function showPortfolio() {
|
||||||
console.log('Symbol\tShares\tAvg Price\tCurrent\tValue\tP&L\tP&L%');
|
console.log('Symbol\tShares\tAvg Price\tCurrent\tValue\tP&L\tP&L%');
|
||||||
for (const pos of p.positions) {
|
for (const pos of p.positions) {
|
||||||
try {
|
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 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 value = pos.shares * currentPrice;
|
||||||
const cost = pos.shares * pos.avgPrice;
|
const cost = pos.shares * pos.avgPrice;
|
||||||
const pnl = value - cost;
|
const pnl = value - cost;
|
||||||
|
|
|
||||||
3
memory/onvista-cache.json
Normal file
3
memory/onvista-cache.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"IE000YYE6WK5": "241121751"
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,35 @@
|
||||||
{
|
{
|
||||||
"startingCapital": 1000,
|
"startingCapital": 1000,
|
||||||
"cash": 1000,
|
"cash": 300,
|
||||||
"currency": "EUR",
|
"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": [],
|
"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.",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue