clawd/scripts/stock-monitor.js

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);