xstock-mcp 1.2.0 → 1.4.0

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/README.md CHANGED
@@ -11,17 +11,45 @@
11
11
 
12
12
  ## 工具列表
13
13
 
14
+ ### 美股
15
+
14
16
  | 工具 | 说明 |
15
17
  |------|------|
16
- | `get_quote` | 实时报价(美股 / 加密货币 / A股 / 港股) |
18
+ | `get_quote` | 实时行情 |
17
19
  | `get_kline` | K线数据,支持日线 / 周线 / 月线 |
18
- | `search_stock` | 按关键词搜索股票和加密货币 |
19
- | `get_us_indices` | 美国主要市场指数 |
20
- | `get_stock_profile` | 公司基本面信息 |
21
- | `get_crypto_overview` | 全球加密市场总市值 + 恐惧贪婪指数 |
20
+ | `get_kline_with_indicators` | K线 + MA5/10/20/60、RSI14、MACD、布林带(精确计算) |
21
+ | `get_us_indices` | 主要指数:标普500、纳斯达克、道琼斯、罗素2000、VIX |
22
+ | `get_us_sector_heatmap` | 11大行业板块今日涨跌(SPDR ETF) |
23
+ | `get_stock_profile` | 公司基本面:市值、PE、行业、简介 |
24
+ | `get_financials` | 财务数据:营收、毛利率、净利润、自由现金流、负债率(年报/季报) |
25
+ | `get_analyst_rating` | 分析师评级分布、目标价、近期评级变动 |
26
+ | `get_earnings_calendar` | 下次财报日期 + 近4季度 EPS 历史 |
27
+ | `get_stock_news` | 个股最新新闻(近7天) |
28
+ | `get_insider_activity` | 高管/内部人买卖记录(SEC Form 4) |
29
+ | `get_market_movers` | 当日涨幅榜 / 跌幅榜 / 成交量异动榜 |
30
+ | `get_dividend_history` | 分红历史、股息率、除息日 |
31
+ | `get_institutional_holders` | 前10大机构持仓比例及变动 |
32
+ | `get_similar_stocks` | 同行业可比公司估值对比 |
33
+ | `get_short_interest` | 做空比例、空头回补天数、与上月对比 |
34
+ | `get_stock_full_overview` | 复合工具:行情 + 基本面 + 评级 + 新闻,一次返回 |
35
+ | `search_stock` | 按名称或代码搜索 |
36
+
37
+ ### 加密货币
38
+
39
+ | 工具 | 说明 |
40
+ |------|------|
41
+ | `get_crypto_overview` | 全球市值 + 恐惧贪婪指数 |
22
42
  | `get_crypto_top` | 按市值排名的 Top N 币种 |
23
- | `get_crypto_categories` | 加密货币赛道分类(DeFi、Layer1、AI、GameFi…) |
43
+ | `get_crypto_categories` | 赛道分类(DeFi、Layer1、AI、GameFi…) |
24
44
  | `get_funding_rate` | Binance 永续合约资金费率 |
45
+ | `get_crypto_liquidation` | 合约市场情绪:OI、多空比、吃单比 |
46
+
47
+ ### A股 / 港股
48
+
49
+ | 工具 | 说明 |
50
+ |------|------|
51
+ | `get_quote` | 实时行情(腾讯财经) |
52
+ | `get_kline` | K线数据 |
25
53
 
26
54
  ## 使用方法
27
55
 
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  // src/server.ts
27
27
  var import_server = require("@modelcontextprotocol/sdk/server/index.js");
28
28
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
29
- var import_types7 = require("@modelcontextprotocol/sdk/types.js");
29
+ var import_types8 = require("@modelcontextprotocol/sdk/types.js");
30
30
 
31
31
  // src/tools/quotes.ts
32
32
  var import_zod = require("zod");
@@ -368,6 +368,115 @@ async function fetchInsiderActivity(symbol) {
368
368
  sharesAfter: tx.shareholderAfter ?? null
369
369
  }));
370
370
  }
