xstock-mcp 1.0.0 → 1.1.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 +23 -92
- package/dist/index.js +393 -1
- package/package.json +1 -1
- package/src/data/binance.ts +50 -0
- package/src/data/yahoo.ts +50 -0
- package/src/tools/crypto.ts +53 -0
- package/src/tools/index.ts +7 -3
- package/src/tools/kline.ts +91 -0
- package/src/tools/us-market.ts +78 -1
- package/src/utils/indicators.ts +178 -0
package/README.md
CHANGED
|
@@ -1,125 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
<a href="#english">English</a> | <a href="#中文">中文</a>
|
|
3
|
-
</div>
|
|
1
|
+
# xstock-mcp
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
基于 MCP 协议的股票与加密货币分析服务器,支持美股、加密货币、A股和港股。
|
|
6
4
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
A MCP server for stock and crypto market analysis. Supports US stocks, cryptocurrency, A-shares, and Hong Kong stocks.
|
|
10
|
-
|
|
11
|
-
### Features
|
|
12
|
-
|
|
13
|
-
- **US Stocks** — real-time quotes, K-line data, company profile, major indices (S&P 500, NASDAQ, Dow Jones, Russell 2000, VIX)
|
|
14
|
-
- **Cryptocurrency** — market overview, fear & greed index, top coins by market cap, sector categories, perpetual funding rates
|
|
15
|
-
- **A-shares & HK Stocks** — real-time quotes via Tencent Finance
|
|
16
|
-
- **Stock Search** — search any symbol by name or ticker
|
|
17
|
-
|
|
18
|
-
### Tools
|
|
19
|
-
|
|
20
|
-
| Tool | Description |
|
|
21
|
-
|------|-------------|
|
|
22
|
-
| `get_quote` | Real-time price quote for US stocks, crypto, A-shares, HK stocks |
|
|
23
|
-
| `get_kline` | K-line (OHLCV) data — daily / weekly / monthly |
|
|
24
|
-
| `search_stock` | Search stocks and crypto by keyword |
|
|
25
|
-
| `get_us_indices` | Major US market indices |
|
|
26
|
-
| `get_stock_profile` | Company profile and fundamentals |
|
|
27
|
-
| `get_crypto_overview` | Global crypto market cap + fear & greed index |
|
|
28
|
-
| `get_crypto_top` | Top N coins by market cap |
|
|
29
|
-
| `get_crypto_categories` | Crypto sector categories (DeFi, Layer1, AI, GameFi…) |
|
|
30
|
-
| `get_funding_rate` | Binance perpetual contract funding rates |
|
|
31
|
-
|
|
32
|
-
### Installation
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npx stock-mcp
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Or install globally:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
npm install -g stock-mcp
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### Claude Desktop Configuration
|
|
45
|
-
|
|
46
|
-
Add to your `claude_desktop_config.json`:
|
|
47
|
-
|
|
48
|
-
```json
|
|
49
|
-
{
|
|
50
|
-
"mcpServers": {
|
|
51
|
-
"stock-mcp": {
|
|
52
|
-
"command": "npx",
|
|
53
|
-
"args": ["-y", "stock-mcp"]
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### Development
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
git clone https://github.com/gxz2019/stock-mcp.git
|
|
63
|
-
cd stock-mcp
|
|
64
|
-
npm install
|
|
65
|
-
npm run build
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
---
|
|
69
|
-
|
|
70
|
-
## 中文
|
|
71
|
-
|
|
72
|
-
股票与加密货币市场分析的 MCP 服务器,支持美股、加密货币、A股和港股。
|
|
73
|
-
|
|
74
|
-
### 功能特性
|
|
5
|
+
## 功能
|
|
75
6
|
|
|
76
7
|
- **美股** — 实时行情、K线数据、公司简介、主要指数(标普500、纳斯达克、道琼斯、罗素2000、VIX)
|
|
77
8
|
- **加密货币** — 市场概况、恐惧贪婪指数、市值排名、赛道分类、永续合约资金费率
|
|
78
|
-
- **A股 & 港股** —
|
|
9
|
+
- **A股 & 港股** — 腾讯财经实时行情
|
|
79
10
|
- **股票搜索** — 按名称或代码搜索任意标的
|
|
80
11
|
|
|
81
|
-
|
|
12
|
+
## 工具列表
|
|
82
13
|
|
|
83
14
|
| 工具 | 说明 |
|
|
84
15
|
|------|------|
|
|
85
|
-
| `get_quote` |
|
|
86
|
-
| `get_kline` | K
|
|
16
|
+
| `get_quote` | 实时报价(美股 / 加密货币 / A股 / 港股) |
|
|
17
|
+
| `get_kline` | K线数据,支持日线 / 周线 / 月线 |
|
|
87
18
|
| `search_stock` | 按关键词搜索股票和加密货币 |
|
|
88
19
|
| `get_us_indices` | 美国主要市场指数 |
|
|
89
|
-
| `get_stock_profile` |
|
|
20
|
+
| `get_stock_profile` | 公司基本面信息 |
|
|
90
21
|
| `get_crypto_overview` | 全球加密市场总市值 + 恐惧贪婪指数 |
|
|
91
22
|
| `get_crypto_top` | 按市值排名的 Top N 币种 |
|
|
92
23
|
| `get_crypto_categories` | 加密货币赛道分类(DeFi、Layer1、AI、GameFi…) |
|
|
93
24
|
| `get_funding_rate` | Binance 永续合约资金费率 |
|
|
94
25
|
|
|
95
|
-
|
|
26
|
+
## 使用方法
|
|
96
27
|
|
|
97
|
-
|
|
98
|
-
npx stock-mcp
|
|
99
|
-
```
|
|
28
|
+
### Claude Desktop
|
|
100
29
|
|
|
101
|
-
|
|
30
|
+
编辑配置文件:
|
|
31
|
+
- macOS:`~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
32
|
+
- Windows:`%APPDATA%\Claude\claude_desktop_config.json`
|
|
102
33
|
|
|
103
|
-
|
|
104
|
-
npm install -g stock-mcp
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### Claude Desktop 配置
|
|
108
|
-
|
|
109
|
-
在 `claude_desktop_config.json` 中添加:
|
|
34
|
+
添加以下内容:
|
|
110
35
|
|
|
111
36
|
```json
|
|
112
37
|
{
|
|
113
38
|
"mcpServers": {
|
|
114
|
-
"
|
|
39
|
+
"xstock-mcp": {
|
|
115
40
|
"command": "npx",
|
|
116
|
-
"args": ["-y", "
|
|
41
|
+
"args": ["-y", "xstock-mcp"]
|
|
117
42
|
}
|
|
118
43
|
}
|
|
119
44
|
}
|
|
120
45
|
```
|
|
121
46
|
|
|
122
|
-
|
|
47
|
+
保存后重启 Claude Desktop 即可使用。
|
|
48
|
+
|
|
49
|
+
### 其他支持 MCP 的客户端(Cursor、OpenClaw 等)
|
|
50
|
+
|
|
51
|
+
配置方式相同,将上面的 JSON 添加到对应客户端的 MCP 配置文件中。
|
|
52
|
+
|
|
53
|
+
## 本地开发
|
|
123
54
|
|
|
124
55
|
```bash
|
|
125
56
|
git clone https://github.com/gxz2019/stock-mcp.git
|
package/dist/index.js
CHANGED
|
@@ -225,6 +225,35 @@ async function fetchUSKlines(symbol, startDate, endDate, interval = "1d") {
|
|
|
225
225
|
`);
|
|
226
226
|
return bars;
|
|
227
227
|
}
|
|
228
|
+
async function fetchEarningsCalendar(symbol) {
|
|
229
|
+
process.stderr.write(`[yahoo] fetchEarningsCalendar symbol=${symbol}
|
|
230
|
+
`);
|
|
231
|
+
const summary = await yf.quoteSummary(symbol, {
|
|
232
|
+
modules: ["calendarEvents", "earningsHistory"]
|
|
233
|
+
});
|
|
234
|
+
const cal = summary.calendarEvents;
|
|
235
|
+
const history = summary.earningsHistory;
|
|
236
|
+
const earningsDates = cal?.earnings;
|
|
237
|
+
const dateArr = earningsDates?.earningsDate;
|
|
238
|
+
const nextDate = dateArr && dateArr.length > 0 ? dateArr[0] : null;
|
|
239
|
+
const endDate = dateArr && dateArr.length > 1 ? dateArr[dateArr.length - 1] : null;
|
|
240
|
+
const histArr = history?.history ?? [];
|
|
241
|
+
const recentEPS = histArr.slice(-4).map((h) => {
|
|
242
|
+
const date = h.quarter instanceof Date ? h.quarter.toISOString().slice(0, 10) : String(h.quarter ?? "");
|
|
243
|
+
const actual = typeof h.epsActual === "number" ? h.epsActual : null;
|
|
244
|
+
const estimate = typeof h.epsEstimate === "number" ? h.epsEstimate : null;
|
|
245
|
+
const surprise = actual !== null && estimate !== null && estimate !== 0 ? Math.round((actual - estimate) / Math.abs(estimate) * 1e4) / 100 : null;
|
|
246
|
+
return { date, actual, estimate, surprise };
|
|
247
|
+
});
|
|
248
|
+
return {
|
|
249
|
+
symbol,
|
|
250
|
+
nextEarningsDate: nextDate ? nextDate.toISOString().slice(0, 10) : null,
|
|
251
|
+
earningsDatesRange: nextDate && endDate ? `${nextDate.toISOString().slice(0, 10)} ~ ${endDate.toISOString().slice(0, 10)}` : null,
|
|
252
|
+
exDividendDate: cal?.exDividendDate instanceof Date ? cal.exDividendDate.toISOString().slice(0, 10) : null,
|
|
253
|
+
dividendDate: cal?.dividendDate instanceof Date ? cal.dividendDate.toISOString().slice(0, 10) : null,
|
|
254
|
+
recentEPS
|
|
255
|
+
};
|
|
256
|
+
}
|
|
228
257
|
async function fetchStockProfile(symbol) {
|
|
229
258
|
process.stderr.write(`[yahoo] fetchStockProfile symbol=${symbol}
|
|
230
259
|
`);
|
|
@@ -295,6 +324,38 @@ async function fetchCryptoTickers(symbols) {
|
|
|
295
324
|
);
|
|
296
325
|
return results.filter(Boolean);
|
|
297
326
|
}
|
|
327
|
+
async function fetchMarketSentiment(symbol, period = "1h") {
|
|
328
|
+
const sym = normalizeSymbol(symbol);
|
|
329
|
+
process.stderr.write(`[binance] fetchMarketSentiment symbol=${sym} period=${period}
|
|
330
|
+
`);
|
|
331
|
+
const base = "https://fapi.binance.com";
|
|
332
|
+
const params = `symbol=${sym}&period=${period}&limit=1`;
|
|
333
|
+
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 })
|
|
339
|
+
]);
|
|
340
|
+
const oi = oiRes.data;
|
|
341
|
+
const global = globalRes.data[0] ?? {};
|
|
342
|
+
const topAcc = topAccRes.data[0] ?? {};
|
|
343
|
+
const topPos = topPosRes.data[0] ?? {};
|
|
344
|
+
const taker = takerRes.data[0] ?? {};
|
|
345
|
+
return {
|
|
346
|
+
symbol: sym,
|
|
347
|
+
openInterest: parseFloat(oi.openInterest),
|
|
348
|
+
openInterestUnit: sym.replace("USDT", ""),
|
|
349
|
+
globalLongRatio: parseFloat(global.longAccount ?? "0"),
|
|
350
|
+
globalShortRatio: parseFloat(global.shortAccount ?? "0"),
|
|
351
|
+
topAccountLongRatio: parseFloat(topAcc.longAccount ?? "0"),
|
|
352
|
+
topAccountShortRatio: parseFloat(topAcc.shortAccount ?? "0"),
|
|
353
|
+
topPositionLongRatio: parseFloat(topPos.longPosition ?? "0"),
|
|
354
|
+
topPositionShortRatio: parseFloat(topPos.shortPosition ?? "0"),
|
|
355
|
+
takerBuyRatio: parseFloat(taker.buySellRatio ?? "0"),
|
|
356
|
+
takerSellRatio: taker.buySellRatio ? 1 / parseFloat(taker.buySellRatio) : 0
|
|
357
|
+
};
|
|
358
|
+
}
|
|
298
359
|
async function fetchCryptoKlines(symbol, interval = "1d", limit = 100) {
|
|
299
360
|
const sym = normalizeSymbol(symbol);
|
|
300
361
|
process.stderr.write(`[binance] fetchCryptoKlines symbol=${sym} interval=${interval} limit=${limit}
|
|
@@ -374,6 +435,138 @@ var getQuoteTool = {
|
|
|
374
435
|
|
|
375
436
|
// src/tools/kline.ts
|
|
376
437
|
var import_zod2 = require("zod");
|
|
438
|
+
|
|
439
|
+
// src/utils/indicators.ts
|
|
440
|
+
function round2(n) {
|
|
441
|
+
return Math.round(n * 100) / 100;
|
|
442
|
+
}
|
|
443
|
+
function calcMA(closes, period) {
|
|
444
|
+
return closes.map((_, i) => {
|
|
445
|
+
if (i < period - 1) return null;
|
|
446
|
+
const sum = closes.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
|
|
447
|
+
return round2(sum / period);
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
function calcEMA(closes, period) {
|
|
451
|
+
const result = new Array(closes.length).fill(null);
|
|
452
|
+
const k = 2 / (period + 1);
|
|
453
|
+
let prev = null;
|
|
454
|
+
for (let i = 0; i < closes.length; i++) {
|
|
455
|
+
if (prev === null) {
|
|
456
|
+
if (i < period - 1) continue;
|
|
457
|
+
const sum = closes.slice(0, period).reduce((a, b) => a + b, 0);
|
|
458
|
+
prev = sum / period;
|
|
459
|
+
result[i] = round2(prev);
|
|
460
|
+
} else {
|
|
461
|
+
prev = closes[i] * k + prev * (1 - k);
|
|
462
|
+
result[i] = round2(prev);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
467
|
+
function calcRSI(closes, period = 14) {
|
|
468
|
+
const result = new Array(closes.length).fill(null);
|
|
469
|
+
if (closes.length < period + 1) return result;
|
|
470
|
+
let avgGain = 0;
|
|
471
|
+
let avgLoss = 0;
|
|
472
|
+
for (let i = 1; i <= period; i++) {
|
|
473
|
+
const diff = closes[i] - closes[i - 1];
|
|
474
|
+
if (diff > 0) avgGain += diff;
|
|
475
|
+
else avgLoss += Math.abs(diff);
|
|
476
|
+
}
|
|
477
|
+
avgGain /= period;
|
|
478
|
+
avgLoss /= period;
|
|
479
|
+
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
|
|
480
|
+
result[period] = round2(100 - 100 / (1 + rs));
|
|
481
|
+
for (let i = period + 1; i < closes.length; i++) {
|
|
482
|
+
const diff = closes[i] - closes[i - 1];
|
|
483
|
+
const gain = diff > 0 ? diff : 0;
|
|
484
|
+
const loss = diff < 0 ? Math.abs(diff) : 0;
|
|
485
|
+
avgGain = (avgGain * (period - 1) + gain) / period;
|
|
486
|
+
avgLoss = (avgLoss * (period - 1) + loss) / period;
|
|
487
|
+
const r = avgLoss === 0 ? 100 : avgGain / avgLoss;
|
|
488
|
+
result[i] = round2(100 - 100 / (1 + r));
|
|
489
|
+
}
|
|
490
|
+
return result;
|
|
491
|
+
}
|
|
492
|
+
function calcMACD(closes, fast = 12, slow = 26, signal = 9) {
|
|
493
|
+
const emaFast = calcEMA(closes, fast);
|
|
494
|
+
const emaSlow = calcEMA(closes, slow);
|
|
495
|
+
const macdLine = closes.map((_, i) => {
|
|
496
|
+
if (emaFast[i] === null || emaSlow[i] === null) return null;
|
|
497
|
+
return round2(emaFast[i] - emaSlow[i]);
|
|
498
|
+
});
|
|
499
|
+
const signalLine = new Array(closes.length).fill(null);
|
|
500
|
+
const histogram = new Array(closes.length).fill(null);
|
|
501
|
+
const k = 2 / (signal + 1);
|
|
502
|
+
let prev = null;
|
|
503
|
+
let count = 0;
|
|
504
|
+
for (let i = 0; i < macdLine.length; i++) {
|
|
505
|
+
if (macdLine[i] === null) continue;
|
|
506
|
+
count++;
|
|
507
|
+
if (prev === null) {
|
|
508
|
+
if (count < signal) continue;
|
|
509
|
+
let sum = 0;
|
|
510
|
+
let cnt = 0;
|
|
511
|
+
for (let j = i; j >= 0 && cnt < signal; j--) {
|
|
512
|
+
if (macdLine[j] !== null) {
|
|
513
|
+
sum += macdLine[j];
|
|
514
|
+
cnt++;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
prev = sum / signal;
|
|
518
|
+
signalLine[i] = round2(prev);
|
|
519
|
+
} else {
|
|
520
|
+
prev = macdLine[i] * k + prev * (1 - k);
|
|
521
|
+
signalLine[i] = round2(prev);
|
|
522
|
+
}
|
|
523
|
+
if (signalLine[i] !== null) {
|
|
524
|
+
histogram[i] = round2(macdLine[i] - signalLine[i]);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return { macd: macdLine, signal: signalLine, histogram };
|
|
528
|
+
}
|
|
529
|
+
function calcBOLL(closes, period = 20, multiplier = 2) {
|
|
530
|
+
const upper = new Array(closes.length).fill(null);
|
|
531
|
+
const middle = new Array(closes.length).fill(null);
|
|
532
|
+
const lower = new Array(closes.length).fill(null);
|
|
533
|
+
for (let i = period - 1; i < closes.length; i++) {
|
|
534
|
+
const slice = closes.slice(i - period + 1, i + 1);
|
|
535
|
+
const mean = slice.reduce((a, b) => a + b, 0) / period;
|
|
536
|
+
const variance = slice.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / period;
|
|
537
|
+
const std = Math.sqrt(variance);
|
|
538
|
+
middle[i] = round2(mean);
|
|
539
|
+
upper[i] = round2(mean + multiplier * std);
|
|
540
|
+
lower[i] = round2(mean - multiplier * std);
|
|
541
|
+
}
|
|
542
|
+
return { upper, middle, lower };
|
|
543
|
+
}
|
|
544
|
+
function attachIndicators(bars) {
|
|
545
|
+
const closes = bars.map((b) => b.close);
|
|
546
|
+
const ma5 = calcMA(closes, 5);
|
|
547
|
+
const ma10 = calcMA(closes, 10);
|
|
548
|
+
const ma20 = calcMA(closes, 20);
|
|
549
|
+
const ma60 = calcMA(closes, 60);
|
|
550
|
+
const rsi14 = calcRSI(closes, 14);
|
|
551
|
+
const macdResult = calcMACD(closes);
|
|
552
|
+
const bollResult = calcBOLL(closes);
|
|
553
|
+
return bars.map((b, i) => ({
|
|
554
|
+
...b,
|
|
555
|
+
ma5: ma5[i],
|
|
556
|
+
ma10: ma10[i],
|
|
557
|
+
ma20: ma20[i],
|
|
558
|
+
ma60: ma60[i],
|
|
559
|
+
rsi14: rsi14[i],
|
|
560
|
+
macd: macdResult.macd[i],
|
|
561
|
+
macdSignal: macdResult.signal[i],
|
|
562
|
+
macdHist: macdResult.histogram[i],
|
|
563
|
+
bollUpper: bollResult.upper[i],
|
|
564
|
+
bollMiddle: bollResult.middle[i],
|
|
565
|
+
bollLower: bollResult.lower[i]
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/tools/kline.ts
|
|
377
570
|
var periodToYahoo = {
|
|
378
571
|
daily: "1d",
|
|
379
572
|
weekly: "1wk",
|
|
@@ -452,6 +645,85 @@ var getKlineTool = {
|
|
|
452
645
|
}
|
|
453
646
|
}
|
|
454
647
|
};
|
|
648
|
+
var IndicatorInput = import_zod2.z.object({
|
|
649
|
+
market: import_zod2.z.enum(["us", "crypto"]),
|
|
650
|
+
code: import_zod2.z.string(),
|
|
651
|
+
period: import_zod2.z.enum(["daily", "weekly", "monthly"]).optional(),
|
|
652
|
+
start: import_zod2.z.string().optional(),
|
|
653
|
+
end: import_zod2.z.string().optional(),
|
|
654
|
+
interval: import_zod2.z.string().optional(),
|
|
655
|
+
limit: import_zod2.z.number().int().min(1).max(500).optional()
|
|
656
|
+
});
|
|
657
|
+
var getKlineWithIndicatorsTool = {
|
|
658
|
+
tool: {
|
|
659
|
+
name: "get_kline_with_indicators",
|
|
660
|
+
description: "\u83B7\u53D6K\u7EBF\u6570\u636E\u5E76\u9644\u5E26\u8BA1\u7B97\u597D\u7684\u6280\u672F\u6307\u6807\uFF08MA5/10/20/60\u3001RSI14\u3001MACD\u3001\u5E03\u6797\u5E26\uFF09\u3002\u652F\u6301\u7F8E\u80A1\uFF08market=us\uFF09\u548C\u52A0\u5BC6\u8D27\u5E01\uFF08market=crypto\uFF09\u3002\u8FD4\u56DE\u6700\u8FD1\u6570\u636E\uFF0C\u6307\u6807\u5747\u4E3A\u7CBE\u786E\u8BA1\u7B97\u503C\uFF0C\u53EF\u76F4\u63A5\u7528\u4E8E\u5206\u6790\u3002",
|
|
661
|
+
inputSchema: {
|
|
662
|
+
type: "object",
|
|
663
|
+
properties: {
|
|
664
|
+
market: {
|
|
665
|
+
type: "string",
|
|
666
|
+
enum: ["us", "crypto"],
|
|
667
|
+
description: "\u5E02\u573A\u7C7B\u578B\uFF1Aus=\u7F8E\u80A1\uFF0Ccrypto=\u52A0\u5BC6\u8D27\u5E01"
|
|
668
|
+
},
|
|
669
|
+
code: {
|
|
670
|
+
type: "string",
|
|
671
|
+
description: "\u4EE3\u7801\uFF0C\u5982 AAPL\u3001NVDA\u3001BTC\u3001ETH"
|
|
672
|
+
},
|
|
673
|
+
period: {
|
|
674
|
+
type: "string",
|
|
675
|
+
enum: ["daily", "weekly", "monthly"],
|
|
676
|
+
description: "\u7F8E\u80A1\u5468\u671F\uFF0C\u9ED8\u8BA4 daily"
|
|
677
|
+
},
|
|
678
|
+
start: {
|
|
679
|
+
type: "string",
|
|
680
|
+
description: "\u5F00\u59CB\u65E5\u671F YYYY-MM-DD\uFF08\u4EC5\u7F8E\u80A1\uFF0C\u9ED8\u8BA4\u8FD1180\u5929\uFF09"
|
|
681
|
+
},
|
|
682
|
+
end: {
|
|
683
|
+
type: "string",
|
|
684
|
+
description: "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD\uFF08\u4EC5\u7F8E\u80A1\uFF0C\u9ED8\u8BA4\u4ECA\u5929\uFF09"
|
|
685
|
+
},
|
|
686
|
+
interval: {
|
|
687
|
+
type: "string",
|
|
688
|
+
description: "\u52A0\u5BC6\u8D27\u5E01\u5468\u671F\uFF0C\u5982 1d\u30014h\u30011h\uFF0C\u9ED8\u8BA4 1d"
|
|
689
|
+
},
|
|
690
|
+
limit: {
|
|
691
|
+
type: "number",
|
|
692
|
+
description: "\u52A0\u5BC6\u8D27\u5E01\u8FD4\u56DE\u6761\u6570\uFF0C\u9ED8\u8BA4200\uFF08\u6307\u6807\u9700\u8981\u8DB3\u591F\u5386\u53F2\u6570\u636E\uFF09"
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
required: ["market", "code"]
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
handler: async (input) => {
|
|
699
|
+
const parsed = IndicatorInput.safeParse(input);
|
|
700
|
+
if (!parsed.success) return err(parsed.error.message);
|
|
701
|
+
const { market, code, period, start, end, interval, limit } = parsed.data;
|
|
702
|
+
try {
|
|
703
|
+
let bars;
|
|
704
|
+
if (market === "us") {
|
|
705
|
+
const endDate = end ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
706
|
+
const startDate = start ?? new Date(Date.now() - 180 * 864e5).toISOString().slice(0, 10);
|
|
707
|
+
const yInterval = periodToYahoo[period ?? "daily"] ?? "1d";
|
|
708
|
+
bars = await fetchUSKlines(code, startDate, endDate, yInterval);
|
|
709
|
+
} else {
|
|
710
|
+
bars = await fetchCryptoKlines(code, interval ?? "1d", limit ?? 200);
|
|
711
|
+
}
|
|
712
|
+
if (bars.length < 2) return err("\u6570\u636E\u4E0D\u8DB3\uFF0C\u65E0\u6CD5\u8BA1\u7B97\u6307\u6807");
|
|
713
|
+
const withIndicators = attachIndicators(bars);
|
|
714
|
+
const recent = withIndicators.slice(-60);
|
|
715
|
+
return ok({
|
|
716
|
+
symbol: code.toUpperCase(),
|
|
717
|
+
market,
|
|
718
|
+
total: bars.length,
|
|
719
|
+
returned: recent.length,
|
|
720
|
+
bars: recent
|
|
721
|
+
});
|
|
722
|
+
} catch (e) {
|
|
723
|
+
return err(e.message);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
};
|
|
455
727
|
|
|
456
728
|
// src/tools/search.ts
|
|
457
729
|
var import_zod3 = require("zod");
|
|
@@ -535,6 +807,75 @@ var getUsIndicesTool = {
|
|
|
535
807
|
}
|
|
536
808
|
}
|
|
537
809
|
};
|
|
810
|
+
var SECTOR_ETFS = {
|
|
811
|
+
XLK: "\u79D1\u6280",
|
|
812
|
+
XLF: "\u91D1\u878D",
|
|
813
|
+
XLV: "\u533B\u7597\u5065\u5EB7",
|
|
814
|
+
XLE: "\u80FD\u6E90",
|
|
815
|
+
XLI: "\u5DE5\u4E1A",
|
|
816
|
+
XLY: "\u975E\u5FC5\u9700\u6D88\u8D39",
|
|
817
|
+
XLP: "\u5FC5\u9700\u6D88\u8D39",
|
|
818
|
+
XLB: "\u6750\u6599",
|
|
819
|
+
XLU: "\u516C\u7528\u4E8B\u4E1A",
|
|
820
|
+
XLRE: "\u623F\u5730\u4EA7",
|
|
821
|
+
XLC: "\u901A\u4FE1\u670D\u52A1"
|
|
822
|
+
};
|
|
823
|
+
var getUsSectorHeatmapTool = {
|
|
824
|
+
tool: {
|
|
825
|
+
name: "get_us_sector_heatmap",
|
|
826
|
+
description: "\u83B7\u53D6\u7F8E\u80A111\u5927\u884C\u4E1A\u677F\u5757\u4ECA\u65E5\u6DA8\u8DCC\u5E45\uFF0C\u57FA\u4E8E SPDR \u884C\u4E1A ETF\uFF08XLK/XLF/XLE \u7B49\uFF09\uFF0C\u7528\u4E8E\u5224\u65AD\u8D44\u91D1\u6D41\u5411\u548C\u677F\u5757\u8F6E\u52A8\u3002",
|
|
827
|
+
inputSchema: {
|
|
828
|
+
type: "object",
|
|
829
|
+
properties: {},
|
|
830
|
+
required: []
|
|
831
|
+
}
|
|
832
|
+
},
|
|
833
|
+
handler: async () => {
|
|
834
|
+
try {
|
|
835
|
+
const symbols = Object.keys(SECTOR_ETFS);
|
|
836
|
+
const quotes = await fetchUSQuotes(symbols);
|
|
837
|
+
const result = quotes.map((q) => ({
|
|
838
|
+
etf: q.symbol,
|
|
839
|
+
sector: SECTOR_ETFS[q.symbol] ?? q.symbol,
|
|
840
|
+
price: q.price,
|
|
841
|
+
change: q.change,
|
|
842
|
+
changePercent: q.changePercent,
|
|
843
|
+
volume: q.volume
|
|
844
|
+
})).sort((a, b) => parseFloat(b.changePercent) - parseFloat(a.changePercent));
|
|
845
|
+
return ok(result);
|
|
846
|
+
} catch (e) {
|
|
847
|
+
return err(e.message);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
var SymbolInput = import_zod4.z.object({
|
|
852
|
+
symbol: import_zod4.z.string().min(1)
|
|
853
|
+
});
|
|
854
|
+
var getEarningsCalendarTool = {
|
|
855
|
+
tool: {
|
|
856
|
+
name: "get_earnings_calendar",
|
|
857
|
+
description: "\u67E5\u8BE2\u7F8E\u80A1\u4E0A\u5E02\u516C\u53F8\u7684\u4E0B\u6B21\u8D22\u62A5\u65E5\u671F\u3001EPS\u9884\u671F\uFF0C\u4EE5\u53CA\u8FD14\u5B63\u5EA6\u7684\u5B9E\u9645EPS\u3001\u9884\u671FEPS\u548C\u8D85\u9884\u671F\u5E45\u5EA6\uFF08surprise%\uFF09\u3002",
|
|
858
|
+
inputSchema: {
|
|
859
|
+
type: "object",
|
|
860
|
+
properties: {
|
|
861
|
+
symbol: {
|
|
862
|
+
type: "string",
|
|
863
|
+
description: "\u7F8E\u80A1\u4EE3\u7801\uFF0C\u5982 AAPL\u3001NVDA\u3001TSLA"
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
required: ["symbol"]
|
|
867
|
+
}
|
|
868
|
+
},
|
|
869
|
+
handler: async (input) => {
|
|
870
|
+
const parsed = SymbolInput.safeParse(input);
|
|
871
|
+
if (!parsed.success) return err(parsed.error.message);
|
|
872
|
+
try {
|
|
873
|
+
return ok(await fetchEarningsCalendar(parsed.data.symbol));
|
|
874
|
+
} catch (e) {
|
|
875
|
+
return err(e.message);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
};
|
|
538
879
|
var ProfileInput = import_zod4.z.object({
|
|
539
880
|
symbol: import_zod4.z.string().min(1)
|
|
540
881
|
});
|
|
@@ -772,18 +1113,69 @@ var getCryptoFundingTool = {
|
|
|
772
1113
|
}
|
|
773
1114
|
}
|
|
774
1115
|
};
|
|
1116
|
+
var LiquidationInput = import_zod5.z.object({
|
|
1117
|
+
symbols: import_zod5.z.array(import_zod5.z.string()).min(1).max(10),
|
|
1118
|
+
period: import_zod5.z.enum(["5m", "15m", "30m", "1h", "4h", "1d"]).optional()
|
|
1119
|
+
});
|
|
1120
|
+
var getCryptoLiquidationTool = {
|
|
1121
|
+
tool: {
|
|
1122
|
+
name: "get_crypto_liquidation",
|
|
1123
|
+
description: "\u83B7\u53D6\u52A0\u5BC6\u8D27\u5E01\u5408\u7EA6\u5E02\u573A\u60C5\u7EEA\u6570\u636E\uFF1A\u672A\u5E73\u4ED3\u5408\u7EA6\uFF08OI\uFF09\u3001\u5168\u7403\u591A\u7A7A\u8D26\u6237\u6BD4\u3001\u4E3B\u529B\u591A\u7A7A\u6301\u4ED3\u6BD4\u3001\u5403\u5355\u4E70\u5356\u6BD4\u3002\u53EF\u7528\u4E8E\u5224\u65AD\u6760\u6746\u805A\u96C6\u7A0B\u5EA6\u548C\u6E05\u7B97\u98CE\u9669\u3002symbols \u4F20\u5E01\u79CD\u4EE3\u7801\u5982 ['BTC','ETH']\u3002",
|
|
1124
|
+
inputSchema: {
|
|
1125
|
+
type: "object",
|
|
1126
|
+
properties: {
|
|
1127
|
+
symbols: {
|
|
1128
|
+
type: "array",
|
|
1129
|
+
items: { type: "string" },
|
|
1130
|
+
description: "\u5E01\u79CD\u4EE3\u7801\u5217\u8868\uFF0C\u5982 ['BTC','ETH']",
|
|
1131
|
+
minItems: 1,
|
|
1132
|
+
maxItems: 10
|
|
1133
|
+
},
|
|
1134
|
+
period: {
|
|
1135
|
+
type: "string",
|
|
1136
|
+
enum: ["5m", "15m", "30m", "1h", "4h", "1d"],
|
|
1137
|
+
description: "\u7EDF\u8BA1\u5468\u671F\uFF0C\u9ED8\u8BA4 1h"
|
|
1138
|
+
}
|
|
1139
|
+
},
|
|
1140
|
+
required: ["symbols"]
|
|
1141
|
+
}
|
|
1142
|
+
},
|
|
1143
|
+
handler: async (input) => {
|
|
1144
|
+
const parsed = LiquidationInput.safeParse(input);
|
|
1145
|
+
if (!parsed.success) return err(parsed.error.message);
|
|
1146
|
+
const { symbols, period = "1h" } = parsed.data;
|
|
1147
|
+
try {
|
|
1148
|
+
const results = await Promise.all(
|
|
1149
|
+
symbols.map(async (sym) => {
|
|
1150
|
+
try {
|
|
1151
|
+
return await fetchMarketSentiment(sym, period);
|
|
1152
|
+
} catch {
|
|
1153
|
+
return { symbol: sym.toUpperCase() + "USDT", error: "not available or not a perpetual contract" };
|
|
1154
|
+
}
|
|
1155
|
+
})
|
|
1156
|
+
);
|
|
1157
|
+
return ok(results);
|
|
1158
|
+
} catch (e) {
|
|
1159
|
+
return err(e.message);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
775
1163
|
|
|
776
1164
|
// src/tools/index.ts
|
|
777
1165
|
var tools = [
|
|
778
1166
|
getQuoteTool,
|
|
779
1167
|
getKlineTool,
|
|
1168
|
+
getKlineWithIndicatorsTool,
|
|
780
1169
|
searchStockTool,
|
|
781
1170
|
getUsIndicesTool,
|
|
782
1171
|
getStockProfileTool,
|
|
1172
|
+
getEarningsCalendarTool,
|
|
1173
|
+
getUsSectorHeatmapTool,
|
|
783
1174
|
getCryptoOverviewTool,
|
|
784
1175
|
getCryptoTopTool,
|
|
785
1176
|
getCryptoCatsTool,
|
|
786
|
-
getCryptoFundingTool
|
|
1177
|
+
getCryptoFundingTool,
|
|
1178
|
+
getCryptoLiquidationTool
|
|
787
1179
|
];
|
|
788
1180
|
var toolMap = new Map(
|
|
789
1181
|
tools.map((t) => [t.tool.name, t])
|
package/package.json
CHANGED
package/src/data/binance.ts
CHANGED
|
@@ -63,6 +63,56 @@ export async function fetchCryptoTickers(symbols: string[]): Promise<CryptoTicke
|
|
|
63
63
|
return results.filter(Boolean) as CryptoTicker[];
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
export interface MarketSentiment {
|
|
67
|
+
symbol: string;
|
|
68
|
+
openInterest: number;
|
|
69
|
+
openInterestUnit: string;
|
|
70
|
+
globalLongRatio: number;
|
|
71
|
+
globalShortRatio: number;
|
|
72
|
+
topAccountLongRatio: number;
|
|
73
|
+
topAccountShortRatio: number;
|
|
74
|
+
topPositionLongRatio: number;
|
|
75
|
+
topPositionShortRatio: number;
|
|
76
|
+
takerBuyRatio: number;
|
|
77
|
+
takerSellRatio: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function fetchMarketSentiment(symbol: string, period = "1h"): Promise<MarketSentiment> {
|
|
81
|
+
const sym = normalizeSymbol(symbol);
|
|
82
|
+
process.stderr.write(`[binance] fetchMarketSentiment symbol=${sym} period=${period}\n`);
|
|
83
|
+
|
|
84
|
+
const base = "https://fapi.binance.com";
|
|
85
|
+
const params = `symbol=${sym}&period=${period}&limit=1`;
|
|
86
|
+
|
|
87
|
+
const [oiRes, globalRes, topAccRes, topPosRes, takerRes] = await Promise.all([
|
|
88
|
+
axios.get(`${base}/fapi/v1/openInterest?symbol=${sym}`, { timeout: 5000 }),
|
|
89
|
+
axios.get(`${base}/futures/data/globalLongShortAccountRatio?${params}`, { timeout: 5000 }),
|
|
90
|
+
axios.get(`${base}/futures/data/topLongShortAccountRatio?${params}`, { timeout: 5000 }),
|
|
91
|
+
axios.get(`${base}/futures/data/topLongShortPositionRatio?${params}`, { timeout: 5000 }),
|
|
92
|
+
axios.get(`${base}/futures/data/takerlongshortRatio?${params}`, { timeout: 5000 }),
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const oi = oiRes.data;
|
|
96
|
+
const global = globalRes.data[0] ?? {};
|
|
97
|
+
const topAcc = topAccRes.data[0] ?? {};
|
|
98
|
+
const topPos = topPosRes.data[0] ?? {};
|
|
99
|
+
const taker = takerRes.data[0] ?? {};
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
symbol: sym,
|
|
103
|
+
openInterest: parseFloat(oi.openInterest),
|
|
104
|
+
openInterestUnit: sym.replace("USDT", ""),
|
|
105
|
+
globalLongRatio: parseFloat(global.longAccount ?? "0"),
|
|
106
|
+
globalShortRatio: parseFloat(global.shortAccount ?? "0"),
|
|
107
|
+
topAccountLongRatio: parseFloat(topAcc.longAccount ?? "0"),
|
|
108
|
+
topAccountShortRatio: parseFloat(topAcc.shortAccount ?? "0"),
|
|
109
|
+
topPositionLongRatio: parseFloat(topPos.longPosition ?? "0"),
|
|
110
|
+
topPositionShortRatio: parseFloat(topPos.shortPosition ?? "0"),
|
|
111
|
+
takerBuyRatio: parseFloat(taker.buySellRatio ?? "0"),
|
|
112
|
+
takerSellRatio: taker.buySellRatio ? (1 / parseFloat(taker.buySellRatio)) : 0,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
66
116
|
export async function fetchCryptoKlines(
|
|
67
117
|
symbol: string,
|
|
68
118
|
interval: string = "1d",
|
package/src/data/yahoo.ts
CHANGED
|
@@ -108,6 +108,56 @@ export async function fetchUSKlines(
|
|
|
108
108
|
return bars;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
export interface EarningsCalendar {
|
|
112
|
+
symbol: string;
|
|
113
|
+
nextEarningsDate: string | null;
|
|
114
|
+
earningsDatesRange: string | null;
|
|
115
|
+
exDividendDate: string | null;
|
|
116
|
+
dividendDate: string | null;
|
|
117
|
+
recentEPS: { date: string; actual: number | null; estimate: number | null; surprise: number | null }[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function fetchEarningsCalendar(symbol: string): Promise<EarningsCalendar> {
|
|
121
|
+
process.stderr.write(`[yahoo] fetchEarningsCalendar symbol=${symbol}\n`);
|
|
122
|
+
const summary = await yf.quoteSummary(symbol, {
|
|
123
|
+
modules: ["calendarEvents", "earningsHistory"],
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const cal = summary.calendarEvents as Record<string, unknown> | undefined;
|
|
127
|
+
const history = summary.earningsHistory as Record<string, unknown> | undefined;
|
|
128
|
+
|
|
129
|
+
// 财报日期
|
|
130
|
+
const earningsDates = cal?.earnings as Record<string, unknown> | undefined;
|
|
131
|
+
const dateArr = earningsDates?.earningsDate as Date[] | undefined;
|
|
132
|
+
const nextDate = dateArr && dateArr.length > 0 ? dateArr[0] : null;
|
|
133
|
+
const endDate = dateArr && dateArr.length > 1 ? dateArr[dateArr.length - 1] : null;
|
|
134
|
+
|
|
135
|
+
// 近期 EPS 历史
|
|
136
|
+
const histArr = (history?.history as Record<string, unknown>[] | undefined) ?? [];
|
|
137
|
+
const recentEPS = histArr.slice(-4).map((h) => {
|
|
138
|
+
const date = h.quarter instanceof Date ? h.quarter.toISOString().slice(0, 10) : String(h.quarter ?? "");
|
|
139
|
+
const actual = typeof h.epsActual === "number" ? h.epsActual : null;
|
|
140
|
+
const estimate = typeof h.epsEstimate === "number" ? h.epsEstimate : null;
|
|
141
|
+
const surprise = actual !== null && estimate !== null && estimate !== 0
|
|
142
|
+
? Math.round(((actual - estimate) / Math.abs(estimate)) * 10000) / 100
|
|
143
|
+
: null;
|
|
144
|
+
return { date, actual, estimate, surprise };
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
symbol,
|
|
149
|
+
nextEarningsDate: nextDate ? (nextDate as Date).toISOString().slice(0, 10) : null,
|
|
150
|
+
earningsDatesRange: nextDate && endDate
|
|
151
|
+
? `${(nextDate as Date).toISOString().slice(0, 10)} ~ ${(endDate as Date).toISOString().slice(0, 10)}`
|
|
152
|
+
: null,
|
|
153
|
+
exDividendDate: cal?.exDividendDate instanceof Date
|
|
154
|
+
? cal.exDividendDate.toISOString().slice(0, 10) : null,
|
|
155
|
+
dividendDate: cal?.dividendDate instanceof Date
|
|
156
|
+
? cal.dividendDate.toISOString().slice(0, 10) : null,
|
|
157
|
+
recentEPS,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
111
161
|
export async function fetchStockProfile(symbol: string): Promise<StockProfile> {
|
|
112
162
|
process.stderr.write(`[yahoo] fetchStockProfile symbol=${symbol}\n`);
|
|
113
163
|
const summary = await yf.quoteSummary(symbol, {
|
package/src/tools/crypto.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
import { fetchGlobalMarket, fetchTopCoins, fetchFearGreed, fetchCategories } from "../data/coingecko";
|
|
4
|
+
import { fetchMarketSentiment } from "../data/binance";
|
|
4
5
|
import type { ToolDef } from "./types";
|
|
5
6
|
import { ok, err } from "./types";
|
|
6
7
|
|
|
@@ -143,3 +144,55 @@ export const getCryptoFundingTool: ToolDef = {
|
|
|
143
144
|
}
|
|
144
145
|
},
|
|
145
146
|
};
|
|
147
|
+
|
|
148
|
+
const LiquidationInput = z.object({
|
|
149
|
+
symbols: z.array(z.string()).min(1).max(10),
|
|
150
|
+
period: z.enum(["5m", "15m", "30m", "1h", "4h", "1d"]).optional(),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
export const getCryptoLiquidationTool: ToolDef = {
|
|
154
|
+
tool: {
|
|
155
|
+
name: "get_crypto_liquidation",
|
|
156
|
+
description:
|
|
157
|
+
"获取加密货币合约市场情绪数据:未平仓合约(OI)、全球多空账户比、主力多空持仓比、吃单买卖比。" +
|
|
158
|
+
"可用于判断杠杆聚集程度和清算风险。symbols 传币种代码如 ['BTC','ETH']。",
|
|
159
|
+
inputSchema: {
|
|
160
|
+
type: "object",
|
|
161
|
+
properties: {
|
|
162
|
+
symbols: {
|
|
163
|
+
type: "array",
|
|
164
|
+
items: { type: "string" },
|
|
165
|
+
description: "币种代码列表,如 ['BTC','ETH']",
|
|
166
|
+
minItems: 1,
|
|
167
|
+
maxItems: 10,
|
|
168
|
+
},
|
|
169
|
+
period: {
|
|
170
|
+
type: "string",
|
|
171
|
+
enum: ["5m", "15m", "30m", "1h", "4h", "1d"],
|
|
172
|
+
description: "统计周期,默认 1h",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
required: ["symbols"],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
handler: async (input) => {
|
|
179
|
+
const parsed = LiquidationInput.safeParse(input);
|
|
180
|
+
if (!parsed.success) return err(parsed.error.message);
|
|
181
|
+
const { symbols, period = "1h" } = parsed.data;
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const results = await Promise.all(
|
|
185
|
+
symbols.map(async (sym) => {
|
|
186
|
+
try {
|
|
187
|
+
return await fetchMarketSentiment(sym, period);
|
|
188
|
+
} catch {
|
|
189
|
+
return { symbol: sym.toUpperCase() + "USDT", error: "not available or not a perpetual contract" };
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
return ok(results);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
return err((e as Error).message);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
};
|
package/src/tools/index.ts
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import type { ToolDef } from "./types";
|
|
2
2
|
import { getQuoteTool } from "./quotes";
|
|
3
|
-
import { getKlineTool } from "./kline";
|
|
3
|
+
import { getKlineTool, getKlineWithIndicatorsTool } from "./kline";
|
|
4
4
|
import { searchStockTool } from "./search";
|
|
5
|
-
import { getUsIndicesTool, getStockProfileTool } from "./us-market";
|
|
6
|
-
import { getCryptoOverviewTool, getCryptoTopTool, getCryptoCatsTool, getCryptoFundingTool } from "./crypto";
|
|
5
|
+
import { getUsIndicesTool, getStockProfileTool, getEarningsCalendarTool, getUsSectorHeatmapTool } from "./us-market";
|
|
6
|
+
import { getCryptoOverviewTool, getCryptoTopTool, getCryptoCatsTool, getCryptoFundingTool, getCryptoLiquidationTool } from "./crypto";
|
|
7
7
|
|
|
8
8
|
export * from "./types";
|
|
9
9
|
|
|
10
10
|
export const tools: ToolDef[] = [
|
|
11
11
|
getQuoteTool,
|
|
12
12
|
getKlineTool,
|
|
13
|
+
getKlineWithIndicatorsTool,
|
|
13
14
|
searchStockTool,
|
|
14
15
|
getUsIndicesTool,
|
|
15
16
|
getStockProfileTool,
|
|
17
|
+
getEarningsCalendarTool,
|
|
18
|
+
getUsSectorHeatmapTool,
|
|
16
19
|
getCryptoOverviewTool,
|
|
17
20
|
getCryptoTopTool,
|
|
18
21
|
getCryptoCatsTool,
|
|
19
22
|
getCryptoFundingTool,
|
|
23
|
+
getCryptoLiquidationTool,
|
|
20
24
|
];
|
|
21
25
|
|
|
22
26
|
export const toolMap = new Map<string, ToolDef>(
|
package/src/tools/kline.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { fetchAShareKline, fetchHKKline } from "../data/tencent";
|
|
3
3
|
import { fetchUSKlines } from "../data/yahoo";
|
|
4
4
|
import { fetchCryptoKlines } from "../data/binance";
|
|
5
|
+
import { attachIndicators } from "../utils/indicators";
|
|
5
6
|
import type { ToolDef } from "./types";
|
|
6
7
|
import { ok, err } from "./types";
|
|
7
8
|
|
|
@@ -89,3 +90,93 @@ export const getKlineTool: ToolDef = {
|
|
|
89
90
|
}
|
|
90
91
|
},
|
|
91
92
|
};
|
|
93
|
+
|
|
94
|
+
const IndicatorInput = z.object({
|
|
95
|
+
market: z.enum(["us", "crypto"]),
|
|
96
|
+
code: z.string(),
|
|
97
|
+
period: z.enum(["daily", "weekly", "monthly"]).optional(),
|
|
98
|
+
start: z.string().optional(),
|
|
99
|
+
end: z.string().optional(),
|
|
100
|
+
interval: z.string().optional(),
|
|
101
|
+
limit: z.number().int().min(1).max(500).optional(),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export const getKlineWithIndicatorsTool: ToolDef = {
|
|
105
|
+
tool: {
|
|
106
|
+
name: "get_kline_with_indicators",
|
|
107
|
+
description:
|
|
108
|
+
"获取K线数据并附带计算好的技术指标(MA5/10/20/60、RSI14、MACD、布林带)。" +
|
|
109
|
+
"支持美股(market=us)和加密货币(market=crypto)。" +
|
|
110
|
+
"返回最近数据,指标均为精确计算值,可直接用于分析。",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: "object",
|
|
113
|
+
properties: {
|
|
114
|
+
market: {
|
|
115
|
+
type: "string",
|
|
116
|
+
enum: ["us", "crypto"],
|
|
117
|
+
description: "市场类型:us=美股,crypto=加密货币",
|
|
118
|
+
},
|
|
119
|
+
code: {
|
|
120
|
+
type: "string",
|
|
121
|
+
description: "代码,如 AAPL、NVDA、BTC、ETH",
|
|
122
|
+
},
|
|
123
|
+
period: {
|
|
124
|
+
type: "string",
|
|
125
|
+
enum: ["daily", "weekly", "monthly"],
|
|
126
|
+
description: "美股周期,默认 daily",
|
|
127
|
+
},
|
|
128
|
+
start: {
|
|
129
|
+
type: "string",
|
|
130
|
+
description: "开始日期 YYYY-MM-DD(仅美股,默认近180天)",
|
|
131
|
+
},
|
|
132
|
+
end: {
|
|
133
|
+
type: "string",
|
|
134
|
+
description: "结束日期 YYYY-MM-DD(仅美股,默认今天)",
|
|
135
|
+
},
|
|
136
|
+
interval: {
|
|
137
|
+
type: "string",
|
|
138
|
+
description: "加密货币周期,如 1d、4h、1h,默认 1d",
|
|
139
|
+
},
|
|
140
|
+
limit: {
|
|
141
|
+
type: "number",
|
|
142
|
+
description: "加密货币返回条数,默认200(指标需要足够历史数据)",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
required: ["market", "code"],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
handler: async (input) => {
|
|
149
|
+
const parsed = IndicatorInput.safeParse(input);
|
|
150
|
+
if (!parsed.success) return err(parsed.error.message);
|
|
151
|
+
const { market, code, period, start, end, interval, limit } = parsed.data;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
let bars: { date: string; open: number; high: number; low: number; close: number; volume: number }[];
|
|
155
|
+
|
|
156
|
+
if (market === "us") {
|
|
157
|
+
const endDate = end ?? new Date().toISOString().slice(0, 10);
|
|
158
|
+
const startDate = start ?? new Date(Date.now() - 180 * 86400_000).toISOString().slice(0, 10);
|
|
159
|
+
const yInterval = periodToYahoo[period ?? "daily"] ?? "1d";
|
|
160
|
+
bars = await fetchUSKlines(code, startDate, endDate, yInterval);
|
|
161
|
+
} else {
|
|
162
|
+
bars = await fetchCryptoKlines(code, interval ?? "1d", limit ?? 200);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (bars.length < 2) return err("数据不足,无法计算指标");
|
|
166
|
+
|
|
167
|
+
const withIndicators = attachIndicators(bars);
|
|
168
|
+
// 只返回最近60条,减少 token,但保留足够上下文
|
|
169
|
+
const recent = withIndicators.slice(-60);
|
|
170
|
+
|
|
171
|
+
return ok({
|
|
172
|
+
symbol: code.toUpperCase(),
|
|
173
|
+
market,
|
|
174
|
+
total: bars.length,
|
|
175
|
+
returned: recent.length,
|
|
176
|
+
bars: recent,
|
|
177
|
+
});
|
|
178
|
+
} catch (e) {
|
|
179
|
+
return err((e as Error).message);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
};
|
package/src/tools/us-market.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { fetchUSQuotes, fetchStockProfile } from "../data/yahoo";
|
|
2
|
+
import { fetchUSQuotes, fetchStockProfile, fetchEarningsCalendar } from "../data/yahoo";
|
|
3
3
|
import type { ToolDef } from "./types";
|
|
4
4
|
import { ok, err } from "./types";
|
|
5
5
|
|
|
@@ -38,6 +38,83 @@ export const getUsIndicesTool: ToolDef = {
|
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
const SECTOR_ETFS: Record<string, string> = {
|
|
42
|
+
XLK: "科技",
|
|
43
|
+
XLF: "金融",
|
|
44
|
+
XLV: "医疗健康",
|
|
45
|
+
XLE: "能源",
|
|
46
|
+
XLI: "工业",
|
|
47
|
+
XLY: "非必需消费",
|
|
48
|
+
XLP: "必需消费",
|
|
49
|
+
XLB: "材料",
|
|
50
|
+
XLU: "公用事业",
|
|
51
|
+
XLRE: "房地产",
|
|
52
|
+
XLC: "通信服务",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const getUsSectorHeatmapTool: ToolDef = {
|
|
56
|
+
tool: {
|
|
57
|
+
name: "get_us_sector_heatmap",
|
|
58
|
+
description:
|
|
59
|
+
"获取美股11大行业板块今日涨跌幅,基于 SPDR 行业 ETF(XLK/XLF/XLE 等),用于判断资金流向和板块轮动。",
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {},
|
|
63
|
+
required: [],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
handler: async () => {
|
|
67
|
+
try {
|
|
68
|
+
const symbols = Object.keys(SECTOR_ETFS);
|
|
69
|
+
const quotes = await fetchUSQuotes(symbols);
|
|
70
|
+
const result = quotes
|
|
71
|
+
.map((q) => ({
|
|
72
|
+
etf: q.symbol,
|
|
73
|
+
sector: SECTOR_ETFS[q.symbol] ?? q.symbol,
|
|
74
|
+
price: q.price,
|
|
75
|
+
change: q.change,
|
|
76
|
+
changePercent: q.changePercent,
|
|
77
|
+
volume: q.volume,
|
|
78
|
+
}))
|
|
79
|
+
.sort((a, b) => parseFloat(b.changePercent) - parseFloat(a.changePercent));
|
|
80
|
+
return ok(result);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
return err((e as Error).message);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const SymbolInput = z.object({
|
|
88
|
+
symbol: z.string().min(1),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const getEarningsCalendarTool: ToolDef = {
|
|
92
|
+
tool: {
|
|
93
|
+
name: "get_earnings_calendar",
|
|
94
|
+
description:
|
|
95
|
+
"查询美股上市公司的下次财报日期、EPS预期,以及近4季度的实际EPS、预期EPS和超预期幅度(surprise%)。",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
symbol: {
|
|
100
|
+
type: "string",
|
|
101
|
+
description: "美股代码,如 AAPL、NVDA、TSLA",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
required: ["symbol"],
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
handler: async (input) => {
|
|
108
|
+
const parsed = SymbolInput.safeParse(input);
|
|
109
|
+
if (!parsed.success) return err(parsed.error.message);
|
|
110
|
+
try {
|
|
111
|
+
return ok(await fetchEarningsCalendar(parsed.data.symbol));
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return err((e as Error).message);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
41
118
|
const ProfileInput = z.object({
|
|
42
119
|
symbol: z.string().min(1),
|
|
43
120
|
});
|
package/src/utils/indicators.ts
CHANGED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// 技术指标计算,输入均为收盘价数组(时间顺序,oldest first)
|
|
2
|
+
|
|
3
|
+
function round2(n: number): number {
|
|
4
|
+
return Math.round(n * 100) / 100;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function calcMA(closes: number[], period: number): (number | null)[] {
|
|
8
|
+
return closes.map((_, i) => {
|
|
9
|
+
if (i < period - 1) return null;
|
|
10
|
+
const sum = closes.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
|
|
11
|
+
return round2(sum / period);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function calcEMA(closes: number[], period: number): (number | null)[] {
|
|
16
|
+
const result: (number | null)[] = new Array(closes.length).fill(null);
|
|
17
|
+
const k = 2 / (period + 1);
|
|
18
|
+
let prev: number | null = null;
|
|
19
|
+
for (let i = 0; i < closes.length; i++) {
|
|
20
|
+
if (prev === null) {
|
|
21
|
+
if (i < period - 1) continue;
|
|
22
|
+
// 第一个 EMA 用 SMA 初始化
|
|
23
|
+
const sum = closes.slice(0, period).reduce((a, b) => a + b, 0);
|
|
24
|
+
prev = sum / period;
|
|
25
|
+
result[i] = round2(prev);
|
|
26
|
+
} else {
|
|
27
|
+
prev = closes[i] * k + prev * (1 - k);
|
|
28
|
+
result[i] = round2(prev);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function calcRSI(closes: number[], period = 14): (number | null)[] {
|
|
35
|
+
const result: (number | null)[] = new Array(closes.length).fill(null);
|
|
36
|
+
if (closes.length < period + 1) return result;
|
|
37
|
+
|
|
38
|
+
let avgGain = 0;
|
|
39
|
+
let avgLoss = 0;
|
|
40
|
+
for (let i = 1; i <= period; i++) {
|
|
41
|
+
const diff = closes[i] - closes[i - 1];
|
|
42
|
+
if (diff > 0) avgGain += diff;
|
|
43
|
+
else avgLoss += Math.abs(diff);
|
|
44
|
+
}
|
|
45
|
+
avgGain /= period;
|
|
46
|
+
avgLoss /= period;
|
|
47
|
+
|
|
48
|
+
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
|
|
49
|
+
result[period] = round2(100 - 100 / (1 + rs));
|
|
50
|
+
|
|
51
|
+
for (let i = period + 1; i < closes.length; i++) {
|
|
52
|
+
const diff = closes[i] - closes[i - 1];
|
|
53
|
+
const gain = diff > 0 ? diff : 0;
|
|
54
|
+
const loss = diff < 0 ? Math.abs(diff) : 0;
|
|
55
|
+
avgGain = (avgGain * (period - 1) + gain) / period;
|
|
56
|
+
avgLoss = (avgLoss * (period - 1) + loss) / period;
|
|
57
|
+
const r = avgLoss === 0 ? 100 : avgGain / avgLoss;
|
|
58
|
+
result[i] = round2(100 - 100 / (1 + r));
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface MACDResult {
|
|
64
|
+
macd: (number | null)[];
|
|
65
|
+
signal: (number | null)[];
|
|
66
|
+
histogram: (number | null)[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function calcMACD(closes: number[], fast = 12, slow = 26, signal = 9): MACDResult {
|
|
70
|
+
const emaFast = calcEMA(closes, fast);
|
|
71
|
+
const emaSlow = calcEMA(closes, slow);
|
|
72
|
+
const macdLine: (number | null)[] = closes.map((_, i) => {
|
|
73
|
+
if (emaFast[i] === null || emaSlow[i] === null) return null;
|
|
74
|
+
return round2((emaFast[i] as number) - (emaSlow[i] as number));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// signal = EMA9 of macdLine(只对非 null 段计算)
|
|
78
|
+
const signalLine: (number | null)[] = new Array(closes.length).fill(null);
|
|
79
|
+
const histogram: (number | null)[] = new Array(closes.length).fill(null);
|
|
80
|
+
|
|
81
|
+
const k = 2 / (signal + 1);
|
|
82
|
+
let prev: number | null = null;
|
|
83
|
+
let count = 0;
|
|
84
|
+
for (let i = 0; i < macdLine.length; i++) {
|
|
85
|
+
if (macdLine[i] === null) continue;
|
|
86
|
+
count++;
|
|
87
|
+
if (prev === null) {
|
|
88
|
+
if (count < signal) continue;
|
|
89
|
+
// 用前 signal 个 macd 值初始化
|
|
90
|
+
let sum = 0;
|
|
91
|
+
let cnt = 0;
|
|
92
|
+
for (let j = i; j >= 0 && cnt < signal; j--) {
|
|
93
|
+
if (macdLine[j] !== null) { sum += macdLine[j] as number; cnt++; }
|
|
94
|
+
}
|
|
95
|
+
prev = sum / signal;
|
|
96
|
+
signalLine[i] = round2(prev);
|
|
97
|
+
} else {
|
|
98
|
+
prev = (macdLine[i] as number) * k + prev * (1 - k);
|
|
99
|
+
signalLine[i] = round2(prev);
|
|
100
|
+
}
|
|
101
|
+
if (signalLine[i] !== null) {
|
|
102
|
+
histogram[i] = round2((macdLine[i] as number) - (signalLine[i] as number));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { macd: macdLine, signal: signalLine, histogram };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface BOLLResult {
|
|
110
|
+
upper: (number | null)[];
|
|
111
|
+
middle: (number | null)[];
|
|
112
|
+
lower: (number | null)[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function calcBOLL(closes: number[], period = 20, multiplier = 2): BOLLResult {
|
|
116
|
+
const upper: (number | null)[] = new Array(closes.length).fill(null);
|
|
117
|
+
const middle: (number | null)[] = new Array(closes.length).fill(null);
|
|
118
|
+
const lower: (number | null)[] = new Array(closes.length).fill(null);
|
|
119
|
+
|
|
120
|
+
for (let i = period - 1; i < closes.length; i++) {
|
|
121
|
+
const slice = closes.slice(i - period + 1, i + 1);
|
|
122
|
+
const mean = slice.reduce((a, b) => a + b, 0) / period;
|
|
123
|
+
const variance = slice.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / period;
|
|
124
|
+
const std = Math.sqrt(variance);
|
|
125
|
+
middle[i] = round2(mean);
|
|
126
|
+
upper[i] = round2(mean + multiplier * std);
|
|
127
|
+
lower[i] = round2(mean - multiplier * std);
|
|
128
|
+
}
|
|
129
|
+
return { upper, middle, lower };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface BarWithIndicators {
|
|
133
|
+
date: string;
|
|
134
|
+
open: number;
|
|
135
|
+
high: number;
|
|
136
|
+
low: number;
|
|
137
|
+
close: number;
|
|
138
|
+
volume: number;
|
|
139
|
+
ma5: number | null;
|
|
140
|
+
ma10: number | null;
|
|
141
|
+
ma20: number | null;
|
|
142
|
+
ma60: number | null;
|
|
143
|
+
rsi14: number | null;
|
|
144
|
+
macd: number | null;
|
|
145
|
+
macdSignal: number | null;
|
|
146
|
+
macdHist: number | null;
|
|
147
|
+
bollUpper: number | null;
|
|
148
|
+
bollMiddle: number | null;
|
|
149
|
+
bollLower: number | null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function attachIndicators(
|
|
153
|
+
bars: { date: string; open: number; high: number; low: number; close: number; volume: number }[]
|
|
154
|
+
): BarWithIndicators[] {
|
|
155
|
+
const closes = bars.map((b) => b.close);
|
|
156
|
+
const ma5 = calcMA(closes, 5);
|
|
157
|
+
const ma10 = calcMA(closes, 10);
|
|
158
|
+
const ma20 = calcMA(closes, 20);
|
|
159
|
+
const ma60 = calcMA(closes, 60);
|
|
160
|
+
const rsi14 = calcRSI(closes, 14);
|
|
161
|
+
const macdResult = calcMACD(closes);
|
|
162
|
+
const bollResult = calcBOLL(closes);
|
|
163
|
+
|
|
164
|
+
return bars.map((b, i) => ({
|
|
165
|
+
...b,
|
|
166
|
+
ma5: ma5[i],
|
|
167
|
+
ma10: ma10[i],
|
|
168
|
+
ma20: ma20[i],
|
|
169
|
+
ma60: ma60[i],
|
|
170
|
+
rsi14: rsi14[i],
|
|
171
|
+
macd: macdResult.macd[i],
|
|
172
|
+
macdSignal: macdResult.signal[i],
|
|
173
|
+
macdHist: macdResult.histogram[i],
|
|
174
|
+
bollUpper: bollResult.upper[i],
|
|
175
|
+
bollMiddle: bollResult.middle[i],
|
|
176
|
+
bollLower: bollResult.lower[i],
|
|
177
|
+
}));
|
|
178
|
+
}
|