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,
+ }];
+ },
+});