371
+ async function fetchShortInterest(symbol) {
372
+ process.stderr.write(`[yahoo] fetchShortInterest symbol=${symbol}
373
+ `);
374
+ const summary = await yf.quoteSummary(symbol, {
375
+ modules: ["defaultKeyStatistics"]
376
+ });
377
+ const stats = summary.defaultKeyStatistics;
378
+ const sharesShort = stats?.sharesShort != null ? Number(stats.sharesShort) : null;
379
+ const priorMonth = stats?.sharesShortPriorMonth != null ? Number(stats.sharesShortPriorMonth) : null;
380
+ const change = sharesShort !== null && priorMonth !== null && priorMonth !== 0 ? Math.round((sharesShort - priorMonth) / priorMonth * 1e4) / 100 : null;
381
+ return {
382
+ symbol,
383
+ sharesShort,
384
+ sharesShortPriorMonth: priorMonth,
385
+ shortRatio: stats?.shortRatio != null ? Number(stats.shortRatio) : null,
386
+ shortPercentOfFloat: stats?.shortPercentOfFloat != null ? Math.round(Number(stats.shortPercentOfFloat) * 1e4) / 100 : null,
387
+ floatShares: stats?.floatShares != null ? Number(stats.floatShares) : null,
388
+ sharesOutstanding: stats?.sharesOutstanding != null ? Number(stats.sharesOutstanding) : null,
389
+ settlementDate: stats?.dateShortInterest instanceof Date ? stats.dateShortInterest.toISOString().slice(0, 10) : null,
390
+ changeFromPriorMonth: change
391
+ };
392
+ }
393
+ function mapQuoteToMover(q) {
394
+ return {
395
+ symbol: String(q.symbol ?? ""),
396
+ name: String(q.longName ?? q.shortName ?? q.symbol ?? ""),
397
+ price: Number(q.regularMarketPrice ?? 0),
398
+ change: Number(q.regularMarketChange ?? 0),
399
+ changePercent: Number(q.regularMarketChangePercent ?? 0).toFixed(2) + "%",
400
+ volume: Number(q.regularMarketVolume ?? 0),
401
+ marketCap: q.marketCap != null ? Number(q.marketCap) : null
402
+ };
403
+ }
404
+ async function fetchMarketMovers(type, count = 20) {
405
+ process.stderr.write(`[yahoo] fetchMarketMovers type=${type} count=${count}
406
+ `);
407
+ let result;
408
+ if (type === "gainers") {
409
+ result = await yf.dailyGainers({ count, region: "US" });
410
+ } else if (type === "losers") {
411
+ result = await yf.dailyLosers({ count, region: "US" });
412
+ } else {
413
+ result = await yf.mostActives({ count, region: "US" });
414
+ }
415
+ const quotes = result.quotes ?? [];
416
+ return quotes.map(mapQuoteToMover);
417
+ }
418
+ async function fetchDividendHistory(symbol) {
419
+ process.stderr.write(`[yahoo] fetchDividendHistory symbol=${symbol}
420
+ `);
421
+ const fiveYearsAgo = new Date(Date.now() - 5 * 365 * 864e5).toISOString().slice(0, 10);
422
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
423
+ const [summaryResult, histResult] = await Promise.allSettled([
424
+ yf.quoteSummary(symbol, { modules: ["summaryDetail", "defaultKeyStatistics"] }),
425
+ yf.historical(symbol, { period1: fiveYearsAgo, period2: today, events: "dividends" })
426
+ ]);
427
+ const detail = summaryResult.status === "fulfilled" ? summaryResult.value.summaryDetail : void 0;
428
+ const history = histResult.status === "fulfilled" ? histResult.value.filter((r) => r.dividends != null).map((r) => ({ date: r.date.toISOString().slice(0, 10), amount: r.dividends })).reverse() : [];
429
+ const consecutiveYears = history.length > 0 ? new Set(history.map((h) => h.date.slice(0, 4))).size : null;
430
+ return {
431
+ symbol,
432
+ currentYield: detail?.dividendYield ?? null,
433
+ annualDividend: detail?.dividendRate ?? null,
434
+ exDividendDate: detail?.exDividendDate instanceof Date ? detail.exDividendDate.toISOString().slice(0, 10) : null,
435
+ consecutiveYears,
436
+ history
437
+ };
438
+ }
439
+ async function fetchInstitutionalHolders(symbol) {
440
+ process.stderr.write(`[yahoo] fetchInstitutionalHolders symbol=${symbol}
441
+ `);
442
+ const summary = await yf.quoteSummary(symbol, {
443
+ modules: ["institutionOwnership"]
444
+ });
445
+ const ownership = summary.institutionOwnership;
446
+ const holders = ownership?.ownershipList ?? [];
447
+ return holders.slice(0, 10).map((h) => ({
448
+ organization: String(h.organization ?? ""),
449
+ pctHeld: h.pctHeld != null ? Math.round(Number(h.pctHeld) * 1e4) / 100 : null,
450
+ shares: h.position != null ? Number(h.position) : null,
451
+ value: h.value != null ? Number(h.value) : null,
452
+ reportDate: h.reportDate instanceof Date ? h.reportDate.toISOString().slice(0, 10) : null
453
+ }));
454
+ }
455
+ async function fetchSimilarStocks(symbol) {
456
+ process.stderr.write(`[yahoo] fetchSimilarStocks symbol=${symbol}
457
+ `);
458
+ const recs = await yf.recommendedSymbols(symbol);
459
+ const symbols = (recs.recommendedSymbols ?? []).slice(0, 8).map((r) => String(r.symbol ?? "")).filter(Boolean);
460
+ if (symbols.length === 0) return [];
461
+ const quotes = await fetchUSQuotes(symbols);
462
+ const profiles = await Promise.allSettled(
463
+ symbols.map((s) => yf.quoteSummary(s, { modules: ["price", "summaryProfile"] }))
464
+ );
465
+ return quotes.map((q, i) => {
466
+ const p = profiles[i];
467
+ const profile = p.status === "fulfilled" ? p.value.summaryProfile : void 0;
468
+ const price = p.status === "fulfilled" ? p.value.price : void 0;
469
+ return {
470
+ symbol: q.symbol,
471
+ name: q.name,
472
+ price: q.price,
473
+ changePercent: q.changePercent,
474
+ marketCap: price?.marketCap != null ? Number(price.marketCap) : q.marketCap,
475
+ pe: price?.trailingPE != null ? Number(price.trailingPE) : null,
476
+ sector: profile?.sector ? String(profile.sector) : null
477
+ };
478
+ });
479
+ }
371
480
  async function fetchStockProfile(symbol) {
372
481
  process.stderr.write(`[yahoo] fetchStockProfile symbol=${symbol}
373
482
  `);
@@ -1412,6 +1521,132 @@ var getStockFullOverviewTool = {
1412
1521
  }
1413
1522
  };
