Files
Alex Yang 7d67321a31 Fix tradingview plugin func signature for browser:false commands (#69)
opencli runtime (1.7.19) calls `cmd.func(kwargs, debug)` for `browser: false`
commands, but every data command in this plugin declared the legacy
two-arg `(_page, args)` form. That made `args` receive `debug` (undefined)
and every ticker / strike / filter argument deserialize to `undefined` —
e.g. `quote --ticker SMTC` failed with `No quote returned for UNDEFINED:UNDEFINED`.

Drop the unused `_page` parameter so the signature matches the runtime
contract for non-browser commands. Affected commands: alerts, chart-state,
launch, news, options-chain, options-expiries, quote, screener, screenshot,
search, watchlists. (status was already correct — takes no args.)

Verified: `opencli tradingview quote --ticker SMTC --exchange NASDAQ` returns
`NASDAQ:SMTC close 139.74` after the fix. Plugin tests (42/42) still pass.
2026-05-13 16:38:53 -07:00

88 lines
3.0 KiB
JavaScript

/**
* tradingview options-chain — full chain or filtered slice via scanner.tradingview.com.
*
* One POST to /options/scan2 returns the entire chain (all expiries, all strikes,
* calls + puts) in TradingView's compressed `{fields, symbols}` form.
*/
import { cli, Strategy } from '@jackwener/opencli/registry';
import {
buildChainBody,
buildQuoteBody,
decodeScannerRows,
normalizeChainRow,
scannerFetch,
strikesAroundSpot,
} from './lib/scanner.js';
const DEFAULT_HALF_BAND = 6;
cli({
site: 'tradingview',
name: 'options-chain',
description: 'Options chain (full or ATM-band slice) with greeks, IV, and theoretical price',
access: 'read',
strategy: Strategy.PUBLIC,
browser: false,
args: [
{ name: 'ticker', required: true, help: 'Underlying ticker (e.g. SNDK)' },
{ name: 'exchange', default: 'NASDAQ', help: 'TradingView exchange code' },
{ name: 'expiry', help: 'ISO expiry (YYYY-MM-DD). Omit for all expiries.' },
{ name: 'type', choices: ['call', 'put'], help: 'Filter to call or put only' },
{
name: 'strikes-around-spot',
type: 'int',
default: DEFAULT_HALF_BAND,
help: `Half-band; total strikes = 2N+1. 0 = full strike list. (default ${DEFAULT_HALF_BAND})`,
},
{
name: 'include-expired',
type: 'boolean',
default: false,
help: 'Include expired contracts (DTE < 0). Default: false',
},
],
columns: [
'expiry', 'dte', 'strike', 'type', 'bid', 'ask', 'mid', 'iv',
'delta', 'gamma', 'theta', 'vega', 'rho', 'theo', 'bid_iv', 'ask_iv', 'symbol',
],
func: async (args) => {
const ticker = String(args.ticker).toUpperCase().trim();
const exchange = String(args.exchange).toUpperCase().trim();
const wantExpiry = args.expiry ? String(args.expiry) : null;
const wantType = args.type ? String(args.type).toLowerCase() : null;
const halfBand = Number(args['strikes-around-spot'] ?? DEFAULT_HALF_BAND);
const includeExpired = Boolean(args['include-expired']);
const [chainPayload, quotePayload] = await Promise.all([
scannerFetch('options/scan2', buildChainBody(exchange, ticker)),
scannerFetch('global/scan2', buildQuoteBody(exchange, ticker)),
]);
let rows = decodeScannerRows(chainPayload).map((r) => normalizeChainRow(r));
if (rows.length === 0) {
throw new Error(
`Empty chain for ${exchange}:${ticker}. The logged-in account's tier may not include options for this symbol.`,
);
}
if (!includeExpired) rows = rows.filter((r) => r.dte >= 0);
if (wantExpiry) rows = rows.filter((r) => r.expiry === wantExpiry);
if (wantType) rows = rows.filter((r) => r.type === wantType);
if (halfBand > 0) {
const quoteRows = decodeScannerRows(quotePayload);
const spot = Number(quoteRows[0]?.close);
if (Number.isFinite(spot)) {
rows = strikesAroundSpot(rows, spot, halfBand);
}
}
rows.sort((a, b) =>
a.expiry.localeCompare(b.expiry) || a.strike - b.strike || a.type.localeCompare(b.type),
);
return rows;
},
});