@jackwener/opencli 0.7.9 → 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.
Files changed (66) hide show
  1. package/.github/workflows/pkg-pr-new.yml +30 -0
  2. package/README.md +1 -0
  3. package/README.zh-CN.md +1 -0
  4. package/dist/browser/discover.d.ts +15 -0
  5. package/dist/browser/discover.js +60 -12
  6. package/dist/browser/index.d.ts +5 -1
  7. package/dist/browser/index.js +5 -1
  8. package/dist/browser/mcp.js +7 -6
  9. package/dist/browser.test.js +135 -1
  10. package/dist/cli-manifest.json +170 -88
  11. package/dist/clis/barchart/flow.js +117 -0
  12. package/dist/clis/barchart/greeks.js +119 -0
  13. package/dist/clis/barchart/options.js +106 -0
  14. package/dist/clis/barchart/quote.js +133 -0
  15. package/dist/clis/twitter/bookmarks.d.ts +1 -0
  16. package/dist/clis/twitter/bookmarks.js +171 -0
  17. package/dist/clis/twitter/delete.js +0 -1
  18. package/dist/clis/twitter/followers.js +5 -16
  19. package/dist/clis/twitter/following.js +3 -4
  20. package/dist/clis/twitter/like.js +0 -1
  21. package/dist/clis/twitter/notifications.js +17 -7
  22. package/dist/clis/twitter/search.js +14 -6
  23. package/dist/clis/twitter/trending.yaml +8 -2
  24. package/dist/main.js +0 -0
  25. package/package.json +1 -1
  26. package/src/browser/discover.ts +73 -12
  27. package/src/browser/index.ts +5 -1
  28. package/src/browser/mcp.ts +7 -5
  29. package/src/browser.test.ts +140 -1
  30. package/src/clis/barchart/flow.ts +121 -0
  31. package/src/clis/barchart/greeks.ts +123 -0
  32. package/src/clis/barchart/options.ts +110 -0
  33. package/src/clis/barchart/quote.ts +137 -0
  34. package/src/clis/twitter/bookmarks.ts +201 -0
  35. package/src/clis/twitter/delete.ts +0 -1
  36. package/src/clis/twitter/followers.ts +5 -16
  37. package/src/clis/twitter/following.ts +3 -5
  38. package/src/clis/twitter/like.ts +0 -1
  39. package/src/clis/twitter/notifications.ts +18 -9
  40. package/src/clis/twitter/search.ts +14 -7
  41. package/src/clis/twitter/trending.yaml +8 -2
  42. package/vitest.config.ts +7 -0
  43. package/dist/_debug.js +0 -7
  44. package/dist/browser-tab.d.ts +0 -2
  45. package/dist/browser-tab.js +0 -30
  46. package/dist/browser.d.ts +0 -105
  47. package/dist/browser.js +0 -644
  48. package/dist/clis/github/search.js +0 -20
  49. package/dist/clis/index.d.ts +0 -27
  50. package/dist/clis/index.js +0 -41
  51. package/dist/clis/twitter/bookmarks.yaml +0 -85
  52. package/dist/clis/xiaohongshu/me.js +0 -86
  53. package/dist/pipeline/_debug.js +0 -7
  54. package/dist/promote.d.ts +0 -1
  55. package/dist/promote.js +0 -3
  56. package/dist/register.d.ts +0 -2
  57. package/dist/register.js +0 -2
  58. package/dist/scaffold.d.ts +0 -2
  59. package/dist/scaffold.js +0 -2
  60. package/dist/smoke.d.ts +0 -2
  61. package/dist/smoke.js +0 -2
  62. package/src/clis/twitter/bookmarks.yaml +0 -85
  63. /package/dist/{_debug.d.ts → clis/barchart/flow.d.ts} +0 -0
  64. /package/dist/clis/{github/search.d.ts → barchart/greeks.d.ts} +0 -0
  65. /package/dist/clis/{xiaohongshu/me.d.ts → barchart/options.d.ts} +0 -0
  66. /package/dist/{pipeline/_debug.d.ts → clis/barchart/quote.d.ts} +0 -0
