xstock-mcp 1.1.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_types6 = 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");
@@ -169,6 +169,7 @@ async function fetchHKKline(code, period = "daily") {
169
169
 
170
170
  // src/data/yahoo.ts
171
171
  var import_yahoo_finance2 = __toESM(require("yahoo-finance2"));
172
+ var import_axios2 = __toESM(require("axios"));
172
173
  var yf = new import_yahoo_finance2.default({ suppressNotices: ["yahooSurvey"] });
173
174
  async function fetchUSQuotes(symbols) {
174
175
  process.stderr.write(`[yahoo] fetchUSQuotes symbols=${symbols.join(",")}
@@ -254,6 +255,206 @@ async function fetchEarningsCalendar(symbol) {
254
255
  recentEPS
255
256
  };
256
257
  }
258
+ async function fetchFinancials(symbol, quarterly = false) {
259
+ process.stderr.write(`[yahoo] fetchFinancials symbol=${symbol} quarterly=${quarterly}
260
+ `);
261
+ const modules = quarterly ? ["incomeStatementHistoryQuarterly", "cashflowStatementHistoryQuarterly", "balanceSheetHistoryQuarterly"] : ["incomeStatementHistory", "cashflowStatementHistory", "balanceSheetHistory"];
262
+ const summary = await yf.quoteSummary(symbol, { modules });
263
+ const incomeKey = quarterly ? "incomeStatementHistoryQuarterly" : "incomeStatementHistory";
264
+ const cashKey = quarterly ? "cashflowStatementHistoryQuarterly" : "cashflowStatementHistory";
265
+ const balanceKey = quarterly ? "balanceSheetHistoryQuarterly" : "balanceSheetHistory";
266
+ const incomeArr = summary[incomeKey]?.incomeStatementHistory ?? [];
267
+ const cashArr = summary[cashKey]?.cashflowStatements ?? [];
268
+ const balanceArr = summary[balanceKey]?.balanceSheetStatements ?? [];
269
+ return incomeArr.slice(0, 4).map((inc, i) => {
270
+ const cash = cashArr[i] ?? {};
271
+ const bal = balanceArr[i] ?? {};
272
+ const date = inc.endDate instanceof Date ? inc.endDate.toISOString().slice(0, 10) : String(inc.endDate ?? "");
273
+ const revenue = inc.totalRevenue ?? null;
274
+ const gross = inc.grossProfit ?? null;
275
+ const net = inc.netIncome ?? null;
276
+ const opCash = cash.totalCashFromOperatingActivities ?? null;
277
+ const capex = cash.capitalExpenditures ?? null;
278
+ const debt = bal.totalDebt ?? (bal.longTermDebt ?? null);
279
+ const equity = bal.totalStockholderEquity ?? null;
280
+ return {
281
+ date,
282
+ totalRevenue: revenue,
283
+ grossProfit: gross,
284
+ grossMargin: revenue && gross ? Math.round(gross / revenue * 1e4) / 100 : null,
285
+ netIncome: net,
286
+ netMargin: revenue && net ? Math.round(net / revenue * 1e4) / 100 : null,
287
+ epsDiluted: inc.dilutedEPS ?? null,
288
+ operatingCashFlow: opCash,
289
+ freeCashFlow: opCash !== null && capex !== null ? opCash + capex : null,
290
+ totalDebt: debt,
291
+ stockholdersEquity: equity,
292
+ debtToEquity: debt !== null && equity !== null && equity !== 0 ? Math.round(debt / equity * 100) / 100 : null
293
+ };
294
+ });
295
+ }
296
+ async function fetchAnalystRating(symbol) {
297
+ process.stderr.write(`[yahoo] fetchAnalystRating symbol=${symbol}
298
+ `);
299
+ const summary = await yf.quoteSummary(symbol, {
300
+ modules: ["financialData", "recommendationTrend", "upgradeDowngradeHistory"]
301
+ });
302
+ const fin = summary.financialData;
303
+ const trend = summary.recommendationTrend;
304
+ const history = summary.upgradeDowngradeHistory;
305
+ const trendArr = trend?.trend ?? [];
306
+ const latest = trendArr[0] ?? {};
307
+ const changes = (history?.history ?? []).slice(0, 10).map((h) => ({
308
+ date: h.epochGradeDate instanceof Date ? h.epochGradeDate.toISOString().slice(0, 10) : String(h.epochGradeDate ?? ""),
309
+ firm: String(h.firm ?? ""),
310
+ action: String(h.action ?? ""),
311
+ from: h.fromGrade ? String(h.fromGrade) : null,
312
+ to: h.toGrade ? String(h.toGrade) : null
313
+ }));
314
+ return {
315
+ symbol,
316
+ consensus: fin?.recommendationKey ?? null,
317
+ targetPriceMean: fin?.targetMeanPrice ?? null,
318
+ targetPriceHigh: fin?.targetHighPrice ?? null,
319
+ targetPriceLow: fin?.targetLowPrice ?? null,
320
+ distribution: {
321
+ strongBuy: Number(latest.strongBuy ?? 0),
322
+ buy: Number(latest.buy ?? 0),
323
+ hold: Number(latest.hold ?? 0),
324
+ sell: Number(latest.sell ?? 0),
325
+ strongSell: Number(latest.strongSell ?? 0)
326
+ },
327
+ recentChanges: changes
328
+ };
329
+ }
330
+ function extractXmlTag(xml, tag) {
331
+ const match = xml.match(new RegExp(`<${tag}[^>]*>(?:<!\\[CDATA\\[)?([\\s\\S]*?)(?:\\]\\]>)?</${tag}>`, "i"));
332
+ return match ? match[1].trim() : "";
333
+ }
334
+ async function fetchStockNews(symbol, limit = 20) {
335
+ process.stderr.write(`[yahoo] fetchStockNews symbol=${symbol}
336
+ `);
337
+ const url = `https://feeds.finance.yahoo.com/rss/2.0/headline?s=${symbol}&region=US&lang=en-US`;
338
+ const res = await import_axios2.default.get(url, { timeout: 8e3, responseType: "text" });
339
+ const xml = res.data;
340
+ const items = [];
341
+ const itemMatches = xml.match(/<item>[\s\S]*?<\/item>/g) ?? [];
342
+ for (const item of itemMatches.slice(0, limit)) {
343
+ items.push({
344
+ title: extractXmlTag(item, "title"),
345
+ link: extractXmlTag(item, "link") || extractXmlTag(item, "guid"),
346
+ summary: extractXmlTag(item, "description").replace(/<[^>]+>/g, "").slice(0, 200),
347
+ pubDate: extractXmlTag(item, "pubDate")
348
+ });
349
+ }
350
+ process.stderr.write(`[yahoo] fetchStockNews \u8FD4\u56DE ${items.length} \u6761
351
+ `);
352
+ return items;
353
+ }
354
+ async function fetchInsiderActivity(symbol) {
355
+ process.stderr.write(`[yahoo] fetchInsiderActivity symbol=${symbol}
356
+ `);
357
+ const summary = await yf.quoteSummary(symbol, { modules: ["insiderTransactions"] });
358
+ const raw = summary.insiderTransactions;
359
+ const txArr = raw?.transactions ?? [];
360
+ return txArr.slice(0, 20).map((tx) => ({
361
+ date: tx.startDate instanceof Date ? tx.startDate.toISOString().slice(0, 10) : String(tx.startDate ?? ""),
362
+ name: String(tx.filerName ?? ""),
363
+ relation: String(tx.filerRelation ?? ""),
364
+ transactionType: String(tx.transactionDescription ?? ""),
365
+ shares: tx.shares ?? null,
366
+ value: tx.value ?? null,
367
+ sharesBefore: tx.shareholderBefore ?? null,
368
+ sharesAfter: tx.shareholderAfter ?? null
369
+ }));
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
+ }
257
458
  async function fetchStockProfile(symbol) {
258
459
  process.stderr.write(`[yahoo] fetchStockProfile symbol=${symbol}
259
460
  `);
@@ -286,7 +487,7 @@ async function fetchStockProfile(symbol) {
286
487
  }
287
488
 
288
489
  // src/data/binance.ts
289
- var import_axios2 = __toESM(require("axios"));
490
+ var import_axios3 = __toESM(require("axios"));
290
491
  function normalizeSymbol(symbol) {
291
492
  const s = symbol.toUpperCase();
292
493
  return s.endsWith("USDT") ? s : s + "USDT";
@@ -298,7 +499,7 @@ async function fetchCryptoTickers(symbols) {
298
499
  symbols.map(async (raw) => {
299
500
  const sym = normalizeSymbol(raw);
300
501
  try {
301
- const res = await import_axios2.default.get(
502
+ const res = await import_axios3.default.get(
302
503
  `https://api.binance.com/api/v3/ticker/24hr?symbol=${sym}`,
303
504
  { timeout: 5e3 }
304
505
  );
@@ -331,11 +532,11 @@ async function fetchMarketSentiment(symbol, period = "1h") {
331
532
  const base = "https://fapi.binance.com";
332
533
  const params = `symbol=${sym}&period=${period}&limit=1`;
333
534
  const [oiRes, globalRes, topAccRes, topPosRes, takerRes] = await Promise.all([
334
- import_axios2.default.get(`${base}/fapi/v1/openInterest?symbol=${sym}`, { timeout: 5e3 }),
335
- import_axios2.default.get(`${base}/futures/data/globalLongShortAccountRatio?${params}`, { timeout: 5e3 }),
336
- import_axios2.default.get(`${base}/futures/data/topLongShortAccountRatio?${params}`, { timeout: 5e3 }),
337
- import_axios2.default.get(`${base}/futures/data/topLongShortPositionRatio?${params}`, { timeout: 5e3 }),
338
- import_axios2.default.get(`${base}/futures/data/takerlongshortRatio?${params}`, { timeout: 5e3 })
535
+ import_axios3.default.get(`${base}/fapi/v1/openInterest?symbol=${sym}`, { timeout: 5e3 }),
536
+ import_axios3.default.get(`${base}/futures/data/globalLongShortAccountRatio?${params}`, { timeout: 5e3 }),
537
+ import_axios3.default.get(`${base}/futures/data/topLongShortAccountRatio?${params}`, { timeout: 5e3 }),
538
+ import_axios3.default.get(`${base}/futures/data/topLongShortPositionRatio?${params}`, { timeout: 5e3 }),
539
+ import_axios3.default.get(`${base}/futures/data/takerlongshortRatio?${params}`, { timeout: 5e3 })
339
540
  ]);
340
541
  const oi = oiRes.data;
341
542
  const global = globalRes.data[0] ?? {};
@@ -360,7 +561,7 @@ async function fetchCryptoKlines(symbol, interval = "1d", limit = 100) {
360
561
  const sym = normalizeSymbol(symbol);
361
562
  process.stderr.write(`[binance] fetchCryptoKlines symbol=${sym} interval=${interval} limit=${limit}
362
563
  `);
363
- const res = await import_axios2.default.get(
564
+ const res = await import_axios3.default.get(
364
565
  `https://api.binance.com/api/v3/klines?symbol=${sym}&interval=${interval}&limit=${limit}`,
365
566
  { timeout: 5e3 }
366
567
  );
@@ -909,16 +1110,16 @@ var getStockProfileTool = {
909
1110
 
910
1111
  // src/tools/crypto.ts
911
1112
  var import_zod5 = require("zod");
912
- var import_axios4 = __toESM(require("axios"));
1113
+ var import_axios5 = __toESM(require("axios"));
913
1114
 
914
1115
  // src/data/coingecko.ts
915
- var import_axios3 = __toESM(require("axios"));
1116
+ var import_axios4 = __toESM(require("axios"));
916
1117
  var BASE = "https://api.coingecko.com/api/v3";
917
1118
  var FEAR_GREED_URL = "https://api.alternative.me/fng/?limit=1";
918
1119
  async function fetchGlobalMarket() {
919
1120
  process.stderr.write(`[coingecko] fetchGlobalMarket
920
1121
  `);
921
- const res = await import_axios3.default.get(`${BASE}/global`, { timeout: 8e3 });
1122
+ const res = await import_axios4.default.get(`${BASE}/global`, { timeout: 8e3 });
922
1123
  const d = res.data.data;
923
1124
  const result = {
924
1125
  totalMarketCapUsd: d.total_market_cap?.usd ?? 0,
@@ -935,7 +1136,7 @@ async function fetchGlobalMarket() {
935
1136
  async function fetchTopCoins(limit = 50) {
936
1137
  process.stderr.write(`[coingecko] fetchTopCoins limit=${limit}
937
1138
  `);
938
- const res = await import_axios3.default.get(`${BASE}/coins/markets`, {
1139
+ const res = await import_axios4.default.get(`${BASE}/coins/markets`, {
939
1140
  timeout: 8e3,
940
1141
  params: {
941
1142
  vs_currency: "usd",
@@ -962,7 +1163,7 @@ async function fetchTopCoins(limit = 50) {
962
1163
  async function fetchCategories() {
963
1164
  process.stderr.write(`[coingecko] fetchCategories
964
1165
  `);
965
- const res = await import_axios3.default.get(`${BASE}/coins/categories`, { timeout: 8e3 });
1166
+ const res = await import_axios4.default.get(`${BASE}/coins/categories`, { timeout: 8e3 });
966
1167
  const categories = res.data.map((c) => ({
967
1168
  id: c.id,
968
1169
  name: c.name,
@@ -978,7 +1179,7 @@ async function fetchCategories() {
978
1179
  async function fetchFearGreed() {
979
1180
  process.stderr.write(`[coingecko] fetchFearGreed
980
1181
  `);
981
- const res = await import_axios3.default.get(FEAR_GREED_URL, { timeout: 5e3 });
1182
+ const res = await import_axios4.default.get(FEAR_GREED_URL, { timeout: 5e3 });
982
1183
  const d = res.data.data[0];
983
1184
  const result = {
984
1185
  value: parseInt(d.value),
@@ -1089,7 +1290,7 @@ var getCryptoFundingTool = {
1089
1290
  symbols.map(async (raw) => {
1090
1291
  const sym = raw.toUpperCase().endsWith("USDT") ? raw.toUpperCase() : raw.toUpperCase() + "USDT";
1091
1292
  try {
1092
- const res = await import_axios4.default.get(
1293
+ const res = await import_axios5.default.get(
1093
1294
  `https://fapi.binance.com/fapi/v1/premiumIndex?symbol=${sym}`,
1094
1295
  { timeout: 5e3 }
1095
1296
  );
@@ -1161,6 +1362,247 @@ var getCryptoLiquidationTool = {
1161
1362
  }
1162
1363
  };
1163
1364
 
1365
+ // src/tools/us-fundamentals.ts
1366
+ var import_zod6 = require("zod");
1367
+ var SymbolInput2 = import_zod6.z.object({ symbol: import_zod6.z.string().min(1) });
1368
+ var FinancialsInput = import_zod6.z.object({
1369
+ symbol: import_zod6.z.string().min(1),
1370
+ quarterly: import_zod6.z.boolean().optional()
1371
+ });
1372
+ var getFinancialsTool = {
1373
+ tool: {
1374
+ name: "get_financials",
1375
+ description: "\u83B7\u53D6\u7F8E\u80A1\u8D22\u52A1\u6570\u636E\uFF1A\u8425\u6536\u3001\u6BDB\u5229\u7387\u3001\u51C0\u5229\u6DA6\u3001\u51C0\u5229\u7387\u3001EPS\u3001\u81EA\u7531\u73B0\u91D1\u6D41\u3001\u8D1F\u503A\u7387\u3002\u9ED8\u8BA4\u8FD4\u56DE\u8FD14\u5E74\u5E74\u62A5\uFF0Cquarterly=true \u8FD4\u56DE\u8FD14\u5B63\u5EA6\u5B63\u62A5\u3002",
1376
+ inputSchema: {
1377
+ type: "object",
1378
+ properties: {
1379
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 AAPL\u3001NVDA" },
1380
+ quarterly: { type: "boolean", description: "true=\u5B63\u62A5\uFF0Cfalse=\u5E74\u62A5\uFF08\u9ED8\u8BA4\uFF09" }
1381
+ },
1382
+ required: ["symbol"]
1383
+ }
1384
+ },
1385
+ handler: async (input) => {
1386
+ const parsed = FinancialsInput.safeParse(input);
1387
+ if (!parsed.success) return err(parsed.error.message);
1388
+ try {
1389
+ return ok(await fetchFinancials(parsed.data.symbol, parsed.data.quarterly ?? false));
1390
+ } catch (e) {
1391
+ return err(e.message);
1392
+ }
1393
+ }
1394
+ };
1395
+ var getAnalystRatingTool = {
1396
+ tool: {
1397
+ name: "get_analyst_rating",
1398
+ description: "\u83B7\u53D6\u534E\u5C14\u8857\u5206\u6790\u5E08\u8BC4\u7EA7\uFF1A\u5171\u8BC6\u8BC4\u7EA7\uFF08buy/hold/sell\uFF09\u3001\u5E73\u5747\u76EE\u6807\u4EF7\u3001\u9AD8\u4F4E\u76EE\u6807\u4EF7\u3001\u5F3A\u4E70/\u4E70/\u6301\u6709/\u5356/\u5F3A\u5356\u4EBA\u6570\u5206\u5E03\uFF0C\u4EE5\u53CA\u6700\u8FD110\u6761\u8BC4\u7EA7\u53D8\u52A8\u8BB0\u5F55\u3002",
1399
+ inputSchema: {
1400
+ type: "object",
1401
+ properties: {
1402
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 AAPL\u3001NVDA" }
1403
+ },
1404
+ required: ["symbol"]
1405
+ }
1406
+ },
1407
+ handler: async (input) => {
1408
+ const parsed = SymbolInput2.safeParse(input);
1409
+ if (!parsed.success) return err(parsed.error.message);
1410
+ try {
1411
+ return ok(await fetchAnalystRating(parsed.data.symbol));
1412
+ } catch (e) {
1413
+ return err(e.message);
1414
+ }
1415
+ }
1416
+ };
1417
+ var NewsInput = import_zod6.z.object({
1418
+ symbol: import_zod6.z.string().min(1),
1419
+ limit: import_zod6.z.number().int().min(1).max(30).optional()
1420
+ });
1421
+ var getStockNewsTool = {
1422
+ tool: {
1423
+ name: "get_stock_news",
1424
+ description: "\u83B7\u53D6\u7F8E\u80A1\u4E2A\u80A1\u6700\u65B0\u65B0\u95FB\uFF08\u8FD17\u5929\uFF09\uFF0C\u542B\u6807\u9898\u3001\u6458\u8981\u3001\u94FE\u63A5\u3001\u53D1\u5E03\u65F6\u95F4\u3002\u6700\u591A30\u6761\uFF0C\u9ED8\u8BA415\u6761\u3002",
1425
+ inputSchema: {
1426
+ type: "object",
1427
+ properties: {
1428
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 AAPL\u3001NVDA" },
1429
+ limit: { type: "number", description: "\u8FD4\u56DE\u6761\u6570\uFF0C\u9ED8\u8BA415\uFF0C\u6700\u592730" }
1430
+ },
1431
+ required: ["symbol"]
1432
+ }
1433
+ },
1434
+ handler: async (input) => {
1435
+ const parsed = NewsInput.safeParse(input);
1436
+ if (!parsed.success) return err(parsed.error.message);
1437
+ try {
1438
+ return ok(await fetchStockNews(parsed.data.symbol, parsed.data.limit ?? 15));
1439
+ } catch (e) {
1440
+ return err(e.message);
1441
+ }
1442
+ }
1443
+ };
1444
+ var getInsiderActivityTool = {
1445
+ tool: {
1446
+ name: "get_insider_activity",
1447
+ description: "\u83B7\u53D6\u7F8E\u80A1\u5185\u90E8\u4EBA\uFF08\u9AD8\u7BA1/\u8463\u4E8B\uFF09\u8FD1\u671F\u4E70\u5356\u8BB0\u5F55\uFF0C\u57FA\u4E8E SEC Form 4 \u516C\u5F00\u6570\u636E\u3002\u542B\u4EA4\u6613\u4EBA\u59D3\u540D\u3001\u804C\u4F4D\u3001\u4EA4\u6613\u7C7B\u578B\u3001\u80A1\u6570\u3001\u4EA4\u6613\u91D1\u989D\u3002",
1448
+ inputSchema: {
1449
+ type: "object",
1450
+ properties: {
1451
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 AAPL\u3001NVDA" }
1452
+ },
1453
+ required: ["symbol"]
1454
+ }
1455
+ },
1456
+ handler: async (input) => {
1457
+ const parsed = SymbolInput2.safeParse(input);
1458
+ if (!parsed.success) return err(parsed.error.message);
1459
+ try {
1460
+ return ok(await fetchInsiderActivity(parsed.data.symbol));
1461
+ } catch (e) {
1462
+ return err(e.message);
1463
+ }
1464
+ }
1465
+ };
1466
+ var getStockFullOverviewTool = {
1467
+ tool: {
1468
+ name: "get_stock_full_overview",
1469
+ description: "\u4E00\u6B21\u8C03\u7528\u83B7\u53D6\u7F8E\u80A1\u5B8C\u6574\u5206\u6790\u6570\u636E\uFF1A\u5B9E\u65F6\u884C\u60C5 + \u516C\u53F8\u57FA\u672C\u9762 + \u5206\u6790\u5E08\u8BC4\u7EA7 + \u8FD1\u671F\u65B0\u95FB\uFF08\u5E76\u884C\u83B7\u53D6\uFF09\u3002\u9002\u5408\u56DE\u7B54\u7EFC\u5408\u5206\u6790\u7C7B\u95EE\u9898\uFF08\u5982\uFF1A\u5E2E\u6211\u5206\u6790\u4E00\u4E0B NVDA\uFF09\uFF0C\u907F\u514D\u591A\u6B21\u8C03\u7528\u4E0D\u540C\u5DE5\u5177\u3002",
1470
+ inputSchema: {
1471
+ type: "object",
1472
+ properties: {
1473
+ symbol: { type: "string", description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 AAPL\u3001NVDA\u3001TSLA" }
1474
+ },
1475
+ required: ["symbol"]
1476
+ }
1477
+ },
1478
+ handler: async (input) => {
1479
+ const parsed = SymbolInput2.safeParse(input);
1480
+ if (!parsed.success) return err(parsed.error.message);
1481
+ const { symbol } = parsed.data;
1482
+ try {
1483
+ const [quotes, profile, analyst, news] = await Promise.allSettled([
1484
+ fetchUSQuotes([symbol]),
1485
+ fetchStockProfile(symbol),
1486
+ fetchAnalystRating(symbol),
1487
+ fetchStockNews(symbol, 10)
1488
+ ]);
1489
+ return ok({
1490
+ symbol,
1491
+ quote: quotes.status === "fulfilled" ? quotes.value[0] ?? null : null,
1492
+ profile: profile.status === "fulfilled" ? profile.value : null,
1493
+ analyst: analyst.status === "fulfilled" ? analyst.value : null,
1494
+ news: news.status === "fulfilled" ? news.value : []
1495
+ });
1496
+ } catch (e) {
1497
+ return err(e.message);
1498
+ }
1499
+ }
1500
+ };
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
+
1164
1606
  // src/tools/index.ts
1165
1607
  var tools = [
1166
1608
  getQuoteTool,
@@ -1175,7 +1617,16 @@ var tools = [
1175
1617
  getCryptoTopTool,
1176
1618
  getCryptoCatsTool,
1177
1619
  getCryptoFundingTool,
1178
- getCryptoLiquidationTool
1620
+ getCryptoLiquidationTool,
1621
+ getFinancialsTool,
1622
+ getAnalystRatingTool,
1623
+ getStockNewsTool,
1624
+ getInsiderActivityTool,
1625
+ getStockFullOverviewTool,
1626
+ getMarketMoversTool,
1627
+ getDividendHistoryTool,
1628
+ getInstitutionalHoldersTool,
1629
+ getSimilarStocksTool
1179
1630
  ];
1180
1631
  var toolMap = new Map(
1181
1632
  tools.map((t) => [t.tool.name, t])
@@ -1186,10 +1637,10 @@ var server = new import_server.Server(
1186
1637
  { name: "stock-mcp", version: "1.0.0" },
1187
1638
  { capabilities: { tools: {} } }
1188
1639
  );
1189
- server.setRequestHandler(import_types6.ListToolsRequestSchema, async () => ({
1640
+ server.setRequestHandler(import_types8.ListToolsRequestSchema, async () => ({
1190
1641
  tools: tools.map((t) => t.tool)
1191
1642
  }));
1192
- server.setRequestHandler(import_types6.CallToolRequestSchema, async (request) => {
1643
+ server.setRequestHandler(import_types8.CallToolRequestSchema, async (request) => {
1193
1644
  const { name, arguments: args } = request.params;
1194
1645
  const def = toolMap.get(name);
1195
1646
  if (!def) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xstock-mcp",
3
- "version": "1.1.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
@@ -1,4 +1,5 @@
1
1
  import { default as YahooFinance } from "yahoo-finance2";
2
+ import axios from "axios";
2
3
 
3
4
  // v3 需要实例化
4
5
  const yf = new YahooFinance({ suppressNotices: ["yahooSurvey"] });
@@ -158,6 +159,345 @@ export async function fetchEarningsCalendar(symbol: string): Promise<EarningsCal
158
159
  };
159
160
  }
160
161
 
162
+ // ─── Financials ───────────────────────────────────────────────────────────────
163
+
164
+ export interface FinancialPeriod {
165
+ date: string;
166
+ totalRevenue: number | null;
167
+ grossProfit: number | null;
168
+ grossMargin: number | null;
169
+ netIncome: number | null;
170
+ netMargin: number | null;
171
+ epsDiluted: number | null;
172
+ operatingCashFlow: number | null;
173
+ freeCashFlow: number | null;
174
+ totalDebt: number | null;
175
+ stockholdersEquity: number | null;
176
+ debtToEquity: number | null;
177
+ }
178
+
179
+ export async function fetchFinancials(symbol: string, quarterly = false): Promise<FinancialPeriod[]> {
180
+ process.stderr.write(`[yahoo] fetchFinancials symbol=${symbol} quarterly=${quarterly}\n`);
181
+ const modules = quarterly
182
+ ? ["incomeStatementHistoryQuarterly", "cashflowStatementHistoryQuarterly", "balanceSheetHistoryQuarterly"]
183
+ : ["incomeStatementHistory", "cashflowStatementHistory", "balanceSheetHistory"];
184
+
185
+ const summary = await yf.quoteSummary(symbol, { modules: modules as never[] });
186
+
187
+ const incomeKey = quarterly ? "incomeStatementHistoryQuarterly" : "incomeStatementHistory";
188
+ const cashKey = quarterly ? "cashflowStatementHistoryQuarterly" : "cashflowStatementHistory";
189
+ const balanceKey = quarterly ? "balanceSheetHistoryQuarterly" : "balanceSheetHistory";
190
+
191
+ const incomeArr = ((summary[incomeKey as keyof typeof summary] as Record<string, unknown>)
192
+ ?.incomeStatementHistory as Record<string, unknown>[]) ?? [];
193
+ const cashArr = ((summary[cashKey as keyof typeof summary] as Record<string, unknown>)
194
+ ?.cashflowStatements as Record<string, unknown>[]) ?? [];
195
+ const balanceArr = ((summary[balanceKey as keyof typeof summary] as Record<string, unknown>)
196
+ ?.balanceSheetStatements as Record<string, unknown>[]) ?? [];
197
+
198
+ return incomeArr.slice(0, 4).map((inc, i) => {
199
+ const cash = cashArr[i] ?? {};
200
+ const bal = balanceArr[i] ?? {};
201
+ const date = inc.endDate instanceof Date ? inc.endDate.toISOString().slice(0, 10) : String(inc.endDate ?? "");
202
+ const revenue = inc.totalRevenue as number | null ?? null;
203
+ const gross = inc.grossProfit as number | null ?? null;
204
+ const net = inc.netIncome as number | null ?? null;
205
+ const opCash = cash.totalCashFromOperatingActivities as number | null ?? null;
206
+ const capex = cash.capitalExpenditures as number | null ?? null;
207
+ const debt = bal.totalDebt as number | null ?? (bal.longTermDebt as number | null ?? null);
208
+ const equity = bal.totalStockholderEquity as number | null ?? null;
209
+
210
+ return {
211
+ date,
212
+ totalRevenue: revenue,
213
+ grossProfit: gross,
214
+ grossMargin: revenue && gross ? Math.round((gross / revenue) * 10000) / 100 : null,
215
+ netIncome: net,
216
+ netMargin: revenue && net ? Math.round((net / revenue) * 10000) / 100 : null,
217
+ epsDiluted: inc.dilutedEPS as number | null ?? null,
218
+ operatingCashFlow: opCash,
219
+ freeCashFlow: opCash !== null && capex !== null ? opCash + capex : null,
220
+ totalDebt: debt,
221
+ stockholdersEquity: equity,
222
+ debtToEquity: debt !== null && equity !== null && equity !== 0
223
+ ? Math.round((debt / equity) * 100) / 100 : null,
224
+ };
225
+ });
226
+ }
227
+
228
+ // ─── Analyst Rating ───────────────────────────────────────────────────────────
229
+
230
+ export interface AnalystRating {
231
+ symbol: string;
232
+ consensus: string | null;
233
+ targetPriceMean: number | null;
234
+ targetPriceHigh: number | null;
235
+ targetPriceLow: number | null;
236
+ distribution: { strongBuy: number; buy: number; hold: number; sell: number; strongSell: number };
237
+ recentChanges: { date: string; firm: string; action: string; from: string | null; to: string | null }[];
238
+ }
239
+
240
+ export async function fetchAnalystRating(symbol: string): Promise<AnalystRating> {
241
+ process.stderr.write(`[yahoo] fetchAnalystRating symbol=${symbol}\n`);
242
+ const summary = await yf.quoteSummary(symbol, {
243
+ modules: ["financialData", "recommendationTrend", "upgradeDowngradeHistory"],
244
+ });
245
+
246
+ const fin = summary.financialData as Record<string, unknown> | undefined;
247
+ const trend = summary.recommendationTrend as Record<string, unknown> | undefined;
248
+ const history = summary.upgradeDowngradeHistory as Record<string, unknown> | undefined;
249
+
250
+ const trendArr = (trend?.trend as Record<string, unknown>[]) ?? [];
251
+ const latest = trendArr[0] ?? {};
252
+
253
+ const changes = ((history?.history as Record<string, unknown>[]) ?? []).slice(0, 10).map((h) => ({
254
+ date: h.epochGradeDate instanceof Date ? h.epochGradeDate.toISOString().slice(0, 10) : String(h.epochGradeDate ?? ""),
255
+ firm: String(h.firm ?? ""),
256
+ action: String(h.action ?? ""),
257
+ from: h.fromGrade ? String(h.fromGrade) : null,
258
+ to: h.toGrade ? String(h.toGrade) : null,
259
+ }));
260
+
261
+ return {
262
+ symbol,
263
+ consensus: fin?.recommendationKey as string | null ?? null,
264
+ targetPriceMean: fin?.targetMeanPrice as number | null ?? null,
265
+ targetPriceHigh: fin?.targetHighPrice as number | null ?? null,
266
+ targetPriceLow: fin?.targetLowPrice as number | null ?? null,
267
+ distribution: {
268
+ strongBuy: Number(latest.strongBuy ?? 0),
269
+ buy: Number(latest.buy ?? 0),
270
+ hold: Number(latest.hold ?? 0),
271
+ sell: Number(latest.sell ?? 0),
272
+ strongSell: Number(latest.strongSell ?? 0),
273
+ },
274
+ recentChanges: changes,
275
+ };
276
+ }
277
+
278
+ // ─── News ─────────────────────────────────────────────────────────────────────
279
+
280
+ export interface StockNewsItem {
281
+ title: string;
282
+ link: string;
283
+ summary: string;
284
+ pubDate: string;
285
+ }
286
+
287
+ function extractXmlTag(xml: string, tag: string): string {
288
+ const match = xml.match(new RegExp(`<${tag}[^>]*>(?:<!\\[CDATA\\[)?([\\s\\S]*?)(?:\\]\\]>)?</${tag}>`, "i"));
289
+ return match ? match[1].trim() : "";
290
+ }
291
+
292
+ export async function fetchStockNews(symbol: string, limit = 20): Promise<StockNewsItem[]> {
293
+ process.stderr.write(`[yahoo] fetchStockNews symbol=${symbol}\n`);
294
+ const url = `https://feeds.finance.yahoo.com/rss/2.0/headline?s=${symbol}&region=US&lang=en-US`;
295
+ const res = await axios.get<string>(url, { timeout: 8000, responseType: "text" });
296
+ const xml = res.data;
297
+
298
+ const items: StockNewsItem[] = [];
299
+ const itemMatches = xml.match(/<item>[\s\S]*?<\/item>/g) ?? [];
300
+ for (const item of itemMatches.slice(0, limit)) {
301
+ items.push({
302
+ title: extractXmlTag(item, "title"),
303
+ link: extractXmlTag(item, "link") || extractXmlTag(item, "guid"),
304
+ summary: extractXmlTag(item, "description").replace(/<[^>]+>/g, "").slice(0, 200),
305
+ pubDate: extractXmlTag(item, "pubDate"),
306
+ });
307
+ }
308
+ process.stderr.write(`[yahoo] fetchStockNews 返回 ${items.length} 条\n`);
309
+ return items;
310
+ }
311
+
312
+ // ─── Insider Activity ─────────────────────────────────────────────────────────
313
+
314
+ export interface InsiderTransaction {
315
+ date: string;
316
+ name: string;
317
+ relation: string;
318
+ transactionType: string;
319
+ shares: number | null;
320
+ value: number | null;
321
+ sharesBefore: number | null;
322
+ sharesAfter: number | null;
323
+ }
324
+
325
+ export async function fetchInsiderActivity(symbol: string): Promise<InsiderTransaction[]> {
326
+ process.stderr.write(`[yahoo] fetchInsiderActivity symbol=${symbol}\n`);
327
+ const summary = await yf.quoteSummary(symbol, { modules: ["insiderTransactions"] });
328
+ const raw = summary.insiderTransactions as Record<string, unknown> | undefined;
329
+ const txArr = (raw?.transactions as Record<string, unknown>[]) ?? [];
330
+
331
+ return txArr.slice(0, 20).map((tx) => ({
332
+ date: tx.startDate instanceof Date ? tx.startDate.toISOString().slice(0, 10) : String(tx.startDate ?? ""),
333
+ name: String(tx.filerName ?? ""),
334
+ relation: String(tx.filerRelation ?? ""),
335
+ transactionType: String(tx.transactionDescription ?? ""),
336
+ shares: tx.shares as number | null ?? null,
337
+ value: tx.value as number | null ?? null,
338
+ sharesBefore: tx.shareholderBefore as number | null ?? null,
339
+ sharesAfter: tx.shareholderAfter as number | null ?? null,
340
+ }));
341
+ }
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
+
161
501
  export async function fetchStockProfile(symbol: string): Promise<StockProfile> {
162
502
  process.stderr.write(`[yahoo] fetchStockProfile symbol=${symbol}\n`);
163
503
  const summary = await yf.quoteSummary(symbol, {
@@ -4,6 +4,8 @@ import { getKlineTool, getKlineWithIndicatorsTool } from "./kline";
4
4
  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
+ import { getFinancialsTool, getAnalystRatingTool, getStockNewsTool, getInsiderActivityTool, getStockFullOverviewTool } from "./us-fundamentals";
8
+ import { getMarketMoversTool, getDividendHistoryTool, getInstitutionalHoldersTool, getSimilarStocksTool } from "./us-market-b";
7
9
 
8
10
  export * from "./types";
9
11
 
@@ -21,6 +23,15 @@ export const tools: ToolDef[] = [
21
23
  getCryptoCatsTool,
22
24
  getCryptoFundingTool,
23
25
  getCryptoLiquidationTool,
26
+ getFinancialsTool,
27
+ getAnalystRatingTool,
28
+ getStockNewsTool,
29
+ getInsiderActivityTool,
30
+ getStockFullOverviewTool,
31
+ getMarketMoversTool,
32
+ getDividendHistoryTool,
33
+ getInstitutionalHoldersTool,
34
+ getSimilarStocksTool,
24
35
  ];
25
36
 
26
37
  export const toolMap = new Map<string, ToolDef>(
@@ -0,0 +1,174 @@
1
+ import { z } from "zod";
2
+ import {
3
+ fetchFinancials,
4
+ fetchAnalystRating,
5
+ fetchStockNews,
6
+ fetchInsiderActivity,
7
+ fetchUSQuotes,
8
+ fetchStockProfile,
9
+ } from "../data/yahoo";
10
+ import type { ToolDef } from "./types";
11
+ import { ok, err } from "./types";
12
+
13
+ const SymbolInput = z.object({ symbol: z.string().min(1) });
14
+
15
+ // ─── get_financials ───────────────────────────────────────────────────────────
16
+
17
+ const FinancialsInput = z.object({
18
+ symbol: z.string().min(1),
19
+ quarterly: z.boolean().optional(),
20
+ });
21
+
22
+ export const getFinancialsTool: ToolDef = {
23
+ tool: {
24
+ name: "get_financials",
25
+ description:
26
+ "获取美股财务数据:营收、毛利率、净利润、净利率、EPS、自由现金流、负债率。" +
27
+ "默认返回近4年年报,quarterly=true 返回近4季度季报。",
28
+ inputSchema: {
29
+ type: "object",
30
+ properties: {
31
+ symbol: { type: "string", description: "美股代码,如 AAPL、NVDA" },
32
+ quarterly: { type: "boolean", description: "true=季报,false=年报(默认)" },
33
+ },
34
+ required: ["symbol"],
35
+ },
36
+ },
37
+ handler: async (input) => {
38
+ const parsed = FinancialsInput.safeParse(input);
39
+ if (!parsed.success) return err(parsed.error.message);
40
+ try {
41
+ return ok(await fetchFinancials(parsed.data.symbol, parsed.data.quarterly ?? false));
42
+ } catch (e) {
43
+ return err((e as Error).message);
44
+ }
45
+ },
46
+ };
47
+
48
+ // ─── get_analyst_rating ───────────────────────────────────────────────────────
49
+
50
+ export const getAnalystRatingTool: ToolDef = {
51
+ tool: {
52
+ name: "get_analyst_rating",
53
+ description:
54
+ "获取华尔街分析师评级:共识评级(buy/hold/sell)、平均目标价、高低目标价、" +
55
+ "强买/买/持有/卖/强卖人数分布,以及最近10条评级变动记录。",
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ symbol: { type: "string", description: "美股代码,如 AAPL、NVDA" },
60
+ },
61
+ required: ["symbol"],
62
+ },
63
+ },
64
+ handler: async (input) => {
65
+ const parsed = SymbolInput.safeParse(input);
66
+ if (!parsed.success) return err(parsed.error.message);
67
+ try {
68
+ return ok(await fetchAnalystRating(parsed.data.symbol));
69
+ } catch (e) {
70
+ return err((e as Error).message);
71
+ }
72
+ },
73
+ };
74
+
75
+ // ─── get_stock_news ───────────────────────────────────────────────────────────
76
+
77
+ const NewsInput = z.object({
78
+ symbol: z.string().min(1),
79
+ limit: z.number().int().min(1).max(30).optional(),
80
+ });
81
+
82
+ export const getStockNewsTool: ToolDef = {
83
+ tool: {
84
+ name: "get_stock_news",
85
+ description:
86
+ "获取美股个股最新新闻(近7天),含标题、摘要、链接、发布时间。最多30条,默认15条。",
87
+ inputSchema: {
88
+ type: "object",
89
+ properties: {
90
+ symbol: { type: "string", description: "美股代码,如 AAPL、NVDA" },
91
+ limit: { type: "number", description: "返回条数,默认15,最大30" },
92
+ },
93
+ required: ["symbol"],
94
+ },
95
+ },
96
+ handler: async (input) => {
97
+ const parsed = NewsInput.safeParse(input);
98
+ if (!parsed.success) return err(parsed.error.message);
99
+ try {
100
+ return ok(await fetchStockNews(parsed.data.symbol, parsed.data.limit ?? 15));
101
+ } catch (e) {
102
+ return err((e as Error).message);
103
+ }
104
+ },
105
+ };
106
+
107
+ // ─── get_insider_activity ─────────────────────────────────────────────────────
108
+
109
+ export const getInsiderActivityTool: ToolDef = {
110
+ tool: {
111
+ name: "get_insider_activity",
112
+ description:
113
+ "获取美股内部人(高管/董事)近期买卖记录,基于 SEC Form 4 公开数据。" +
114
+ "含交易人姓名、职位、交易类型、股数、交易金额。",
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ symbol: { type: "string", description: "美股代码,如 AAPL、NVDA" },
119
+ },
120
+ required: ["symbol"],
121
+ },
122
+ },
123
+ handler: async (input) => {
124
+ const parsed = SymbolInput.safeParse(input);
125
+ if (!parsed.success) return err(parsed.error.message);
126
+ try {
127
+ return ok(await fetchInsiderActivity(parsed.data.symbol));
128
+ } catch (e) {
129
+ return err((e as Error).message);
130
+ }
131
+ },
132
+ };
133
+
134
+ // ─── get_stock_full_overview ──────────────────────────────────────────────────
135
+
136
+ export const getStockFullOverviewTool: ToolDef = {
137
+ tool: {
138
+ name: "get_stock_full_overview",
139
+ description:
140
+ "一次调用获取美股完整分析数据:实时行情 + 公司基本面 + 分析师评级 + 近期新闻(并行获取)。" +
141
+ "适合回答综合分析类问题(如:帮我分析一下 NVDA),避免多次调用不同工具。",
142
+ inputSchema: {
143
+ type: "object",
144
+ properties: {
145
+ symbol: { type: "string", description: "美股代码,如 AAPL、NVDA、TSLA" },
146
+ },
147
+ required: ["symbol"],
148
+ },
149
+ },
150
+ handler: async (input) => {
151
+ const parsed = SymbolInput.safeParse(input);
152
+ if (!parsed.success) return err(parsed.error.message);
153
+ const { symbol } = parsed.data;
154
+
155
+ try {
156
+ const [quotes, profile, analyst, news] = await Promise.allSettled([
157
+ fetchUSQuotes([symbol]),
158
+ fetchStockProfile(symbol),
159
+ fetchAnalystRating(symbol),
160
+ fetchStockNews(symbol, 10),
161
+ ]);
162
+
163
+ return ok({
164
+ symbol,
165
+ quote: quotes.status === "fulfilled" ? quotes.value[0] ?? null : null,
166
+ profile: profile.status === "fulfilled" ? profile.value : null,
167
+ analyst: analyst.status === "fulfilled" ? analyst.value : null,
168
+ news: news.status === "fulfilled" ? news.value : [],
169
+ });
170
+ } catch (e) {
171
+ return err((e as Error).message);
172
+ }
173
+ },
174
+ };
@@ -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
+ };