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 +34 -6
- package/dist/index.js +244 -4
- package/package.json +1 -1
- package/src/data/yahoo.ts +200 -0
- package/src/tools/index.ts +6 -0
- package/src/tools/us-market-b.ts +156 -0
package/README.md
CHANGED
|
@@ -11,17 +11,45 @@
|
|
|
11
11
|
|
|
12
12
|
## 工具列表
|
|
13
13
|
|
|
14
|
+
### 美股
|
|
15
|
+
|
|
14
16
|
| 工具 | 说明 |
|
|
15
17
|
|------|------|
|
|
16
|
-
| `get_quote` |
|
|
18
|
+
| `get_quote` | 实时行情 |
|
|
17
19
|
| `get_kline` | K线数据,支持日线 / 周线 / 月线 |
|
|
18
|
-
| `
|
|
19
|
-
| `get_us_indices` |
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
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` |
|
|
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
|
|
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(
|
|
1685
|
+
server.setRequestHandler(import_types8.ListToolsRequestSchema, async () => ({
|
|
1446
1686
|
tools: tools.map((t) => t.tool)
|
|
1447
1687
|
}));
|
|
1448
|
-
server.setRequestHandler(
|
|
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
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, {
|
package/src/tools/index.ts
CHANGED
|
@@ -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
|
+
};
|