@@ -1,4 +1,167 @@
1
1
  [
2
+ {
3
+ "site": "barchart",
4
+ "name": "flow",
5
+ "description": "Barchart unusual options activity / options flow",
6
+ "strategy": "cookie",
7
+ "browser": true,
8
+ "args": [
9
+ {
10
+ "name": "type",
11
+ "type": "str",
12
+ "default": "all",
13
+ "required": false,
14
+ "help": "Filter: all, call, or put"
15
+ },
16
+ {
17
+ "name": "limit",
18
+ "type": "int",
19
+ "default": 20,
20
+ "required": false,
21
+ "help": "Number of results"
22
+ }
23
+ ],
24
+ "type": "ts",
25
+ "modulePath": "barchart/flow.js",
26
+ "domain": "www.barchart.com",
27
+ "columns": [
28
+ "symbol",
29
+ "type",
30
+ "strike",
31
+ "expiration",
32
+ "last",
33
+ "volume",
34
+ "openInterest",
35
+ "volOiRatio",
36
+ "iv"
37
+ ]
38
+ },
39
+ {
40
+ "site": "barchart",
41
+ "name": "greeks",
42
+ "description": "Barchart options greeks overview (IV, delta, gamma, theta, vega)",
43
+ "strategy": "cookie",
44
+ "browser": true,
45
+ "args": [
46
+ {
47
+ "name": "symbol",
48
+ "type": "str",
49
+ "required": true,
50
+ "help": "Stock ticker (e.g. AAPL)"
51
+ },
52
+ {
53
+ "name": "expiration",
54
+ "type": "str",
55
+ "required": false,
56
+ "help": "Expiration date (YYYY-MM-DD). Defaults to the nearest available expiration."
57
+ },
58
+ {
59
+ "name": "limit",
60
+ "type": "int",
61
+ "default": 10,
62
+ "required": false,
63
+ "help": "Number of near-the-money strikes per type"
64
+ }
65
+ ],
66
+ "type": "ts",
67
+ "modulePath": "barchart/greeks.js",
68
+ "domain": "www.barchart.com",
69
+ "columns": [
70
+ "type",
71
+ "strike",
72
+ "last",
73
+ "iv",
74
+ "delta",
75
+ "gamma",
76
+ "theta",
77
+ "vega",
78
+ "rho",
79
+ "volume",
80
+ "openInterest",
81
+ "expiration"
82
+ ]
83
+ },
84
+ {
85
+ "site": "barchart",
86
+ "name": "options",
87
+ "description": "Barchart options chain with greeks, IV, volume, and open interest",
88
+ "strategy": "cookie",
89
+ "browser": true,
90
+ "args": [
91
+ {
92
+ "name": "symbol",
93
+ "type": "str",
94
+ "required": true,
95
+ "help": "Stock ticker (e.g. AAPL)"
96
+ },
97
+ {
98
+ "name": "type",
99
+ "type": "str",
100
+ "default": "Call",
101
+ "required": false,
102
+ "help": "Option type: Call or Put"
103
+ },
104
+ {
105
+ "name": "limit",
106
+ "type": "int",
107
+ "default": 20,
108
+ "required": false,
109
+ "help": "Max number of strikes to return"
110
+ }
111
+ ],
112
+ "type": "ts",
113
+ "modulePath": "barchart/options.js",
114
+ "domain": "www.barchart.com",
115
+ "columns": [
116
+ "strike",
117
+ "bid",
118
+ "ask",
119
+ "last",
120
+ "change",
121
+ "volume",
122
+ "openInterest",
123
+ "iv",
124
+ "delta",
125
+ "gamma",
126
+ "theta",
127
+ "vega",
128
+ "expiration"
129
+ ]
130
+ },
131
+ {
132
+ "site": "barchart",
133
+ "name": "quote",
134
+ "description": "Barchart stock quote with price, volume, and key metrics",
135
+ "strategy": "cookie",
136
+ "browser": true,
137
+ "args": [
138
+ {
139
+ "name": "symbol",
140
+ "type": "str",
141
+ "required": true,
142
+ "help": "Stock ticker (e.g. AAPL, MSFT, TSLA)"
143
+ }
144
+ ],
145
+ "type": "ts",
146
+ "modulePath": "barchart/quote.js",
147
+ "domain": "www.barchart.com",
148
+ "columns": [
149
+ "symbol",
150
+ "name",
151
+ "price",
152
+ "change",
153
+ "changePct",
154
+ "open",
155
+ "high",
156
+ "low",
157
+ "prevClose",
158
+ "volume",
159
+ "avgVolume",
160
+ "marketCap",
161
+ "peRatio",
162
+ "eps"
163
+ ]
164
+ },
2
165
  {
3
166
  "site": "bbc",
4
167
  "name": "news",
@@ -622,45 +785,6 @@
622
785
  "url"
623
786
  ]
624
787
  },
