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:
1 parent
de5495bdd7
commit
c806f795cc
4 files changed
+481
No files matched your search
@@ -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,
|
||||
}));
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
}));
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
}));
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
}];
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user