xstock-mcp 1.0.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/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/index.js +827 -0
- package/package.json +27 -0
- package/src/data/binance.ts +87 -0
- package/src/data/coingecko.ts +113 -0
- package/src/data/tencent.ts +187 -0
- package/src/data/yahoo.ts +139 -0
- package/src/index.ts +7 -0
- package/src/server.ts +40 -0
- package/src/tools/crypto.ts +145 -0
- package/src/tools/index.ts +24 -0
- package/src/tools/kline.ts +91 -0
- package/src/tools/quotes.ts +52 -0
- package/src/tools/search.ts +54 -0
- package/src/tools/types.ts +44 -0
- package/src/tools/us-market.ts +73 -0
- package/src/utils/indicators.ts +0 -0
- package/src/utils/symbol.ts +0 -0
- package/test/data-binance.ts +68 -0
- package/test/data-coingecko.ts +71 -0
- package/test/data-tencent.ts +90 -0
- package/test/data-yahoo.ts +84 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import { fetchGlobalMarket, fetchTopCoins, fetchFearGreed, fetchCategories } from "../data/coingecko";
|
|
4
|
+
import type { ToolDef } from "./types";
|
|
5
|
+
import { ok, err } from "./types";
|
|
6
|
+
|
|
7
|
+
export const getCryptoOverviewTool: ToolDef = {
|
|
8
|
+
tool: {
|
|
9
|
+
name: "get_crypto_overview",
|
|
10
|
+
description:
|
|
11
|
+
"获取加密货币市场全局概况:总市值、24h成交量、BTC/ETH占比、市值变化,以及恐惧贪婪指数。",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {},
|
|
15
|
+
required: [],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
handler: async () => {
|
|
19
|
+
try {
|
|
20
|
+
const [market, fg] = await Promise.all([fetchGlobalMarket(), fetchFearGreed()]);
|
|
21
|
+
return ok({ market, fearGreed: fg });
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return err((e as Error).message);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const TopCoinsInput = z.object({
|
|
29
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const getCryptoTopTool: ToolDef = {
|
|
33
|
+
tool: {
|
|
34
|
+
name: "get_crypto_top",
|
|
35
|
+
description: "按市值排名获取 Top N 加密货币,含价格、涨跌幅、市值、24h成交量。默认 Top 20。",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
limit: {
|
|
40
|
+
type: "number",
|
|
41
|
+
description: "返回数量,默认20,最大100",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
required: [],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
handler: async (input) => {
|
|
48
|
+
const parsed = TopCoinsInput.safeParse(input);
|
|
49
|
+
if (!parsed.success) return err(parsed.error.message);
|
|
50
|
+
const { limit = 20 } = parsed.data;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
return ok(await fetchTopCoins(limit));
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return err((e as Error).message);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const getCryptoCatsTool: ToolDef = {
|
|
61
|
+
tool: {
|
|
62
|
+
name: "get_crypto_categories",
|
|
63
|
+
description: "获取加密货币赛道分类数据(DeFi、Layer1、AI、GameFi等),含各赛道市值和24h涨跌幅。",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {},
|
|
67
|
+
required: [],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
handler: async () => {
|
|
71
|
+
try {
|
|
72
|
+
const cats = await fetchCategories();
|
|
73
|
+
const sorted = cats.sort((a, b) => (b.marketCap ?? 0) - (a.marketCap ?? 0));
|
|
74
|
+
return ok(sorted);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
return err((e as Error).message);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const FundingInput = z.object({
|
|
82
|
+
symbols: z.array(z.string()).min(1).max(20),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
interface FundingRate {
|
|
86
|
+
symbol: string;
|
|
87
|
+
markPrice: string;
|
|
88
|
+
indexPrice: string;
|
|
89
|
+
lastFundingRate: string;
|
|
90
|
+
nextFundingTime: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const getCryptoFundingTool: ToolDef = {
|
|
94
|
+
tool: {
|
|
95
|
+
name: "get_funding_rate",
|
|
96
|
+
description:
|
|
97
|
+
"获取 Binance 永续合约资金费率。symbols 传币种代码,如 ['BTC','ETH'],自动补全 USDT 后缀。",
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
symbols: {
|
|
102
|
+
type: "array",
|
|
103
|
+
items: { type: "string" },
|
|
104
|
+
description: "币种代码列表,如 ['BTC','ETH','SOL']",
|
|
105
|
+
minItems: 1,
|
|
106
|
+
maxItems: 20,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
required: ["symbols"],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
handler: async (input) => {
|
|
113
|
+
const parsed = FundingInput.safeParse(input);
|
|
114
|
+
if (!parsed.success) return err(parsed.error.message);
|
|
115
|
+
const { symbols } = parsed.data;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const results = await Promise.all(
|
|
119
|
+
symbols.map(async (raw) => {
|
|
120
|
+
const sym = raw.toUpperCase().endsWith("USDT") ? raw.toUpperCase() : raw.toUpperCase() + "USDT";
|
|
121
|
+
try {
|
|
122
|
+
const res = await axios.get<FundingRate>(
|
|
123
|
+
`https://fapi.binance.com/fapi/v1/premiumIndex?symbol=${sym}`,
|
|
124
|
+
{ timeout: 5000 }
|
|
125
|
+
);
|
|
126
|
+
const d = res.data;
|
|
127
|
+
return {
|
|
128
|
+
symbol: sym,
|
|
129
|
+
markPrice: parseFloat(d.markPrice),
|
|
130
|
+
indexPrice: parseFloat(d.indexPrice),
|
|
131
|
+
fundingRate: parseFloat(d.lastFundingRate),
|
|
132
|
+
fundingRatePct: (parseFloat(d.lastFundingRate) * 100).toFixed(4) + "%",
|
|
133
|
+
nextFundingTime: new Date(d.nextFundingTime).toISOString(),
|
|
134
|
+
};
|
|
135
|
+
} catch {
|
|
136
|
+
return { symbol: sym, error: "not found or not a perpetual contract" };
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
);
|
|
140
|
+
return ok(results);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
return err((e as Error).message);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ToolDef } from "./types";
|
|
2
|
+
import { getQuoteTool } from "./quotes";
|
|
3
|
+
import { getKlineTool } from "./kline";
|
|
4
|
+
import { searchStockTool } from "./search";
|
|
5
|
+
import { getUsIndicesTool, getStockProfileTool } from "./us-market";
|
|
6
|
+
import { getCryptoOverviewTool, getCryptoTopTool, getCryptoCatsTool, getCryptoFundingTool } from "./crypto";
|
|
7
|
+
|
|
8
|
+
export * from "./types";
|
|
9
|
+
|
|
10
|
+
export const tools: ToolDef[] = [
|
|
11
|
+
getQuoteTool,
|
|
12
|
+
getKlineTool,
|
|
13
|
+
searchStockTool,
|
|
14
|
+
getUsIndicesTool,
|
|
15
|
+
getStockProfileTool,
|
|
16
|
+
getCryptoOverviewTool,
|
|
17
|
+
getCryptoTopTool,
|
|
18
|
+
getCryptoCatsTool,
|
|
19
|
+
getCryptoFundingTool,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export const toolMap = new Map<string, ToolDef>(
|
|
23
|
+
tools.map((t) => [t.tool.name, t])
|
|
24
|
+
);
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fetchAShareKline, fetchHKKline } from "../data/tencent";
|
|
3
|
+
import { fetchUSKlines } from "../data/yahoo";
|
|
4
|
+
import { fetchCryptoKlines } from "../data/binance";
|
|
5
|
+
import type { ToolDef } from "./types";
|
|
6
|
+
import { ok, err } from "./types";
|
|
7
|
+
|
|
8
|
+
const periodToYahoo: Record<string, "1d" | "1wk" | "1mo"> = {
|
|
9
|
+
daily: "1d",
|
|
10
|
+
weekly: "1wk",
|
|
11
|
+
monthly: "1mo",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const Input = z.object({
|
|
15
|
+
market: z.enum(["a-share", "hk", "us", "crypto"]),
|
|
16
|
+
code: z.string(),
|
|
17
|
+
period: z.enum(["daily", "weekly", "monthly"]).optional(),
|
|
18
|
+
start: z.string().optional(),
|
|
19
|
+
end: z.string().optional(),
|
|
20
|
+
interval: z.string().optional(),
|
|
21
|
+
limit: z.number().int().min(1).max(500).optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const getKlineTool: ToolDef = {
|
|
25
|
+
tool: {
|
|
26
|
+
name: "get_kline",
|
|
27
|
+
description:
|
|
28
|
+
"获取K线数据(OHLCV)。股票支持 daily/weekly/monthly;加密货币支持 1m/5m/15m/1h/4h/1d/1w 等 Binance 标准周期。" +
|
|
29
|
+
"美股可指定 start/end 日期(YYYY-MM-DD);加密可指定 limit(最多500条)。",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
market: {
|
|
34
|
+
type: "string",
|
|
35
|
+
enum: ["a-share", "hk", "us", "crypto"],
|
|
36
|
+
description: "市场类型",
|
|
37
|
+
},
|
|
38
|
+
code: {
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "代码,如 AAPL、600519、00700、BTC",
|
|
41
|
+
},
|
|
42
|
+
period: {
|
|
43
|
+
type: "string",
|
|
44
|
+
enum: ["daily", "weekly", "monthly"],
|
|
45
|
+
description: "周期(股票市场),默认 daily",
|
|
46
|
+
},
|
|
47
|
+
start: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "开始日期 YYYY-MM-DD(仅美股,默认近90天)",
|
|
50
|
+
},
|
|
51
|
+
end: {
|
|
52
|
+
type: "string",
|
|
53
|
+
description: "结束日期 YYYY-MM-DD(仅美股,默认今天)",
|
|
54
|
+
},
|
|
55
|
+
interval: {
|
|
56
|
+
type: "string",
|
|
57
|
+
description: "加密货币周期,如 1d、4h、1h、15m,默认 1d",
|
|
58
|
+
},
|
|
59
|
+
limit: {
|
|
60
|
+
type: "number",
|
|
61
|
+
description: "加密货币返回条数,最大500,默认100",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
required: ["market", "code"],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
handler: async (input) => {
|
|
68
|
+
const parsed = Input.safeParse(input);
|
|
69
|
+
if (!parsed.success) return err(parsed.error.message);
|
|
70
|
+
const { market, code, period, start, end, interval, limit } = parsed.data;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
if (market === "a-share") {
|
|
74
|
+
return ok(await fetchAShareKline(code, period ?? "daily"));
|
|
75
|
+
}
|
|
76
|
+
if (market === "hk") {
|
|
77
|
+
return ok(await fetchHKKline(code, period ?? "daily"));
|
|
78
|
+
}
|
|
79
|
+
if (market === "us") {
|
|
80
|
+
const endDate = end ?? new Date().toISOString().slice(0, 10);
|
|
81
|
+
const startDate = start ?? new Date(Date.now() - 90 * 86400_000).toISOString().slice(0, 10);
|
|
82
|
+
const yInterval = periodToYahoo[period ?? "daily"] ?? "1d";
|
|
83
|
+
return ok(await fetchUSKlines(code, startDate, endDate, yInterval));
|
|
84
|
+
}
|
|
85
|
+
// crypto
|
|
86
|
+
return ok(await fetchCryptoKlines(code, interval ?? "1d", limit ?? 100));
|
|
87
|
+
} catch (e) {
|
|
88
|
+
return err((e as Error).message);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fetchAShareQuotes, fetchHKQuotes } from "../data/tencent";
|
|
3
|
+
import { fetchUSQuotes } from "../data/yahoo";
|
|
4
|
+
import { fetchCryptoTickers } from "../data/binance";
|
|
5
|
+
import type { ToolDef } from "./types";
|
|
6
|
+
import { ok, err } from "./types";
|
|
7
|
+
|
|
8
|
+
const Input = z.object({
|
|
9
|
+
market: z.enum(["a-share", "hk", "us", "crypto"]),
|
|
10
|
+
codes: z.array(z.string()).min(1).max(20),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const getQuoteTool: ToolDef = {
|
|
14
|
+
tool: {
|
|
15
|
+
name: "get_quote",
|
|
16
|
+
description:
|
|
17
|
+
"获取股票或加密货币实时行情。支持美股(us)、A股(a-share)、港股(hk)、加密货币(crypto)。" +
|
|
18
|
+
"美股示例:codes=['AAPL','NVDA'];A股:codes=['600519','000858'];港股:codes=['00700','09988'];加密:codes=['BTC','ETH']。",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
market: {
|
|
23
|
+
type: "string",
|
|
24
|
+
enum: ["a-share", "hk", "us", "crypto"],
|
|
25
|
+
description: "市场类型:a-share=A股, hk=港股, us=美股, crypto=加密货币",
|
|
26
|
+
},
|
|
27
|
+
codes: {
|
|
28
|
+
type: "array",
|
|
29
|
+
items: { type: "string" },
|
|
30
|
+
description: "股票/币种代码列表,最多20个",
|
|
31
|
+
minItems: 1,
|
|
32
|
+
maxItems: 20,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ["market", "codes"],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
handler: async (input) => {
|
|
39
|
+
const parsed = Input.safeParse(input);
|
|
40
|
+
if (!parsed.success) return err(parsed.error.message);
|
|
41
|
+
const { market, codes } = parsed.data;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (market === "a-share") return ok(await fetchAShareQuotes(codes));
|
|
45
|
+
if (market === "hk") return ok(await fetchHKQuotes(codes));
|
|
46
|
+
if (market === "us") return ok(await fetchUSQuotes(codes));
|
|
47
|
+
return ok(await fetchCryptoTickers(codes));
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return err((e as Error).message);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { default as YahooFinance } from "yahoo-finance2";
|
|
3
|
+
import type { ToolDef } from "./types";
|
|
4
|
+
import { ok, err } from "./types";
|
|
5
|
+
|
|
6
|
+
const yf = new YahooFinance({ suppressNotices: ["yahooSurvey"] });
|
|
7
|
+
|
|
8
|
+
const Input = z.object({
|
|
9
|
+
query: z.string().min(1),
|
|
10
|
+
limit: z.number().int().min(1).max(20).optional(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const searchStockTool: ToolDef = {
|
|
14
|
+
tool: {
|
|
15
|
+
name: "search_stock",
|
|
16
|
+
description:
|
|
17
|
+
"按关键词搜索股票或ETF,返回匹配的股票列表(含代码、名称、交易所、类型)。适用于不知道确切代码时查找标的。",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
query: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "搜索关键词,如公司名或代码,如 'Apple'、'TSLA'、'nvidia'",
|
|
24
|
+
},
|
|
25
|
+
limit: {
|
|
26
|
+
type: "number",
|
|
27
|
+
description: "最多返回条数,默认10,最大20",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
required: ["query"],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
handler: async (input) => {
|
|
34
|
+
const parsed = Input.safeParse(input);
|
|
35
|
+
if (!parsed.success) return err(parsed.error.message);
|
|
36
|
+
const { query, limit = 10 } = parsed.data;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const res = await yf.search(query);
|
|
40
|
+
const items = (res.quotes ?? []).slice(0, limit).map((q) => {
|
|
41
|
+
const r = q as Record<string, unknown>;
|
|
42
|
+
return {
|
|
43
|
+
symbol: r.symbol,
|
|
44
|
+
name: r.longname ?? r.shortname ?? "",
|
|
45
|
+
exchange: r.exchange ?? "",
|
|
46
|
+
type: r.typeDisp ?? r.quoteType ?? "",
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
return ok(items);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return err((e as Error).message);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// MCP-compatible types for tools layer
|
|
2
|
+
|
|
3
|
+
export interface ToolInputSchema {
|
|
4
|
+
type: "object";
|
|
5
|
+
properties?: Record<string, object>;
|
|
6
|
+
required?: string[];
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface MCPTool {
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
inputSchema: ToolInputSchema;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TextContent {
|
|
17
|
+
type: "text";
|
|
18
|
+
text: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CallToolResult {
|
|
22
|
+
content: TextContent[];
|
|
23
|
+
isError?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ToolHandler = (input: Record<string, unknown>) => Promise<CallToolResult>;
|
|
27
|
+
|
|
28
|
+
export interface ToolDef {
|
|
29
|
+
tool: MCPTool;
|
|
30
|
+
handler: ToolHandler;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function ok(data: unknown): CallToolResult {
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function err(message: string): CallToolResult {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
42
|
+
isError: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fetchUSQuotes, fetchStockProfile } from "../data/yahoo";
|
|
3
|
+
import type { ToolDef } from "./types";
|
|
4
|
+
import { ok, err } from "./types";
|
|
5
|
+
|
|
6
|
+
// 主要美股指数 ETF + 直接指数代码
|
|
7
|
+
const US_INDICES = ["^GSPC", "^IXIC", "^DJI", "^RUT", "^VIX"];
|
|
8
|
+
const INDEX_NAMES: Record<string, string> = {
|
|
9
|
+
"^GSPC": "S&P 500",
|
|
10
|
+
"^IXIC": "Nasdaq Composite",
|
|
11
|
+
"^DJI": "Dow Jones",
|
|
12
|
+
"^RUT": "Russell 2000",
|
|
13
|
+
"^VIX": "VIX 恐慌指数",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const getUsIndicesTool: ToolDef = {
|
|
17
|
+
tool: {
|
|
18
|
+
name: "get_us_indices",
|
|
19
|
+
description:
|
|
20
|
+
"获取美股主要指数实时行情,包括标普500、纳斯达克、道琼斯、罗素2000、VIX恐慌指数。",
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {},
|
|
24
|
+
required: [],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
handler: async () => {
|
|
28
|
+
try {
|
|
29
|
+
const quotes = await fetchUSQuotes(US_INDICES);
|
|
30
|
+
const result = quotes.map((q) => ({
|
|
31
|
+
...q,
|
|
32
|
+
indexName: INDEX_NAMES[q.symbol] ?? q.symbol,
|
|
33
|
+
}));
|
|
34
|
+
return ok(result);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return err((e as Error).message);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const ProfileInput = z.object({
|
|
42
|
+
symbol: z.string().min(1),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const getStockProfileTool: ToolDef = {
|
|
46
|
+
tool: {
|
|
47
|
+
name: "get_stock_profile",
|
|
48
|
+
description:
|
|
49
|
+
"获取美股上市公司基本面信息:行业、市值、PE、EPS、Beta、52周高低点、员工数、公司简介等。",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
symbol: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "美股代码,如 AAPL、TSLA、NVDA",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
required: ["symbol"],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
handler: async (input) => {
|
|
62
|
+
const parsed = ProfileInput.safeParse(input);
|
|
63
|
+
if (!parsed.success) return err(parsed.error.message);
|
|
64
|
+
const { symbol } = parsed.data;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const profile = await fetchStockProfile(symbol);
|
|
68
|
+
return ok(profile);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
return err((e as Error).message);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 测试 data/binance.ts
|
|
3
|
+
* 运行:npx ts-node test/data-binance.ts
|
|
4
|
+
*/
|
|
5
|
+
import { fetchCryptoTickers, fetchCryptoKlines, normalizeSymbol } from "../src/data/binance";
|
|
6
|
+
|
|
7
|
+
function section(title: string) {
|
|
8
|
+
console.log(`\n${"─".repeat(50)}`);
|
|
9
|
+
console.log(` ${title}`);
|
|
10
|
+
console.log("─".repeat(50));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
// ── 1. Symbol 标准化 ──────────────────────────────
|
|
15
|
+
section("normalizeSymbol");
|
|
16
|
+
const cases = ["BTC", "btc", "ETHUSDT", "solUsdt", "PEPE"];
|
|
17
|
+
for (const s of cases) {
|
|
18
|
+
console.log(` ${s.padEnd(10)} → ${normalizeSymbol(s)}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ── 2. 多币种行情 ─────────────────────────────────
|
|
22
|
+
section("fetchCryptoTickers — BTC / ETH / SOL");
|
|
23
|
+
try {
|
|
24
|
+
const tickers = await fetchCryptoTickers(["BTC", "ETH", "SOL"]);
|
|
25
|
+
for (const t of tickers) {
|
|
26
|
+
console.log(
|
|
27
|
+
` ${t.symbol.padEnd(10)} 价格=$${t.price.toLocaleString()}` +
|
|
28
|
+
` 涨跌幅=${t.changePercent} 成交量=${t.volume.toFixed(2)}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error(" ERROR:", e);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── 3. 错误 symbol ────────────────────────────────
|
|
36
|
+
section("fetchCryptoTickers — 不存在的 symbol");
|
|
37
|
+
try {
|
|
38
|
+
const tickers = await fetchCryptoTickers(["NOTEXIST123"]);
|
|
39
|
+
console.log(" 返回:", tickers.length === 0 ? "空数组(符合预期)" : JSON.stringify(tickers));
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error(" ERROR:", e);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── 4. BTC 日K ────────────────────────────────────
|
|
45
|
+
section("fetchCryptoKlines — BTC 日K 最近 10 条");
|
|
46
|
+
try {
|
|
47
|
+
const bars = await fetchCryptoKlines("BTC", "1d", 10);
|
|
48
|
+
console.log(` 共 ${bars.length} 条`);
|
|
49
|
+
for (const b of bars) {
|
|
50
|
+
console.log(` ${b.date} open=${b.open} close=${b.close} vol=${b.volume.toFixed(2)}`);
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error(" ERROR:", e);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── 5. ETH 4h K 线 ───────────────────────────────
|
|
57
|
+
section("fetchCryptoKlines — ETH 4h 最近 5 条");
|
|
58
|
+
try {
|
|
59
|
+
const bars = await fetchCryptoKlines("ETH", "4h", 5);
|
|
60
|
+
const last = bars[bars.length - 1];
|
|
61
|
+
console.log(` 共 ${bars.length} 条`);
|
|
62
|
+
console.log(` 最新: date=${last?.date} close=${last?.close}`);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error(" ERROR:", e);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 测试 data/coingecko.ts
|
|
3
|
+
* 运行:npx ts-node test/data-coingecko.ts
|
|
4
|
+
*
|
|
5
|
+
* 注意:CoinGecko 免费接口有频率限制,测试时请勿频繁运行
|
|
6
|
+
*/
|
|
7
|
+
import { fetchGlobalMarket, fetchTopCoins, fetchCategories, fetchFearGreed } from "../src/data/coingecko";
|
|
8
|
+
|
|
9
|
+
function section(title: string) {
|
|
10
|
+
console.log(`\n${"─".repeat(50)}`);
|
|
11
|
+
console.log(` ${title}`);
|
|
12
|
+
console.log("─".repeat(50));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
// ── 1. 全市场概况 ─────────────────────────────────
|
|
17
|
+
section("fetchGlobalMarket");
|
|
18
|
+
try {
|
|
19
|
+
const g = await fetchGlobalMarket();
|
|
20
|
+
console.log(` 总市值: $${(g.totalMarketCapUsd / 1e12).toFixed(2)}T`);
|
|
21
|
+
console.log(` 24h 成交量: $${(g.totalVolumeUsd / 1e9).toFixed(2)}B`);
|
|
22
|
+
console.log(` BTC 占比: ${g.btcDominance}%`);
|
|
23
|
+
console.log(` ETH 占比: ${g.ethDominance}%`);
|
|
24
|
+
console.log(` 24h 涨跌: ${g.marketCapChangePercent24h}%`);
|
|
25
|
+
console.log(` 活跃币种: ${g.activeCryptos}`);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error(" ERROR:", e);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── 2. 市值 Top 10 ───────────────────────────────
|
|
31
|
+
section("fetchTopCoins — Top 10");
|
|
32
|
+
try {
|
|
33
|
+
const coins = await fetchTopCoins(10);
|
|
34
|
+
for (const c of coins) {
|
|
35
|
+
const cap = (c.marketCap / 1e9).toFixed(1);
|
|
36
|
+
console.log(
|
|
37
|
+
` #${String(c.rank).padEnd(3)} ${c.symbol.padEnd(8)} $${c.price.toFixed(4).padStart(12)}` +
|
|
38
|
+
` ${c.changePercent24h >= 0 ? "+" : ""}${c.changePercent24h}% 市值=$${cap}B`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error(" ERROR:", e);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── 3. 恐惧贪婪指数 ───────────────────────────────
|
|
46
|
+
section("fetchFearGreed");
|
|
47
|
+
try {
|
|
48
|
+
const fg = await fetchFearGreed();
|
|
49
|
+
console.log(` 指数值: ${fg.value} 状态: ${fg.label} 日期: ${fg.timestamp}`);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.error(" ERROR:", e);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── 4. 赛道分类(前 10 条)────────────────────────
|
|
55
|
+
section("fetchCategories — 前 10 个赛道");
|
|
56
|
+
try {
|
|
57
|
+
const cats = await fetchCategories();
|
|
58
|
+
const top10 = cats
|
|
59
|
+
.sort((a, b) => (b.marketCap ?? 0) - (a.marketCap ?? 0))
|
|
60
|
+
.slice(0, 10);
|
|
61
|
+
for (const c of top10) {
|
|
62
|
+
const cap = c.marketCap ? `$${(c.marketCap / 1e9).toFixed(1)}B` : "N/A";
|
|
63
|
+
const chg = c.marketCapChange24h >= 0 ? `+${c.marketCapChange24h}%` : `${c.marketCapChange24h}%`;
|
|
64
|
+
console.log(` ${c.name.padEnd(30)} 市值=${cap.padStart(10)} 24h=${chg}`);
|
|
65
|
+
}
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.error(" ERROR:", e);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main().catch(console.error);
|