#!/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);