xstock-mcp 1.2.0 → 1.3.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,44 @@
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_stock_full_overview` | 复合工具:行情 + 基本面 + 评级 + 新闻,一次返回 |
34
+ | `search_stock` | 按名称或代码搜索 |
35
+
36
+ ### 加密货币
37
+
38
+ | 工具 | 说明 |
39
+ |------|------|
40
+ | `get_crypto_overview` | 全球市值 + 恐惧贪婪指数 |
22
41
  | `get_crypto_top` | 按市值排名的 Top N 币种 |
23
- | `get_crypto_categories` | 加密货币赛道分类(DeFi、Layer1、AI、GameFi…) |
42
+ | `get_crypto_categories` | 赛道分类(DeFi、Layer1、AI、GameFi…) |
24
43
  | `get_funding_rate` | Binance 永续合约资金费率 |
44
+ | `get_crypto_liquidation` | 合约市场情绪:OI、多空比、吃单比 |
45
+
46
+ ### A股 / 港股
47
+
48
+ | 工具 | 说明 |
49
+ |------|------|
50
+ | `get_quote` | 实时行情(腾讯财经) |
51
+ | `get_kline` | K线数据 |
25
52
 
26
53
  ## 使用方法
27
54
 
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,93 @@ async function fetchInsiderActivity(symbol) {
368
368
  sharesAfter: tx.shareholderAfter ?? null
369
369
  }));
370
370
  }
371
+ function mapQuoteToMover(q) {
372
+ return {
373
+ symbol: String(q.symbol ?? ""),
374
+ name: String(q.longName ?? q.shortName ?? q.symbol ?? ""),
375
+ price: Number(q.regularMarketPrice ?? 0),
376
+ change: Number(q.regularMarketChange ?? 0),
377
+ changePercent: Number(q.regularMarketChangePercent ?? 0).toFixed(2) + "%",
378
+ volume: Number(q.regularMarketVolume ?? 0),
379
+ marketCap: q.marketCap != null ? Number(q.marketCap) : null
380
+ };
381
+ }
382
+ async function fetchMarketMovers(type, count = 20) {
383
+ process.stderr.write(`[yahoo] fetchMarketMovers type=${type} count=${count}
384
+ `);
385
+ let result;
386
+ if (type === "gainers") {
387
+ result = await yf.dailyGainers({ count, region: "US" });
388
+ } else if (type === "losers") {
389
+ result = await yf.dailyLosers({ count, region: "US" });
390
+ } else {
391
+ result = await yf.mostActives({ count, region: "US" });
392
+ }
393
+ const quotes = result.quotes ?? [];
394
+ return quotes.map(mapQuoteToMover);
395
+ }
396
+ async function fetchDividendHistory(symbol) {
397
+ process.stderr.write(`[yahoo] fetchDividendHistory symbol=${symbol}
398
+ `);
399
+ const fiveYearsAgo = new Date(Date.now() - 5 * 365 * 864e5).toISOString().slice(0, 10);
400
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
401
+ const [summaryResult, histResult] = await Promise.allSettled([
402
+ yf.quoteSummary(symbol, { modules: ["summaryDetail", "defaultKeyStatistics"] }),
403
+ yf.historical(symbol, { period1: fiveYearsAgo, period2: today, events: "dividends" })
404
+ ]);
405
+ const detail = summaryResult.status === "fulfilled" ? summaryResult.value.summaryDetail : void 0;
406
+ 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() : [];
407
+ const consecutiveYears = history.length > 0 ? new Set(history.map((h) => h.date.slice(0, 4))).size : null;
408
+ return {
409
+ symbol,
410
+ currentYield: detail?.dividendYield ?? null,
411
+ annualDividend: detail?.dividendRate ?? null,
412
+ exDividendDate: detail?.exDividendDate instanceof Date ? detail.exDividendDate.toISOString().slice(0, 10) : null,
413
+ consecutiveYears,
414
+ history
415
+ };
416
+ }
417
+ async function fetchInstitutionalHolders(symbol) {
418
+ process.stderr.write(`[yahoo] fetchInstitutionalHolders symbol=${symbol}
419
+ `);
420
+ const summary = await yf.quoteSummary(symbol, {
421
+ modules: ["institutionOwnership"]
422
+ });
423
+ const ownership = summary.institutionOwnership;
424
+ const holders = ownership?.ownershipList ?? [];
425
+ return holders.slice(0, 10).map((h) => ({
426
+ organization: String(h.organization ?? ""),
427
+ pctHeld: h.pctHeld != null ? Math.round(Number(h.pctHeld) * 1e4) / 100 : null,
428
+ shares: h.position != null ? Number(h.position) : null,
429
+ value: h.value != null ? Number(h.value) : null,
430
+ reportDate: h.reportDate instanceof Date ? h.reportDate.toISOString().slice(0, 10) : null
431
+ }));
432
+ }
433
+ async function fetchSimilarStocks(symbol) {
434
+ process.stderr.write(`[yahoo] fetchSimilarStocks symbol=${symbol}
435
+ `);
436
+ const recs = await yf.recommendedSymbols(symbol);
437
+ const symbols = (recs.recommendedSymbols ?? []).slice(0, 8).map((r) => String(r.symbol ?? "")).filter(Boolean);
438
+ if (symbols.length === 0) return [];
439
+ const quotes = await fetchUSQuotes(symbols);
440
+ const profiles = await Promise.allSettled(
441
+ symbols.map((s) => yf.quoteSummary(s, { modules: ["price", "summaryProfile"] }))
442
+ );
443
+ return quotes.map((q, i) => {
444
+ const p = profiles[i];
445
+ const profile = p.status === "fulfilled" ? p.value.summaryProfile : void 0;
446
+ const price = p.status === "fulfilled" ? p.value.price : void 0;
447
+ return {
448
+ symbol: q.symbol,
449
+ name: q.name,
450
+ price: q.price,
451
+ changePercent: q.changePercent,
452
+ marketCap: price?.marketCap != null ? Number(price.marketCap) : q.marketCap,
453
+ pe: price?.trailingPE != null ? Number(price.trailingPE) : null,
454
+ sector: profile?.sector ? String(profile.sector) : null
455
+ };
456
+ });
457
+ }
371
458
  async function fetchStockProfile(symbol) {
372
459
  process.stderr.write(`[yahoo] fetchStockProfile symbol=${symbol}
