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);
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden of behavioral disclosure. The description mentions the tool '支持' (supports) specific frequencies and requires start/end datetime, but doesn't disclose critical behavioral traits like whether this is a read-only operation, potential rate limits, authentication needs, data freshness, error handling, or what the output looks like. For a data retrieval tool with no annotations, this leaves significant gaps.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise and front-loaded, stating the core purpose in the first phrase. The single sentence efficiently covers markets, frequencies, and time range requirement. However, it could be slightly more structured by separating market support from frequency options for clarity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity (5 required parameters, no annotations, no output schema), the description is incomplete. It adequately states what the tool does but lacks crucial context: no output format information, no behavioral details (rate limits, errors), no differentiation from siblings, and minimal parameter guidance beyond schema repetition. For a data retrieval tool with multiple parameters, this leaves the agent under-informed.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description adds minimal value beyond the input schema. It mentions the supported frequencies (1MIN/5MIN/15MIN/30MIN/60MIN) and that time range is required, but the schema already has 100% coverage with detailed descriptions for all 5 parameters. The description doesn't provide additional context like parameter interactions, constraints, or examples beyond what's in the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: '获取分钟K线数据' (get minute K-line data) for A股/加密 (A-shares/crypto). It specifies the resource (K-line data) and scope (minute-level, specific markets). However, it doesn't explicitly distinguish this from sibling tools like 'stock_data' or 'index_data', which might also provide stock-related data.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It mentions supported markets (A股/加密) and frequency options, but doesn't explain when this tool is appropriate compared to other stock data tools in the sibling list, such as 'stock_data' (which might provide different granularity) or 'index_data' (which might provide index-level data).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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