From c806f795ccfa76ff180e89a7117caafa8729c4ac Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Tue, 17 Mar 2026 21:13:25 -0700 Subject: [PATCH] feat(barchart): add stock quote, options, greeks, and flow commands (#45) * feat(barchart): add stock quote, options chain, greeks, and flow commands Add 4 new barchart.com CLI commands: - `barchart quote`: stock price, volume, market cap, P/E, EPS - `barchart options`: options chain with strike, bid/ask, greeks, IV, OI - `barchart greeks`: near-the-money greeks overview (delta, gamma, theta, vega, rho) - `barchart flow`: unusual options activity sorted by volume/OI ratio Auth uses CSRF token from + session cookies via the internal proxy API, with DOM fallback for the quote command. * feat(barchart): add --expiration date filter to greeks command --- src/clis/barchart/flow.ts | 129 +++++++++++++++++++++++++++++++++ src/clis/barchart/greeks.ts | 105 +++++++++++++++++++++++++++ src/clis/barchart/options.ts | 110 ++++++++++++++++++++++++++++ src/clis/barchart/quote.ts | 137 +++++++++++++++++++++++++++++++++++ 4 files changed, 481 insertions(+) create mode 100644 src/clis/barchart/flow.ts create mode 100644 src/clis/barchart/greeks.ts create mode 100644 src/clis/barchart/options.ts create mode 100644 src/clis/barchart/quote.ts diff --git a/src/clis/barchart/flow.ts b/src/clis/barchart/flow.ts new file mode 100644 index 0000000..7bae0f6 --- /dev/null +++ b/src/clis/barchart/flow.ts @@ -0,0 +1,129 @@ +/** + * Barchart unusual options activity (options flow). + * Shows high volume/OI ratio trades that may indicate institutional activity. + * Auth: CSRF token from + session cookies. + */ +import { cli, Strategy } from '../../registry.js'; + +cli({ + site: 'barchart', + name: 'flow', + description: 'Barchart unusual options activity / options flow', + domain: 'www.barchart.com', + strategy: Strategy.COOKIE, + args: [ + { name: 'type', type: 'str', default: 'all', help: 'Filter: all, call, or put', choices: ['all', 'call', 'put'] }, + { name: 'limit', type: 'int', default: 20, help: 'Number of results' }, + ], + columns: [ + 'symbol', 'type', 'strike', 'expiration', 'last', + 'volume', 'openInterest', 'volOiRatio', 'iv', + ], + func: async (page, kwargs) => { + const optionType = kwargs.type || 'all'; + const limit = kwargs.limit ?? 20; + + await page.goto('https://www.barchart.com/options/unusual-activity/stocks'); + await page.wait(5); + + const data = await page.evaluate(` + (async () => { + const limit = ${limit}; + const typeFilter = '${optionType}'.toLowerCase(); + const csrf = document.querySelector('meta[name="csrf-token"]')?.content || ''; + const headers = { 'X-CSRF-TOKEN': csrf }; + + const fields = [ + 'baseSymbol','strikePrice','expirationDate','optionType', + 'lastPrice','volume','openInterest','volumeOpenInterestRatio','volatility', + ].join(','); + + // Try unusual_activity list first, fall back to mostActive + // Fetch extra rows when filtering by type since server-side filter may not work + const fetchLimit = typeFilter !== 'all' ? limit * 3 : limit; + const lists = [ + 'options.unusual_activity.stocks.us', + 'options.mostActive.us', + ]; + + for (const list of lists) { + try { + const url = '/proxies/core-api/v1/options/get?list=' + list + + '&fields=' + fields + + '&orderBy=volumeOpenInterestRatio&orderDir=desc' + + '&raw=1&limit=' + fetchLimit; + + const resp = await fetch(url, { credentials: 'include', headers }); + if (resp.ok) { + const d = await resp.json(); + let items = d?.data || []; + if (items.length > 0) { + // Apply client-side type filter + if (typeFilter !== 'all') { + items = items.filter(i => { + const t = ((i.raw || i).optionType || '').toLowerCase(); + return t === typeFilter; + }); + } + return items.slice(0, limit).map(i => { + const r = i.raw || i; + return { + symbol: r.baseSymbol || r.symbol, + type: r.optionType, + strike: r.strikePrice, + expiration: r.expirationDate, + last: r.lastPrice, + volume: r.volume, + openInterest: r.openInterest, + volOiRatio: r.volumeOpenInterestRatio, + iv: r.volatility, + }; + }); + } + } + } catch(e) {} + } + + // Fallback: parse from DOM table + try { + const rows = document.querySelectorAll('tr[data-ng-repeat], tbody tr'); + const results = []; + for (const row of rows) { + const cells = row.querySelectorAll('td'); + if (cells.length < 6) continue; + const getText = (idx) => cells[idx]?.textContent?.trim() || null; + results.push({ + symbol: getText(0), + type: getText(1), + strike: getText(2), + expiration: getText(3), + last: getText(4), + volume: getText(5), + openInterest: cells.length > 6 ? getText(6) : null, + volOiRatio: cells.length > 7 ? getText(7) : null, + iv: cells.length > 8 ? getText(8) : null, + }); + if (results.length >= limit) break; + } + return results; + } catch(e) { + return []; + } + })() + `); + + if (!data || !Array.isArray(data)) return []; + + return data.slice(0, limit).map(r => ({ + symbol: r.symbol || '', + type: r.type || '', + strike: r.strike, + expiration: r.expiration ?? null, + last: r.last != null ? Number(Number(r.last).toFixed(2)) : null, + volume: r.volume, + openInterest: r.openInterest, + volOiRatio: r.volOiRatio != null ? Number(Number(r.volOiRatio).toFixed(2)) : null, + iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null, + })); + }, +}); diff --git a/src/clis/barchart/greeks.ts b/src/clis/barchart/greeks.ts new file mode 100644 index 0000000..2a5732f --- /dev/null +++ b/src/clis/barchart/greeks.ts @@ -0,0 +1,105 @@ +/** + * Barchart options greeks overview — IV, delta, gamma, theta, vega, rho + * for near-the-money options on a given symbol. + * Auth: CSRF token from + session cookies. + */ +import { cli, Strategy } from '../../registry.js'; + +cli({ + site: 'barchart', + name: 'greeks', + description: 'Barchart options greeks overview (IV, delta, gamma, theta, vega)', + domain: 'www.barchart.com', + strategy: Strategy.COOKIE, + args: [ + { name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL)' }, + { name: 'expiration', type: 'str', help: 'Expiration date (YYYY-MM-DD). Defaults to nearest.' }, + { name: 'limit', type: 'int', default: 10, help: 'Number of near-the-money strikes per type' }, + ], + columns: [ + 'type', 'strike', 'last', 'iv', 'delta', 'gamma', 'theta', 'vega', 'rho', + 'volume', 'openInterest', 'expiration', + ], + func: async (page, kwargs) => { + const symbol = kwargs.symbol.toUpperCase().trim(); + const expiration = kwargs.expiration ?? ''; + const limit = kwargs.limit ?? 10; + + await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/options`); + await page.wait(4); + + const data = await page.evaluate(` + (async () => { + const sym = '${symbol}'; + const expDate = '${expiration}'; + const limit = ${limit}; + const csrf = document.querySelector('meta[name="csrf-token"]')?.content || ''; + const headers = { 'X-CSRF-TOKEN': csrf }; + + try { + const fields = [ + 'strikePrice','lastPrice','volume','openInterest', + 'volatility','delta','gamma','theta','vega','rho', + 'expirationDate','optionType','percentFromLast', + ].join(','); + + let url = '/proxies/core-api/v1/options/chain?symbol=' + encodeURIComponent(sym) + + '&fields=' + fields + '&raw=1'; + if (expDate) url += '&expirationDate=' + encodeURIComponent(expDate); + const resp = await fetch(url, { credentials: 'include', headers }); + if (resp.ok) { + const d = await resp.json(); + const items = d?.data || []; + + // Separate calls and puts, sort by distance from current price + const calls = items + .filter(i => ((i.raw || i).optionType || '').toLowerCase() === 'call') + .sort((a, b) => Math.abs((a.raw || a).percentFromLast || 999) - Math.abs((b.raw || b).percentFromLast || 999)) + .slice(0, limit); + const puts = items + .filter(i => ((i.raw || i).optionType || '').toLowerCase() === 'put') + .sort((a, b) => Math.abs((a.raw || a).percentFromLast || 999) - Math.abs((b.raw || b).percentFromLast || 999)) + .slice(0, limit); + + return [...calls, ...puts].map(i => { + const r = i.raw || i; + return { + type: r.optionType, + strike: r.strikePrice, + last: r.lastPrice, + iv: r.volatility, + delta: r.delta, + gamma: r.gamma, + theta: r.theta, + vega: r.vega, + rho: r.rho, + volume: r.volume, + openInterest: r.openInterest, + expiration: r.expirationDate, + }; + }); + } + } catch(e) {} + + return []; + })() + `); + + if (!data || !Array.isArray(data)) return []; + + return data.map(r => ({ + type: r.type || '', + strike: r.strike, + last: r.last != null ? Number(Number(r.last).toFixed(2)) : null, + iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null, + delta: r.delta != null ? Number(Number(r.delta).toFixed(4)) : null, + gamma: r.gamma != null ? Number(Number(r.gamma).toFixed(4)) : null, + theta: r.theta != null ? Number(Number(r.theta).toFixed(4)) : null, + vega: r.vega != null ? Number(Number(r.vega).toFixed(4)) : null, + rho: r.rho != null ? Number(Number(r.rho).toFixed(4)) : null, + volume: r.volume, + openInterest: r.openInterest, + expiration: r.expiration ?? null, + })); + }, +}); diff --git a/src/clis/barchart/options.ts b/src/clis/barchart/options.ts new file mode 100644 index 0000000..56dd523 --- /dev/null +++ b/src/clis/barchart/options.ts @@ -0,0 +1,110 @@ +/** + * Barchart options chain — strike, bid/ask, volume, OI, greeks, IV. + * Auth: CSRF token from + session cookies. + */ +import { cli, Strategy } from '../../registry.js'; + +cli({ + site: 'barchart', + name: 'options', + description: 'Barchart options chain with greeks, IV, volume, and open interest', + domain: 'www.barchart.com', + strategy: Strategy.COOKIE, + args: [ + { name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL)' }, + { name: 'type', type: 'str', default: 'Call', help: 'Option type: Call or Put', choices: ['Call', 'Put'] }, + { name: 'limit', type: 'int', default: 20, help: 'Max number of strikes to return' }, + ], + columns: [ + 'strike', 'bid', 'ask', 'last', 'change', 'volume', 'openInterest', + 'iv', 'delta', 'gamma', 'theta', 'vega', 'expiration', + ], + func: async (page, kwargs) => { + const symbol = kwargs.symbol.toUpperCase().trim(); + const optType = kwargs.type || 'Call'; + const limit = kwargs.limit ?? 20; + + await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/options`); + await page.wait(4); + + const data = await page.evaluate(` + (async () => { + const sym = '${symbol}'; + const type = '${optType}'; + const limit = ${limit}; + const csrf = document.querySelector('meta[name="csrf-token"]')?.content || ''; + const headers = { 'X-CSRF-TOKEN': csrf }; + + // API: options chain with greeks + try { + const fields = [ + 'strikePrice','bidPrice','askPrice','lastPrice','priceChange', + 'volume','openInterest','volatility', + 'delta','gamma','theta','vega', + 'expirationDate','optionType','percentFromLast', + ].join(','); + + const url = '/proxies/core-api/v1/options/chain?symbol=' + encodeURIComponent(sym) + + '&fields=' + fields + '&raw=1'; + const resp = await fetch(url, { credentials: 'include', headers }); + if (resp.ok) { + const d = await resp.json(); + let items = d?.data || []; + + // Filter by type + items = items.filter(i => { + const t = (i.raw || i).optionType || ''; + return t.toLowerCase() === type.toLowerCase(); + }); + + // Sort by closeness to current price + items.sort((a, b) => { + const aD = Math.abs((a.raw || a).percentFromLast || 999); + const bD = Math.abs((b.raw || b).percentFromLast || 999); + return aD - bD; + }); + + return items.slice(0, limit).map(i => { + const r = i.raw || i; + return { + strike: r.strikePrice, + bid: r.bidPrice, + ask: r.askPrice, + last: r.lastPrice, + change: r.priceChange, + volume: r.volume, + openInterest: r.openInterest, + iv: r.volatility, + delta: r.delta, + gamma: r.gamma, + theta: r.theta, + vega: r.vega, + expiration: r.expirationDate, + }; + }); + } + } catch(e) {} + + return []; + })() + `); + + if (!data || !Array.isArray(data)) return []; + + return data.map(r => ({ + strike: r.strike, + bid: r.bid != null ? Number(Number(r.bid).toFixed(2)) : null, + ask: r.ask != null ? Number(Number(r.ask).toFixed(2)) : null, + last: r.last != null ? Number(Number(r.last).toFixed(2)) : null, + change: r.change != null ? Number(Number(r.change).toFixed(2)) : null, + volume: r.volume, + openInterest: r.openInterest, + iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null, + delta: r.delta != null ? Number(Number(r.delta).toFixed(4)) : null, + gamma: r.gamma != null ? Number(Number(r.gamma).toFixed(4)) : null, + theta: r.theta != null ? Number(Number(r.theta).toFixed(4)) : null, + vega: r.vega != null ? Number(Number(r.vega).toFixed(4)) : null, + expiration: r.expiration ?? null, + })); + }, +}); diff --git a/src/clis/barchart/quote.ts b/src/clis/barchart/quote.ts new file mode 100644 index 0000000..6d27014 --- /dev/null +++ b/src/clis/barchart/quote.ts @@ -0,0 +1,137 @@ +/** + * Barchart stock quote — price, volume, market cap, P/E, EPS, and key metrics. + * Auth: CSRF token from + session cookies. + */ +import { cli, Strategy } from '../../registry.js'; + +cli({ + site: 'barchart', + name: 'quote', + description: 'Barchart stock quote with price, volume, and key metrics', + domain: 'www.barchart.com', + strategy: Strategy.COOKIE, + args: [ + { name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL, MSFT, TSLA)' }, + ], + columns: [ + 'symbol', 'name', 'price', 'change', 'changePct', + 'open', 'high', 'low', 'prevClose', 'volume', + 'avgVolume', 'marketCap', 'peRatio', 'eps', + ], + func: async (page, kwargs) => { + const symbol = kwargs.symbol.toUpperCase().trim(); + await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/overview`); + await page.wait(4); + + const data = await page.evaluate(` + (async () => { + const sym = '${symbol}'; + const csrf = document.querySelector('meta[name="csrf-token"]')?.content || ''; + + // Strategy 1: internal proxy API with CSRF token + try { + const fields = [ + 'symbol','symbolName','lastPrice','priceChange','percentChange', + 'highPrice','lowPrice','openPrice','previousPrice','volume','averageVolume', + 'marketCap','peRatio','earningsPerShare','tradeTime', + ].join(','); + const url = '/proxies/core-api/v1/quotes/get?symbol=' + encodeURIComponent(sym) + '&fields=' + fields; + const resp = await fetch(url, { + credentials: 'include', + headers: { 'X-CSRF-TOKEN': csrf }, + }); + if (resp.ok) { + const d = await resp.json(); + const row = d?.data?.[0] || null; + if (row) { + return { source: 'api', row }; + } + } + } catch(e) {} + + // Strategy 2: parse from DOM + try { + const priceEl = document.querySelector('span.last-change'); + const price = priceEl ? priceEl.textContent.trim() : null; + + // Change values are sibling spans inside .pricechangerow > .last-change + const changeParent = priceEl?.parentElement; + const changeSpans = changeParent ? changeParent.querySelectorAll('span') : []; + let change = null; + let changePct = null; + for (const s of changeSpans) { + const t = s.textContent.trim(); + if (s === priceEl) continue; + if (t.includes('%')) changePct = t.replace(/[()]/g, ''); + else if (t.match(/^[+-]?[\\d.]+$/)) change = t; + } + + // Financial data rows + const rows = document.querySelectorAll('.financial-data-row'); + const fdata = {}; + for (const row of rows) { + const spans = row.querySelectorAll('span'); + if (spans.length >= 2) { + const label = spans[0].textContent.trim(); + const valSpan = row.querySelector('span.right span:not(.ng-hide)'); + fdata[label] = valSpan ? valSpan.textContent.trim() : ''; + } + } + + // Day high/low from row chart + const dayLow = document.querySelector('.bc-quote-row-chart .small-6:first-child .inline:not(.ng-hide)'); + const dayHigh = document.querySelector('.bc-quote-row-chart .text-right .inline:not(.ng-hide)'); + const openEl = document.querySelector('.mark span'); + const openText = openEl ? openEl.textContent.trim().replace('Open ', '') : null; + + const name = document.querySelector('h1 span.symbol'); + + return { + source: 'dom', + row: { + symbol: sym, + symbolName: name ? name.textContent.trim() : sym, + lastPrice: price, + priceChange: change, + percentChange: changePct, + open: openText, + highPrice: dayHigh ? dayHigh.textContent.trim() : null, + lowPrice: dayLow ? dayLow.textContent.trim() : null, + previousClose: fdata['Previous Close'] || null, + volume: fdata['Volume'] || null, + averageVolume: fdata['Average Volume'] || null, + marketCap: null, + peRatio: null, + earningsPerShare: null, + } + }; + } catch(e) { + return { error: 'Could not fetch quote for ' + sym + ': ' + e.message }; + } + })() + `); + + if (!data || data.error) return []; + + const r = data.row || {}; + // API returns formatted strings like "+1.41" and "+0.56%"; use raw if available + const raw = r.raw || {}; + + return [{ + symbol: r.symbol || symbol, + name: r.symbolName || r.name || symbol, + price: r.lastPrice ?? null, + change: r.priceChange ?? null, + changePct: r.percentChange ?? null, + open: r.openPrice ?? r.open ?? null, + high: r.highPrice ?? null, + low: r.lowPrice ?? null, + prevClose: r.previousPrice ?? r.previousClose ?? null, + volume: r.volume ?? null, + avgVolume: r.averageVolume ?? null, + marketCap: r.marketCap ?? null, + peRatio: r.peRatio ?? null, + eps: r.earningsPerShare ?? null, + }]; + }, +});