Skip to main content
Glama

Financial News and Notes MCP Server

MIT License
129
199
  • Apple
  • Linux
blockTrade.ts10.3 kB
import { TUSHARE_CONFIG } from '../config.js'; import { resolveStockCodes } from '../utils/stockCodeResolver.js'; export const blockTrade = { name: "block_trade", description: "获取大宗交易数据,包括成交价格、成交量、买卖双方营业部等详细信息", parameters: { type: "object", properties: { code: { type: "string", description: "股票代码(可选),如'000001.SZ'表示平安银行。不填写则查询全市场大宗交易" }, start_date: { type: "string", description: "起始日期,格式为YYYYMMDD,如'20230101'" }, end_date: { type: "string", description: "结束日期,格式为YYYYMMDD,如'20231231'" } }, required: ["start_date", "end_date"] }, async run(args: { code?: string; start_date: string; end_date: string }) { try { console.log('大宗交易查询参数:', args); const TUSHARE_API_KEY = TUSHARE_CONFIG.API_TOKEN; const TUSHARE_API_URL = TUSHARE_CONFIG.API_URL; if (!TUSHARE_API_KEY) { throw new Error('请配置TUSHARE_TOKEN环境变量'); } // 构建请求参数 const requestParams: any = { start_date: args.start_date, end_date: args.end_date }; // 只有提供了code时才添加ts_code参数 if (args.code) { requestParams.ts_code = args.code; } const params = { api_name: "block_trade", token: TUSHARE_API_KEY, params: requestParams // 不设置fields参数,返回所有字段 }; console.log(`请求大宗交易数据,API: block_trade,参数:`, params.params); // 设置请求超时 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), TUSHARE_CONFIG.TIMEOUT); try { const response = await fetch(TUSHARE_API_URL!, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`Tushare API请求失败: ${response.status}`); } const data = await response.json(); if (data.code !== 0) { throw new Error(`Tushare API错误: ${data.msg}`); } if (!data.data || !data.data.items || data.data.items.length === 0) { return { content: [{ type: "text", text: `# 📊 ${args.code || '全市场'} 大宗交易数据\n\n查询期间: ${args.start_date} - ${args.end_date}\n\n❌ 暂无大宗交易记录\n\n在指定时间范围内,${args.code ? '该股票' : '全市场'}没有大宗交易数据。` }] }; } // 获取字段名 const fieldsArray = data.data.fields; // 将数据转换为对象数组 const tradeData = data.data.items.map((item: any) => { const result: Record<string, any> = {}; fieldsArray.forEach((field: string, index: number) => { result[field] = item[index]; }); return result; }); console.log(`成功获取到${tradeData.length}条大宗交易记录`); // 格式化输出 const formattedOutput = await formatBlockTradeData(tradeData, args.code || '全市场', args.start_date, args.end_date); return { content: [{ type: "text", text: formattedOutput }] }; } catch (error) { clearTimeout(timeoutId); throw error; } } catch (error) { console.error('大宗交易查询错误:', error); return { content: [{ type: "text", text: `查询大宗交易数据时发生错误: ${error instanceof Error ? error.message : '未知错误'}` }] }; } } }; // 格式化大宗交易数据 async function formatBlockTradeData(data: any[], code: string, startDate: string, endDate: string): Promise<string> { let output = `# 📊 ${code} 大宗交易数据\n\n`; output += `查询期间: ${startDate} - ${endDate}\n`; output += `交易记录: 共 ${data.length} 条\n\n`; // 统计信息 const totalVolume = data.reduce((sum, item) => sum + (parseFloat(item.vol) || 0), 0); const totalAmount = data.reduce((sum, item) => sum + (parseFloat(item.amount) || 0), 0); const avgPrice = totalAmount > 0 && totalVolume > 0 ? totalAmount / totalVolume : 0; output += `## 📈 统计摘要\n\n`; output += `- 累计成交量: ${formatNumber(totalVolume)} 万股\n`; output += `- 累计成交金额: ${formatNumber(totalAmount)} 万元\n`; output += `- 平均成交价: ${avgPrice > 0 ? avgPrice.toFixed(2) : 'N/A'} 元/股\n`; output += `- 交易天数: ${new Set(data.map(item => item.trade_date)).size} 天\n`; output += `- 涉及股票: ${new Set(data.map(item => item.ts_code)).size} 只\n\n`; // 按交易日期分组 const groupedData: Record<string, any[]> = {}; for (const item of data) { const date = item.trade_date || 'unknown'; if (!groupedData[date]) { groupedData[date] = []; } groupedData[date].push(item); } // 按日期排序(最新的在前) const sortedDates = Object.keys(groupedData).sort((a, b) => b.localeCompare(a)); output += `## 📋 交易明细\n\n`; for (const date of sortedDates) { const dayTrades = groupedData[date]; const dayVolume = dayTrades.reduce((sum, item) => sum + (parseFloat(item.vol) || 0), 0); const dayAmount = dayTrades.reduce((sum, item) => sum + (parseFloat(item.amount) || 0), 0); output += `### 📅 ${date}\n\n`; output += `当日汇总: ${dayTrades.length} 笔交易,成交量 ${formatNumber(dayVolume)} 万股,成交额 ${formatNumber(dayAmount)} 万元\n\n`; // 创建表格 output += `| 股票代码 | 成交价(元) | 成交量(万股) | 成交金额(万元) | 买方营业部 | 卖方营业部 |\n`; output += `|---------|-----------|------------|-------------|-----------|----------|\n`; for (const trade of dayTrades) { const tsCode = trade.ts_code || 'N/A'; const price = trade.price ? parseFloat(trade.price).toFixed(2) : 'N/A'; const volume = trade.vol ? formatNumber(trade.vol) : 'N/A'; const amount = trade.amount ? formatNumber(trade.amount) : 'N/A'; const buyer = trade.buyer || 'N/A'; const seller = trade.seller || 'N/A'; // 截断过长的营业部名称 const buyerShort = buyer.length > 20 ? buyer.substring(0, 17) + '...' : buyer; const sellerShort = seller.length > 20 ? seller.substring(0, 17) + '...' : seller; output += `| ${tsCode} | ${price} | ${volume} | ${amount} | ${buyerShort} | ${sellerShort} |\n`; } output += '\n'; } // 股票活跃度统计(仅在查询全市场时显示) if (!code || code === '全市场') { const stockCount: Record<string, number> = {}; const stockVolume: Record<string, number> = {}; for (const trade of data) { if (trade.ts_code) { stockCount[trade.ts_code] = (stockCount[trade.ts_code] || 0) + 1; stockVolume[trade.ts_code] = (stockVolume[trade.ts_code] || 0) + (parseFloat(trade.vol) || 0); } } // 按交易次数排序的TOP5股票 const topStocksByCount = Object.entries(stockCount) .sort(([,a], [,b]) => b - a) .slice(0, 5); if (topStocksByCount.length > 0) { output += `## 📈 最活跃股票统计\n\n`; output += `### 🔥 大宗交易次数 TOP5\n\n`; output += `| 排名 | 股票代码 | 交易次数 | 累计成交量(万股) |\n`; output += `|-----|---------|--------|--------------|\n`; topStocksByCount.forEach(([tsCode, count], index) => { const volume = stockVolume[tsCode] || 0; output += `| ${index + 1} | ${tsCode} | ${count} 次 | ${formatNumber(volume)} |\n`; }); output += '\n'; } } // 买卖营业部统计 const buyerCount: Record<string, number> = {}; const sellerCount: Record<string, number> = {}; for (const trade of data) { if (trade.buyer && trade.buyer !== 'N/A') { buyerCount[trade.buyer] = (buyerCount[trade.buyer] || 0) + 1; } if (trade.seller && trade.seller !== 'N/A') { sellerCount[trade.seller] = (sellerCount[trade.seller] || 0) + 1; } } // 买方营业部TOP5 const topBuyers = Object.entries(buyerCount) .sort(([,a], [,b]) => b - a) .slice(0, 5); if (topBuyers.length > 0) { output += `## 🏆 活跃营业部统计\n\n`; output += `### 🟢 买方营业部 TOP5\n\n`; output += `| 排名 | 营业部名称 | 交易次数 |\n`; output += `|-----|-----------|--------|\n`; topBuyers.forEach(([name, count], index) => { const nameShort = name.length > 30 ? name.substring(0, 27) + '...' : name; output += `| ${index + 1} | ${nameShort} | ${count} 次 |\n`; }); output += '\n'; } // 卖方营业部TOP5 const topSellers = Object.entries(sellerCount) .sort(([,a], [,b]) => b - a) .slice(0, 5); if (topSellers.length > 0) { output += `### 🔴 卖方营业部 TOP5\n\n`; output += `| 排名 | 营业部名称 | 交易次数 |\n`; output += `|-----|-----------|--------|\n`; topSellers.forEach(([name, count], index) => { const nameShort = name.length > 30 ? name.substring(0, 27) + '...' : name; output += `| ${index + 1} | ${nameShort} | ${count} 次 |\n`; }); output += '\n'; } output += `---\n\n*数据来源: Tushare Pro*`; // 收集所有股票代码并生成说明 const stockCodes: string[] = []; for (const trade of data) { if (trade.ts_code) { stockCodes.push(String(trade.ts_code)); } } const stockExplanation = await resolveStockCodes(stockCodes); output += stockExplanation; return output; } // 辅助函数:格式化数字 function formatNumber(num: any): string { if (num === null || num === undefined || num === '' || isNaN(parseFloat(num))) { return 'N/A'; } const number = parseFloat(num); return number.toLocaleString('zh-CN', { maximumFractionDigits: 2 }); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/guangxiangdebizi/my-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server