107 lines
3.2 KiB
JavaScript
Executable File
107 lines
3.2 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Stock price monitor - checks for big moves and news
|
|
* Usage: node stock-monitor.js [--alert]
|
|
*
|
|
* Watchlist defined below. Alerts on >5% moves.
|
|
*/
|
|
|
|
const https = require('https');
|
|
|
|
// Watchlist with context
|
|
const WATCHLIST = [
|
|
{ symbol: 'NABL', name: 'N-able', note: 'PE takeover watch' },
|
|
{ symbol: 'S', name: 'SentinelOne', note: 'Johan short position' },
|
|
{ symbol: 'MSFT', name: 'Microsoft', note: 'Big tech' },
|
|
{ symbol: 'NVDA', name: 'NVIDIA', note: 'Big tech' },
|
|
{ symbol: 'TSLA', name: 'Tesla', note: 'Volatile' },
|
|
];
|
|
|
|
const ALERT_THRESHOLD = 5.0; // percent
|
|
|
|
function fetchQuote(symbol) {
|
|
return new Promise((resolve, reject) => {
|
|
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${symbol}?interval=1d&range=2d`;
|
|
|
|
https.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
try {
|
|
const json = JSON.parse(data);
|
|
const result = json.chart?.result?.[0];
|
|
if (!result) {
|
|
resolve({ symbol, error: 'No data' });
|
|
return;
|
|
}
|
|
|
|
const meta = result.meta;
|
|
const price = meta.regularMarketPrice;
|
|
const prevClose = meta.chartPreviousClose || meta.previousClose;
|
|
const change = price - prevClose;
|
|
const pctChange = (change / prevClose) * 100;
|
|
|
|
resolve({
|
|
symbol,
|
|
price: price.toFixed(2),
|
|
prevClose: prevClose.toFixed(2),
|
|
change: change.toFixed(2),
|
|
pctChange: pctChange.toFixed(2),
|
|
isAlert: Math.abs(pctChange) >= ALERT_THRESHOLD
|
|
});
|
|
} catch (e) {
|
|
resolve({ symbol, error: e.message });
|
|
}
|
|
});
|
|
}).on('error', e => resolve({ symbol, error: e.message }));
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
const alertOnly = process.argv.includes('--alert');
|
|
const results = await Promise.all(WATCHLIST.map(w => fetchQuote(w.symbol)));
|
|
|
|
const alerts = [];
|
|
|
|
console.log('📈 Stock Monitor\n');
|
|
console.log('Symbol Price Change %Chg Note');
|
|
console.log('------ ----- ------ ---- ----');
|
|
|
|
for (const r of results) {
|
|
const info = WATCHLIST.find(w => w.symbol === r.symbol);
|
|
|
|
if (r.error) {
|
|
console.log(`${r.symbol.padEnd(8)} ERROR: ${r.error}`);
|
|
continue;
|
|
}
|
|
|
|
const direction = parseFloat(r.change) >= 0 ? '+' : '';
|
|
const flag = r.isAlert ? '⚠️ ' : ' ';
|
|
|
|
console.log(
|
|
`${flag}${r.symbol.padEnd(6)} $${r.price.padStart(7)} ${direction}${r.change.padStart(7)} ${direction}${r.pctChange.padStart(5)}% ${info?.note || ''}`
|
|
);
|
|
|
|
if (r.isAlert) {
|
|
alerts.push({ ...r, note: info?.note });
|
|
}
|
|
}
|
|
|
|
if (alerts.length > 0) {
|
|
console.log('\n🚨 ALERTS (>5% move):');
|
|
for (const a of alerts) {
|
|
const direction = parseFloat(a.pctChange) >= 0 ? '📈' : '📉';
|
|
console.log(` ${direction} ${a.symbol}: ${a.pctChange}% (${a.note})`);
|
|
}
|
|
}
|
|
|
|
// Return alerts for piping to other tools
|
|
if (alertOnly && alerts.length === 0) {
|
|
process.exit(0);
|
|
}
|
|
|
|
return alerts;
|
|
}
|
|
|
|
main().catch(console.error);
|