625
- {
626
- "site": "github",
627
- "name": "search",
628
- "description": "Search GitHub repositories",
629
- "strategy": "public",
630
- "browser": false,
631
- "args": [
632
- {
633
- "name": "keyword",
634
- "type": "str",
635
- "required": true,
636
- "help": "Search keyword"
637
- },
638
- {
639
- "name": "sort",
640
- "type": "str",
641
- "default": "stars",
642
- "required": false,
643
- "help": "Sort by: stars, forks, updated"
644
- },
645
- {
646
- "name": "limit",
647
- "type": "int",
648
- "default": 20,
649
- "required": false,
650
- "help": "Number of results"
651
- }
652
- ],
653
- "type": "ts",
654
- "modulePath": "github/search.js",
655
- "domain": "github.com",
656
- "columns": [
657
- "rank",
658
- "name",
659
- "stars",
660
- "language",
661
- "description"
662
- ]
663
- },
664
788
  {
665
789
  "site": "hackernews",
666
790
  "name": "top",
@@ -1565,8 +1689,7 @@
1565
1689
  {
1566
1690
  "site": "twitter",
1567
1691
  "name": "bookmarks",
1568
- "description": "获取 Twitter 书签列表",
1569
- "domain": "x.com",
1692
+ "description": "Fetch Twitter/X bookmarks",
1570
1693
  "strategy": "cookie",
1571
1694
  "browser": true,
1572
1695
  "args": [
@@ -1575,38 +1698,18 @@
1575
1698
  "type": "int",
1576
1699
  "default": 20,
1577
1700
  "required": false,
1578
- "help": "Number of bookmarks to return (default 20)"
1701
+ "help": ""
1579
1702
  }
1580
1703
  ],
1704
+ "type": "ts",
1705
+ "modulePath": "twitter/bookmarks.js",
1706
+ "domain": "x.com",
1581
1707
  "columns": [
1582
1708
  "author",
1583
1709
  "text",
1584
1710
  "likes",
1585
1711
  "url"
1586
- ],
1587
- "pipeline": [
1588
- {
1589
- "navigate": "https://x.com/i/bookmarks"
1590
- },
1591
- {
1592
- "wait": 2
1593
- },
1594
- {
1595
- "evaluate": "(async () => {\n const ct0 = document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1];\n if (!ct0) throw new Error('No ct0 cookie. Hint: Not logged into x.com.');\n const bearer = decodeURIComponent('AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA');\n const _h = {'Authorization':'Bearer '+bearer, 'X-Csrf-Token':ct0, 'X-Twitter-Auth-Type':'OAuth2Session', 'X-Twitter-Active-User':'yes'};\n\n const count = Math.min(${{ args.limit }}, 100);\n const variables = JSON.stringify({count, includePromotedContent: false});\n const features = JSON.stringify({\n rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true,\n responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: false,\n verified_phone_label_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true,\n responsive_web_graphql_timeline_navigation_enabled: true,\n responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,\n premium_content_api_read_enabled: false, communities_web_enable_tweet_community_results_fetch: true,\n c9s_tweet_anatomy_moderator_badge_enabled: true,\n articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true,\n graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,\n view_counts_everywhere_api_enabled: true, longform_notetweets_consumption_enabled: true,\n responsive_web_twitter_article_tweet_consumption_enabled: true,\n tweet_awards_web_tipping_enabled: false,\n content_disclosure_indicator_enabled: true, content_disclosure_ai_generated_indicator_enabled: true,\n freedom_of_speech_not_reach_fetch_enabled: true, standardized_nudges_misinfo: true,\n tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,\n longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: false,\n responsive_web_enhance_cards_enabled: false\n });\n const url = '/i/api/graphql/Fy0QMy4q_aZCpkO0PnyLYw/Bookmarks?variables=' + encodeURIComponent(variables) + '&features=' + encodeURIComponent(features);\n const resp = await fetch(url, {headers: _h, credentials: 'include'});\n if (!resp.ok) throw new Error('HTTP ' + resp.status + '. Hint: queryId may have changed.');\n const d = await resp.json();\n\n const instructions = d.data?.bookmark_timeline_v2?.timeline?.instructions || d.data?.bookmark_timeline?.timeline?.instructions || [];\n let tweets = [], seen = new Set();\n for (const inst of instructions) {\n for (const entry of (inst.entries || [])) {\n const r = entry.content?.itemContent?.tweet_results?.result;\n if (!r) continue;\n const tw = r.tweet || r;\n const l = tw.legacy || {};\n if (!tw.rest_id || seen.has(tw.rest_id)) continue;\n seen.add(tw.rest_id);\n const u = tw.core?.user_results?.result;\n const nt = tw.note_tweet?.note_tweet_results?.result?.text;\n const screenName = u?.legacy?.screen_name || u?.core?.screen_name;\n tweets.push({\n id: tw.rest_id, \n author: screenName,\n name: u?.legacy?.name || u?.core?.name,\n url: 'https://x.com/' + (screenName || '_') + '/status/' + tw.rest_id,\n text: nt || l.full_text || '',\n likes: l.favorite_count, \n retweets: l.retweet_count,\n created_at: l.created_at\n });\n }\n }\n return tweets;\n})()\n"
1596
- },
1597
- {
1598
- "map": {
1599
- "author": "${{ item.author }}",
1600
- "text": "${{ item.text }}",
1601
- "likes": "${{ item.likes }}",
1602
- "url": "${{ item.url }}"
1603
- }
1604
- },
1605
- {
1606
- "limit": "${{ args.limit }}"
1607
- }
1608
- ],
1609
- "type": "yaml"
1712
+ ]
1610
1713
  },
