UNPKG

tenzetta

Version:

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

224 lines (184 loc) 6.6 kB
const axios = require('axios'); const chalk = require('chalk'); const ora = require('ora'); const readline = require('readline'); const config = require('./config'); class QuoteViewer { constructor(symbol) { this.symbol = symbol.toUpperCase(); this.currentTab = 'overview'; this.data = {}; this.spinner = ora(`Loading data for ${this.symbol}...`).start(); } async run() { try { // Setup keyboard input if (process.stdin.isTTY) { readline.emitKeypressEvents(process.stdin); process.stdin.setRawMode(true); this.setupKeyboardHandler(); } // Load all data await this.loadAllData(); this.spinner.stop(); // Start display this.display(); // Handle shutdown process.on('SIGINT', () => { console.log(chalk.yellow('\n\nExiting...')); if (process.stdin.isTTY) { process.stdin.setRawMode(false); } process.exit(0); }); } catch (error) { this.spinner.fail(`Failed to load data for ${this.symbol}`); console.error(chalk.red('Error:'), error.message); process.exit(1); } } async loadAllData() { const promises = [ axios.get(`${config.MCP_SERVER_URL}${config.endpoints.quote(this.symbol)}`) .then(res => this.data.quote = res.data) .catch(() => this.data.quote = null), axios.get(`${config.MCP_SERVER_URL}${config.endpoints.profile(this.symbol)}`) .then(res => this.data.profile = res.data) .catch(() => this.data.profile = null), axios.get(`${config.MCP_SERVER_URL}${config.endpoints.news(this.symbol)}`) .then(res => this.data.news = res.data) .catch(() => this.data.news = null) ]; await Promise.all(promises); } setupKeyboardHandler() { process.stdin.on('keypress', (str, key) => { if (key.ctrl && key.name === 'c') { if (process.stdin.isTTY) { process.stdin.setRawMode(false); } process.exit(0); } switch(str) { case '1': this.currentTab = 'overview'; this.display(); break; case '2': this.currentTab = 'profile'; this.display(); break; case '3': this.currentTab = 'news'; this.display(); break; } }); } display() { console.clear(); const quote = this.data.quote; if (!quote) { console.log(chalk.red('No quote data available')); return; } // Header console.log(chalk.cyan.bold(`${quote.symbol} - ${quote.name || 'N/A'}`)); console.log(chalk.gray('─'.repeat(60))); 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(`Price: ${chalk.bold.white(formatCurrency(quote.price))} ${changeColor(changeStr)}`); console.log(''); // Tab content switch(this.currentTab) { case 'profile': this.displayProfile(); break; case 'news': this.displayNews(); break; default: this.displayOverview(); } console.log(chalk.gray('\nPress 1: Overview | 2: Profile | 3: News | Ctrl+C: Exit')); } displayOverview() { const quote = this.data.quote; console.log(chalk.yellow.bold('Overview\n')); // Price metrics console.log(`Volume: ${formatNumber(quote.volume)}`); console.log(`Market Cap: ${formatNumber(quote.marketCap)}`); console.log(`Avg Volume: ${formatNumber(quote.avgVolume)}`); console.log(''); // Ranges console.log(`Day Range: ${formatCurrency(quote.dayLow)} - ${formatCurrency(quote.dayHigh)}`); console.log(`52W Range: ${formatCurrency(quote.yearLow)} - ${formatCurrency(quote.yearHigh)}`); console.log(''); // Valuation console.log(`P/E Ratio: ${quote.pe?.toFixed(2) || 'N/A'}`); console.log(`EPS: ${formatCurrency(quote.eps)}`); console.log(`Beta: ${quote.beta?.toFixed(2) || 'N/A'}`); console.log(`Dividend Yield: ${quote.dividendYield ? (quote.dividendYield * 100).toFixed(2) + '%' : 'N/A'}`); } displayProfile() { const profile = this.data.profile; console.log(chalk.yellow.bold('Company Profile\n')); if (!profile || profile.length === 0) { console.log(chalk.gray('No profile data available')); return; } const company = Array.isArray(profile) ? profile[0] : profile; console.log(`Industry: ${company.industry || 'N/A'}`); console.log(`Sector: ${company.sector || 'N/A'}`); console.log(`Country: ${company.country || 'N/A'}`); console.log(`Exchange: ${company.exchangeShortName || 'N/A'}`); console.log(''); console.log(`Employees: ${formatNumber(company.fullTimeEmployees)}`); console.log(`Founded: ${company.ipoDate || 'N/A'}`); console.log(''); if (company.description) { console.log('Description:'); const desc = company.description.substring(0, 300); console.log(chalk.gray(desc + (company.description.length > 300 ? '...' : ''))); } if (company.website) { console.log(`\nWebsite: ${chalk.blue.underline(company.website)}`); } } displayNews() { const news = this.data.news; console.log(chalk.yellow.bold('Recent News\n')); if (!news || !Array.isArray(news) || news.length === 0) { console.log(chalk.gray('No news available')); return; } news.slice(0, 8).forEach((article, i) => { const date = new Date(article.publishedDate).toLocaleDateString(); console.log(`${(i + 1).toString().padStart(2)}. ${chalk.bold(article.title)}`); console.log(` ${chalk.gray(date)}${chalk.blue(article.site || 'Unknown')}`); if (article.text) { const summary = article.text.substring(0, 120); console.log(` ${chalk.dim(summary + (article.text.length > 120 ? '...' : ''))}`); } console.log(''); }); } } module.exports = { run: async (symbol) => { const viewer = new QuoteViewer(symbol); viewer.run(); } }; function formatCurrency(num) { if (num === null || num === undefined) return 'N/A'; return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(num); } function formatNumber(num) { if (num === null || num === undefined) return 'N/A'; return new Intl.NumberFormat('en-US').format(num); }