/ clis / barchart / quote.js
quote.js
  1  /**
  2   * Barchart stock quote — price, volume, market cap, P/E, EPS, and key metrics.
  3   * Auth: CSRF token from <meta name="csrf-token"> + session cookies.
  4   */
  5  import { cli, Strategy } from '@jackwener/opencli/registry';
  6  import { CommandExecutionError } from '@jackwener/opencli/errors';
  7  cli({
  8      site: 'barchart',
  9      name: 'quote',
 10      description: 'Barchart stock quote with price, volume, and key metrics',
 11      domain: 'www.barchart.com',
 12      strategy: Strategy.COOKIE,
 13      args: [
 14          { name: 'symbol', required: true, positional: true, help: 'Stock ticker (e.g. AAPL, MSFT, TSLA)' },
 15      ],
 16      columns: [
 17          'symbol', 'name', 'price', 'change', 'changePct',
 18          'open', 'high', 'low', 'prevClose', 'volume',
 19          'avgVolume', 'marketCap', 'peRatio', 'eps',
 20      ],
 21      func: async (page, kwargs) => {
 22          const symbol = kwargs.symbol.toUpperCase().trim();
 23          await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/overview`);
 24          await page.wait(4);
 25          const data = await page.evaluate(`
 26        (async () => {
 27          const sym = ${JSON.stringify(symbol)};
 28          const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
 29  
 30          // Strategy 1: internal proxy API with CSRF token
 31          try {
 32            const fields = [
 33              'symbol','symbolName','lastPrice','priceChange','percentChange',
 34              'highPrice','lowPrice','openPrice','previousPrice','volume','averageVolume',
 35              'marketCap','peRatio','earningsPerShare','tradeTime',
 36            ].join(',');
 37            const url = '/proxies/core-api/v1/quotes/get?symbol=' + encodeURIComponent(sym) + '&fields=' + fields;
 38            const resp = await fetch(url, {
 39              credentials: 'include',
 40              headers: { 'X-CSRF-TOKEN': csrf },
 41            });
 42            if (resp.ok) {
 43              const d = await resp.json();
 44              const row = d?.data?.[0] || null;
 45              if (row) {
 46                return { source: 'api', row };
 47              }
 48            }
 49          } catch(e) {}
 50  
 51          // Strategy 2: parse from DOM
 52          try {
 53            const priceEl = document.querySelector('span.last-change');
 54            const price = priceEl ? priceEl.textContent.trim() : null;
 55  
 56            // Change values are sibling spans inside .pricechangerow > .last-change
 57            const changeParent = priceEl?.parentElement;
 58            const changeSpans = changeParent ? changeParent.querySelectorAll('span') : [];
 59            let change = null;
 60            let changePct = null;
 61            for (const s of changeSpans) {
 62              const t = s.textContent.trim();
 63              if (s === priceEl) continue;
 64              if (t.includes('%')) changePct = t.replace(/[()]/g, '');
 65              else if (t.match(/^[+-]?[\\d.]+$/)) change = t;
 66            }
 67  
 68            // Financial data rows
 69            const rows = document.querySelectorAll('.financial-data-row');
 70            const fdata = {};
 71            for (const row of rows) {
 72              const spans = row.querySelectorAll('span');
 73              if (spans.length >= 2) {
 74                const label = spans[0].textContent.trim();
 75                const valSpan = row.querySelector('span.right span:not(.ng-hide)');
 76                fdata[label] = valSpan ? valSpan.textContent.trim() : '';
 77              }
 78            }
 79  
 80            // Day high/low from row chart
 81            const dayLow = document.querySelector('.bc-quote-row-chart .small-6:first-child .inline:not(.ng-hide)');
 82            const dayHigh = document.querySelector('.bc-quote-row-chart .text-right .inline:not(.ng-hide)');
 83            const openEl = document.querySelector('.mark span');
 84            const openText = openEl ? openEl.textContent.trim().replace('Open ', '') : null;
 85  
 86            const name = document.querySelector('h1 span.symbol');
 87  
 88            return {
 89              source: 'dom',
 90              row: {
 91                symbol: sym,
 92                symbolName: name ? name.textContent.trim() : sym,
 93                lastPrice: price,
 94                priceChange: change,
 95                percentChange: changePct,
 96                open: openText,
 97                highPrice: dayHigh ? dayHigh.textContent.trim() : null,
 98                lowPrice: dayLow ? dayLow.textContent.trim() : null,
 99                previousClose: fdata['Previous Close'] || null,
100                volume: fdata['Volume'] || null,
101                averageVolume: fdata['Average Volume'] || null,
102                marketCap: null,
103                peRatio: null,
104                earningsPerShare: null,
105              }
106            };
107          } catch(e) {
108            return { error: 'Could not fetch quote for ' + sym + ': ' + e.message };
109          }
110        })()
111      `);
112          if (!data || data.error)
113              throw new CommandExecutionError(data?.error || `Failed to fetch quote for ${symbol}`);
114          const r = data.row || {};
115          // API returns formatted strings like "+1.41" and "+0.56%"; use raw if available
116          const raw = r.raw || {};
117          return [{
118                  symbol: r.symbol || symbol,
119                  name: r.symbolName || r.name || symbol,
120                  price: r.lastPrice ?? null,
121                  change: r.priceChange ?? null,
122                  changePct: r.percentChange ?? null,
123                  open: r.openPrice ?? r.open ?? null,
124                  high: r.highPrice ?? null,
125                  low: r.lowPrice ?? null,
126                  prevClose: r.previousPrice ?? r.previousClose ?? null,
127                  volume: r.volume ?? null,
128                  avgVolume: r.averageVolume ?? null,
129                  marketCap: r.marketCap ?? null,
130                  peRatio: r.peRatio ?? null,
131                  eps: r.earningsPerShare ?? null,
132              }];
133      },
134  });