Skip to main content
Glama

Financial News and Notes MCP Server

MIT License
129
199
  • Apple
  • Linux
fundData.ts20.4 kB
import { TUSHARE_CONFIG } from '../config.js'; export const fundData = { name: "fund_data", description: "获取公募基金全面数据,包括基金列表、基金经理、基金净值、基金分红、基金持仓等数据。", parameters: { type: "object", properties: { ts_code: { type: "string", description: "基金代码,如'150018.SZ'表示银华深证100分级,'001753.OF'表示场外基金。注意:查询基金列表(basic)时必须提供此参数" }, data_type: { type: "string", description: "数据类型,可选值:basic(基金列表)、manager(基金经理)、nav(基金净值)、dividend(基金分红)、portfolio(基金持仓)、all(全部数据)", enum: ["basic", "manager", "nav", "dividend", "portfolio", "all"] }, start_date: { type: "string", description: "起始日期,格式为YYYYMMDD,如'20230101'。重要:对于基金持仓(portfolio)数据和基金净值(nav)数据,如果不指定时间参数,将返回所有历史数据,可能数据量很大。建议指定时间范围或使用period参数" }, end_date: { type: "string", description: "结束日期,格式为YYYYMMDD,如'20231231'。配合start_date使用可限制数据范围" }, period: { type: "string", description: "特定报告期,格式为YYYYMMDD。例如:'20231231'表示2023年年报,'20240630'表示2024年中报,'20220630'表示2022年三季报,'20240331'表示2024年一季报。指定此参数时将忽略start_date和end_date" } }, required: ["data_type","ts_code"] }, async run(args: { ts_code?: string; data_type: string; start_date?: string; end_date?: string; period?: 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 today = new Date(); const currentYear = today.getFullYear(); const defaultEndDate = `${currentYear}1231`; const defaultStartDate = `${currentYear - 1}0101`; const results: any[] = []; // 根据data_type决定要查询的API const dataTypes = args.data_type === 'all' ? ['basic', 'manager', 'nav', 'dividend', 'portfolio'] : [args.data_type]; for (const dataType of dataTypes) { try { // 基金列表(basic)模块必须提供基金代码,否则跳过 if (dataType === 'basic' && !args.ts_code) { console.warn('基金列表查询需要提供基金代码,跳过basic模块'); results.push({ type: dataType, error: '基金列表查询需要提供基金代码(ts_code)参数,否则数据量过大' }); continue; } const result = await fetchFundData( dataType, args.ts_code, args.period, args.start_date || defaultStartDate, args.end_date || defaultEndDate, TUSHARE_API_KEY, TUSHARE_API_URL ); if (result.data && result.data.length > 0) { results.push({ type: dataType, data: result.data, fields: result.fields }); } } catch (error) { console.warn(`获取${dataType}数据失败:`, error); results.push({ type: dataType, error: error instanceof Error ? error.message : '未知错误' }); } } if (results.length === 0) { throw new Error(`未找到相关基金数据`); } // 格式化输出 const formattedOutput = formatFundData(results, args.ts_code); return { content: [{ type: "text", text: formattedOutput }] }; } catch (error) { console.error('基金数据查询错误:', error); return { content: [{ type: "text", text: `查询基金数据时发生错误: ${error instanceof Error ? error.message : '未知错误'}` }] }; } } }; // 获取基金数据的通用函数 async function fetchFundData( dataType: string, tsCode?: string, period?: string, startDate?: string, endDate?: string, apiKey?: string, apiUrl?: string ) { const apiConfigs: Record<string, any> = { basic: { api_name: "fund_basic", default_fields: "ts_code,name,management,custodian,fund_type,found_date,due_date,list_date,issue_date,delist_date,issue_amount,m_fee,c_fee,duration_year,p_value,min_amount,exp_return,benchmark,status,invest_type,type,trustee,purc_startdate,redm_startdate,market" }, manager: { api_name: "fund_manager", default_fields: "ts_code,ann_date,name,gender,birth_year,edu,nationality,begin_date,end_date,resume" }, nav: { api_name: "fund_nav", default_fields: "ts_code,ann_date,nav_date,unit_nav,accum_nav,accum_div,net_asset,total_netasset,adj_nav" }, dividend: { api_name: "fund_div", default_fields: "ts_code,ann_date,imp_anndate,base_date,div_proc,record_date,ex_date,pay_date,earpay_date,net_ex_date,div_cash,base_unit,ear_distr,ear_amount,account_date,base_year" }, portfolio: { api_name: "fund_portfolio", default_fields: "ts_code,ann_date,end_date,symbol,mkv,amount,stk_mkv_ratio,stk_float_ratio" } }; const config = apiConfigs[dataType]; if (!config) { throw new Error(`不支持的数据类型: ${dataType}`); } // 构建请求参数 const params: any = { api_name: config.api_name, token: apiKey, params: {}, fields: config.default_fields }; // 根据不同的API添加特定参数 if (dataType === 'basic') { if (tsCode) params.params.ts_code = tsCode; } else if (dataType === 'manager') { if (tsCode) params.params.ts_code = tsCode; } else if (dataType === 'nav') { if (tsCode) params.params.ts_code = tsCode; if (period) { params.params.nav_date = period; } else { if (startDate) params.params.start_date = startDate; if (endDate) params.params.end_date = endDate; } } else if (dataType === 'dividend') { if (tsCode) params.params.ts_code = tsCode; } else if (dataType === 'portfolio') { if (tsCode) params.params.ts_code = tsCode; if (period) { params.params.period = period; } else { if (startDate) params.params.start_date = startDate; if (endDate) params.params.end_date = endDate; } } console.log(`调用${config.api_name} API,参数:`, JSON.stringify(params, null, 2)); // 设置请求超时 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), TUSHARE_CONFIG.TIMEOUT); try { const response = await fetch(apiUrl || 'https://api.tushare.pro', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(params), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`API请求失败: ${response.status} ${response.statusText}`); } const result = await response.json(); if (result.code !== 0) { throw new Error(`API返回错误: ${result.msg || '未知错误'}`); } if (!result.data || !result.data.items) { return { data: [], fields: result.data?.fields || [] }; } // 转换数据格式 const formattedData = result.data.items.map((item: any[]) => { const obj: any = {}; result.data.fields.forEach((field: string, index: number) => { obj[field] = item[index]; }); return obj; }); // 对某些数据类型进行日期范围过滤 let filteredData = formattedData; if (['dividend'].includes(dataType) && startDate && endDate && !period) { filteredData = formattedData.filter((item: any) => { const annDate = item.ann_date; if (!annDate) return true; return annDate >= startDate && annDate <= endDate; }); console.log(`日期范围过滤后剩余${filteredData.length}条${dataType}记录`); } console.log(`成功获取到${filteredData.length}条${dataType}数据记录`); // 如果是净值数据且有基金代码,尝试获取基金份额数据并合并 if (dataType === 'nav' && tsCode && filteredData.length > 0) { try { const shareResult = await fetchFundShareData(tsCode, startDate, endDate, period, apiKey, apiUrl); if (shareResult.data && shareResult.data.length > 0) { // 创建份额数据的映射表,以交易日期为键 const shareMap = new Map(); shareResult.data.forEach((shareItem: any) => { shareMap.set(shareItem.trade_date, shareItem.fd_share); }); // 将份额数据合并到净值数据中 filteredData.forEach((navItem: any) => { const tradeDate = navItem.nav_date || navItem.ann_date; navItem.fd_share = shareMap.get(tradeDate) || null; }); console.log(`成功合并${shareResult.data.length}条基金份额数据`); } } catch (error) { console.warn('获取基金份额数据失败,将继续返回净值数据:', error); } } return { data: filteredData, fields: result.data.fields }; } catch (error) { clearTimeout(timeoutId); throw error; } } // 获取基金份额数据的函数 async function fetchFundShareData( tsCode: string, startDate?: string, endDate?: string, period?: string, apiKey?: string, apiUrl?: string ) { const params: any = { api_name: "fund_share", token: apiKey, params: { ts_code: tsCode }, fields: "ts_code,trade_date,fd_share" }; // 添加时间参数 if (period) { params.params.trade_date = period; } else { if (startDate) params.params.start_date = startDate; if (endDate) params.params.end_date = endDate; } console.log(`调用fund_share API,参数:`, JSON.stringify(params, null, 2)); // 设置请求超时 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), TUSHARE_CONFIG.TIMEOUT); try { const response = await fetch(apiUrl || 'https://api.tushare.pro', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(params), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`API请求失败: ${response.status} ${response.statusText}`); } const result = await response.json(); if (result.code !== 0) { throw new Error(`API返回错误: ${result.msg || '未知错误'}`); } if (!result.data || !result.data.items) { return { data: [], fields: result.data?.fields || [] }; } // 转换数据格式 const formattedData = result.data.items.map((item: any[]) => { const obj: any = {}; result.data.fields.forEach((field: string, index: number) => { obj[field] = item[index]; }); return obj; }); return { data: formattedData, fields: result.data.fields }; } catch (error) { clearTimeout(timeoutId); throw error; } } // 格式化基金数据输出 function formatFundData(results: any[], tsCode?: string): string { let output = `# 基金数据查询结果\n\n`; if (tsCode) { output += `基金代码: ${tsCode}\n\n`; } for (const result of results) { if (result.error) { output += `# ${getDataTypeName(result.type)}\n❌ 查询失败: ${result.error}\n\n`; continue; } output += `# ${getDataTypeName(result.type)}\n`; output += `数据条数: ${result.data.length}\n\n`; if (result.data.length > 0) { switch (result.type) { case 'basic': output += formatBasicData(result.data); break; case 'manager': output += formatManagerData(result.data); break; case 'nav': output += formatNavData(result.data); break; case 'dividend': output += formatDividendData(result.data); break; case 'portfolio': output += formatPortfolioData(result.data); break; default: output += formatGenericFundData(result.data, result.fields); } } output += '\n'; } return output; } function getDataTypeName(type: string): string { const names: Record<string, string> = { basic: '基金基本信息', manager: '基金经理', nav: '基金净值', dividend: '基金分红', portfolio: '基金持仓' }; return names[type] || type; } function formatBasicData(data: any[]): string { let output = ''; data.forEach((item, index) => { output += `## ${index + 1}. ${item.name || '未知基金'} (${item.ts_code})\n`; output += `- 管理人: ${item.management || 'N/A'}\n`; output += `- 托管人: ${item.custodian || 'N/A'}\n`; output += `- 投资类型: ${item.fund_type || 'N/A'}\n`; output += `- 成立日期: ${item.found_date || 'N/A'}\n`; output += `- 上市时间: ${item.list_date || 'N/A'}\n`; output += `- 存续状态: ${item.status || 'N/A'}\n`; output += `- 市场: ${item.market === 'E' ? '场内' : item.market === 'O' ? '场外' : item.market || 'N/A'}\n`; if (item.m_fee) output += `- 管理费: ${formatPercent(item.m_fee)}%\n`; if (item.c_fee) output += `- 托管费: ${formatPercent(item.c_fee)}%\n`; output += '\n'; }); return output; } function formatManagerData(data: any[]): string { let output = ''; data.forEach((item, index) => { output += `## ${index + 1}. ${item.name || '未知经理'} (${item.ts_code})\n`; output += `- 性别: ${item.gender === 'M' ? '男' : item.gender === 'F' ? '女' : item.gender || 'N/A'}\n`; output += `- 出生年份: ${item.birth_year || 'N/A'}\n`; output += `- 学历: ${item.edu || 'N/A'}\n`; output += `- 国籍: ${item.nationality || 'N/A'}\n`; output += `- 任职日期: ${item.begin_date || 'N/A'}\n`; output += `- 离任日期: ${item.end_date || '在任'}\n`; output += `- 公告日期: ${item.ann_date || 'N/A'}\n`; if (item.resume) { const resumeShort = item.resume; output += `- 简历: ${resumeShort}\n`; } output += '\n'; }); return output; } function formatNavData(data: any[]): string { let output = ''; // 按日期排序,最新的在前 const sortedData = data.sort((a, b) => { const dateA = a.nav_date || a.ann_date || ''; const dateB = b.nav_date || b.ann_date || ''; return dateB.localeCompare(dateA); }); // 检查是否有基金份额数据 const hasShareData = sortedData.some(item => item.fd_share !== null && item.fd_share !== undefined); if (hasShareData) { output += '| 净值日期 | 单位净值 | 累计净值 | 复权净值 | 资产净值 | 基金份额(万份) |\n'; output += '|---------|----------|----------|----------|----------|---------------|\n'; sortedData.forEach(item => { const shareFormatted = item.fd_share ? formatNumber(item.fd_share) : 'N/A'; output += `| ${item.nav_date || 'N/A'} | ${formatNumber(item.unit_nav)} | ${formatNumber(item.accum_nav)} | ${formatNumber(item.adj_nav)} | ${formatNumber(item.net_asset)} | ${shareFormatted} |\n`; }); } else { output += '| 净值日期 | 单位净值 | 累计净值 | 复权净值 | 资产净值 |\n'; output += '|---------|----------|----------|----------|----------|\n'; sortedData.forEach(item => { output += `| ${item.nav_date || 'N/A'} | ${formatNumber(item.unit_nav)} | ${formatNumber(item.accum_nav)} | ${formatNumber(item.adj_nav)} | ${formatNumber(item.net_asset)} |\n`; }); } return output; } function formatDividendData(data: any[]): string { let output = ''; // 按公告日期排序,最新的在前 const sortedData = data.sort((a, b) => { const dateA = a.ann_date || ''; const dateB = b.ann_date || ''; return dateB.localeCompare(dateA); }); output += '| 公告日期 | 基准日期 | 分红方案 | 每股派息(元) | 除息日 | 派息日 | 权益登记日 |\n'; output += '|---------|----------|----------|-------------|-------|-------|----------|\n'; sortedData.forEach(item => { output += `| ${item.ann_date || 'N/A'} | ${item.base_date || 'N/A'} | ${item.div_proc || 'N/A'} | ${formatNumber(item.div_cash)} | ${item.ex_date || 'N/A'} | ${item.pay_date || 'N/A'} | ${item.record_date || 'N/A'} |\n`; }); return output; } function formatPortfolioData(data: any[]): string { let output = ''; // 按报告期分组 const groupedByPeriod = data.reduce((groups: any, item: any) => { const periodKey = `${item.end_date || 'Unknown'}_${item.ann_date || 'Unknown'}`; if (!groups[periodKey]) { groups[periodKey] = { end_date: item.end_date, ann_date: item.ann_date, holdings: [] }; } groups[periodKey].holdings.push(item); return groups; }, {} as any); // 按报告期排序(最新的在前) const sortedPeriods = Object.values(groupedByPeriod).sort((a: any, b: any) => { const dateA = a.end_date || a.ann_date || ''; const dateB = b.end_date || b.ann_date || ''; return dateB.localeCompare(dateA); }); sortedPeriods.forEach((period: any) => { output += `## 📊 报告期: ${period.end_date || 'N/A'} (公告日期: ${period.ann_date || 'N/A'})\n`; output += `持仓股票数量: ${period.holdings.length}只\n\n`; // 按持有市值排序,从大到小 const sortedHoldings = period.holdings.sort((a: any, b: any) => { const mvkA = parseFloat(a.mkv) || 0; const mvkB = parseFloat(b.mkv) || 0; return mvkB - mvkA; }); output += '| 股票代码 | 持有市值(万元) | 持有数量(股) | 占基金净值比(%) | 占流通股本比(%) |\n'; output += '|---------|---------------|-------------|----------------|----------------|\n'; // 只显示前20大重仓股 sortedHoldings.slice(0, 20).forEach((item: any) => { const mkv = formatNumber(parseFloat(item.mkv) / 10000); // 转换为万元 const amount = formatNumber(item.amount); const mkvRatio = formatPercent(item.stk_mkv_ratio); const floatRatio = formatPercent(item.stk_float_ratio); output += `| ${item.symbol || 'N/A'} | ${mkv} | ${amount} | ${mkvRatio} | ${floatRatio} |\n`; }); if (sortedHoldings.length > 20) { output += `\n💡 注:仅显示前20大重仓股,共持有${sortedHoldings.length}只股票\n`; } output += '\n---\n\n'; }); return output; } function formatGenericFundData(data: any[], fields: string[]): string { let output = ''; if (data.length === 0) return '暂无数据\n'; // 表头 output += '| ' + fields.slice(0, 6).join(' | ') + ' |\n'; output += '|' + fields.slice(0, 6).map(() => '-------').join('|') + '|\n'; // 数据行 data.forEach(item => { const row = fields.slice(0, 6).map(field => { const value = item[field]; if (value === null || value === undefined) return 'N/A'; if (typeof value === 'string' && value.length > 15) { return value.substring(0, 12) + '...'; } return String(value); }); output += '| ' + row.join(' | ') + ' |\n'; }); return output; } function formatNumber(num: any): string { if (num === null || num === undefined || num === '') return 'N/A'; const value = parseFloat(num); if (isNaN(value)) return 'N/A'; if (Math.abs(value) >= 1e8) { return (value / 1e8).toFixed(2) + '亿'; } else if (Math.abs(value) >= 1e4) { return (value / 1e4).toFixed(2) + '万'; } else { return value.toFixed(2); } } function formatPercent(num: any): string { if (num === null || num === undefined || num === '') return 'N/A'; const value = parseFloat(num); if (isNaN(value)) return 'N/A'; return value.toFixed(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