Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

209 lines
8.8 KiB
JavaScript

/**
* 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 '@jackwener/opencli/registry';
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
const DEFAULT_LIMIT = 10;
const MIN_LIMIT = 1;
const MAX_LIMIT = 100;
function normalizeSymbol(value) {
const symbol = String(value ?? '').trim().toUpperCase();
if (!symbol) throw new ArgumentError('symbol is required');
return symbol;
}
function normalizeExpiration(value) {
const expiration = String(value ?? '').trim();
if (!expiration) return '';
if (!/^\d{4}-\d{2}-\d{2}$/.test(expiration)) {
throw new ArgumentError('--expiration must use YYYY-MM-DD format');
}
const parsed = new Date(`${expiration}T00:00:00Z`);
if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== expiration) {
throw new ArgumentError('--expiration must be a valid calendar date');
}
return expiration;
}
function parseLimit(value) {
if (value === undefined || value === null || value === '') return DEFAULT_LIMIT;
const limit = Number(value);
if (!Number.isInteger(limit) || limit < MIN_LIMIT || limit > MAX_LIMIT) {
throw new ArgumentError(`--limit must be an integer between ${MIN_LIMIT} and ${MAX_LIMIT}`);
}
return limit;
}
function unwrapBrowserResult(value) {
if (value && typeof value === 'object' && 'session' in value && 'data' in value) {
return value.data;
}
return value;
}
cli({
site: 'barchart',
name: 'greeks',
access: 'read',
description: 'Barchart options greeks overview (IV, delta, gamma, theta, vega)',
domain: 'www.barchart.com',
strategy: Strategy.COOKIE,
args: [
{ name: 'symbol', required: true, positional: true, help: 'Stock ticker (e.g. AAPL)' },
{ name: 'expiration', type: 'str', help: 'Expiration date (YYYY-MM-DD). Defaults to the nearest available expiration.' },
{ name: 'limit', type: 'int', default: DEFAULT_LIMIT, help: 'Number of near-the-money strikes per type (1-100)' },
],
columns: [
'type', 'strike', 'last', 'iv', 'delta', 'gamma', 'theta', 'vega', 'rho',
'volume', 'openInterest', 'expiration',
],
func: async (page, kwargs) => {
const symbol = normalizeSymbol(kwargs.symbol);
const expiration = normalizeExpiration(kwargs.expiration);
const limit = parseLimit(kwargs.limit);
await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/options`);
await page.wait(4);
const data = unwrapBrowserResult(await page.evaluate(`
(async () => {
const sym = ${JSON.stringify(symbol)};
const expDate = ${JSON.stringify(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) {
return { ok: false, reason: 'http', status: resp.status, statusText: resp.statusText || '' };
}
const d = await resp.json();
const allItems = d?.data;
if (!Array.isArray(allItems)) {
return { ok: false, reason: 'malformed' };
}
let items = allItems;
if (!expDate) {
const expirations = items
.map(i => (i.raw || i).expirationDate || null)
.filter(Boolean)
.sort((a, b) => {
const aTime = Date.parse(a);
const bTime = Date.parse(b);
if (Number.isNaN(aTime) && Number.isNaN(bTime)) return 0;
if (Number.isNaN(aTime)) return 1;
if (Number.isNaN(bTime)) return -1;
return aTime - bTime;
});
const nearestExpiration = expirations[0];
if (nearestExpiration) {
items = items.filter(i => ((i.raw || i).expirationDate || null) === nearestExpiration);
}
}
// 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);
const selected = [...calls, ...puts];
if (items.length > 0 && selected.length === 0) {
return { ok: false, reason: 'malformed', message: 'options rows did not include call or put identities' };
}
return {
ok: true,
rows: selected.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 { ok: false, reason: 'exception', message: e?.message || String(e) };
}
})()
`));
if (!data || data.ok !== true) {
if (data?.reason === 'http') {
throw new CommandExecutionError(`Barchart greeks request failed: HTTP ${data.status}${data.statusText ? ` ${data.statusText}` : ''}`);
}
if (data?.reason === 'malformed') {
throw new CommandExecutionError(`Barchart greeks returned an unreadable options payload${data.message ? `: ${data.message}` : ''}`);
}
if (data?.reason === 'exception') {
throw new CommandExecutionError(`Barchart greeks request failed: ${data.message || 'unknown error'}`);
}
throw new CommandExecutionError(`Failed to fetch Barchart greeks for ${symbol}`);
}
if (!Array.isArray(data.rows)) {
throw new CommandExecutionError('Barchart greeks returned an unreadable options payload');
}
if (data.rows.length === 0) {
throw new EmptyResultError('barchart greeks', `No option greeks were returned for ${symbol}. Confirm the symbol, expiration, and Barchart login state.`);
}
return data.rows.map(r => {
if (!r || typeof r !== 'object' || Array.isArray(r)) {
throw new CommandExecutionError('Barchart greeks returned a malformed option row');
}
const type = String(r.type || '').trim();
const expirationValue = String(r.expiration || '').trim();
if (!/^(call|put)$/i.test(type) || r.strike === undefined || r.strike === null || r.strike === '' || !expirationValue) {
throw new CommandExecutionError('Barchart greeks returned a malformed option row identity');
}
return {
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: expirationValue,
};
});
},
});
export const __test__ = {
normalizeSymbol,
normalizeExpiration,
parseLimit,
unwrapBrowserResult,
};