Skip to main content
Glama
Xxx00xxX33

FinanceMCP

by Xxx00xxX33

stock_data_minutes

Retrieve minute-level K-line data for A-shares and cryptocurrencies with configurable intervals from 1 to 60 minutes, enabling detailed technical analysis and market monitoring.

Instructions

获取分钟K线数据:A股/加密。支持1MIN/5MIN/15MIN/30MIN/60MIN,时间范围需提供起止日期时间

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
codeYes股票代码,如 '600519.SH' 或 '000001.SZ'
market_typeYes市场类型:'cn'(A股,Tushare)、'crypto'(加密币对,Binance)
start_datetimeYes起始日期时间,支持 'YYYYMMDDHHmmss' 或 'YYYY-MM-DD HH:mm:ss'
end_datetimeYes结束日期时间,支持 'YYYYMMDDHHmmss' 或 'YYYY-MM-DD HH:mm:ss'
freqYes分钟周期:1MIN/5MIN/15MIN/30MIN/60MIN(不区分大小写)

Implementation Reference

  • Core execution logic for stock_data_minutes tool. Supports A-share minute K-lines via Tushare (cn) and crypto via Binance (crypto). Validates/normalizes inputs, calls APIs (with pagination for Binance), processes data into descending-time Markdown tables, handles errors.
    async run(args: { code: string; market_type: string; start_datetime: string; end_datetime: string; freq: string }) {
      try {
        const TUSHARE_API_KEY = TUSHARE_CONFIG.API_TOKEN;
        const TUSHARE_API_URL = TUSHARE_CONFIG.API_URL;
    
        // 归一化时间:转为 YYYYMMDDHHmmss(剔除非数字)
        const normalizeDT = (v: string) => v.replace(/[^0-9]/g, '').padEnd(14, '0').slice(0, 14);
        const startTime = normalizeDT(args.start_datetime);
        const endTime = normalizeDT(args.end_datetime);
        if (startTime.length !== 14 || endTime.length !== 14) {
          throw new Error('起止时间格式不正确,请使用 YYYYMMDDHHmmss 或 YYYY-MM-DD HH:mm:ss');
        }
        if (endTime <= startTime) {
          throw new Error('结束时间必须大于起始时间');
        }
    
        // 归一化频率
        const rawFreq = String(args.freq || '').trim().toLowerCase();
        const freqMap: Record<string, string> = {
          '1min': '1min', '1m': '1min',
          '5min': '5min', '5m': '5min',
          '15min': '15min', '15m': '15min',
          '30min': '30min', '30m': '30min',
          '60min': '60min', '60m': '60min', '1h': '60min'
        };
        // 兼容 1MIN/5MIN 等写法(统一转小写再映射)
        const normalizedFreq = freqMap[rawFreq] || freqMap[rawFreq.replace('min', 'min')] || freqMap[rawFreq.replace('minute', 'min')] || rawFreq;
        const freq = normalizedFreq;
        const allowed = new Set(['1min', '5min', '15min', '30min', '60min']);
        if (!allowed.has(freq)) {
          throw new Error(`不支持的频率:${args.freq},允许值:1MIN/5MIN/15MIN/30MIN/60MIN`);
        }
    
        // 市场分支
        const market = String(args.market_type || '').trim().toLowerCase();
        if (!['cn', 'crypto'].includes(market)) {
          throw new Error(`不支持的 market_type: ${args.market_type},仅支持 'cn' 或 'crypto'`);
        }
    
        if (market === 'cn') {
          if (!TUSHARE_API_KEY) {
            throw new Error('缺少 Tushare Token:请在请求头 X-Tushare-Token 或环境变量 TUSHARE_TOKEN 中提供');
          }
          // 组装请求(Tushare 分钟线:stk_mins)
          const requestBody: any = {
            api_name: 'stk_mins',
            token: TUSHARE_API_KEY,
            params: {
              ts_code: args.code,
              start_time: startTime,
              end_time: endTime,
              freq: freq
            }
            // 不指定 fields,默认返回全部
          };
    
          // 超时控制
          const controller = new AbortController();
          const timeoutId = setTimeout(() => controller.abort(), TUSHARE_CONFIG.TIMEOUT);
    
          try {
            const resp = await fetch(TUSHARE_API_URL, {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify(requestBody),
              signal: controller.signal
            });
    
            clearTimeout(timeoutId);
    
            if (!resp.ok) {
              throw new Error(`Tushare HTTP ${resp.status}`);
            }
            const data = await resp.json();
            if (data.code !== 0) {
              throw new Error(`Tushare API错误: ${data.msg || 'unknown error'}`);
            }
    
            const fields: string[] = data.data?.fields || [];
            const items: any[] = data.data?.items || [];
            if (!fields.length || !items.length) {
              return {
                content: [{
                  type: 'text',
                  text: `# ${args.code} 分钟K线(${args.freq})\n\n区间: ${startTime} - ${endTime}\n\n暂无数据。`
                }]
              };
            }
    
            // 转对象数组
            const rows = items.map(row => {
              const obj: Record<string, any> = {};
              fields.forEach((f: string, i: number) => (obj[f] = row[i]));
              return obj;
            });
    
            // 常见字段名兼容:trade_time/time/datetime
            const timeKey = ['trade_time', 'time', 'datetime'].find(k => fields.includes(k)) || 'trade_time';
            const openKey = ['open'].find(k => fields.includes(k)) || 'open';
            const highKey = ['high'].find(k => fields.includes(k)) || 'high';
            const lowKey = ['low'].find(k => fields.includes(k)) || 'low';
            const closeKey = ['close'].find(k => fields.includes(k)) || 'close';
            const volKey = ['vol', 'volume'].find(k => fields.includes(k)) || 'vol';
            const amountKey = ['amount', 'amt'].find(k => fields.includes(k)) || 'amount';
    
            // 按时间倒序(最新在上)
            rows.sort((a, b) => String(b[timeKey] || '').localeCompare(String(a[timeKey] || '')));
    
            // 构造表格
            let out = `# ${args.code} 分钟K线(${freq})\n\n`;
            out += `查询区间: ${startTime} - ${endTime}\n`;
            out += `返回条数: ${rows.length}\n\n`;
            const headers = ['时间', '开盘', '最高', '最低', '收盘', '成交量', '成交额(万元)'];
            out += `| ${headers.join(' | ')} |\n`;
            out += `|${headers.map(() => '--------').join('|')}|\n`;
            for (const r of rows) {
              const t = r[timeKey] ?? 'N/A';
              const o = safeNum(r[openKey]);
              const h = safeNum(r[highKey]);
              const l = safeNum(r[lowKey]);
              const c = safeNum(r[closeKey]);
              const v = r[volKey] == null ? 'N/A' : String(r[volKey]);
              const amt = r[amountKey] == null ? 'N/A' : String(r[amountKey]);
              out += `| ${t} | ${fmt(o)} | ${fmt(h)} | ${fmt(l)} | ${fmt(c)} | ${v} | ${amt} |\n`;
            }
    
            // 收集股票代码并生成说明(仅对A股)
            let stockExplanation = '';
            if (market === 'cn') {
              stockExplanation = await resolveStockCodes([args.code]);
            }
            
            return { content: [{ type: 'text', text: out + stockExplanation }] };
          } finally {
            // 兜底清理
            // clearTimeout 在 try 内处理;此处确保未走到前面时也释放
          }
        }
    
        // ===== crypto 分支(Binance 分钟线)=====
        // 频率映射到 Binance interval
        const binanceIntervalMap: Record<string, string> = {
          '1min': '1m',
          '5min': '5m',
          '15min': '15m',
          '30min': '30m',
          '60min': '1h'
        };
        const interval = binanceIntervalMap[freq];
        if (!interval) {
          throw new Error(`不支持的频率:${args.freq}(Binance)`);
        }
    
        // 时间转换为毫秒(UTC)
        const toMs = (s: string): number => {
          const y = parseInt(s.slice(0, 4));
          const m = parseInt(s.slice(4, 6));
          const d = parseInt(s.slice(6, 8));
          const hh = parseInt(s.slice(8, 10));
          const mm = parseInt(s.slice(10, 12));
          const ss = parseInt(s.slice(12, 14));
          return Date.UTC(y, m - 1, d, hh, mm, ss, 0);
        };
        const startMs = toMs(startTime);
        const endMs = toMs(endTime);
    
        // 交易对解析(兼容 BTCUSDT / BTC-USDT / BTC/USDT / coinid.USDT),USD 自动映射到 USDT
        const idToTicker: Record<string, string> = {
          'bitcoin': 'BTC', 'ethereum': 'ETH', 'tether': 'USDT', 'usd-coin': 'USDC', 'solana': 'SOL',
          'binancecoin': 'BNB', 'ripple': 'XRP', 'cardano': 'ADA', 'polkadot': 'DOT', 'chainlink': 'LINK',
          'litecoin': 'LTC', 'shiba-inu': 'SHIB', 'tron': 'TRX', 'toncoin': 'TON', 'bitcoin-cash': 'BCH',
          'ethereum-classic': 'ETC'
        };
        const parseBinanceSymbol = (raw: string): string => {
          const trimmed = String(raw || '').trim();
          const upper = trimmed.toUpperCase();
          const validQuotes = new Set(['USDT','USDC','FDUSD','TUSD','BUSD','BTC','ETH']);
          if (!upper.includes('-') && !upper.includes('/') && !upper.includes('.')) {
            return upper;
          }
          let base = '';
          let quote = '';
          if (upper.includes('-') || upper.includes('/')) {
            const sep = upper.includes('-') ? '-' : '/';
            const [b, q] = upper.split(sep);
            base = b;
            quote = q;
          } else if (upper.includes('.')) {
            const [id, vs] = upper.split('.');
            base = idToTicker[id.toLowerCase()] || id;
            quote = vs;
          }
          if (quote === 'USD') quote = 'USDT';
          if (!validQuotes.has(quote)) {
            throw new Error(`不支持的报价资产: ${quote}。支持: ${Array.from(validQuotes).join(', ')}`);
          }
          return `${base}${quote}`;
        };
        const symbol = parseBinanceSymbol(args.code);
    
        // 分页请求
        const allKlines: any[] = [];
        let cursor = startMs;
        let pageIndex = 0;
        const maxPages = 500;
        while (cursor < endMs && pageIndex < maxPages) {
          const url = `https://api.binance.com/api/v3/klines?symbol=${encodeURIComponent(symbol)}&interval=${encodeURIComponent(interval)}&startTime=${cursor}&endTime=${endMs}&limit=1000`;
          const resp = await fetch(url);
          if (!resp.ok) {
            try {
              const ct = resp.headers.get('content-type') || '';
              if (ct.includes('application/json')) {
                const err = await resp.json();
                const msg = err?.msg || `HTTP ${resp.status}`;
                if (Number(err?.code) === -1121 || /invalid symbol/i.test(String(msg))) {
                  throw new Error(`Binance 无效交易对: ${symbol}。该币对在 Binance 不存在或已下线,请更换有效币对(例如:BTCUSDT、ETHUSDT、SOLUSDT)。也支持 BTC-USDT、BTC/USDT、或 coinid.USDT 写法。`);
                }
                throw new Error(`Binance K线请求失败: ${resp.status} - ${msg}`);
              } else {
                const text = await resp.text();
                throw new Error(`Binance K线请求失败: ${resp.status}${text ? ` - ${text}` : ''}`);
              }
            } catch (e) {
              if (e instanceof Error) throw e;
              throw new Error(`Binance K线请求失败: ${resp.status}`);
            }
          }
          const kl = await resp.json();
          if (!Array.isArray(kl)) throw new Error('Binance 返回的 K 线数据格式异常');
          if (kl.length === 0) break;
          allKlines.push(...kl);
          const lastOpenTime = Number(kl[kl.length - 1][0]);
          if (!(lastOpenTime > cursor)) break;
          cursor = lastOpenTime + 1;
          pageIndex += 1;
          if (kl.length < 1000) break;
        }
    
        // 转换为统一对象
        const toYMDHMS = (ms: number): string => {
          const d = new Date(ms);
          const y = d.getUTCFullYear();
          const m = String(d.getUTCMonth() + 1).padStart(2, '0');
          const dd = String(d.getUTCDate()).padStart(2, '0');
          const hh = String(d.getUTCHours()).padStart(2, '0');
          const mm = String(d.getUTCMinutes()).padStart(2, '0');
          const ss = String(d.getUTCSeconds()).padStart(2, '0');
          return `${y}-${m}-${dd} ${hh}:${mm}:${ss}`;
        };
        const rows = allKlines.map(row => {
          const openTime = Number(row[0]);
          return {
            trade_time: toYMDHMS(openTime),
            open: Number(row[1]),
            high: Number(row[2]),
            low: Number(row[3]),
            close: Number(row[4]),
            vol: Number(row[5])
          } as Record<string, any>;
        });
    
        // 严格过滤在用户区间内(Binance 已按毫秒过滤,此处再按显示字段兜底)
        const filtered = rows.filter(r => true);
        filtered.sort((a, b) => String(b.trade_time).localeCompare(String(a.trade_time)));
    
        // 输出
        let out = `# ${args.code} 分钟K线(${freq},Binance)\n\n`;
        out += `查询区间: ${startTime} - ${endTime}\n`;
        out += `返回条数: ${filtered.length}\n\n`;
        const headers = ['时间', '开盘', '最高', '最低', '收盘', '成交量'];
        out += `| ${headers.join(' | ')} |\n`;
        out += `|${headers.map(() => '--------').join('|')}|\n`;
        for (const r of filtered) {
          out += `| ${r.trade_time} | ${fmt(safeNum(r.open))} | ${fmt(safeNum(r.high))} | ${fmt(safeNum(r.low))} | ${fmt(safeNum(r.close))} | ${r.vol == null ? 'N/A' : String(r.vol)} |\n`;
        }
    
        return { content: [{ type: 'text', text: out }] };
      } catch (err) {
        return {
          content: [{
            type: 'text',
            text: `获取分钟K线失败:${err instanceof Error ? err.message : String(err)}`
          }],
          isError: true
        };
      }
    }
  • Input schema definition for the stock_data_minutes tool, specifying required parameters with descriptions.
    parameters: {
      type: 'object',
      properties: {
        code: {
          type: 'string',
          description: "股票代码,如 '600519.SH' 或 '000001.SZ'"
        },
        market_type: {
          type: 'string',
          description: "市场类型:'cn'(A股,Tushare)、'crypto'(加密币对,Binance)"
        },
        start_datetime: {
          type: 'string',
          description: "起始日期时间,支持 'YYYYMMDDHHmmss' 或 'YYYY-MM-DD HH:mm:ss'"
        },
        end_datetime: {
          type: 'string',
          description: "结束日期时间,支持 'YYYYMMDDHHmmss' 或 'YYYY-MM-DD HH:mm:ss'"
        },
        freq: {
          type: 'string',
          description: "分钟周期:1MIN/5MIN/15MIN/30MIN/60MIN(不区分大小写)"
        }
      },
      required: ['code', 'market_type', 'start_datetime', 'end_datetime', 'freq']
    },
  • src/index.ts:288-295 (registration)
    Tool registration in MCP stdio server (src/index.ts): listed in tools/list (lines 184-188) and dispatched in tools/call switch.
    case "stock_data_minutes": {
      const code = String(request.params.arguments?.code);
      const market_type = String(request.params.arguments?.market_type);
      const start_datetime = String(request.params.arguments?.start_datetime);
      const end_datetime = String(request.params.arguments?.end_datetime);
      const freq = String(request.params.arguments?.freq);
      return normalizeResult(await stockDataMinutes.run({ code, market_type, start_datetime, end_datetime, freq }));
    }
  • Tool registration in HTTP MCP server (src/httpServer.ts): listed in toolList (line 119) and dispatched in /mcp POST tools/call.
    case 'stock_data_minutes':
      return await stockDataMinutes.run({
        code: String(args?.code),
        market_type: String(args?.market_type),
        start_datetime: String(args?.start_datetime),
        end_datetime: String(args?.end_datetime),
        freq: String(args?.freq)
      });
  • Helper functions safeNum (safe number conversion) and fmt (format number to fixed decimals or 'N/A') used in data processing and table output.
    function safeNum(v: any): number | null {
      const n = Number(v);
      return Number.isFinite(n) ? n : null;
    }
    function fmt(n: number | null, d = 2): string {
      return n == null ? 'N/A' : n.toFixed(d);
    }

Latest Blog Posts

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/Xxx00xxX33/FinanceMCP'

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