1611
1714
  {
1612
1715
  "site": "twitter",
@@ -1969,7 +2072,7 @@
1969
2072
  "navigate": "https://x.com/explore/tabs/trending"
1970
2073
  },
1971
2074
  {
1972
- "evaluate": "(async () => {\n const cookies = document.cookie.split(';').reduce((acc, c) => {\n const [k, v] = c.trim().split('=');\n acc[k] = v;\n return acc;\n }, {});\n const csrfToken = cookies['ct0'] || '';\n const bearerToken = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';\n const res = await fetch('/i/api/2/guide.json?include_page_configuration=true', {\n credentials: 'include',\n headers: { 'x-twitter-active-user': 'yes', 'x-csrf-token': csrfToken, 'authorization': 'Bearer ' + bearerToken }\n });\n const data = await res.json();\n const trends = data?.timeline?.instructions?.[1]?.addEntries?.entries || [];\n return trends.filter(e => e.content?.timelineModule).flatMap(e => e.content.timelineModule.items || []).map(t => t?.item?.content?.trend).filter(Boolean);\n})()\n"
2075
+ "evaluate": "(async () => {\n const cookies = document.cookie.split(';').reduce((acc, c) => {\n const [k, v] = c.trim().split('=');\n acc[k] = v;\n return acc;\n }, {});\n const csrfToken = cookies['ct0'] || '';\n const bearerToken = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';\n const res = await fetch('/i/api/2/guide.json?include_page_configuration=true', {\n credentials: 'include',\n headers: { 'x-twitter-active-user': 'yes', 'x-csrf-token': csrfToken, 'authorization': 'Bearer ' + bearerToken }\n });\n if (!res.ok) throw new Error('HTTP ' + res.status + '. Hint: trending endpoint may require login or API shape changed.');\n const data = await res.json();\n const instructions = data?.timeline?.instructions || [];\n const entries = instructions.flatMap(inst => inst?.addEntries?.entries || inst?.entries || []);\n return entries\n .filter(e => e.content?.timelineModule)\n .flatMap(e => e.content.timelineModule.items || [])\n .map(t => t?.item?.content?.trend)\n .filter(Boolean);\n})()\n"
1973
2076
  },
