stonks-dashboard 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/config.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "tickers": ["BTC", "ETH", "SOL", "AAPL", "TSLA", "NVDA", "GOOGL", "SPY", "QQQ", "VOO"],
3
+ "updateInterval": 120000,
4
+ "cryptoIds": {
5
+ "BTC": "bitcoin",
6
+ "ETH": "ethereum",
7
+ "SOL": "solana"
8
+ }
9
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "stonks-dashboard",
3
+ "version": "1.0.0",
4
+ "description": "A cyberpunk-style real-time financial monitor for the terminal",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "stonks-dashboard": "./src/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/index.js",
12
+ "dev": "node src/index.js"
13
+ },
14
+ "keywords": [
15
+ "stocks",
16
+ "crypto",
17
+ "terminal",
18
+ "dashboard",
19
+ "TUI"
20
+ ],
21
+ "author": "",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "axios": "^1.6.2",
25
+ "blessed": "^0.1.81",
26
+ "blessed-contrib": "^4.11.0",
27
+ "chalk": "^5.3.0"
28
+ }
29
+ }
@@ -0,0 +1,355 @@
1
+ import axios from 'axios';
2
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
3
+
4
+ // Global rate limiter for CoinGecko - sequential with delay
5
+ let lastCoinGeckoCall = 0;
6
+ const COINGECKO_DELAY = 5000; // 5 seconds between calls to avoid 429
7
+
8
+ // File cache settings
9
+ const CACHE_FILE = './cache.json';
10
+ const CACHE_TTL = 60 * 1000; // 1 minute cache validity
11
+ const DETAIL_TTL = 30 * 60 * 1000; // 30 minutes for crypto detail
12
+
13
+ async function waitForCoinGecko() {
14
+ const now = Date.now();
15
+ const elapsed = now - lastCoinGeckoCall;
16
+ if (elapsed < COINGECKO_DELAY) {
17
+ await new Promise(resolve => setTimeout(resolve, COINGECKO_DELAY - elapsed));
18
+ }
19
+ lastCoinGeckoCall = Date.now();
20
+ }
21
+
22
+ async function axiosGetWithRetry(url, options, retries = 3, baseDelay = 1000) {
23
+ for (let attempt = 0; attempt <= retries; attempt++) {
24
+ try {
25
+ await waitForCoinGecko();
26
+ return await axios.get(url, options);
27
+ } catch (e) {
28
+ const status = e.response?.status;
29
+ const shouldRetry = status === 429 || (status >= 500) || !status;
30
+ if (attempt < retries && shouldRetry) {
31
+ const jitter = Math.floor(Math.random() * 300);
32
+ const delay = baseDelay * Math.pow(2, attempt) + jitter;
33
+ await new Promise(r => setTimeout(r, delay));
34
+ continue;
35
+ }
36
+ throw e;
37
+ }
38
+ }
39
+ }
40
+
41
+ export class DataService {
42
+ constructor() {
43
+ this.cache = new Map();
44
+ this.loadFileCache();
45
+ }
46
+
47
+ loadFileCache() {
48
+ try {
49
+ if (existsSync(CACHE_FILE)) {
50
+ const data = JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
51
+ for (const [key, value] of Object.entries(data)) {
52
+ this.cache.set(key, value);
53
+ }
54
+ console.log(`[Cache] Loaded ${Object.keys(data).length} entries from file`);
55
+ }
56
+ } catch (error) {
57
+ console.error('[Cache] Error loading cache file:', error.message);
58
+ }
59
+ }
60
+
61
+ saveFileCache() {
62
+ try {
63
+ const data = Object.fromEntries(this.cache);
64
+ writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
65
+ } catch (error) {
66
+ console.error('[Cache] Error saving cache file:', error.message);
67
+ }
68
+ }
69
+
70
+ isCacheValid(cacheKey, ttl = CACHE_TTL) {
71
+ const cached = this.cache.get(cacheKey);
72
+ if (!cached || !cached.timestamp) return false;
73
+ return (Date.now() - cached.timestamp) < ttl;
74
+ }
75
+
76
+ async fetchCryptoData(symbol, coinId, days = 7) {
77
+ const chartKey = `crypto-${symbol}-${days}`;
78
+ const detailKey = `detail-${coinId}`;
79
+
80
+ // Use cached chart if valid
81
+ if (this.isCacheValid(chartKey)) {
82
+ return { ...this.cache.get(chartKey), fromCache: true };
83
+ }
84
+
85
+ try {
86
+ // Fetch chart sequentially with delay
87
+ const chartResponse = await axiosGetWithRetry(`https://api.coingecko.com/api/v3/coins/${coinId}/market_chart`, {
88
+ params: { vs_currency: 'usd', days: days },
89
+ headers: { 'User-Agent': 'Mozilla/5.0' },
90
+ timeout: 10000
91
+ });
92
+
93
+ // Build aligned timestamps + prices arrays, filtering invalid points
94
+ const pairs = chartResponse.data.prices || [];
95
+ const timestamps = [];
96
+ const prices = [];
97
+ for (const p of pairs) {
98
+ const ts = p?.[0];
99
+ const val = p?.[1];
100
+ if (val !== null && val !== undefined && !isNaN(val)) {
101
+ timestamps.push(typeof ts === 'number' ? ts : Date.now());
102
+ prices.push(val);
103
+ }
104
+ }
105
+ if (prices.length === 0) prices.push(0);
106
+ const validPrices = prices.filter(p => p > 0);
107
+
108
+ // Try to use cached details (longer TTL)
109
+ let detailData = this.isCacheValid(detailKey, DETAIL_TTL) ? this.cache.get(detailKey) : null;
110
+ if (!detailData) {
111
+ // Space requests to avoid burst 429
112
+ try {
113
+ const detailResponse = await axiosGetWithRetry(`https://api.coingecko.com/api/v3/coins/${coinId}`, {
114
+ params: { localization: false, tickers: false, community_data: false, developer_data: false },
115
+ headers: { 'User-Agent': 'Mozilla/5.0' },
116
+ timeout: 10000
117
+ });
118
+ const md = detailResponse.data.market_data || {};
119
+ detailData = {
120
+ currentPrice: md.current_price?.usd || null,
121
+ change24h: md.price_change_percentage_24h || 0,
122
+ high24h: md.high_24h?.usd || null,
123
+ low24h: md.low_24h?.usd || null,
124
+ ath: md.ath?.usd || null,
125
+ atl: md.atl?.usd || null,
126
+ marketCap: md.market_cap?.usd || 0,
127
+ volume24h: md.total_volume?.usd || 0,
128
+ circulatingSupply: md.circulating_supply || 0,
129
+ totalSupply: md.total_supply || 0,
130
+ rank: detailResponse.data.market_cap_rank || 0,
131
+ timestamp: Date.now()
132
+ };
133
+ this.cache.set(detailKey, detailData);
134
+ this.saveFileCache();
135
+ } catch (e) {
136
+ // Details optional; continue with chart-only data
137
+ detailData = null;
138
+ }
139
+ }
140
+
141
+ const currentPrice = (detailData?.currentPrice ?? prices[prices.length - 1]) || 0;
142
+ const firstPrice = prices[0] || currentPrice;
143
+ const change = firstPrice > 0 ? ((currentPrice - firstPrice) / firstPrice) * 100 : 0;
144
+
145
+ const result = {
146
+ symbol,
147
+ type: 'crypto',
148
+ price: currentPrice,
149
+ change,
150
+ change24h: detailData?.change24h ?? 0,
151
+ history: prices,
152
+ timestamps,
153
+ // Extended
154
+ open: prices[0] || 0,
155
+ high: (detailData?.high24h ?? (validPrices.length > 0 ? Math.max(...validPrices) : 0)) || 0,
156
+ low: (detailData?.low24h ?? (validPrices.length > 0 ? Math.min(...validPrices) : 0)) || 0,
157
+ high52w: detailData?.ath ?? 0,
158
+ low52w: detailData?.atl ?? 0,
159
+ marketCap: detailData?.marketCap ?? 0,
160
+ volume: detailData?.volume24h ?? 0,
161
+ circulatingSupply: detailData?.circulatingSupply ?? 0,
162
+ totalSupply: detailData?.totalSupply ?? 0,
163
+ rank: detailData?.rank ?? 0,
164
+ timestamp: Date.now(),
165
+ error: false
166
+ };
167
+
168
+ this.cache.set(chartKey, result);
169
+ this.saveFileCache();
170
+ return result;
171
+
172
+ } catch (error) {
173
+ console.error(`[Crypto] ${symbol}: ${error.message}`);
174
+
175
+ if (this.cache.has(chartKey)) {
176
+ return { ...this.cache.get(chartKey), error: true, fromCache: true };
177
+ }
178
+
179
+ return {
180
+ symbol,
181
+ type: 'crypto',
182
+ price: 0,
183
+ change: 0,
184
+ change24h: 0,
185
+ history: [0],
186
+ open: 0,
187
+ high: 0,
188
+ low: 0,
189
+ high52w: 0,
190
+ low52w: 0,
191
+ marketCap: 0,
192
+ volume: 0,
193
+ circulatingSupply: 0,
194
+ totalSupply: 0,
195
+ rank: 0,
196
+ error: true
197
+ };
198
+ }
199
+ }
200
+
201
+ async fetchStockData(symbol, days = 7) {
202
+ const cacheKey = `stock-${symbol}-${days}`;
203
+
204
+ // Return cached data if valid
205
+ if (this.isCacheValid(cacheKey)) {
206
+ return { ...this.cache.get(cacheKey), fromCache: true };
207
+ }
208
+
209
+ let range = '7d';
210
+ let interval = '1d';
211
+ if (days <= 1) {
212
+ range = '1d';
213
+ interval = '1h';
214
+ } else if (days <= 7) {
215
+ range = '7d';
216
+ interval = '1d';
217
+ } else if (days <= 30) {
218
+ range = '1mo';
219
+ interval = '1d';
220
+ } else {
221
+ range = '3mo';
222
+ interval = '1d';
223
+ }
224
+
225
+ try {
226
+ // Fetch chart data
227
+ const response = await axios.get(`https://query1.finance.yahoo.com/v8/finance/chart/${symbol}`, {
228
+ params: { interval, range },
229
+ headers: { 'User-Agent': 'Mozilla/5.0' },
230
+ timeout: 10000
231
+ });
232
+
233
+ const result = response.data.chart.result[0];
234
+ const quote = result.indicators.quote[0];
235
+ const meta = result.meta;
236
+
237
+ // Align timestamps with close prices, filtering invalid points
238
+ const rawCloses = quote.close || [];
239
+ const rawTimestamps = result.timestamp || [];
240
+ const closePrices = [];
241
+ const timestamps = [];
242
+ for (let i = 0; i < rawCloses.length; i++) {
243
+ const val = rawCloses[i];
244
+ if (val !== null && val !== undefined && !isNaN(val)) {
245
+ closePrices.push(val);
246
+ const ts = rawTimestamps[i];
247
+ timestamps.push(typeof ts === 'number' ? ts * 1000 : Date.now());
248
+ }
249
+ }
250
+
251
+ if (closePrices.length === 0) closePrices.push(0);
252
+
253
+ const currentPrice = meta.regularMarketPrice || closePrices[closePrices.length - 1] || 0;
254
+ const previousClose = meta.chartPreviousClose || closePrices[0] || currentPrice;
255
+ const change = previousClose > 0 ? ((currentPrice - previousClose) / previousClose) * 100 : 0;
256
+ const validPrices = closePrices.filter(p => p > 0);
257
+
258
+ // Get first price of the day as open (approximation)
259
+ const openPrices = (quote.open || []).filter(p => p !== null && !isNaN(p));
260
+ const openPrice = openPrices[openPrices.length - 1] || previousClose;
261
+
262
+ const data = {
263
+ symbol,
264
+ type: 'stock',
265
+ price: currentPrice,
266
+ change,
267
+ change24h: change,
268
+ history: closePrices,
269
+ timestamps,
270
+ // Extended data
271
+ open: openPrice,
272
+ previousClose: previousClose,
273
+ high: meta.regularMarketDayHigh || (validPrices.length > 0 ? Math.max(...validPrices) : 0),
274
+ low: meta.regularMarketDayLow || (validPrices.length > 0 ? Math.min(...validPrices) : 0),
275
+ high52w: meta.fiftyTwoWeekHigh || 0,
276
+ low52w: meta.fiftyTwoWeekLow || 0,
277
+ marketCap: 0,
278
+ volume: meta.regularMarketVolume || 0,
279
+ avgVolume: 0,
280
+ pe: 0,
281
+ timestamp: Date.now(),
282
+ error: false
283
+ };
284
+
285
+ // Try to get additional quote data
286
+ try {
287
+ const quoteResponse = await axios.get(`https://query1.finance.yahoo.com/v7/finance/quote`, {
288
+ params: { symbols: symbol },
289
+ headers: { 'User-Agent': 'Mozilla/5.0' },
290
+ timeout: 5000
291
+ });
292
+
293
+ const quoteData = quoteResponse.data.quoteResponse?.result?.[0];
294
+ if (quoteData) {
295
+ data.marketCap = quoteData.marketCap || 0;
296
+ data.pe = quoteData.trailingPE || quoteData.forwardPE || 0;
297
+ data.avgVolume = quoteData.averageDailyVolume3Month || quoteData.averageDailyVolume10Day || 0;
298
+ data.high52w = quoteData.fiftyTwoWeekHigh || data.high52w;
299
+ data.low52w = quoteData.fiftyTwoWeekLow || data.low52w;
300
+ data.open = quoteData.regularMarketOpen || data.open;
301
+ }
302
+ } catch (e) {
303
+ // Quote data is optional, continue without it
304
+ }
305
+
306
+ this.cache.set(cacheKey, data);
307
+ this.saveFileCache();
308
+ return data;
309
+
310
+ } catch (error) {
311
+ console.error(`[Stock] ${symbol}: ${error.message}`);
312
+
313
+ if (this.cache.has(cacheKey)) {
314
+ return { ...this.cache.get(cacheKey), error: true, fromCache: true };
315
+ }
316
+
317
+ return {
318
+ symbol,
319
+ type: 'stock',
320
+ price: 0,
321
+ change: 0,
322
+ change24h: 0,
323
+ history: [0],
324
+ open: 0,
325
+ previousClose: 0,
326
+ high: 0,
327
+ low: 0,
328
+ high52w: 0,
329
+ low52w: 0,
330
+ marketCap: 0,
331
+ volume: 0,
332
+ avgVolume: 0,
333
+ pe: 0,
334
+ error: true
335
+ };
336
+ }
337
+ }
338
+
339
+ async fetchAllAssets(tickers, cryptoIds, days = 7) {
340
+ const results = [];
341
+
342
+ // Sequential fetching to respect rate limits
343
+ for (const ticker of tickers) {
344
+ const coinId = cryptoIds[ticker];
345
+ if (coinId) {
346
+ results.push(await this.fetchCryptoData(ticker, coinId, days));
347
+ } else {
348
+ results.push(await this.fetchStockData(ticker, days));
349
+ }
350
+ }
351
+
352
+ return results;
353
+ }
354
+ }
355
+