373
460
  `);
@@ -1412,6 +1499,110 @@ var getStockFullOverviewTool = {
1412
1499
  }
1413
1500
  };
1414
1501
 
1502
+ // src/tools/us-market-b.ts
1503
+ var import_zod7 = require("zod");
1504
+ var MoversInput = import_zod7.z.object({
1505
+ type: import_zod7.z.enum(["gainers", "losers", "actives"]).optional(),
1506
+ count: import_zod7.z.number().int().min(1).max(50).optional()
1507
+ });
1508
+ var getMarketMoversTool = {
1509
+ tool: {
1510
+ name: "get_market_movers",
1511
+ 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",
1512
+ inputSchema: {
1513
+ type: "object",
1514
+ properties: {
1515
+ type: {
1516
+ type: "string",
1517
+ enum: ["gainers", "losers", "actives"],
1518
+ description: "\u699C\u5355\u7C7B\u578B\uFF1Againers=\u6DA8\u5E45\u699C\uFF0Closers=\u8DCC\u5E45\u699C\uFF0Cactives=\u6210\u4EA4\u91CF\u699C\uFF0C\u9ED8\u8BA4 gainers"
1519
+ },
1520
+ count: {
1521
+ type: "number",
1522
+ description: "\u8FD4\u56DE\u6570\u91CF\uFF0C\u9ED8\u8BA420\uFF0C\u6700\u592750"
1523
+ }
1524
+ },
1525
+ required: []
1526
+ }
1527
+ },
1528
+ handler: async (input) => {
1529
+ const parsed = MoversInput.safeParse(input);
1530
+ if (!parsed.success) return err(parsed.error.message);
1531
+ try {
1532
+ return ok(await fetchMarketMovers(parsed.data.type ?? "gainers", parsed.data.count ?? 20));
1533
+ } catch (e) {
1534
+ return err(e.message);
1535
+ }
1536
+ }
1537
+ };
1538
+ var SymbolInput3 = import_zod7.z.object({ symbol: import_zod7.z.string().min(1) });
1539
+ var getDividendHistoryTool = {
1540
+ tool: {
1541
+ name: "get_dividend_history",
1542
+ 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",
1543
+ inputSchema: {
1544
+ type: "object",
1545
+ properties: {
1546
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 AAPL\u3001JNJ\u3001KO" }
1547
+ },
1548
+ required: ["symbol"]
1549
+ }
1550
+ },
1551
+ handler: async (input) => {
1552
+ const parsed = SymbolInput3.safeParse(input);
1553
+ if (!parsed.success) return err(parsed.error.message);
1554
+ try {
1555
+ return ok(await fetchDividendHistory(parsed.data.symbol));
1556
+ } catch (e) {
1557
+ return err(e.message);
1558
+ }
1559
+ }
1560
+ };
1561
+ var getInstitutionalHoldersTool = {
1562
+ tool: {
1563
+ name: "get_institutional_holders",
1564
+ 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",
1565
+ inputSchema: {
1566
+ type: "object",
1567
+ properties: {
1568
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 AAPL\u3001NVDA" }
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 fetchInstitutionalHolders(parsed.data.symbol));
1578
+ } catch (e) {
1579
+ return err(e.message);
1580
+ }
1581
+ }
1582
+ };
1583
+ var getSimilarStocksTool = {
1584
+ tool: {
1585
+ name: "get_similar_stocks",
1586
+ 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",
1587
+ inputSchema: {
1588
+ type: "object",
1589
+ properties: {
1590
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 NVDA\u3001TSLA\u3001AAPL" }
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 fetchSimilarStocks(parsed.data.symbol));
1600
+ } catch (e) {
1601
+ return err(e.message);
1602
+ }
1603
+ }
1604
+ };
1605
+
1415
1606
  // src/tools/index.ts
1416
1607
  var tools = [
1417
1608
  getQuoteTool,
@@ -1431,7 +1622,11 @@ var tools = [
1431
1622
  getAnalystRatingTool,
1432
1623
  getStockNewsTool,
1433
1624
  getInsiderActivityTool,
1434
- getStockFullOverviewTool
1625
+ getStockFullOverviewTool,
1626
+ getMarketMoversTool,
1627
+ getDividendHistoryTool,
1628
+ getInstitutionalHoldersTool,
1629
+ getSimilarStocksTool
1435
1630
  ];
1436
1631
  var toolMap = new Map(
1437
1632
  tools.map((t) => [t.tool.name, t])
@@ -1442,10 +1637,10 @@ var server = new import_server.Server(
1442
1637
  { name: "stock-mcp", version: "1.0.0" },
1443
1638
  { capabilities: { tools: {} } }
1444
1639
  );
1445
- server.setRequestHandler(import_types7.ListToolsRequestSchema, async () => ({
1640
+ server.setRequestHandler(import_types8.ListToolsRequestSchema, async () => ({
1446
1641
  tools: tools.map((t) => t.tool)
1447
1642
  }));
1448
- server.setRequestHandler(import_types7.CallToolRequestSchema, async (request) => {
1643
+ server.setRequestHandler(import_types8.CallToolRequestSchema, async (request) => {
1449
1644
  const { name, arguments: args } = request.params;
1450
1645
  const def = toolMap.get(name);
1451
1646
  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.3.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,164 @@ export async function fetchInsiderActivity(symbol: string): Promise<InsiderTrans
340
340
  }));
341
341
  }
342
342
 
343
+ // ─── Market Movers ────────────────────────────────────────────────────────────
344
+
345
+ export interface MoverItem {
346
+ symbol: string;
347
+ name: string;
348
+ price: number;
349
+ change: number;
350
+ changePercent: string;
351
+ volume: number;
352
+ marketCap: number | null;
353
+ }
354
+
355
+ function mapQuoteToMover(q: Record<string, unknown>): MoverItem {
356
+ return {
357
+ symbol: String(q.symbol ?? ""),
358
+ name: String(q.longName ?? q.shortName ?? q.symbol ?? ""),
359
+ price: Number(q.regularMarketPrice ?? 0),
360
+ change: Number(q.regularMarketChange ?? 0),
361
+ changePercent: (Number(q.regularMarketChangePercent ?? 0).toFixed(2)) + "%",
362
+ volume: Number(q.regularMarketVolume ?? 0),
363
+ marketCap: q.marketCap != null ? Number(q.marketCap) : null,
364
+ };
365
+ }
366
+
367
+ export async function fetchMarketMovers(type: "gainers" | "losers" | "actives", count = 20): Promise<MoverItem[]> {
368
+ process.stderr.write(`[yahoo] fetchMarketMovers type=${type} count=${count}\n`);
369
+ let result: Record<string, unknown>;
370
+ if (type === "gainers") {
371
+ result = await (yf as unknown as Record<string, (opts: unknown) => Promise<Record<string, unknown>>>)
372
+ .dailyGainers({ count, region: "US" });
373
+ } else if (type === "losers") {
374
+ result = await (yf as unknown as Record<string, (opts: unknown) => Promise<Record<string, unknown>>>)
375
+ .dailyLosers({ count, region: "US" });
376
+ } else {
377
+ result = await (yf as unknown as Record<string, (opts: unknown) => Promise<Record<string, unknown>>>)
378
+ .mostActives({ count, region: "US" });
379
+ }
380
+ const quotes = (result.quotes as Record<string, unknown>[]) ?? [];
381
+ return quotes.map(mapQuoteToMover);
382
+ }
383
+
384
+ // ─── Dividend History ─────────────────────────────────────────────────────────
385
+
386
+ export interface DividendInfo {
387
+ symbol: string;
388
+ currentYield: number | null;
389
+ annualDividend: number | null;
390
+ exDividendDate: string | null;
391
+ consecutiveYears: number | null;
392
+ history: { date: string; amount: number }[];
393
+ }
394
+
395
+ export async function fetchDividendHistory(symbol: string): Promise<DividendInfo> {
396
+ process.stderr.write(`[yahoo] fetchDividendHistory symbol=${symbol}\n`);
397
+ const fiveYearsAgo = new Date(Date.now() - 5 * 365 * 86400_000).toISOString().slice(0, 10);
398
+ const today = new Date().toISOString().slice(0, 10);
399
+
400
+ const [summaryResult, histResult] = await Promise.allSettled([
401
+ yf.quoteSummary(symbol, { modules: ["summaryDetail", "defaultKeyStatistics"] }),
402
+ yf.historical(symbol, { period1: fiveYearsAgo, period2: today, events: "dividends" }),
403
+ ]);
404
+
405
+ const detail = summaryResult.status === "fulfilled"
406
+ ? summaryResult.value.summaryDetail as Record<string, unknown> | undefined : undefined;
407
+
408
+ const history = histResult.status === "fulfilled"
409
+ ? (histResult.value as unknown as { date: Date; dividends: number }[])
410
+ .filter((r) => r.dividends != null)
411
+ .map((r) => ({ date: r.date.toISOString().slice(0, 10), amount: r.dividends }))
412
+ .reverse()
413
+ : [];
414
+
415
+ const consecutiveYears = history.length > 0
416
+ ? new Set(history.map((h) => h.date.slice(0, 4))).size : null;
417
+
418
+ return {
419
+ symbol,
420
+ currentYield: detail?.dividendYield as number | null ?? null,
421
+ annualDividend: detail?.dividendRate as number | null ?? null,
422
+ exDividendDate: detail?.exDividendDate instanceof Date
423
+ ? detail.exDividendDate.toISOString().slice(0, 10) : null,
424
+ consecutiveYears,
425
+ history,
426
+ };
427
+ }
428
+
429
+ // ─── Institutional Holders ────────────────────────────────────────────────────
430
+
431
+ export interface InstitutionalHolder {
432
+ organization: string;
433
+ pctHeld: number | null;
434
+ shares: number | null;
435
+ value: number | null;
436
+ reportDate: string | null;
437
+ }
438
+
439
+ export async function fetchInstitutionalHolders(symbol: string): Promise<InstitutionalHolder[]> {
440
+ process.stderr.write(`[yahoo] fetchInstitutionalHolders symbol=${symbol}\n`);
441
+ const summary = await yf.quoteSummary(symbol, {
442
+ modules: ["institutionOwnership"] as never[],
443
+ });
444
+ const ownership = summary.institutionOwnership as Record<string, unknown> | undefined;
445
+ const holders = (ownership?.ownershipList as Record<string, unknown>[]) ?? [];
446
+ return holders.slice(0, 10).map((h) => ({
447
+ organization: String(h.organization ?? ""),
448
+ pctHeld: h.pctHeld != null ? Math.round(Number(h.pctHeld) * 10000) / 100 : null,
449
+ shares: h.position != null ? Number(h.position) : null,
450
+ value: h.value != null ? Number(h.value) : null,
451
+ reportDate: h.reportDate instanceof Date ? h.reportDate.toISOString().slice(0, 10) : null,
452
+ }));
453
+ }
454
+
455
+ // ─── Similar Stocks ───────────────────────────────────────────────────────────
456
+
457
+ export interface SimilarStock {
458
+ symbol: string;
459
+ name: string;
460
+ price: number;
461
+ changePercent: string;
462
+ marketCap: number | null;
463
+ pe: number | null;
464
+ sector: string | null;
465
+ }
466
+
467
+ export async function fetchSimilarStocks(symbol: string): Promise<SimilarStock[]> {
468
+ process.stderr.write(`[yahoo] fetchSimilarStocks symbol=${symbol}\n`);
469
+ const recs = await (yf as unknown as Record<string, (sym: string) => Promise<Record<string, unknown>>>)
470
+ .recommendedSymbols(symbol);
471
+ const symbols: string[] = ((recs.recommendedSymbols as Record<string, unknown>[]) ?? [])
472
+ .slice(0, 8)
473
+ .map((r) => String(r.symbol ?? ""))
474
+ .filter(Boolean);
475
+
476
+ if (symbols.length === 0) return [];
477
+
478
+ const quotes = await fetchUSQuotes(symbols);
479
+ const profiles = await Promise.allSettled(
480
+ symbols.map((s) => yf.quoteSummary(s, { modules: ["price", "summaryProfile"] }))
481
+ );
482
+
483
+ return quotes.map((q, i) => {
484
+ const p = profiles[i];
485
+ const profile = p.status === "fulfilled"
486
+ ? p.value.summaryProfile as Record<string, unknown> | undefined : undefined;
487
+ const price = p.status === "fulfilled"
488
+ ? p.value.price as Record<string, unknown> | undefined : undefined;
489
+ return {
490
+ symbol: q.symbol,
491
+ name: q.name,
492
+ price: q.price,
493
+ changePercent: q.changePercent,
494
+ marketCap: price?.marketCap != null ? Number(price.marketCap) : q.marketCap,
495
+ pe: price?.trailingPE != null ? Number(price.trailingPE) : null,
496
+ sector: profile?.sector ? String(profile.sector) : null,
497
+ };
498
+ });
499
+ }
500
+
343
501
  export async function fetchStockProfile(symbol: string): Promise<StockProfile> {
344
502
  process.stderr.write(`[yahoo] fetchStockProfile symbol=${symbol}\n`);
345
503
  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 } from "./us-market-b";
8
9
 
9
10
  export * from "./types";
10
11
 
@@ -27,6 +28,10 @@ export const tools: ToolDef[] = [
27
28
  getStockNewsTool,
28
29
  getInsiderActivityTool,
29
30
  getStockFullOverviewTool,
31
+ getMarketMoversTool,
32
+ getDividendHistoryTool,
33
+ getInstitutionalHoldersTool,
34
+ getSimilarStocksTool,
30
35
  ];
31
36
 
32
37
  export const toolMap = new Map<string, ToolDef>(
@@ -0,0 +1,128 @@
1
+ import { z } from "zod";
2
+ import {
3
+ fetchMarketMovers,
4
+ fetchDividendHistory,
5
+ fetchInstitutionalHolders,
6
+ fetchSimilarStocks,
7
+ } from "../data/yahoo";
8
+ import type { ToolDef } from "./types";
9
+ import { ok, err } from "./types";
10
+
11
+ // ─── get_market_movers ────────────────────────────────────────────────────────
12
+
13
+ const MoversInput = z.object({
14
+ type: z.enum(["gainers", "losers", "actives"]).optional(),
15
+ count: z.number().int().min(1).max(50).optional(),
16
+ });
17
+
18
+ export const getMarketMoversTool: ToolDef = {
19
+ tool: {
20
+ name: "get_market_movers",
21
+ description:
22
+ "获取美股当日榜单:涨幅榜(gainers)、跌幅榜(losers)、成交量异动榜(actives)。默认返回涨幅榜 Top 20。",
23
+ inputSchema: {
24
+ type: "object",
25
+ properties: {
26
+ type: {
27
+ type: "string",
28
+ enum: ["gainers", "losers", "actives"],
29
+ description: "榜单类型:gainers=涨幅榜,losers=跌幅榜,actives=成交量榜,默认 gainers",
30
+ },
31
+ count: {
32
+ type: "number",
33
+ description: "返回数量,默认20,最大50",
34
+ },
35
+ },
36
+ required: [],
37
+ },
38
+ },
39
+ handler: async (input) => {
40
+ const parsed = MoversInput.safeParse(input);
41
+ if (!parsed.success) return err(parsed.error.message);
42
+ try {
43
+ return ok(await fetchMarketMovers(parsed.data.type ?? "gainers", parsed.data.count ?? 20));
44
+ } catch (e) {
45
+ return err((e as Error).message);
46
+ }
47
+ },
48
+ };
49
+
50
+ // ─── get_dividend_history ─────────────────────────────────────────────────────
51
+
52
+ const SymbolInput = z.object({ symbol: z.string().min(1) });
53
+
54
+ export const getDividendHistoryTool: ToolDef = {
55
+ tool: {
56
+ name: "get_dividend_history",
57
+ description:
58
+ "获取美股分红数据:当前股息率、年化分红额、除息日、近5年分红历史记录。适合判断公司是否稳定派息。",
59
+ inputSchema: {
60
+ type: "object",
61
+ properties: {
62
+ symbol: { type: "string", description: "美股代码,如 AAPL、JNJ、KO" },
63
+ },
64
+ required: ["symbol"],
65
+ },
66
+ },
67
+ handler: async (input) => {
68
+ const parsed = SymbolInput.safeParse(input);
69
+ if (!parsed.success) return err(parsed.error.message);
70
+ try {
71
+ return ok(await fetchDividendHistory(parsed.data.symbol));
72
+ } catch (e) {
73
+ return err((e as Error).message);
74
+ }
75
+ },
76
+ };
77
+
78
+ // ─── get_institutional_holders ────────────────────────────────────────────────
79
+
80
+ export const getInstitutionalHoldersTool: ToolDef = {
81
+ tool: {
82
+ name: "get_institutional_holders",
83
+ description:
84
+ "获取美股前10大机构持仓:机构名称、持股比例、持股数量、持仓市值、最新报告日期。",
85
+ inputSchema: {
86
+ type: "object",
87
+ properties: {
88
+ symbol: { type: "string", description: "美股代码,如 AAPL、NVDA" },
89
+ },
90
+ required: ["symbol"],
91
+ },
92
+ },
93
+ handler: async (input) => {
94
+ const parsed = SymbolInput.safeParse(input);
95
+ if (!parsed.success) return err(parsed.error.message);
96
+ try {
97
+ return ok(await fetchInstitutionalHolders(parsed.data.symbol));
98
+ } catch (e) {
99
+ return err((e as Error).message);
100
+ }
101
+ },
102
+ };
103
+
104
+ // ─── get_similar_stocks ───────────────────────────────────────────────────────
105
+
106
+ export const getSimilarStocksTool: ToolDef = {
107
+ tool: {
108
+ name: "get_similar_stocks",
109
+ description:
110
+ "获取与指定股票同行业的可比公司列表,含实时价格、涨跌幅、市值、PE、行业分类。适合横向估值对比。",
111
+ inputSchema: {
112
+ type: "object",
113
+ properties: {
114
+ symbol: { type: "string", description: "美股代码,如 NVDA、TSLA、AAPL" },
115
+ },
116
+ required: ["symbol"],
117
+ },
118
+ },
119
+ handler: async (input) => {
120
+ const parsed = SymbolInput.safeParse(input);
121
+ if (!parsed.success) return err(parsed.error.message);
122
+ try {
123
+ return ok(await fetchSimilarStocks(parsed.data.symbol));
124
+ } catch (e) {
125
+ return err((e as Error).message);
126
+ }
127
+ },
128
+ };