1974
2077
  {
1975
2078
  "map": {
@@ -2290,27 +2393,6 @@
2290
2393
  ],
2291
2394
  "type": "yaml"
2292
2395
  },
2293
- {
2294
- "site": "xiaohongshu",
2295
- "name": "me",
2296
- "description": "我的小红书个人信息",
2297
- "strategy": "cookie",
2298
- "browser": true,
2299
- "args": [],
2300
- "type": "ts",
2301
- "modulePath": "xiaohongshu/me.js",
2302
- "domain": "www.xiaohongshu.com",
2303
- "columns": [
2304
- "nickname",
2305
- "red_id",
2306
- "location",
2307
- "profession",
2308
- "fans",
2309
- "follows",
2310
- "likes_collected",
2311
- "notes"
2312
- ]
2313
- },
2314
2396
  {
2315
2397
  "site": "xiaohongshu",
2316
2398
  "name": "notifications",
@@ -0,0 +1,117 @@
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
+ cli({
8
+ site: 'barchart',
9
+ name: 'flow',
10
+ description: 'Barchart unusual options activity / options flow',
11
+ domain: 'www.barchart.com',
12
+ strategy: Strategy.COOKIE,
13
+ args: [
14
+ { name: 'type', type: 'str', default: 'all', help: 'Filter: all, call, or put', choices: ['all', 'call', 'put'] },
15
+ { name: 'limit', type: 'int', default: 20, help: 'Number of results' },
16
+ ],
17
+ columns: [
18
+ 'symbol', 'type', 'strike', 'expiration', 'last',
19
+ 'volume', 'openInterest', 'volOiRatio', 'iv',
20
+ ],
21
+ func: async (page, kwargs) => {
22
+ const optionType = kwargs.type || 'all';
23
+ const limit = kwargs.limit ?? 20;
24
+ await page.goto('https://www.barchart.com/options/unusual-activity/stocks');
25
+ await page.wait(5);
26
+ const data = await page.evaluate(`
27
+ (async () => {
28
+ const limit = ${limit};
29
+ const typeFilter = '${optionType}'.toLowerCase();
30
+ const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
31
+ const headers = { 'X-CSRF-TOKEN': csrf };
32
+
33
+ const fields = [
34
+ 'baseSymbol','strikePrice','expirationDate','optionType',
35
+ 'lastPrice','volume','openInterest','volumeOpenInterestRatio','volatility',
36
+ ].join(',');
37
+
38
+ // Fetch extra rows when filtering by type since server-side filter may not work
39
+ const fetchLimit = typeFilter !== 'all' ? limit * 3 : limit;
40
+ try {
41
+ const url = '/proxies/core-api/v1/options/get?list=options.unusual_activity.stocks.us'
42
+ + '&fields=' + fields
43
+ + '&orderBy=volumeOpenInterestRatio&orderDir=desc'
44
+ + '&raw=1&limit=' + fetchLimit;
45
+
46
+ const resp = await fetch(url, { credentials: 'include', headers });
47
+ if (resp.ok) {
48
+ const d = await resp.json();
49
+ let items = d?.data || [];
50
+ if (items.length > 0) {
51
+ // Apply client-side type filter
52
+ if (typeFilter !== 'all') {
53
+ items = items.filter(i => {
54
+ const t = ((i.raw || i).optionType || '').toLowerCase();
55
+ return t === typeFilter;
56
+ });
57
+ }
58
+ return items.slice(0, limit).map(i => {
59
+ const r = i.raw || i;
60
+ return {
61
+ symbol: r.baseSymbol || r.symbol,
62
+ type: r.optionType,
63
+ strike: r.strikePrice,
64
+ expiration: r.expirationDate,
65
+ last: r.lastPrice,
66
+ volume: r.volume,
67
+ openInterest: r.openInterest,
68
+ volOiRatio: r.volumeOpenInterestRatio,
69
+ iv: r.volatility,
70
+ };
71
+ });
72
+ }
73
+ }
74
+ } catch(e) {}
75
+
76
+ // Fallback: parse from DOM table
77
+ try {
78
+ const rows = document.querySelectorAll('tr[data-ng-repeat], tbody tr');
79
+ const results = [];
80
+ for (const row of rows) {
81
+ const cells = row.querySelectorAll('td');
82
+ if (cells.length < 6) continue;
83
+ const getText = (idx) => cells[idx]?.textContent?.trim() || null;
84
+ results.push({
85
+ symbol: getText(0),
86
+ type: getText(1),
87
+ strike: getText(2),
88
+ expiration: getText(3),
89
+ last: getText(4),
90
+ volume: getText(5),
91
+ openInterest: cells.length > 6 ? getText(6) : null,
92
+ volOiRatio: cells.length > 7 ? getText(7) : null,
93
+ iv: cells.length > 8 ? getText(8) : null,
94
+ });
95
+ if (results.length >= limit) break;
96
+ }
97
+ return results;
98
+ } catch(e) {
99
+ return [];
100
+ }
101
+ })()
102
+ `);
103
+ if (!data || !Array.isArray(data))
104
+ return [];
105
+ return data.slice(0, limit).map(r => ({
106
+ symbol: r.symbol || '',
107
+ type: r.type || '',
108
+ strike: r.strike,
109
+ expiration: r.expiration ?? null,
110
+ last: r.last != null ? Number(Number(r.last).toFixed(2)) : null,
111
+ volume: r.volume,
112
+ openInterest: r.openInterest,
113
+ volOiRatio: r.volOiRatio != null ? Number(Number(r.volOiRatio).toFixed(2)) : null,
114
+ iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null,
115
+ }));
116
+ },
117
+ });
@@ -0,0 +1,119 @@
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
+ cli({
8
+ site: 'barchart',
9
+ name: 'greeks',
10
+ description: 'Barchart options greeks overview (IV, delta, gamma, theta, vega)',
11
+ domain: 'www.barchart.com',
12
+ strategy: Strategy.COOKIE,
13
+ args: [
14
+ { name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL)' },
15
+ { name: 'expiration', type: 'str', help: 'Expiration date (YYYY-MM-DD). Defaults to the nearest available expiration.' },
16
+ { name: 'limit', type: 'int', default: 10, help: 'Number of near-the-money strikes per type' },
17
+ ],
18
+ columns: [
19
+ 'type', 'strike', 'last', 'iv', 'delta', 'gamma', 'theta', 'vega', 'rho',
20
+ 'volume', 'openInterest', 'expiration',
21
+ ],
22
+ func: async (page, kwargs) => {
23
+ const symbol = kwargs.symbol.toUpperCase().trim();
24
+ const expiration = kwargs.expiration ?? '';
25
+ const limit = kwargs.limit ?? 10;
26
+ await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/options`);
27
+ await page.wait(4);
28
+ const data = await page.evaluate(`
29
+ (async () => {
30
+ const sym = '${symbol}';
31
+ const expDate = '${expiration}';
32
+ const limit = ${limit};
33
+ const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
34
+ const headers = { 'X-CSRF-TOKEN': csrf };
35
+
36
+ try {
37
+ const fields = [
38
+ 'strikePrice','lastPrice','volume','openInterest',
39
+ 'volatility','delta','gamma','theta','vega','rho',
40
+ 'expirationDate','optionType','percentFromLast',
41
+ ].join(',');
42
+
43
+ let url = '/proxies/core-api/v1/options/chain?symbol=' + encodeURIComponent(sym)
44
+ + '&fields=' + fields + '&raw=1';
45
+ if (expDate) url += '&expirationDate=' + encodeURIComponent(expDate);
46
+ const resp = await fetch(url, { credentials: 'include', headers });
47
+ if (resp.ok) {
48
+ const d = await resp.json();
49
+ let items = d?.data || [];
50
+
51
+ if (!expDate) {
52
+ const expirations = items
53
+ .map(i => (i.raw || i).expirationDate || null)
54
+ .filter(Boolean)
55
+ .sort((a, b) => {
56
+ const aTime = Date.parse(a);
57
+ const bTime = Date.parse(b);
58
+ if (Number.isNaN(aTime) && Number.isNaN(bTime)) return 0;
59
+ if (Number.isNaN(aTime)) return 1;
60
+ if (Number.isNaN(bTime)) return -1;
61
+ return aTime - bTime;
62
+ });
63
+ const nearestExpiration = expirations[0];
64
+ if (nearestExpiration) {
65
+ items = items.filter(i => ((i.raw || i).expirationDate || null) === nearestExpiration);
66
+ }
67
+ }
68
+
69
+ // Separate calls and puts, sort by distance from current price
70
+ const calls = items
71
+ .filter(i => ((i.raw || i).optionType || '').toLowerCase() === 'call')
72
+ .sort((a, b) => Math.abs((a.raw || a).percentFromLast || 999) - Math.abs((b.raw || b).percentFromLast || 999))
73
+ .slice(0, limit);
74
+ const puts = items
75
+ .filter(i => ((i.raw || i).optionType || '').toLowerCase() === 'put')
76
+ .sort((a, b) => Math.abs((a.raw || a).percentFromLast || 999) - Math.abs((b.raw || b).percentFromLast || 999))
77
+ .slice(0, limit);
78
+
79
+ return [...calls, ...puts].map(i => {
80
+ const r = i.raw || i;
81
+ return {
82
+ type: r.optionType,
83
+ strike: r.strikePrice,
84
+ last: r.lastPrice,
85
+ iv: r.volatility,
86
+ delta: r.delta,
87
+ gamma: r.gamma,
88
+ theta: r.theta,
89
+ vega: r.vega,
90
+ rho: r.rho,
91
+ volume: r.volume,
92
+ openInterest: r.openInterest,
93
+ expiration: r.expirationDate,
94
+ };
95
+ });
96
+ }
97
+ } catch(e) {}
98
+
99
+ return [];
100
+ })()
101
+ `);
102
+ if (!data || !Array.isArray(data))
103
+ return [];
104
+ return data.map(r => ({
105
+ type: r.type || '',
106
+ strike: r.strike,
107
+ last: r.last != null ? Number(Number(r.last).toFixed(2)) : null,
108
+ iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null,
109
+ delta: r.delta != null ? Number(Number(r.delta).toFixed(4)) : null,
110
+ gamma: r.gamma != null ? Number(Number(r.gamma).toFixed(4)) : null,
111
+ theta: r.theta != null ? Number(Number(r.theta).toFixed(4)) : null,
112
+ vega: r.vega != null ? Number(Number(r.vega).toFixed(4)) : null,
113
+ rho: r.rho != null ? Number(Number(r.rho).toFixed(4)) : null,
114
+ volume: r.volume,
115
+ openInterest: r.openInterest,
116
+ expiration: r.expiration ?? null,
117
+ }));
118
+ },
119
+ });
@@ -0,0 +1,106 @@
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
+ cli({
7
+ site: 'barchart',
8
+ name: 'options',
9
+ description: 'Barchart options chain with greeks, IV, volume, and open interest',
10
+ domain: 'www.barchart.com',
11
+ strategy: Strategy.COOKIE,
12
+ args: [
13
+ { name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL)' },
14
+ { name: 'type', type: 'str', default: 'Call', help: 'Option type: Call or Put', choices: ['Call', 'Put'] },
15
+ { name: 'limit', type: 'int', default: 20, help: 'Max number of strikes to return' },
16
+ ],
17
+ columns: [
18
+ 'strike', 'bid', 'ask', 'last', 'change', 'volume', 'openInterest',
19
+ 'iv', 'delta', 'gamma', 'theta', 'vega', 'expiration',
20
+ ],
21
+ func: async (page, kwargs) => {
22
+ const symbol = kwargs.symbol.toUpperCase().trim();
23
+ const optType = kwargs.type || 'Call';
24
+ const limit = kwargs.limit ?? 20;
25
+ await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/options`);
26
+ await page.wait(4);
27
+ const data = await page.evaluate(`
28
+ (async () => {
29
+ const sym = '${symbol}';
30
+ const type = '${optType}';
31
+ const limit = ${limit};
32
+ const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
33
+ const headers = { 'X-CSRF-TOKEN': csrf };
34
+
35
+ // API: options chain with greeks
36
+ try {
37
+ const fields = [
38
+ 'strikePrice','bidPrice','askPrice','lastPrice','priceChange',
39
+ 'volume','openInterest','volatility',
40
+ 'delta','gamma','theta','vega',
41
+ 'expirationDate','optionType','percentFromLast',
42
+ ].join(',');
43
+
44
+ const url = '/proxies/core-api/v1/options/chain?symbol=' + encodeURIComponent(sym)
45
+ + '&fields=' + fields + '&raw=1';
46
+ const resp = await fetch(url, { credentials: 'include', headers });
47
+ if (resp.ok) {
48
+ const d = await resp.json();
49
+ let items = d?.data || [];
50
+
51
+ // Filter by type
52
+ items = items.filter(i => {
53
+ const t = (i.raw || i).optionType || '';
54
+ return t.toLowerCase() === type.toLowerCase();
55
+ });
56
+
57
+ // Sort by closeness to current price
58
+ items.sort((a, b) => {
59
+ const aD = Math.abs((a.raw || a).percentFromLast || 999);
60
+ const bD = Math.abs((b.raw || b).percentFromLast || 999);
61
+ return aD - bD;
62
+ });
63
+
64
+ return items.slice(0, limit).map(i => {
65
+ const r = i.raw || i;
66
+ return {
67
+ strike: r.strikePrice,
68
+ bid: r.bidPrice,
69
+ ask: r.askPrice,
70
+ last: r.lastPrice,
71
+ change: r.priceChange,
72
+ volume: r.volume,
73
+ openInterest: r.openInterest,
74
+ iv: r.volatility,
75
+ delta: r.delta,
76
+ gamma: r.gamma,
77
+ theta: r.theta,
78
+ vega: r.vega,
79
+ expiration: r.expirationDate,
80
+ };
81
+ });
82
+ }
83
+ } catch(e) {}
84
+
85
+ return [];
86
+ })()
87
+ `);
88
+ if (!data || !Array.isArray(data))
89
+ return [];
90
+ return data.map(r => ({
91
+ strike: r.strike,
92
+ bid: r.bid != null ? Number(Number(r.bid).toFixed(2)) : null,
93
+ ask: r.ask != null ? Number(Number(r.ask).toFixed(2)) : null,
94
+ last: r.last != null ? Number(Number(r.last).toFixed(2)) : null,
95
+ change: r.change != null ? Number(Number(r.change).toFixed(2)) : null,
96
+ volume: r.volume,
97
+ openInterest: r.openInterest,
98
+ iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null,
99
+ delta: r.delta != null ? Number(Number(r.delta).toFixed(4)) : null,
100
+ gamma: r.gamma != null ? Number(Number(r.gamma).toFixed(4)) : null,
101
+ theta: r.theta != null ? Number(Number(r.theta).toFixed(4)) : null,
102
+ vega: r.vega != null ? Number(Number(r.vega).toFixed(4)) : null,
103
+ expiration: r.expiration ?? null,
104
+ }));
105
+ },
106
+ });