1414
1523
 
1524
+ // src/tools/us-market-b.ts
1525
+ var import_zod7 = require("zod");
1526
+ var MoversInput = import_zod7.z.object({
1527
+ type: import_zod7.z.enum(["gainers", "losers", "actives"]).optional(),
1528
+ count: import_zod7.z.number().int().min(1).max(50).optional()
1529
+ });
1530
+ var getMarketMoversTool = {
1531
+ tool: {
1532
+ name: "get_market_movers",
1533
+ description: "\u83B7\u53D6\u7F8E\u80A1\u5F53\u65E5\u699C\u5355\uFF1A\u6DA8\u5E45\u699C\uFF08gainers\uFF09\u3001\u8DCC\u5E45\u699C\uFF08losers\uFF09\u3001\u6210\u4EA4\u91CF\u5F02\u52A8\u699C\uFF08actives\uFF09\u3002\u9ED8\u8BA4\u8FD4\u56DE\u6DA8\u5E45\u699C Top 20\u3002",
1534
+ inputSchema: {
1535
+ type: "object",
1536
+ properties: {
1537
+ type: {
1538
+ type: "string",
1539
+ enum: ["gainers", "losers", "actives"],
1540
+ description: "\u699C\u5355\u7C7B\u578B\uFF1Againers=\u6DA8\u5E45\u699C\uFF0Closers=\u8DCC\u5E45\u699C\uFF0Cactives=\u6210\u4EA4\u91CF\u699C\uFF0C\u9ED8\u8BA4 gainers"
1541
+ },
1542
+ count: {
1543
+ type: "number",
1544
+ description: "\u8FD4\u56DE\u6570\u91CF\uFF0C\u9ED8\u8BA420\uFF0C\u6700\u592750"
1545
+ }
1546
+ },
1547
+ required: []
1548
+ }
1549
+ },
1550
+ handler: async (input) => {
1551
+ const parsed = MoversInput.safeParse(input);
1552
+ if (!parsed.success) return err(parsed.error.message);
1553
+ try {
1554
+ return ok(await fetchMarketMovers(parsed.data.type ?? "gainers", parsed.data.count ?? 20));
1555
+ } catch (e) {
1556
+ return err(e.message);
1557
+ }
1558
+ }
1559
+ };
1560
+ var SymbolInput3 = import_zod7.z.object({ symbol: import_zod7.z.string().min(1) });
1561
+ var getDividendHistoryTool = {
1562
+ tool: {
1563
+ name: "get_dividend_history",
1564
+ description: "\u83B7\u53D6\u7F8E\u80A1\u5206\u7EA2\u6570\u636E\uFF1A\u5F53\u524D\u80A1\u606F\u7387\u3001\u5E74\u5316\u5206\u7EA2\u989D\u3001\u9664\u606F\u65E5\u3001\u8FD15\u5E74\u5206\u7EA2\u5386\u53F2\u8BB0\u5F55\u3002\u9002\u5408\u5224\u65AD\u516C\u53F8\u662F\u5426\u7A33\u5B9A\u6D3E\u606F\u3002",
1565
+ inputSchema: {
1566
+ type: "object",
1567
+ properties: {
1568
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 AAPL\u3001JNJ\u3001KO" }
1569
+ },
1570
+ required: ["symbol"]
1571
+ }
1572
+ },
1573
+ handler: async (input) => {
1574
+ const parsed = SymbolInput3.safeParse(input);
1575
+ if (!parsed.success) return err(parsed.error.message);
1576
+ try {
1577
+ return ok(await fetchDividendHistory(parsed.data.symbol));
1578
+ } catch (e) {
1579
+ return err(e.message);
1580
+ }
1581
+ }
1582
+ };
1583
+ var getInstitutionalHoldersTool = {
1584
+ tool: {
1585
+ name: "get_institutional_holders",
1586
+ description: "\u83B7\u53D6\u7F8E\u80A1\u524D10\u5927\u673A\u6784\u6301\u4ED3\uFF1A\u673A\u6784\u540D\u79F0\u3001\u6301\u80A1\u6BD4\u4F8B\u3001\u6301\u80A1\u6570\u91CF\u3001\u6301\u4ED3\u5E02\u503C\u3001\u6700\u65B0\u62A5\u544A\u65E5\u671F\u3002",
1587
+ inputSchema: {
1588
+ type: "object",
1589
+ properties: {
1590
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 AAPL\u3001NVDA" }
1591
+ },
1592
+ required: ["symbol"]
1593
+ }
1594
+ },
1595
+ handler: async (input) => {
1596
+ const parsed = SymbolInput3.safeParse(input);
1597
+ if (!parsed.success) return err(parsed.error.message);
1598
+ try {
1599
+ return ok(await fetchInstitutionalHolders(parsed.data.symbol));
1600
+ } catch (e) {
1601
+ return err(e.message);
1602
+ }
1603
+ }
1604
+ };
1605
+ var getSimilarStocksTool = {
1606
+ tool: {
1607
+ name: "get_similar_stocks",
1608
+ description: "\u83B7\u53D6\u4E0E\u6307\u5B9A\u80A1\u7968\u540C\u884C\u4E1A\u7684\u53EF\u6BD4\u516C\u53F8\u5217\u8868\uFF0C\u542B\u5B9E\u65F6\u4EF7\u683C\u3001\u6DA8\u8DCC\u5E45\u3001\u5E02\u503C\u3001PE\u3001\u884C\u4E1A\u5206\u7C7B\u3002\u9002\u5408\u6A2A\u5411\u4F30\u503C\u5BF9\u6BD4\u3002",
1609
+ inputSchema: {
1610
+ type: "object",
1611
+ properties: {
1612
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 NVDA\u3001TSLA\u3001AAPL" }
1613
+ },
1614
+ required: ["symbol"]
1615
+ }
1616
+ },
1617
+ handler: async (input) => {
1618
+ const parsed = SymbolInput3.safeParse(input);
1619
+ if (!parsed.success) return err(parsed.error.message);
1620
+ try {
1621
+ return ok(await fetchSimilarStocks(parsed.data.symbol));
1622
+ } catch (e) {
1623
+ return err(e.message);
1624
+ }
1625
+ }
1626
+ };
1627
+ var getShortInterestTool = {
1628
+ tool: {
1629
+ name: "get_short_interest",
1630
+ description: "\u83B7\u53D6\u7F8E\u80A1\u505A\u7A7A\u6570\u636E\uFF1A\u7A7A\u5934\u80A1\u6570\u3001\u505A\u7A7A\u5360\u6D41\u901A\u76D8\u6BD4\u4F8B\u3001\u7A7A\u5934\u56DE\u8865\u5929\u6570\uFF08Short Ratio\uFF09\u3001\u4E0E\u4E0A\u6708\u5BF9\u6BD4\u53D8\u5316\u3002\u7A7A\u5934\u56DE\u8865\u5929\u6570\u8D8A\u9AD8\u4EE3\u8868\u903C\u7A7A\u98CE\u9669\u8D8A\u5927\u3002",
1631
+ inputSchema: {
1632
+ type: "object",
1633
+ properties: {
1634
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 GME\u3001TSLA\u3001NVDA" }
1635
+ },
1636
+ required: ["symbol"]
1637
+ }
1638
+ },
1639
+ handler: async (input) => {
1640
+ const parsed = SymbolInput3.safeParse(input);
1641
+ if (!parsed.success) return err(parsed.error.message);
1642
+ try {
1643
+ return ok(await fetchShortInterest(parsed.data.symbol));
1644
+ } catch (e) {
1645
+ return err(e.message);
1646
+ }
1647
+ }
1648
+ };
1649
+
1415
1650
  // src/tools/index.ts
