UNPKG

vibe-stocks

Version:

Terminal-based stock market viewer with real-time data streaming

164 lines (139 loc) 4.69 kB
const WebSocket = require('ws'); const axios = require('axios'); const chalk = require('chalk'); const ora = require('ora'); const config = require('./config'); class StreamViewer { constructor(symbols, options) { this.symbols = symbols.length > 0 ? symbols : config.defaults.symbols; this.options = options; this.quotes = {}; this.ws = null; this.spinner = ora('Connecting to market data...').start(); } async run() { try { // Fetch initial quotes await this.fetchInitialQuotes(); this.spinner.stop(); // Connect WebSocket this.connectWebSocket(); // Start display loop this.startDisplay(); // Handle graceful shutdown process.on('SIGINT', () => { console.log(chalk.yellow('\n\nShutting down...')); if (this.ws) this.ws.close(); process.exit(0); }); } catch (error) { this.spinner.fail('Failed to connect to market data'); console.error(chalk.red('Error:'), error.message); process.exit(1); } } async fetchInitialQuotes() { const promises = this.symbols.map(symbol => axios.get(`${config.MCP_SERVER_URL}${config.endpoints.quote(symbol)}`) .then(res => ({ symbol, data: res.data })) .catch(() => ({ symbol, data: null })) ); const results = await Promise.all(promises); results.forEach(({ symbol, data }) => { if (data) { this.quotes[symbol] = data; } }); } connectWebSocket() { this.ws = new WebSocket(config.MCP_WS_URL); this.ws.on('open', () => { this.symbols.forEach(symbol => { this.ws.send(JSON.stringify({ type: 'subscribe', symbol: symbol })); }); }); this.ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); if (message.type === 'quote' && message.data) { this.quotes[message.data.symbol] = message.data; } } catch (error) { // Ignore parse errors } }); this.ws.on('error', (error) => { console.error(chalk.red('WebSocket error:'), error.message); }); this.ws.on('close', () => { setTimeout(() => this.connectWebSocket(), 5000); }); } startDisplay() { const displayLoop = () => { console.clear(); this.displayHeader(); this.displayQuotes(); this.displayFooter(); }; displayLoop(); setInterval(displayLoop, parseInt(this.options.interval) || config.defaults.updateInterval); } displayHeader() { console.log(chalk.cyan.bold('Vibe Stocks - Real-Time Market Data')); console.log(chalk.gray(`Last Update: ${new Date().toLocaleTimeString()}\n`)); } displayQuotes() { if (this.options.compact) { this.displayCompact(); } else { this.displayDetailed(); } } displayCompact() { Object.values(this.quotes).forEach(quote => { const change = quote.changesPercentage || 0; const changeColor = change > 0 ? chalk.green : change < 0 ? chalk.red : chalk.gray; const changeStr = `${change >= 0 ? '+' : ''}${change.toFixed(2)}%`; console.log( `${chalk.bold(quote.symbol.padEnd(6))} ${this.formatCurrency(quote.price).padStart(10)} ${changeColor(changeStr.padStart(8))}` ); }); } displayDetailed() { Object.values(this.quotes).forEach(quote => { const change = quote.changesPercentage || 0; const changeColor = change > 0 ? chalk.green : change < 0 ? chalk.red : chalk.gray; console.log(chalk.bold.white(`${quote.symbol} - ${quote.name || 'N/A'}`)); console.log(`Price: ${chalk.bold(this.formatCurrency(quote.price))} ${changeColor(`${change >= 0 ? '+' : ''}${change.toFixed(2)}%`)}`); console.log(`Volume: ${this.formatNumber(quote.volume)} | Market Cap: ${this.formatNumber(quote.marketCap)}`); console.log(`Day Range: ${this.formatCurrency(quote.dayLow)} - ${this.formatCurrency(quote.dayHigh)}`); console.log(''); }); } displayFooter() { console.log(chalk.gray('\nPress Ctrl+C to exit')); } formatCurrency(num) { if (num === null || num === undefined) return 'N/A'; return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(num); } formatNumber(num) { if (num === null || num === undefined) return 'N/A'; return new Intl.NumberFormat('en-US').format(num); } } module.exports = { run: (symbols, options) => { const viewer = new StreamViewer(symbols, options); viewer.run(); } };