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 <meta name="csrf-token"> + session cookies
via the internal proxy API, with DOM fallback for the quote command.

* feat(barchart): add --expiration date filter to greeks command
This commit is contained in:
Alex Yang authored and GitHub committed 2026-03-18 12:13:25 +08:00
1 parent de5495bdd7
commit c806f795cc
4 files changed
+481

No files matched your search

+129
View File
@@ -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 <meta name="csrf-token"> + 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,
}));
},
});
+105
View File
@@ -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 <meta name="csrf-token"> + 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,
}));
},
});
+110
View File
@@ -0,0 +1,110 @@
/**
* Barchart options chain — strike, bid/ask, volume, OI, greeks, IV.
* Auth: CSRF token from <meta name="csrf-token"> + 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,
}));
},
});
+137
View File
@@ -0,0 +1,137 @@
/**
* Barchart stock quote — price, volume, market cap, P/E, EPS, and key metrics.
* Auth: CSRF token from <meta name="csrf-token"> + 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,
}];
},
});