1416
1651
  var tools = [
1417
1652
  getQuoteTool,
@@ -1431,7 +1666,12 @@ var tools = [
1431
1666
  getAnalystRatingTool,
1432
1667
  getStockNewsTool,
1433
1668
  getInsiderActivityTool,
1434
- getStockFullOverviewTool
1669
+ getStockFullOverviewTool,
1670
+ getMarketMoversTool,
1671
+ getDividendHistoryTool,
1672
+ getInstitutionalHoldersTool,
1673
+ getSimilarStocksTool,
1674
+ getShortInterestTool
1435
1675
  ];
1436
1676
  var toolMap = new Map(
1437
1677
  tools.map((t) => [t.tool.name, t])
@@ -1442,10 +1682,10 @@ var server = new import_server.Server(
1442
1682
  { name: "stock-mcp", version: "1.0.0" },
1443
1683
  { capabilities: { tools: {} } }
1444
1684
  );
1445
- server.setRequestHandler(import_types7.ListToolsRequestSchema, async () => ({
1685
+ server.setRequestHandler(import_types8.ListToolsRequestSchema, async () => ({
1446
1686
  tools: tools.map((t) => t.tool)
1447
1687
  }));
1448
- server.setRequestHandler(import_types7.CallToolRequestSchema, async (request) => {
1688
+ server.setRequestHandler(import_types8.CallToolRequestSchema, async (request) => {
1449
1689
  const { name, arguments: args } = request.params;
1450
1690
  const def = toolMap.get(name);
1451
1691
  if (!def) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xstock-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Stock & Crypto analysis MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/data/yahoo.ts CHANGED
@@ -340,6 +340,206 @@ export async function fetchInsiderActivity(symbol: string): Promise<InsiderTrans
340
340
  }));
341
341
  }
342
342
 
343
+ // ─── Short Interest ───────────────────────────────────────────────────────────
344
+
345
+ export interface ShortInterest {
346
+ symbol: string;
347
+ sharesShort: number | null;
348
+ sharesShortPriorMonth: number | null;
349
+ shortRatio: number | null;
350
+ shortPercentOfFloat: number | null;
351
+ floatShares: number | null;
352
+ sharesOutstanding: number | null;
353
+ settlementDate: string | null;
354
+ changeFromPriorMonth: number | null;
355
+ }
356
+
357
+ export async function fetchShortInterest(symbol: string): Promise<ShortInterest> {
358
+ process.stderr.write(`[yahoo] fetchShortInterest symbol=${symbol}\n`);
359
+ const summary = await yf.quoteSummary(symbol, {
360
+ modules: ["defaultKeyStatistics"] as never[],
361
+ });
362
+ const stats = summary.defaultKeyStatistics as Record<string, unknown> | undefined;
363
+
364
+ const sharesShort = stats?.sharesShort != null ? Number(stats.sharesShort) : null;
365
+ const priorMonth = stats?.sharesShortPriorMonth != null ? Number(stats.sharesShortPriorMonth) : null;
366
+ const change = sharesShort !== null && priorMonth !== null && priorMonth !== 0
367
+ ? Math.round(((sharesShort - priorMonth) / priorMonth) * 10000) / 100
368
+ : null;
369
+
370
+ return {
371
+ symbol,
372
+ sharesShort,
373
+ sharesShortPriorMonth: priorMonth,
374
+ shortRatio: stats?.shortRatio != null ? Number(stats.shortRatio) : null,
375
+ shortPercentOfFloat: stats?.shortPercentOfFloat != null
376
+ ? Math.round(Number(stats.shortPercentOfFloat) * 10000) / 100 : null,
377
+ floatShares: stats?.floatShares != null ? Number(stats.floatShares) : null,
378
+ sharesOutstanding: stats?.sharesOutstanding != null ? Number(stats.sharesOutstanding) : null,
379
+ settlementDate: stats?.dateShortInterest instanceof Date
380
+ ? stats.dateShortInterest.toISOString().slice(0, 10) : null,
381
+ changeFromPriorMonth: change,
382
+ };
383
+ }
384
+
385
+ // ─── Market Movers ────────────────────────────────────────────────────────────
386
+
387
+ export interface MoverItem {
388
+ symbol: string;
389
+ name: string;
390
+ price: number;
391
+ change: number;
392
+ changePercent: string;
393
+ volume: number;
394
+ marketCap: number | null;
395
+ }
396
+
397
+ function mapQuoteToMover(q: Record<string, unknown>): MoverItem {
398
+ return {
399
+ symbol: String(q.symbol ?? ""),
400
+ name: String(q.longName ?? q.shortName ?? q.symbol ?? ""),
401
+ price: Number(q.regularMarketPrice ?? 0),
402
+ change: Number(q.regularMarketChange ?? 0),
403
+ changePercent: (Number(q.regularMarketChangePercent ?? 0).toFixed(2)) + "%",
404
+ volume: Number(q.regularMarketVolume ?? 0),
405
+ marketCap: q.marketCap != null ? Number(q.marketCap) : null,
406
+ };
407
+ }
408
+
409
+ export async function fetchMarketMovers(type: "gainers" | "losers" | "actives", count = 20): Promise<MoverItem[]> {
410
+ process.stderr.write(`[yahoo] fetchMarketMovers type=${type} count=${count}\n`);
411
+ let result: Record<string, unknown>;
412
+ if (type === "gainers") {
413
+ result = await (yf as unknown as Record<string, (opts: unknown) => Promise<Record<string, unknown>>>)
414
+ .dailyGainers({ count, region: "US" });
415
+ } else if (type === "losers") {
416
+ result = await (yf as unknown as Record<string, (opts: unknown) => Promise<Record<string, unknown>>>)
417
+ .dailyLosers({ count, region: "US" });
418
+ } else {
419
+ result = await (yf as unknown as Record<string, (opts: unknown) => Promise<Record<string, unknown>>>)
420
+ .mostActives({ count, region: "US" });
421
+ }
422
+ const quotes = (result.quotes as Record<string, unknown>[]) ?? [];
423
+ return quotes.map(mapQuoteToMover);
424
+ }
425
+
426
+ // ─── Dividend History ─────────────────────────────────────────────────────────
427
+
428
+ export interface DividendInfo {
429
+ symbol: string;
430
+ currentYield: number | null;
431
+ annualDividend: number | null;
432
+ exDividendDate: string | null;
433
+ consecutiveYears: number | null;
434
+ history: { date: string; amount: number }[];
435
+ }
436
+
437
+ export async function fetchDividendHistory(symbol: string): Promise<DividendInfo> {
438
+ process.stderr.write(`[yahoo] fetchDividendHistory symbol=${symbol}\n`);
439
+ const fiveYearsAgo = new Date(Date.now() - 5 * 365 * 86400_000).toISOString().slice(0, 10);
440
+ const today = new Date().toISOString().slice(0, 10);
441
+
442
+ const [summaryResult, histResult] = await Promise.allSettled([
443
+ yf.quoteSummary(symbol, { modules: ["summaryDetail", "defaultKeyStatistics"] }),
444
+ yf.historical(symbol, { period1: fiveYearsAgo, period2: today, events: "dividends" }),
445
+ ]);
446
+
447
+ const detail = summaryResult.status === "fulfilled"
448
+ ? summaryResult.value.summaryDetail as Record<string, unknown> | undefined : undefined;
449
+
450
+ const history = histResult.status === "fulfilled"
451
+ ? (histResult.value as unknown as { date: Date; dividends: number }[])
452
+ .filter((r) => r.dividends != null)
453
+ .map((r) => ({ date: r.date.toISOString().slice(0, 10), amount: r.dividends }))
454
+ .reverse()
455
+ : [];
456
+
457
+ const consecutiveYears = history.length > 0
458
+ ? new Set(history.map((h) => h.date.slice(0, 4))).size : null;
459
+
460
+ return {
461
+ symbol,
462
+ currentYield: detail?.dividendYield as number | null ?? null,
463
+ annualDividend: detail?.dividendRate as number | null ?? null,
464
+ exDividendDate: detail?.exDividendDate instanceof Date
465
+ ? detail.exDividendDate.toISOString().slice(0, 10) : null,
466
+ consecutiveYears,
467
+ history,
468
+ };
469
+ }
470
+
471
+ // ─── Institutional Holders ────────────────────────────────────────────────────
472
+
473
+ export interface InstitutionalHolder {
474
+ organization: string;
475
+ pctHeld: number | null;
476
+ shares: number | null;
477
+ value: number | null;
478
+ reportDate: string | null;
479
+ }
480
+
481
+ export async function fetchInstitutionalHolders(symbol: string): Promise<InstitutionalHolder[]> {
482
+ process.stderr.write(`[yahoo] fetchInstitutionalHolders symbol=${symbol}\n`);
483
+ const summary = await yf.quoteSummary(symbol, {
484
+ modules: ["institutionOwnership"] as never[],
485
+ });
486
+ const ownership = summary.institutionOwnership as Record<string, unknown> | undefined;
487
+ const holders = (ownership?.ownershipList as Record<string, unknown>[]) ?? [];
488
+ return holders.slice(0, 10).map((h) => ({
489
+ organization: String(h.organization ?? ""),
490
+ pctHeld: h.pctHeld != null ? Math.round(Number(h.pctHeld) * 10000) / 100 : null,
491
+ shares: h.position != null ? Number(h.position) : null,
492
+ value: h.value != null ? Number(h.value) : null,
493
+ reportDate: h.reportDate instanceof Date ? h.reportDate.toISOString().slice(0, 10) : null,
494
+ }));
495
+ }
496
+
497
+ // ─── Similar Stocks ───────────────────────────────────────────────────────────
498
+
499
+ export interface SimilarStock {
500
+ symbol: string;
501
+ name: string;
502
+ price: number;
503
+ changePercent: string;
504
+ marketCap: number | null;
505
+ pe: number | null;
506
+ sector: string | null;
507
+ }
508
+
509
+ export async function fetchSimilarStocks(symbol: string): Promise<SimilarStock[]> {
510
+ process.stderr.write(`[yahoo] fetchSimilarStocks symbol=${symbol}\n`);
511
+ const recs = await (yf as unknown as Record<string, (sym: string) => Promise<Record<string, unknown>>>)
512
+ .recommendedSymbols(symbol);
513
+ const symbols: string[] = ((recs.recommendedSymbols as Record<string, unknown>[]) ?? [])
514
+ .slice(0, 8)
515
+ .map((r) => String(r.symbol ?? ""))
516
+ .filter(Boolean);
517
+
518
+ if (symbols.length === 0) return [];
519
+
520
+ const quotes = await fetchUSQuotes(symbols);
521
+ const profiles = await Promise.allSettled(
522
+ symbols.map((s) => yf.quoteSummary(s, { modules: ["price", "summaryProfile"] }))
523
+ );
524
+
525
+ return quotes.map((q, i) => {
526
+ const p = profiles[i];
527
+ const profile = p.status === "fulfilled"
528
+ ? p.value.summaryProfile as Record<string, unknown> | undefined : undefined;
529
+ const price = p.status === "fulfilled"
530
+ ? p.value.price as Record<string, unknown> | undefined : undefined;
531
+ return {
532
+ symbol: q.symbol,
533
+ name: q.name,
534
+ price: q.price,
535
+ changePercent: q.changePercent,
536
+ marketCap: price?.marketCap != null ? Number(price.marketCap) : q.marketCap,
537
+ pe: price?.trailingPE != null ? Number(price.trailingPE) : null,
538
+ sector: profile?.sector ? String(profile.sector) : null,
539
+ };
540
+ });
541
+ }
542
+
343
543
  export async function fetchStockProfile(symbol: string): Promise<StockProfile> {
344
544
  process.stderr.write(`[yahoo] fetchStockProfile symbol=${symbol}\n`);
345
545
  const summary = await yf.quoteSummary(symbol, {
@@ -5,6 +5,7 @@ import { searchStockTool } from "./search";
5
5
  import { getUsIndicesTool, getStockProfileTool, getEarningsCalendarTool, getUsSectorHeatmapTool } from "./us-market";
6
6
  import { getCryptoOverviewTool, getCryptoTopTool, getCryptoCatsTool, getCryptoFundingTool, getCryptoLiquidationTool } from "./crypto";
7
7
  import { getFinancialsTool, getAnalystRatingTool, getStockNewsTool, getInsiderActivityTool, getStockFullOverviewTool } from "./us-fundamentals";
8
+ import { getMarketMoversTool, getDividendHistoryTool, getInstitutionalHoldersTool, getSimilarStocksTool, getShortInterestTool } from "./us-market-b";
8
9
 
9
10
  export * from "./types";
10
11
 
@@ -27,6 +28,11 @@ export const tools: ToolDef[] = [
27
28
  getStockNewsTool,
28
29
  getInsiderActivityTool,
29
30
  getStockFullOverviewTool,
31
+ getMarketMoversTool,
32
+ getDividendHistoryTool,
33
+ getInstitutionalHoldersTool,
34
+ getSimilarStocksTool,
35
+ getShortInterestTool,
30
36
  ];
31
37
 
32
38
  export const toolMap = new Map<string, ToolDef>(
@@ -0,0 +1,156 @@
1
+ import { z } from "zod";
2
+ import {
3
+ fetchMarketMovers,
4
+ fetchDividendHistory,
5
+ fetchInstitutionalHolders,
6
+ fetchSimilarStocks,
7
+ fetchShortInterest,
8
+ } from "../data/yahoo";
9
+ import type { ToolDef } from "./types";
10
+ import { ok, err } from "./types";
11
+
12
+ // ─── get_market_movers ────────────────────────────────────────────────────────
13
+
14
+ const MoversInput = z.object({
15
+ type: z.enum(["gainers", "losers", "actives"]).optional(),
16
+ count: z.number().int().min(1).max(50).optional(),
17
+ });
18
+
19
+ export const getMarketMoversTool: ToolDef = {
20
+ tool: {
21
+ name: "get_market_movers",
22
+ description:
23
+ "获取美股当日榜单:涨幅榜(gainers)、跌幅榜(losers)、成交量异动榜(actives)。默认返回涨幅榜 Top 20。",
24
+ inputSchema: {
25
+ type: "object",
26
+ properties: {
27
+ type: {
28
+ type: "string",
29
+ enum: ["gainers", "losers", "actives"],
30
+ description: "榜单类型:gainers=涨幅榜,losers=跌幅榜,actives=成交量榜,默认 gainers",
31
+ },
32
+ count: {
33
+ type: "number",
34
+ description: "返回数量,默认20,最大50",
35
+ },
36
+ },
37
+ required: [],
38
+ },
39
+ },
40
+ handler: async (input) => {
41
+ const parsed = MoversInput.safeParse(input);
42
+ if (!parsed.success) return err(parsed.error.message);
43
+ try {
44
+ return ok(await fetchMarketMovers(parsed.data.type ?? "gainers", parsed.data.count ?? 20));
45
+ } catch (e) {
46
+ return err((e as Error).message);
47
+ }
48
+ },
49
+ };
50
+
51
+ // ─── get_dividend_history ─────────────────────────────────────────────────────
52
+
53
+ const SymbolInput = z.object({ symbol: z.string().min(1) });
54
+
55
+ export const getDividendHistoryTool: ToolDef = {
56
+ tool: {
57
+ name: "get_dividend_history",
58
+ description:
59
+ "获取美股分红数据:当前股息率、年化分红额、除息日、近5年分红历史记录。适合判断公司是否稳定派息。",
60
+ inputSchema: {
61
+ type: "object",
62
+ properties: {
63
+ symbol: { type: "string", description: "美股代码,如 AAPL、JNJ、KO" },
64
+ },
65
+ required: ["symbol"],
66
+ },
67
+ },
68
+ handler: async (input) => {
69
+ const parsed = SymbolInput.safeParse(input);
70
+ if (!parsed.success) return err(parsed.error.message);
71
+ try {
72
+ return ok(await fetchDividendHistory(parsed.data.symbol));
73
+ } catch (e) {
74
+ return err((e as Error).message);
75
+ }
76
+ },
77
+ };
78
+
79
+ // ─── get_institutional_holders ────────────────────────────────────────────────
80
+
81
+ export const getInstitutionalHoldersTool: ToolDef = {
82
+ tool: {
83
+ name: "get_institutional_holders",
84
+ description:
85
+ "获取美股前10大机构持仓:机构名称、持股比例、持股数量、持仓市值、最新报告日期。",
86
+ inputSchema: {
87
+ type: "object",
88
+ properties: {
89
+ symbol: { type: "string", description: "美股代码,如 AAPL、NVDA" },
90
+ },
91
+ required: ["symbol"],
92
+ },
93
+ },
94
+ handler: async (input) => {
95
+ const parsed = SymbolInput.safeParse(input);
96
+ if (!parsed.success) return err(parsed.error.message);
97
+ try {
98
+ return ok(await fetchInstitutionalHolders(parsed.data.symbol));
99
+ } catch (e) {
100
+ return err((e as Error).message);
101
+ }
102
+ },
103
+ };
104
+
105
+ // ─── get_similar_stocks ───────────────────────────────────────────────────────
106
+
107
+ export const getSimilarStocksTool: ToolDef = {
108
+ tool: {
109
+ name: "get_similar_stocks",
110
+ description:
111
+ "获取与指定股票同行业的可比公司列表,含实时价格、涨跌幅、市值、PE、行业分类。适合横向估值对比。",
112
+ inputSchema: {
113
+ type: "object",
114
+ properties: {
115
+ symbol: { type: "string", description: "美股代码,如 NVDA、TSLA、AAPL" },
116
+ },
117
+ required: ["symbol"],
118
+ },
119
+ },
120
+ handler: async (input) => {
121
+ const parsed = SymbolInput.safeParse(input);
122
+ if (!parsed.success) return err(parsed.error.message);
123
+ try {
124
+ return ok(await fetchSimilarStocks(parsed.data.symbol));
125
+ } catch (e) {
126
+ return err((e as Error).message);
127
+ }
128
+ },
129
+ };
130
+
131
+ // ─── get_short_interest ───────────────────────────────────────────────────────
132
+
133
+ export const getShortInterestTool: ToolDef = {
134
+ tool: {
135
+ name: "get_short_interest",
136
+ description:
137
+ "获取美股做空数据:空头股数、做空占流通盘比例、空头回补天数(Short Ratio)、与上月对比变化。" +
138
+ "空头回补天数越高代表逼空风险越大。",
139
+ inputSchema: {
140
+ type: "object",
141
+ properties: {
142
+ symbol: { type: "string", description: "美股代码,如 GME、TSLA、NVDA" },
143
+ },
144
+ required: ["symbol"],
145
+ },
146
+ },
147
+ handler: async (input) => {
148
+ const parsed = SymbolInput.safeParse(input);
149
+ if (!parsed.success) return err(parsed.error.message);
150
+ try {
151
+ return ok(await fetchShortInterest(parsed.data.symbol));
152
+ } catch (e) {
153
+ return err((e as Error).message);
154
+ }
155
+ },
156
+ };