@jackwener/opencli 0.7.10 → 0.7.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/pkg-pr-new.yml +30 -0
- package/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/dist/browser/discover.d.ts +15 -0
- package/dist/browser/discover.js +60 -12
- package/dist/browser/index.d.ts +5 -1
- package/dist/browser/index.js +5 -1
- package/dist/browser/mcp.js +7 -6
- package/dist/browser.test.js +135 -1
- package/dist/cli-manifest.json +163 -0
- package/dist/clis/barchart/flow.d.ts +1 -0
- package/dist/clis/barchart/flow.js +117 -0
- package/dist/clis/barchart/greeks.d.ts +1 -0
- package/dist/clis/barchart/greeks.js +119 -0
- package/dist/clis/barchart/options.d.ts +1 -0
- package/dist/clis/barchart/options.js +106 -0
- package/dist/clis/barchart/quote.d.ts +1 -0
- package/dist/clis/barchart/quote.js +133 -0
- package/package.json +1 -1
- package/src/browser/discover.ts +73 -12
- package/src/browser/index.ts +5 -1
- package/src/browser/mcp.ts +7 -5
- package/src/browser.test.ts +140 -1
- package/src/clis/barchart/flow.ts +121 -0
- package/src/clis/barchart/greeks.ts +123 -0
- package/src/clis/barchart/options.ts +110 -0
- package/src/clis/barchart/quote.ts +137 -0
- package/vitest.config.ts +7 -0
package/src/browser/mcp.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { withTimeoutMs, DEFAULT_BROWSER_CONNECT_TIMEOUT } from '../runtime.js';
|
|
|
9
9
|
import { PKG_VERSION } from '../version.js';
|
|
10
10
|
import { Page } from './page.js';
|
|
11
11
|
import { getTokenFingerprint, formatBrowserConnectError, inferConnectFailureKind } from './errors.js';
|
|
12
|
-
import { findMcpServerPath,
|
|
12
|
+
import { findMcpServerPath, buildMcpLaunchSpec } from './discover.js';
|
|
13
13
|
import { extractTabIdentities, extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
|
|
14
14
|
|
|
15
15
|
const STDERR_BUFFER_LIMIT = 16 * 1024;
|
|
@@ -105,7 +105,6 @@ export class PlaywrightMCP {
|
|
|
105
105
|
if (this._state === 'closed') throw new Error('Playwright MCP session is closed');
|
|
106
106
|
|
|
107
107
|
const mcpPath = findMcpServerPath();
|
|
108
|
-
if (!mcpPath) throw new Error('Playwright MCP server not found. Install: npm install -D @playwright/mcp');
|
|
109
108
|
|
|
110
109
|
PlaywrightMCP._registerGlobalCleanup();
|
|
111
110
|
PlaywrightMCP._activeInsts.add(this);
|
|
@@ -154,17 +153,20 @@ export class PlaywrightMCP {
|
|
|
154
153
|
}));
|
|
155
154
|
}, timeout * 1000);
|
|
156
155
|
|
|
157
|
-
const
|
|
156
|
+
const launchSpec = buildMcpLaunchSpec({
|
|
158
157
|
mcpPath,
|
|
159
158
|
executablePath: process.env.OPENCLI_BROWSER_EXECUTABLE_PATH,
|
|
160
159
|
});
|
|
161
160
|
if (process.env.OPENCLI_VERBOSE) {
|
|
162
161
|
console.error(`[opencli] Mode: ${useExtension ? 'extension' : 'standalone'}`);
|
|
163
162
|
if (useExtension) console.error(`[opencli] Extension token: fingerprint ${tokenFingerprint}`);
|
|
163
|
+
if (launchSpec.usedNpxFallback) {
|
|
164
|
+
console.error('[opencli] Playwright MCP not found locally; bootstrapping via npx @playwright/mcp@latest');
|
|
165
|
+
}
|
|
164
166
|
}
|
|
165
|
-
debugLog(`Spawning
|
|
167
|
+
debugLog(`Spawning ${launchSpec.command} ${launchSpec.args.join(' ')}`);
|
|
166
168
|
|
|
167
|
-
this._proc = spawn(
|
|
169
|
+
this._proc = spawn(launchSpec.command, launchSpec.args, {
|
|
168
170
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
169
171
|
env: { ...process.env },
|
|
170
172
|
});
|
package/src/browser.test.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
1
|
+
import { afterEach, describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { PlaywrightMCP, __test__ } from './browser/index.js';
|
|
3
3
|
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
__test__.resetMcpServerPathCache();
|
|
6
|
+
__test__.setMcpDiscoveryTestHooks();
|
|
7
|
+
delete process.env.OPENCLI_MCP_SERVER_PATH;
|
|
8
|
+
});
|
|
9
|
+
|
|
4
10
|
describe('browser helpers', () => {
|
|
5
11
|
it('creates JSON-RPC requests with unique ids', () => {
|
|
6
12
|
const first = __test__.createJsonRpcRequest('tools/call', { name: 'browser_tabs' });
|
|
@@ -106,9 +112,142 @@ describe('browser helpers', () => {
|
|
|
106
112
|
}
|
|
107
113
|
});
|
|
108
114
|
|
|
115
|
+
it('builds a direct node launch spec when a local MCP path is available', () => {
|
|
116
|
+
const savedCI = process.env.CI;
|
|
117
|
+
delete process.env.CI;
|
|
118
|
+
try {
|
|
119
|
+
expect(__test__.buildMcpLaunchSpec({
|
|
120
|
+
mcpPath: '/tmp/cli.js',
|
|
121
|
+
executablePath: '/usr/bin/google-chrome',
|
|
122
|
+
})).toEqual({
|
|
123
|
+
command: 'node',
|
|
124
|
+
args: ['/tmp/cli.js', '--extension', '--executable-path', '/usr/bin/google-chrome'],
|
|
125
|
+
usedNpxFallback: false,
|
|
126
|
+
});
|
|
127
|
+
} finally {
|
|
128
|
+
if (savedCI !== undefined) {
|
|
129
|
+
process.env.CI = savedCI;
|
|
130
|
+
} else {
|
|
131
|
+
delete process.env.CI;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('falls back to npx bootstrap when no MCP path is available', () => {
|
|
137
|
+
const savedCI = process.env.CI;
|
|
138
|
+
delete process.env.CI;
|
|
139
|
+
try {
|
|
140
|
+
expect(__test__.buildMcpLaunchSpec({
|
|
141
|
+
mcpPath: null,
|
|
142
|
+
})).toEqual({
|
|
143
|
+
command: 'npx',
|
|
144
|
+
args: ['-y', '@playwright/mcp@latest', '--extension'],
|
|
145
|
+
usedNpxFallback: true,
|
|
146
|
+
});
|
|
147
|
+
} finally {
|
|
148
|
+
if (savedCI !== undefined) {
|
|
149
|
+
process.env.CI = savedCI;
|
|
150
|
+
} else {
|
|
151
|
+
delete process.env.CI;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
109
156
|
it('times out slow promises', async () => {
|
|
110
157
|
await expect(__test__.withTimeoutMs(new Promise(() => {}), 10, 'timeout')).rejects.toThrow('timeout');
|
|
111
158
|
});
|
|
159
|
+
|
|
160
|
+
it('prefers OPENCLI_MCP_SERVER_PATH over discovered locations', () => {
|
|
161
|
+
process.env.OPENCLI_MCP_SERVER_PATH = '/env/mcp/cli.js';
|
|
162
|
+
const existsSync = vi.fn((candidate: any) => candidate === '/env/mcp/cli.js');
|
|
163
|
+
const execSync = vi.fn();
|
|
164
|
+
__test__.setMcpDiscoveryTestHooks({ existsSync, execSync: execSync as any });
|
|
165
|
+
|
|
166
|
+
expect(__test__.findMcpServerPath()).toBe('/env/mcp/cli.js');
|
|
167
|
+
expect(execSync).not.toHaveBeenCalled();
|
|
168
|
+
expect(existsSync).toHaveBeenCalledWith('/env/mcp/cli.js');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('discovers global @playwright/mcp from the current Node runtime prefix', () => {
|
|
172
|
+
const originalExecPath = process.execPath;
|
|
173
|
+
const runtimeExecPath = '/opt/homebrew/Cellar/node/25.2.1/bin/node';
|
|
174
|
+
const runtimeGlobalMcp = '/opt/homebrew/Cellar/node/25.2.1/lib/node_modules/@playwright/mcp/cli.js';
|
|
175
|
+
Object.defineProperty(process, 'execPath', {
|
|
176
|
+
value: runtimeExecPath,
|
|
177
|
+
configurable: true,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const existsSync = vi.fn((candidate: any) => candidate === runtimeGlobalMcp);
|
|
181
|
+
const execSync = vi.fn();
|
|
182
|
+
__test__.setMcpDiscoveryTestHooks({ existsSync, execSync: execSync as any });
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
expect(__test__.findMcpServerPath()).toBe(runtimeGlobalMcp);
|
|
186
|
+
expect(execSync).not.toHaveBeenCalled();
|
|
187
|
+
expect(existsSync).toHaveBeenCalledWith(runtimeGlobalMcp);
|
|
188
|
+
} finally {
|
|
189
|
+
Object.defineProperty(process, 'execPath', {
|
|
190
|
+
value: originalExecPath,
|
|
191
|
+
configurable: true,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('falls back to npm root -g when runtime prefix lookup misses', () => {
|
|
197
|
+
const originalExecPath = process.execPath;
|
|
198
|
+
const runtimeExecPath = '/opt/homebrew/Cellar/node/25.2.1/bin/node';
|
|
199
|
+
const runtimeGlobalMcp = '/opt/homebrew/Cellar/node/25.2.1/lib/node_modules/@playwright/mcp/cli.js';
|
|
200
|
+
const npmRootGlobal = '/Users/jakevin/.nvm/versions/node/v22.14.0/lib/node_modules';
|
|
201
|
+
const npmGlobalMcp = '/Users/jakevin/.nvm/versions/node/v22.14.0/lib/node_modules/@playwright/mcp/cli.js';
|
|
202
|
+
Object.defineProperty(process, 'execPath', {
|
|
203
|
+
value: runtimeExecPath,
|
|
204
|
+
configurable: true,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const existsSync = vi.fn((candidate: any) => candidate === npmGlobalMcp);
|
|
208
|
+
const execSync = vi.fn((command: string) => {
|
|
209
|
+
if (String(command).includes('npm root -g')) return `${npmRootGlobal}\n` as any;
|
|
210
|
+
throw new Error(`unexpected command: ${String(command)}`);
|
|
211
|
+
});
|
|
212
|
+
__test__.setMcpDiscoveryTestHooks({ existsSync, execSync: execSync as any });
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
expect(__test__.findMcpServerPath()).toBe(npmGlobalMcp);
|
|
216
|
+
expect(execSync).toHaveBeenCalledOnce();
|
|
217
|
+
expect(existsSync).toHaveBeenCalledWith(runtimeGlobalMcp);
|
|
218
|
+
expect(existsSync).toHaveBeenCalledWith(npmGlobalMcp);
|
|
219
|
+
} finally {
|
|
220
|
+
Object.defineProperty(process, 'execPath', {
|
|
221
|
+
value: originalExecPath,
|
|
222
|
+
configurable: true,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('returns null when new global discovery paths are unavailable', () => {
|
|
228
|
+
const originalExecPath = process.execPath;
|
|
229
|
+
const runtimeExecPath = '/opt/homebrew/Cellar/node/25.2.1/bin/node';
|
|
230
|
+
Object.defineProperty(process, 'execPath', {
|
|
231
|
+
value: runtimeExecPath,
|
|
232
|
+
configurable: true,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const existsSync = vi.fn(() => false);
|
|
236
|
+
const execSync = vi.fn((command: string) => {
|
|
237
|
+
if (String(command).includes('npm root -g')) return '/missing/global/node_modules\n' as any;
|
|
238
|
+
throw new Error(`missing command: ${String(command)}`);
|
|
239
|
+
});
|
|
240
|
+
__test__.setMcpDiscoveryTestHooks({ existsSync, execSync: execSync as any });
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
expect(__test__.findMcpServerPath()).toBeNull();
|
|
244
|
+
} finally {
|
|
245
|
+
Object.defineProperty(process, 'execPath', {
|
|
246
|
+
value: originalExecPath,
|
|
247
|
+
configurable: true,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
});
|
|
112
251
|
});
|
|
113
252
|
|
|
114
253
|
describe('PlaywrightMCP state', () => {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barchart unusual options activity (options flow).
|
|
3
|
+
* Shows high volume/OI ratio trades that may indicate institutional activity.
|
|
4
|
+
* Auth: CSRF token from <meta name="csrf-token"> + session cookies.
|
|
5
|
+
*/
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
|
|
8
|
+
cli({
|
|
9
|
+
site: 'barchart',
|
|
10
|
+
name: 'flow',
|
|
11
|
+
description: 'Barchart unusual options activity / options flow',
|
|
12
|
+
domain: 'www.barchart.com',
|
|
13
|
+
strategy: Strategy.COOKIE,
|
|
14
|
+
args: [
|
|
15
|
+
{ name: 'type', type: 'str', default: 'all', help: 'Filter: all, call, or put', choices: ['all', 'call', 'put'] },
|
|
16
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
17
|
+
],
|
|
18
|
+
columns: [
|
|
19
|
+
'symbol', 'type', 'strike', 'expiration', 'last',
|
|
20
|
+
'volume', 'openInterest', 'volOiRatio', 'iv',
|
|
21
|
+
],
|
|
22
|
+
func: async (page, kwargs) => {
|
|
23
|
+
const optionType = kwargs.type || 'all';
|
|
24
|
+
const limit = kwargs.limit ?? 20;
|
|
25
|
+
|
|
26
|
+
await page.goto('https://www.barchart.com/options/unusual-activity/stocks');
|
|
27
|
+
await page.wait(5);
|
|
28
|
+
|
|
29
|
+
const data = await page.evaluate(`
|
|
30
|
+
(async () => {
|
|
31
|
+
const limit = ${limit};
|
|
32
|
+
const typeFilter = '${optionType}'.toLowerCase();
|
|
33
|
+
const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
34
|
+
const headers = { 'X-CSRF-TOKEN': csrf };
|
|
35
|
+
|
|
36
|
+
const fields = [
|
|
37
|
+
'baseSymbol','strikePrice','expirationDate','optionType',
|
|
38
|
+
'lastPrice','volume','openInterest','volumeOpenInterestRatio','volatility',
|
|
39
|
+
].join(',');
|
|
40
|
+
|
|
41
|
+
// Fetch extra rows when filtering by type since server-side filter may not work
|
|
42
|
+
const fetchLimit = typeFilter !== 'all' ? limit * 3 : limit;
|
|
43
|
+
try {
|
|
44
|
+
const url = '/proxies/core-api/v1/options/get?list=options.unusual_activity.stocks.us'
|
|
45
|
+
+ '&fields=' + fields
|
|
46
|
+
+ '&orderBy=volumeOpenInterestRatio&orderDir=desc'
|
|
47
|
+
+ '&raw=1&limit=' + fetchLimit;
|
|
48
|
+
|
|
49
|
+
const resp = await fetch(url, { credentials: 'include', headers });
|
|
50
|
+
if (resp.ok) {
|
|
51
|
+
const d = await resp.json();
|
|
52
|
+
let items = d?.data || [];
|
|
53
|
+
if (items.length > 0) {
|
|
54
|
+
// Apply client-side type filter
|
|
55
|
+
if (typeFilter !== 'all') {
|
|
56
|
+
items = items.filter(i => {
|
|
57
|
+
const t = ((i.raw || i).optionType || '').toLowerCase();
|
|
58
|
+
return t === typeFilter;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return items.slice(0, limit).map(i => {
|
|
62
|
+
const r = i.raw || i;
|
|
63
|
+
return {
|
|
64
|
+
symbol: r.baseSymbol || r.symbol,
|
|
65
|
+
type: r.optionType,
|
|
66
|
+
strike: r.strikePrice,
|
|
67
|
+
expiration: r.expirationDate,
|
|
68
|
+
last: r.lastPrice,
|
|
69
|
+
volume: r.volume,
|
|
70
|
+
openInterest: r.openInterest,
|
|
71
|
+
volOiRatio: r.volumeOpenInterestRatio,
|
|
72
|
+
iv: r.volatility,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch(e) {}
|
|
78
|
+
|
|
79
|
+
// Fallback: parse from DOM table
|
|
80
|
+
try {
|
|
81
|
+
const rows = document.querySelectorAll('tr[data-ng-repeat], tbody tr');
|
|
82
|
+
const results = [];
|
|
83
|
+
for (const row of rows) {
|
|
84
|
+
const cells = row.querySelectorAll('td');
|
|
85
|
+
if (cells.length < 6) continue;
|
|
86
|
+
const getText = (idx) => cells[idx]?.textContent?.trim() || null;
|
|
87
|
+
results.push({
|
|
88
|
+
symbol: getText(0),
|
|
89
|
+
type: getText(1),
|
|
90
|
+
strike: getText(2),
|
|
91
|
+
expiration: getText(3),
|
|
92
|
+
last: getText(4),
|
|
93
|
+
volume: getText(5),
|
|
94
|
+
openInterest: cells.length > 6 ? getText(6) : null,
|
|
95
|
+
volOiRatio: cells.length > 7 ? getText(7) : null,
|
|
96
|
+
iv: cells.length > 8 ? getText(8) : null,
|
|
97
|
+
});
|
|
98
|
+
if (results.length >= limit) break;
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
} catch(e) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
})()
|
|
105
|
+
`);
|
|
106
|
+
|
|
107
|
+
if (!data || !Array.isArray(data)) return [];
|
|
108
|
+
|
|
109
|
+
return data.slice(0, limit).map(r => ({
|
|
110
|
+
symbol: r.symbol || '',
|
|
111
|
+
type: r.type || '',
|
|
112
|
+
strike: r.strike,
|
|
113
|
+
expiration: r.expiration ?? null,
|
|
114
|
+
last: r.last != null ? Number(Number(r.last).toFixed(2)) : null,
|
|
115
|
+
volume: r.volume,
|
|
116
|
+
openInterest: r.openInterest,
|
|
117
|
+
volOiRatio: r.volOiRatio != null ? Number(Number(r.volOiRatio).toFixed(2)) : null,
|
|
118
|
+
iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null,
|
|
119
|
+
}));
|
|
120
|
+
},
|
|
121
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barchart options greeks overview — IV, delta, gamma, theta, vega, rho
|
|
3
|
+
* for near-the-money options on a given symbol.
|
|
4
|
+
* Auth: CSRF token from <meta name="csrf-token"> + session cookies.
|
|
5
|
+
*/
|
|
6
|
+
import { cli, Strategy } from '../../registry.js';
|
|
7
|
+
|
|
8
|
+
cli({
|
|
9
|
+
site: 'barchart',
|
|
10
|
+
name: 'greeks',
|
|
11
|
+
description: 'Barchart options greeks overview (IV, delta, gamma, theta, vega)',
|
|
12
|
+
domain: 'www.barchart.com',
|
|
13
|
+
strategy: Strategy.COOKIE,
|
|
14
|
+
args: [
|
|
15
|
+
{ name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL)' },
|
|
16
|
+
{ name: 'expiration', type: 'str', help: 'Expiration date (YYYY-MM-DD). Defaults to the nearest available expiration.' },
|
|
17
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Number of near-the-money strikes per type' },
|
|
18
|
+
],
|
|
19
|
+
columns: [
|
|
20
|
+
'type', 'strike', 'last', 'iv', 'delta', 'gamma', 'theta', 'vega', 'rho',
|
|
21
|
+
'volume', 'openInterest', 'expiration',
|
|
22
|
+
],
|
|
23
|
+
func: async (page, kwargs) => {
|
|
24
|
+
const symbol = kwargs.symbol.toUpperCase().trim();
|
|
25
|
+
const expiration = kwargs.expiration ?? '';
|
|
26
|
+
const limit = kwargs.limit ?? 10;
|
|
27
|
+
|
|
28
|
+
await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/options`);
|
|
29
|
+
await page.wait(4);
|
|
30
|
+
|
|
31
|
+
const data = await page.evaluate(`
|
|
32
|
+
(async () => {
|
|
33
|
+
const sym = '${symbol}';
|
|
34
|
+
const expDate = '${expiration}';
|
|
35
|
+
const limit = ${limit};
|
|
36
|
+
const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
37
|
+
const headers = { 'X-CSRF-TOKEN': csrf };
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const fields = [
|
|
41
|
+
'strikePrice','lastPrice','volume','openInterest',
|
|
42
|
+
'volatility','delta','gamma','theta','vega','rho',
|
|
43
|
+
'expirationDate','optionType','percentFromLast',
|
|
44
|
+
].join(',');
|
|
45
|
+
|
|
46
|
+
let url = '/proxies/core-api/v1/options/chain?symbol=' + encodeURIComponent(sym)
|
|
47
|
+
+ '&fields=' + fields + '&raw=1';
|
|
48
|
+
if (expDate) url += '&expirationDate=' + encodeURIComponent(expDate);
|
|
49
|
+
const resp = await fetch(url, { credentials: 'include', headers });
|
|
50
|
+
if (resp.ok) {
|
|
51
|
+
const d = await resp.json();
|
|
52
|
+
let items = d?.data || [];
|
|
53
|
+
|
|
54
|
+
if (!expDate) {
|
|
55
|
+
const expirations = items
|
|
56
|
+
.map(i => (i.raw || i).expirationDate || null)
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.sort((a, b) => {
|
|
59
|
+
const aTime = Date.parse(a);
|
|
60
|
+
const bTime = Date.parse(b);
|
|
61
|
+
if (Number.isNaN(aTime) && Number.isNaN(bTime)) return 0;
|
|
62
|
+
if (Number.isNaN(aTime)) return 1;
|
|
63
|
+
if (Number.isNaN(bTime)) return -1;
|
|
64
|
+
return aTime - bTime;
|
|
65
|
+
});
|
|
66
|
+
const nearestExpiration = expirations[0];
|
|
67
|
+
if (nearestExpiration) {
|
|
68
|
+
items = items.filter(i => ((i.raw || i).expirationDate || null) === nearestExpiration);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Separate calls and puts, sort by distance from current price
|
|
73
|
+
const calls = items
|
|
74
|
+
.filter(i => ((i.raw || i).optionType || '').toLowerCase() === 'call')
|
|
75
|
+
.sort((a, b) => Math.abs((a.raw || a).percentFromLast || 999) - Math.abs((b.raw || b).percentFromLast || 999))
|
|
76
|
+
.slice(0, limit);
|
|
77
|
+
const puts = items
|
|
78
|
+
.filter(i => ((i.raw || i).optionType || '').toLowerCase() === 'put')
|
|
79
|
+
.sort((a, b) => Math.abs((a.raw || a).percentFromLast || 999) - Math.abs((b.raw || b).percentFromLast || 999))
|
|
80
|
+
.slice(0, limit);
|
|
81
|
+
|
|
82
|
+
return [...calls, ...puts].map(i => {
|
|
83
|
+
const r = i.raw || i;
|
|
84
|
+
return {
|
|
85
|
+
type: r.optionType,
|
|
86
|
+
strike: r.strikePrice,
|
|
87
|
+
last: r.lastPrice,
|
|
88
|
+
iv: r.volatility,
|
|
89
|
+
delta: r.delta,
|
|
90
|
+
gamma: r.gamma,
|
|
91
|
+
theta: r.theta,
|
|
92
|
+
vega: r.vega,
|
|
93
|
+
rho: r.rho,
|
|
94
|
+
volume: r.volume,
|
|
95
|
+
openInterest: r.openInterest,
|
|
96
|
+
expiration: r.expirationDate,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} catch(e) {}
|
|
101
|
+
|
|
102
|
+
return [];
|
|
103
|
+
})()
|
|
104
|
+
`);
|
|
105
|
+
|
|
106
|
+
if (!data || !Array.isArray(data)) return [];
|
|
107
|
+
|
|
108
|
+
return data.map(r => ({
|
|
109
|
+
type: r.type || '',
|
|
110
|
+
strike: r.strike,
|
|
111
|
+
last: r.last != null ? Number(Number(r.last).toFixed(2)) : null,
|
|
112
|
+
iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null,
|
|
113
|
+
delta: r.delta != null ? Number(Number(r.delta).toFixed(4)) : null,
|
|
114
|
+
gamma: r.gamma != null ? Number(Number(r.gamma).toFixed(4)) : null,
|
|
115
|
+
theta: r.theta != null ? Number(Number(r.theta).toFixed(4)) : null,
|
|
116
|
+
vega: r.vega != null ? Number(Number(r.vega).toFixed(4)) : null,
|
|
117
|
+
rho: r.rho != null ? Number(Number(r.rho).toFixed(4)) : null,
|
|
118
|
+
volume: r.volume,
|
|
119
|
+
openInterest: r.openInterest,
|
|
120
|
+
expiration: r.expiration ?? null,
|
|
121
|
+
}));
|
|
122
|
+
},
|
|
123
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barchart options chain — strike, bid/ask, volume, OI, greeks, IV.
|
|
3
|
+
* Auth: CSRF token from <meta name="csrf-token"> + session cookies.
|
|
4
|
+
*/
|
|
5
|
+
import { cli, Strategy } from '../../registry.js';
|
|
6
|
+
|
|
7
|
+
cli({
|
|
8
|
+
site: 'barchart',
|
|
9
|
+
name: 'options',
|
|
10
|
+
description: 'Barchart options chain with greeks, IV, volume, and open interest',
|
|
11
|
+
domain: 'www.barchart.com',
|
|
12
|
+
strategy: Strategy.COOKIE,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL)' },
|
|
15
|
+
{ name: 'type', type: 'str', default: 'Call', help: 'Option type: Call or Put', choices: ['Call', 'Put'] },
|
|
16
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Max number of strikes to return' },
|
|
17
|
+
],
|
|
18
|
+
columns: [
|
|
19
|
+
'strike', 'bid', 'ask', 'last', 'change', 'volume', 'openInterest',
|
|
20
|
+
'iv', 'delta', 'gamma', 'theta', 'vega', 'expiration',
|
|
21
|
+
],
|
|
22
|
+
func: async (page, kwargs) => {
|
|
23
|
+
const symbol = kwargs.symbol.toUpperCase().trim();
|
|
24
|
+
const optType = kwargs.type || 'Call';
|
|
25
|
+
const limit = kwargs.limit ?? 20;
|
|
26
|
+
|
|
27
|
+
await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/options`);
|
|
28
|
+
await page.wait(4);
|
|
29
|
+
|
|
30
|
+
const data = await page.evaluate(`
|
|
31
|
+
(async () => {
|
|
32
|
+
const sym = '${symbol}';
|
|
33
|
+
const type = '${optType}';
|
|
34
|
+
const limit = ${limit};
|
|
35
|
+
const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
|
36
|
+
const headers = { 'X-CSRF-TOKEN': csrf };
|
|
37
|
+
|
|
38
|
+
// API: options chain with greeks
|
|
39
|
+
try {
|
|
40
|
+
const fields = [
|
|
41
|
+
'strikePrice','bidPrice','askPrice','lastPrice','priceChange',
|
|
42
|
+
'volume','openInterest','volatility',
|
|
43
|
+
'delta','gamma','theta','vega',
|
|
44
|
+
'expirationDate','optionType','percentFromLast',
|
|
45
|
+
].join(',');
|
|
46
|
+
|
|
47
|
+
const url = '/proxies/core-api/v1/options/chain?symbol=' + encodeURIComponent(sym)
|
|
48
|
+
+ '&fields=' + fields + '&raw=1';
|
|
49
|
+
const resp = await fetch(url, { credentials: 'include', headers });
|
|
50
|
+
if (resp.ok) {
|
|
51
|
+
const d = await resp.json();
|
|
52
|
+
let items = d?.data || [];
|
|
53
|
+
|
|
54
|
+
// Filter by type
|
|
55
|
+
items = items.filter(i => {
|
|
56
|
+
const t = (i.raw || i).optionType || '';
|
|
57
|
+
return t.toLowerCase() === type.toLowerCase();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Sort by closeness to current price
|
|
61
|
+
items.sort((a, b) => {
|
|
62
|
+
const aD = Math.abs((a.raw || a).percentFromLast || 999);
|
|
63
|
+
const bD = Math.abs((b.raw || b).percentFromLast || 999);
|
|
64
|
+
return aD - bD;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return items.slice(0, limit).map(i => {
|
|
68
|
+
const r = i.raw || i;
|
|
69
|
+
return {
|
|
70
|
+
strike: r.strikePrice,
|
|
71
|
+
bid: r.bidPrice,
|
|
72
|
+
ask: r.askPrice,
|
|
73
|
+
last: r.lastPrice,
|
|
74
|
+
change: r.priceChange,
|
|
75
|
+
volume: r.volume,
|
|
76
|
+
openInterest: r.openInterest,
|
|
77
|
+
iv: r.volatility,
|
|
78
|
+
delta: r.delta,
|
|
79
|
+
gamma: r.gamma,
|
|
80
|
+
theta: r.theta,
|
|
81
|
+
vega: r.vega,
|
|
82
|
+
expiration: r.expirationDate,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
} catch(e) {}
|
|
87
|
+
|
|
88
|
+
return [];
|
|
89
|
+
})()
|
|
90
|
+
`);
|
|
91
|
+
|
|
92
|
+
if (!data || !Array.isArray(data)) return [];
|
|
93
|
+
|
|
94
|
+
return data.map(r => ({
|
|
95
|
+
strike: r.strike,
|
|
96
|
+
bid: r.bid != null ? Number(Number(r.bid).toFixed(2)) : null,
|
|
97
|
+
ask: r.ask != null ? Number(Number(r.ask).toFixed(2)) : null,
|
|
98
|
+
last: r.last != null ? Number(Number(r.last).toFixed(2)) : null,
|
|
99
|
+
change: r.change != null ? Number(Number(r.change).toFixed(2)) : null,
|
|
100
|
+
volume: r.volume,
|
|
101
|
+
openInterest: r.openInterest,
|
|
102
|
+
iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null,
|
|
103
|
+
delta: r.delta != null ? Number(Number(r.delta).toFixed(4)) : null,
|
|
104
|
+
gamma: r.gamma != null ? Number(Number(r.gamma).toFixed(4)) : null,
|
|
105
|
+
theta: r.theta != null ? Number(Number(r.theta).toFixed(4)) : null,
|
|
106
|
+
vega: r.vega != null ? Number(Number(r.vega).toFixed(4)) : null,
|
|
107
|
+
expiration: r.expiration ?? null,
|
|
108
|
+
}));
|
|
109
|
+
},
|
|
110
|
+
});
|