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 +33 -6
- package/dist/index.js +470 -19
- package/package.json +1 -1
- package/src/data/yahoo.ts +340 -0
- package/src/tools/index.ts +11 -0
- package/src/tools/us-fundamentals.ts +174 -0
- package/src/tools/us-market-b.ts +128 -0
package/README.md
CHANGED
|
@@ -11,17 +11,44 @@
|
|
|
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_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` |
|
|
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
|
|
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}®ion=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
|
|
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
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
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
|
|
1113
|
+
var import_axios5 = __toESM(require("axios"));
|
|
913
1114
|
|
|
914
1115
|
// src/data/coingecko.ts
|
|
915
|
-
var
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
1640
|
+
server.setRequestHandler(import_types8.ListToolsRequestSchema, async () => ({
|
|
1190
1641
|
tools: tools.map((t) => t.tool)
|
|
1191
1642
|
}));
|
|
1192
|
-
server.setRequestHandler(
|
|
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
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}®ion=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, {
|
package/src/tools/index.ts
CHANGED
|
@@ -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
|
+
};
|