Skip to main content
Glama

Financial News and Notes MCP Server

MIT License
129
199
  • Apple
  • Linux
csiIndexConstituents.js17.3 kB
import { TUSHARE_CONFIG } from '../config.js'; import { resolveStockCodes } from '../utils/stockCodeResolver.js'; function normalizeIndexCode(input) { const s = (input || '').trim(); if (!s) return s; if (s.includes('.') && s.split('.').length === 2) { const [left, right] = s.split('.'); return `${left}.${right.toUpperCase()}`; } const low = s.toLowerCase(); if (low.startsWith('sz') || low.startsWith('sh')) { const market = s.substring(0, 2).toUpperCase(); const digits = s.substring(2); return `${digits}.${market}`; } return s; } function parseDateString(input) { const s = (input || '').trim(); if (!s) throw new Error('日期不能为空'); if (s.includes('-')) { // YYYY-MM-DD -> YYYYMMDD return s.replaceAll('-', ''); } if (/^\d{8}$/.test(s)) return s; throw new Error('日期格式不正确,应为 YYYYMMDD 或 YYYY-MM-DD'); } function addDaysYYYYMMDD(base, deltaDays) { const year = Number(base.slice(0, 4)); const month = Number(base.slice(4, 6)) - 1; const day = Number(base.slice(6, 8)); const dt = new Date(Date.UTC(year, month, day)); dt.setUTCDate(dt.getUTCDate() + deltaDays); const y = dt.getUTCFullYear(); const m = String(dt.getUTCMonth() + 1).padStart(2, '0'); const d = String(dt.getUTCDate()).padStart(2, '0'); return `${y}${m}${d}`; } function mapTushareItemsToObjects(fields, items) { return items.map((row) => { const obj = {}; fields.forEach((f, idx) => { obj[f] = row[idx]; }); return obj; }); } async function callTushare(api_name, params, fields) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), TUSHARE_CONFIG.TIMEOUT); try { const body = { api_name, token: TUSHARE_CONFIG.API_TOKEN, params, ...(fields ? { fields } : {}) }; const resp = await fetch(TUSHARE_CONFIG.API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: controller.signal }); if (!resp.ok) throw new Error(`Tushare API请求失败: ${resp.status}`); const data = await resp.json(); if (data.code !== 0) throw new Error(`Tushare API错误: ${data.msg}`); const fieldsArr = data.data?.fields ?? []; const items = data.data?.items ?? []; return mapTushareItemsToObjects(fieldsArr, items); } finally { clearTimeout(timeoutId); } } function summarizePrices(rows) { if (!rows || rows.length === 0) { return { open_at_start: null, low_min: null, low_min_date: null, high_max: null, high_max_date: null, close_at_end: null }; } // 确保按日期升序 const sorted = [...rows].sort((a, b) => String(a.trade_date).localeCompare(String(b.trade_date))); const openAtStart = sorted[0]?.open != null ? Number(sorted[0].open) : null; const closeAtEnd = sorted[sorted.length - 1]?.close != null ? Number(sorted[sorted.length - 1].close) : null; let lowMin = null; let lowMinDate = null; let highMax = null; let highMaxDate = null; for (const r of sorted) { const low = r.low != null ? Number(r.low) : null; const high = r.high != null ? Number(r.high) : null; const date = String(r.trade_date); if (low != null && (lowMin == null || low < lowMin)) { lowMin = low; lowMinDate = date; } if (high != null && (highMax == null || high > highMax)) { highMax = high; highMaxDate = date; } } return { open_at_start: openAtStart, low_min: lowMin, low_min_date: lowMinDate, high_max: highMax, high_max_date: highMaxDate, close_at_end: closeAtEnd }; } function calcReturn(openStart, closeEnd) { if (openStart == null || closeEnd == null || openStart === 0) return null; return (closeEnd - openStart) / openStart; } async function getIndexDaily(ts_code, start, end) { const rows = await callTushare('index_daily', { ts_code, start_date: start, end_date: end }, 'trade_date,open,high,low,close'); return rows; } async function getStockDaily(ts_code, start, end) { const rows = await callTushare('daily', { ts_code, start_date: start, end_date: end }, 'trade_date,open,high,low,close'); return rows; } async function getDailyBasicMapByDate(date) { // 拉取指定交易日全市场基础指标,构建 ts_code -> 估值映射 const rows = await callTushare('daily_basic', { trade_date: date }, 'ts_code,pe_ttm,pb,dv_ratio'); const out = {}; for (const r of rows) { const code = String(r.ts_code || '').trim(); if (!code) continue; const peTtm = r.pe_ttm != null ? Number(r.pe_ttm) : null; const pb = r.pb != null ? Number(r.pb) : null; const dvRatio = r.dv_ratio != null ? Number(r.dv_ratio) : null; // 按百分比数值 out[code] = { pe_ttm: isFinite(Number(peTtm)) ? peTtm : null, pb: isFinite(Number(pb)) ? pb : null, dividend_yield_pct: isFinite(Number(dvRatio)) ? dvRatio : null }; } return out; } async function getNearestDailyBasicMap(end, maxBackDays = 10) { for (let i = 0; i <= maxBackDays; i++) { const d = addDaysYYYYMMDD(end, -i); try { const map = await getDailyBasicMapByDate(d); if (Object.keys(map).length > 0) { return { date: d, map }; } } catch { // 忽略该日错误,继续回退 } } return { date: null, map: {} }; } async function getFinaIndicatorLatest(ts_code, end) { // 限定窗口,避免返回过多数据:向前约 800 天 const start = addDaysYYYYMMDD(end, -800); try { const rows = await callTushare('fina_indicator', { ts_code, start_date: start, end_date: end }, 'ts_code,ann_date,end_date,roe,roa,netprofit_margin,ocfps,debt_to_assets,or_yoy,assets_turn,grossprofit_margin,saleexp_to_gr,adminexp_of_gr,finaexp_of_gr,eps'); if (!rows || rows.length === 0) return null; const sorted = [...rows].sort((a, b) => { const aKey = String(a.ann_date || a.end_date || ''); const bKey = String(b.ann_date || b.end_date || ''); return bKey.localeCompare(aKey); }); const top = sorted[0] || {}; const roe = top.roe != null ? Number(top.roe) : null; // 已为百分比 const roa = top.roa != null ? Number(top.roa) : null; // 已为百分比 const npm = top.netprofit_margin != null ? Number(top.netprofit_margin) : null; // 已为百分比 const ocfps = top.ocfps != null ? Number(top.ocfps) : null; // 元 const dta = top.debt_to_assets != null ? Number(top.debt_to_assets) : null; // 百分比 const orYoy = top.or_yoy != null ? Number(top.or_yoy) : null; // 百分比 const assetsTurn = top.assets_turn != null ? Number(top.assets_turn) : null; // 次 const grossMargin = top.grossprofit_margin != null ? Number(top.grossprofit_margin) : null; // 百分比 const saleToGr = top.saleexp_to_gr != null ? Number(top.saleexp_to_gr) : null; // 百分比 const adminToGr = top.adminexp_of_gr != null ? Number(top.adminexp_of_gr) : null; // 百分比 const finaToGr = top.finaexp_of_gr != null ? Number(top.finaexp_of_gr) : null; // 百分比 const eps = top.eps != null ? Number(top.eps) : null; // 元 const threeArr = [saleToGr, adminToGr, finaToGr].map(v => (v != null && isFinite(Number(v)) ? Number(v) : 0)); const threeExpense = threeArr.reduce((acc, v) => acc + v, 0); return { roe_pct: isFinite(Number(roe)) ? roe : null, roa_pct: isFinite(Number(roa)) ? roa : null, netprofit_margin_pct: isFinite(Number(npm)) ? npm : null, ocfps: isFinite(Number(ocfps)) ? ocfps : null, debt_to_assets_pct: isFinite(Number(dta)) ? dta : null, revenue_yoy_pct: isFinite(Number(orYoy)) ? orYoy : null, assets_turn: isFinite(Number(assetsTurn)) ? assetsTurn : null, grossprofit_margin_pct: isFinite(Number(grossMargin)) ? grossMargin : null, three_expense_ratio_pct: isFinite(Number(threeExpense)) ? threeExpense : null, eps: isFinite(Number(eps)) ? eps : null }; } catch { return null; } } async function getLatestCashDividendPerShare(ts_code, end) { const start = addDaysYYYYMMDD(end, -365 * 5); try { const rows = await callTushare('dividend', { ts_code, start_date: start, end_date: end }, 'ts_code,ann_date,end_date,cash_div,cash_div_tax'); if (!rows || rows.length === 0) return null; const sorted = [...rows].sort((a, b) => String(b.end_date || b.ann_date || '').localeCompare(String(a.end_date || a.ann_date || ''))); for (const r of sorted) { const cashDivPer10 = r.cash_div != null ? Number(r.cash_div) : null; // 每10股派现(元) if (cashDivPer10 != null && isFinite(Number(cashDivPer10)) && cashDivPer10 > 0) { return cashDivPer10 / 10; // 折为每股 } } return null; } catch { return null; } } async function getIndexWeights(index_code, end) { const norm = normalizeIndexCode(index_code); // 回退最多120天找到最近一次权重 for (let i = 0; i <= 120; i++) { const d = addDaysYYYYMMDD(end, -i); const rows = await callTushare('index_weight', { index_code: norm, trade_date: d }, 'index_code,con_code,trade_date,weight'); if (rows && rows.length > 0) { const arr = rows .map(r => ({ ts_code: String(r.con_code || r.ts_code || '').trim(), weight: Number(r.weight ?? 0) })) .filter(c => !!c.ts_code); // 按权重降序 arr.sort((a, b) => b.weight - a.weight); return arr; } } return []; } export const csiIndexConstituents = { name: 'csi_index_constituents', description: '获取中证指数公司(CSI)指数(含行业/主题)的区间行情、成分权重与估值/财务摘要(PE TTM、PB、股息率、ROE、ROA、净利率、每股经营现金流、资产负债率、营收同比、资产周转率、毛利率、三费比率、现金分红率)。', parameters: { type: 'object', properties: { index_code: { type: 'string', description: "指数代码(仅限CSI,含行业/主题)。请使用 .SH/.SZ 形式且能在 Tushare index_weight 查询到权重的代码,例如中证证券公司 '399975.SZ';也支持宽基 '000300.SH'、'000905.SH',以及 'sh000300'、'sz399006' 形式" }, start_date: { type: 'string', description: '开始日期,YYYYMMDD 或 YYYY-MM-DD' }, end_date: { type: 'string', description: '结束日期,YYYYMMDD 或 YYYY-MM-DD' } }, required: ['index_code', 'start_date', 'end_date'] }, async run(args) { try { const normIndex = normalizeIndexCode(args.index_code); const start = parseDateString(args.start_date); const end = parseDateString(args.end_date); if (!TUSHARE_CONFIG.API_TOKEN) { throw new Error('请配置TUSHARE_TOKEN环境变量'); } // 指数行情 const indexDaily = await getIndexDaily(normIndex, start, end); const indexSummary = summarizePrices(indexDaily); const indexRet = calcReturn(indexSummary.open_at_start, indexSummary.close_at_end); // 成分权重(以end为基准回退查找) const weights = await getIndexWeights(normIndex, end); if (weights.length === 0) { return { content: [ { type: 'text', text: `# ${normIndex} 指数区间与成分股摘要\n\n❌ 未能获取到指数成分权重(仅支持中证指数公司CSI),请检查指数代码或日期范围。` } ] }; } // 使用全部成分股(按权重降序) const allConstituents = weights; // 并发拉取全部成分股行情 const stockRows = await Promise.all(allConstituents.map(c => getStockDaily(normalizeIndexCode(c.ts_code), start, end).catch(() => []))); const stockSummaries = stockRows.map(rows => summarizePrices(rows)); // 估值:以结束日向前回退查找最近可用 daily_basic(最多回退10日) const { date: basicDate, map: basicMap } = await getNearestDailyBasicMap(end, 10); // 财务指标:为每只股票获取最近披露的 ROE/ROA/净利率/资产周转率/毛利率/三费比率/EPS(按公告日或报告期倒序取最近) const finaSnapshots = await Promise.all(allConstituents.map(c => getFinaIndicatorLatest(normalizeIndexCode(c.ts_code), end).catch(() => null))); // 现金分红率:获取最近每股分红,结合 EPS 估算 分红率 = 每股分红 / 每股收益 * 100% const latestDpsList = await Promise.all(allConstituents.map(c => getLatestCashDividendPerShare(normalizeIndexCode(c.ts_code), end).catch(() => null))); // 组装输出 const pct = (v) => v == null ? 'N/A' : (v * 100).toFixed(2) + '%'; const num = (v) => v == null ? 'N/A' : String(Number(v.toFixed(4))); let out = `# ${normIndex} 指数区间与成分股摘要\n\n` + `仅支持中证指数公司(CSI)指数。查询区间: ${start} - ${end}\n\n` + `## 指数价格摘要\n` + `- 起始开盘: ${num(indexSummary.open_at_start)}\n` + `- 区间最低: ${num(indexSummary.low_min)} (${indexSummary.low_min_date || 'N/A'})\n` + `- 区间最高: ${num(indexSummary.high_max)} (${indexSummary.high_max_date || 'N/A'})\n` + `- 结束收盘: ${num(indexSummary.close_at_end)}\n` + `- 区间涨跌幅: ${pct(indexRet)}\n\n` + `## 成分股列表(按权重降序)\n`; out += `| 代码 | 权重(%) | 起始开盘 | 区间最低 | 区间最高 | 结束收盘 | 区间涨跌幅 | PE(TTM) | PB | 股息率(%) | ROE(%) | ROA(%) | 净利率(%) | 每股经营现金流 | 资产负债率(%) | 营收同比(%) | 资产周转率 | 毛利率(%) | 三费比率(%) | 现金分红率(%) |\n`; out += `|-----|---------|-----------|-----------|-----------|-----------|-----------|---------|----|-----------|--------|--------|-----------|--------------|--------------|-----------|------------|-----------|------------|--------------|\n`; allConstituents.forEach((c, i) => { const s = stockSummaries[i]; const r = calcReturn(s.open_at_start, s.close_at_end); const code = normalizeIndexCode(c.ts_code); const val = basicMap[code] || basicMap[c.ts_code] || null; const fmt = (v, digits = 4) => v == null ? 'N/A' : String(Number(v.toFixed(digits))); const f = finaSnapshots[i]; const fmtPct = (v, digits = 2) => v == null ? 'N/A' : String(Number(v.toFixed(digits))); const dps = latestDpsList[i]; const payoutPct = (dps != null && f?.eps != null && f.eps !== 0) ? Number(((dps / f.eps) * 100).toFixed(2)) : null; out += `| ${code} | ${num(c.weight)} | ${num(s.open_at_start)} | ${num(s.low_min)} | ${num(s.high_max)} | ${num(s.close_at_end)} | ${pct(r)} | ${fmt(val?.pe_ttm)} | ${fmt(val?.pb)} | ${val?.dividend_yield_pct == null ? 'N/A' : Number(val.dividend_yield_pct.toFixed(2))} | ${fmtPct(f?.roe_pct)} | ${fmtPct(f?.roa_pct)} | ${fmtPct(f?.netprofit_margin_pct)} | ${fmt(f?.ocfps)} | ${fmtPct(f?.debt_to_assets_pct)} | ${fmtPct(f?.revenue_yoy_pct)} | ${fmt(f?.assets_turn, 3)} | ${fmtPct(f?.grossprofit_margin_pct)} | ${fmtPct(f?.three_expense_ratio_pct)} | ${payoutPct == null ? 'N/A' : payoutPct} |\n`; }); // 收集所有成分股代码并生成说明 const stockCodes = allConstituents.map(c => normalizeIndexCode(c.ts_code)); const stockExplanation = await resolveStockCodes(stockCodes); return { content: [{ type: 'text', text: out + stockExplanation }] }; } catch (error) { return { content: [{ type: 'text', text: `❌ CSI指数成分摘要查询失败: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } };

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