Skip to main content
Glama
Xxx00xxX33
by Xxx00xxX33

stock_data

Retrieve historical market data for stocks, cryptocurrencies, and other assets across multiple markets including A-shares, US, HK, and crypto, with support for technical indicators analysis.

Instructions

获取指定股票/加密资产的历史行情数据,支持A股、美股、港股、外汇、期货、基金、债券逆回购、可转债、期权、加密货币(通过CoinGecko)

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
codeYes股票/合约/加密资产代码。股票示例:'000001.SZ'(A股平安银行)、'AAPL'(美股)、'00700.HK'(港股)、'USDCNH.FXCM'(外汇)、'CU2501.SHF'(期货)、'159919.SZ'(基金)、'204001.SH'(逆回购)、'113008.SH'(可转债)、'10001313.SH'(期权)。加密示例(需 market_type=crypto,Binance):推荐标准写法 'BTCUSDT'、'ETHUSDT'、'USDCUSDT'、'FDUSDUSDT' 等;也兼容 'BTC-USDT' 或 'BTC/USDT'。常见报价币:USDT、USDC、FDUSD、TUSD、BUSD、BTC、ETH。注意:若写 'USD' 会自动映射为 'USDT'(如 'BTC-USD' → 'BTCUSDT')。
market_typeYes市场类型(必需),可选值:cn(A股),us(美股),hk(港股),fx(外汇),futures(期货),fund(基金),repo(债券逆回购),convertible_bond(可转债),options(期权),crypto(加密货币/CoinGecko)
start_dateNo起始日期,格式为YYYYMMDD,如'20230101'
end_dateNo结束日期,格式为YYYYMMDD,如'20230131'
indicatorsNo需要计算的技术指标,多个指标用空格分隔。支持的指标:macd(MACD指标)、rsi(相对强弱指标)、kdj(随机指标)、boll(布林带)、ma(均线指标)。必须明确指定参数,例如:'macd(12,26,9) rsi(14) kdj(9,3,3) boll(20,2) ma(10)'

Implementation Reference

  • The core handler for the 'stock_data' tool. Defines the tool name, description, input schema (parameters), and the async run() method which fetches historical market data for stocks/crypto from Tushare API or Binance, computes technical indicators if requested, applies forward adjustment for CN stocks, and formats output as markdown tables with explanations.
    export const stockData = {
      name: "stock_data",
      description: "获取指定股票/加密资产的历史行情数据,支持A股、美股、港股、外汇、期货、基金、债券逆回购、可转债、期权、加密货币(通过CoinGecko)",
      parameters: {
        type: "object",
        properties: {
          code: {
            type: "string",
            description: "股票/合约/加密资产代码。股票示例:'000001.SZ'(A股平安银行)、'AAPL'(美股)、'00700.HK'(港股)、'USDCNH.FXCM'(外汇)、'CU2501.SHF'(期货)、'159919.SZ'(基金)、'204001.SH'(逆回购)、'113008.SH'(可转债)、'10001313.SH'(期权)。加密示例(需 market_type=crypto,Binance):推荐标准写法 'BTCUSDT'、'ETHUSDT'、'USDCUSDT'、'FDUSDUSDT' 等;也兼容 'BTC-USDT' 或 'BTC/USDT'。常见报价币:USDT、USDC、FDUSD、TUSD、BUSD、BTC、ETH。注意:若写 'USD' 会自动映射为 'USDT'(如 'BTC-USD' → 'BTCUSDT')。"
          },
          market_type: {
            type: "string",
            description: "市场类型(必需),可选值:cn(A股),us(美股),hk(港股),fx(外汇),futures(期货),fund(基金),repo(债券逆回购),convertible_bond(可转债),options(期权),crypto(加密货币/CoinGecko)"
          },
          start_date: {
            type: "string",
            description: "起始日期,格式为YYYYMMDD,如'20230101'"
          },
          end_date: {
            type: "string",
            description: "结束日期,格式为YYYYMMDD,如'20230131'"
          },
          indicators: {
            type: "string",
            description: "需要计算的技术指标,多个指标用空格分隔。支持的指标:macd(MACD指标)、rsi(相对强弱指标)、kdj(随机指标)、boll(布林带)、ma(均线指标)。必须明确指定参数,例如:'macd(12,26,9) rsi(14) kdj(9,3,3) boll(20,2) ma(10)'"
          }
        },
        required: ["code", "market_type"]
      },
      async run(args: { code: string; market_type: string; start_date?: string; end_date?: string; indicators?: string }) {
        try {
          // 添加调试日志
          console.log('接收到的参数:', args);
          
          // 检查market_type参数
          if (!args.market_type) {
            throw new Error('请指定market_type参数:cn(A股)、us(美股)、hk(港股)、fx(外汇)、futures(期货)、fund(基金)、repo(债券逆回购)、convertible_bond(可转债)、options(期权)');
          }
          
          const marketType = args.market_type.trim().toLowerCase();
          console.log(`使用的市场类型: ${marketType}`);
          console.log(`使用Tushare API获取${marketType}市场股票${args.code}的行情数据`);
          
          // 解析技术指标参数
          const requestedIndicators = args.indicators ? args.indicators.trim().split(/\s+/) : [];
          console.log('请求的技术指标:', requestedIndicators);
          
          // 使用全局配置中的Tushare API设置
          const TUSHARE_API_KEY = TUSHARE_CONFIG.API_TOKEN;
          const TUSHARE_API_URL = TUSHARE_CONFIG.API_URL;
          
          // 默认参数设置
          const today = new Date();
          const defaultEndDate = today.toISOString().slice(0, 10).replace(/-/g, '');
          
          const oneMonthAgo = new Date();
          oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
          const defaultStartDate = oneMonthAgo.toISOString().slice(0, 10).replace(/-/g, '');
          
          // 用户请求的时间范围
          const userStartDate = args.start_date || defaultStartDate;
          const userEndDate = args.end_date || defaultEndDate;
          
          // 如果有技术指标请求,计算需要的历史数据并扩展获取范围
          let actualStartDate = userStartDate;
          let actualEndDate = userEndDate;
          
          if (requestedIndicators.length > 0) {
            const requiredDays = calculateRequiredDays(requestedIndicators);
            actualStartDate = calculateExtendedStartDate(userStartDate, requiredDays);
            console.log(`技术指标需要${requiredDays}天历史数据,扩展开始日期从 ${userStartDate} 到 ${actualStartDate}`);
          }
    
          // 验证市场类型
          const validMarkets = ['cn', 'us', 'hk', 'fx', 'futures', 'fund', 'repo', 'convertible_bond', 'options', 'crypto'];
          if (!validMarkets.includes(marketType)) {
            throw new Error(`不支持的市场类型: ${marketType}。支持的类型有: ${validMarkets.join(', ')}`);
          }
          
          // 加密货币市场(Binance)分支:
          if (marketType === 'crypto') {
            console.log(`使用 Binance 获取加密资产 ${args.code} 的逐日K线 (OHLCV)`);
    
            // 日期与符号解析
            const toYMD = (d: Date): string => {
              const y = d.getUTCFullYear();
              const m = String(d.getUTCMonth() + 1).padStart(2, '0');
              const day = String(d.getUTCDate()).padStart(2, '0');
              return `${y}${m}${day}`;
            };
            const ymdToStartMs = (s: string): number => Date.UTC(parseInt(s.slice(0,4)), parseInt(s.slice(4,6)) - 1, parseInt(s.slice(6,8)), 0, 0, 0, 0);
            const ymdToEndMs = (s: string): number => Date.UTC(parseInt(s.slice(0,4)), parseInt(s.slice(4,6)) - 1, parseInt(s.slice(6,8)), 23, 59, 59, 999);
    
            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 = raw.trim();
              const upper = trimmed.toUpperCase();
              const validQuotes = new Set(['USDT','USDC','FDUSD','TUSD','BUSD','BTC','ETH']);
              // 已经是诸如 BTCUSDT
              if (!upper.includes('-') && !upper.includes('/') && !upper.includes('.')) {
                return upper;
              }
              // 支持 TICKER-QUOTE / TICKER/QUOTE / id.vs
              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; // id -> ticker
                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 startYmd = requestedIndicators.length > 0 ? actualStartDate : userStartDate;
            let startMs = ymdToStartMs(startYmd);
            const endMs = ymdToEndMs(userEndDate);
            const allKlines: any[] = [];
            let pageIndex = 0;
            const maxPages = 100; // 安全上限,防止极端情况下的无限循环
            while (startMs < endMs && pageIndex < maxPages) {
              const url = `https://api.binance.com/api/v3/klines?symbol=${encodeURIComponent(symbol)}&interval=1d&startTime=${startMs}&endTime=${endMs}&limit=1000`;
              console.log(`Binance Klines URL[${pageIndex + 1}]:`, url);
    
              const resp = await fetch(url);
              if (!resp.ok) {
                try {
                  const contentType = resp.headers.get('content-type') || '';
                  if (contentType.includes('application/json')) {
                    const errJson: any = await resp.json();
                    const errMsg = errJson?.msg || `HTTP ${resp.status}`;
                    if (Number(errJson?.code) === -1121 || /invalid symbol/i.test(String(errMsg))) {
                      throw new Error(`Binance 无效交易对: ${symbol}。该币对在 Binance 不存在或已下线,请更换有效币对(例如:BTCUSDT、ETHUSDT、SOLUSDT)。也支持 BTC-USDT、BTC/USDT、或 coinid.USDT 写法。`);
                    }
                    throw new Error(`Binance K线请求失败: ${resp.status} - ${errMsg}`);
                  } 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 klines: any[] = await resp.json();
              if (!Array.isArray(klines)) throw new Error('Binance 返回的 K 线数据格式异常');
              if (klines.length === 0) break;
              allKlines.push(...klines);
              const lastOpenTime = Number(klines[klines.length - 1][0]);
              if (!(lastOpenTime > startMs)) break; // 保护,避免相同时间导致死循环
              startMs = lastOpenTime + 1; // 下一页从最后一根K线的下一毫秒开始
              pageIndex += 1;
              if (klines.length < 1000) break; // 已取完
            }
    
            let stockData = allKlines.map(row => {
              const openTime = Number(row[0]);
              const d = toYMD(new Date(openTime));
              return {
                trade_date: d,
                open: Number(row[1]),
                high: Number(row[2]),
                low: Number(row[3]),
                close: Number(row[4]),
                vol: Number(row[5])
              } as Record<string, any>;
            });
            // 若未请求技术指标,才在此处严格按用户区间过滤;
            // 若请求了技术指标,需保留扩展区间用于计算,稍后再截回用户区间
            if (requestedIndicators.length === 0) {
              stockData = stockData.filter(r => r.trade_date >= userStartDate && r.trade_date <= userEndDate);
            }
            stockData.sort((a, b) => b.trade_date.localeCompare(a.trade_date));
            console.log(`Binance 分页返回 共${allKlines.length} 根K线,过滤后 ${stockData.length} 条记录`);
            
            // 计算技术指标
            let indicators: Record<string, any> = {};
            if (requestedIndicators.length > 0) {
              let closes: number[] = stockData.map(d => parseFloat(d.close)).reverse();
              let highs: number[] = stockData.map(d => parseFloat(d.high)).reverse();
              let lows: number[] = stockData.map(d => parseFloat(d.low)).reverse();
              
              for (const indicator of requestedIndicators) {
                try {
                  const { name, params } = parseIndicatorParams(indicator);
                  switch (name) {
                    case 'macd':
                      if (params.length !== 3) throw new Error('MACD指标需要3个参数,格式:macd(快线,慢线,信号线)');
                      indicators.macd = calculateMACD(closes, params[0], params[1], params[2]);
                      break;
                    case 'rsi':
                      if (params.length !== 1) throw new Error('RSI指标需要1个参数,格式:rsi(周期)');
                      indicators.rsi = calculateRSI(closes, params[0]);
                      break;
                    case 'kdj':
                      if (params.length !== 3) throw new Error('KDJ指标需要3个参数,格式:kdj(9,3,3)');
                      indicators.kdj = calculateKDJ(highs, lows, closes, params[0], params[1], params[2]);
                      break;
                    case 'boll':
                      if (params.length !== 2) throw new Error('布林带指标需要2个参数,格式:boll(周期,标准差倍数)');
                      indicators.boll = calculateBOLL(closes, params[0], params[1]);
                      break;
                    case 'ma':
                      if (params.length !== 1) throw new Error('移动平均线需要1个参数,格式:ma(周期)');
                      indicators[`ma${params[0]}`] = calculateSMA(closes, params[0]);
                      break;
                    default:
                      throw new Error(`不支持的技术指标: ${name}`);
                  }
                } catch (e) {
                  console.error(`解析技术指标 ${indicator} 时出错:`, e);
                  throw new Error(`技术指标参数错误: ${indicator}`);
                }
              }
              // 指标逆序以匹配最新在前
              Object.keys(indicators).forEach(key => {
                if (typeof indicators[key] === 'object' && indicators[key] !== null) {
                  if (Array.isArray(indicators[key])) {
                    indicators[key] = indicators[key].reverse();
                  } else {
                    Object.keys(indicators[key]).forEach(subKey => {
                      if (Array.isArray(indicators[key][subKey])) {
                        indicators[key][subKey] = indicators[key][subKey].reverse();
                      }
                    });
                  }
                }
              });
              // 过滤到用户指定区间
              stockData = filterDataToUserRange(stockData, userStartDate, userEndDate);
              console.log(`过滤到用户请求时间范围,剩余${stockData.length}条记录`);
            }
            
            // 表格输出(走默认分支样式)
            const marketTitleMap: Record<string, string> = {
              'crypto': '加密货币'
            };
            const fieldNameMap: Record<string, string> = {
              'trade_date': '交易日期',
              'open': '开盘',
              'close': '收盘',
              'high': '最高', 
              'low': '最低',
              'vol': '成交量'
            };
            let formattedData = '';
            let indicatorData = '';
            if (stockData.length > 0) {
              const coreFields = ['trade_date', 'open', 'close', 'high', 'low', 'vol'];
              const availableFields = Object.keys(stockData[0]);
              const displayFields = coreFields.filter(field => availableFields.includes(field));
              const indicatorHeaders: string[] = [];
              const hasIndicators = Object.keys(indicators).length > 0;
              if (hasIndicators) {
                if (indicators.macd) indicatorHeaders.push('MACD_DIF', 'MACD_DEA', 'MACD');
                if (indicators.rsi) indicatorHeaders.push('RSI');
                if (indicators.kdj) indicatorHeaders.push('KDJ_K', 'KDJ_D', 'KDJ_J');
                if (indicators.boll) indicatorHeaders.push('BOLL_UP', 'BOLL_MID', 'BOLL_LOW');
                const maIndicators = Object.keys(indicators).filter(key => key.startsWith('ma') && key !== 'macd');
                maIndicators.forEach(ma => indicatorHeaders.push(ma.toUpperCase()));
              }
              const allHeaders = [
                ...displayFields.map(field => fieldNameMap[field] || field),
                ...indicatorHeaders
              ];
              formattedData = `| ${allHeaders.join(' | ')} |\n`;
              formattedData += `|${allHeaders.map(() => '--------').join('|')}|\n`;
              stockData.forEach((data: Record<string, any>, index: number) => {
                const basicRow = displayFields.map(field => data[field] ?? 'N/A');
                const indicatorRow: string[] = [];
                if (hasIndicators) {
                  if (indicators.macd) {
                    indicatorRow.push(
                      isNaN(indicators.macd.dif[index]) ? 'N/A' : indicators.macd.dif[index].toFixed(4),
                      isNaN(indicators.macd.dea[index]) ? 'N/A' : indicators.macd.dea[index].toFixed(4),
                      isNaN(indicators.macd.macd[index]) ? 'N/A' : indicators.macd.macd[index].toFixed(4)
                    );
                  }
                  if (indicators.rsi) indicatorRow.push(isNaN(indicators.rsi[index]) ? 'N/A' : indicators.rsi[index].toFixed(2));
                  if (indicators.kdj) indicatorRow.push(
                    isNaN(indicators.kdj.k[index]) ? 'N/A' : indicators.kdj.k[index].toFixed(2),
                    isNaN(indicators.kdj.d[index]) ? 'N/A' : indicators.kdj.d[index].toFixed(2),
                    isNaN(indicators.kdj.j[index]) ? 'N/A' : indicators.kdj.j[index].toFixed(2)
                  );
                  if (indicators.boll) indicatorRow.push(
                    isNaN(indicators.boll.upper[index]) ? 'N/A' : indicators.boll.upper[index].toFixed(2),
                    isNaN(indicators.boll.middle[index]) ? 'N/A' : indicators.boll.middle[index].toFixed(2),
                    isNaN(indicators.boll.lower[index]) ? 'N/A' : indicators.boll.lower[index].toFixed(2)
                  );
                  const maIndicators = Object.keys(indicators).filter(key => key.startsWith('ma') && key !== 'macd');
                  maIndicators.forEach(ma => {
                    indicatorRow.push(isNaN(indicators[ma][index]) ? 'N/A' : indicators[ma][index].toFixed(2));
                  });
                }
                const fullRow = [...basicRow, ...indicatorRow];
                formattedData += `| ${fullRow.join(' | ')} |\n`;
              });
            }
            if (Object.keys(indicators).length > 0) {
              indicatorData = `\n\n## 📊 技术指标说明\n`;
              const indicatorParams: Record<string, string> = {};
              for (const indicator of requestedIndicators) {
                try {
                  const { name, params } = parseIndicatorParams(indicator);
                  indicatorParams[name] = formatIndicatorParams(name, params);
                } catch {}
              }
              if (indicators.macd) indicatorData += `- **MACD${indicatorParams.macd || '(参数未知)'}**: DIF(快线)、DEA(慢线)、MACD(柱状图)\n`;
              if (indicators.rsi) indicatorData += `- **RSI${indicatorParams.rsi || '(参数未知)'}**: 相对强弱指标,范围0-100,>70超买,<30超卖\n`;
              if (indicators.kdj) indicatorData += `- **KDJ${indicatorParams.kdj || '(参数未知)'}**: 随机指标,K线、D线、J线,>80超买,<20超卖\n`;
              if (indicators.boll) indicatorData += `- **BOLL${indicatorParams.boll || '(参数未知)'}**: 布林带,上轨、中轨、下轨\n`;
              const maIndicators = Object.keys(indicators).filter(key => key.startsWith('ma') && key !== 'macd');
              if (maIndicators.length > 0) {
                maIndicators.forEach(ma => {
                  const period = ma.replace('ma', '');
                  indicatorData += `- **${ma.toUpperCase()}(${period})**: 移动平均线,常用判断趋势方向\n`;
                });
              }
            }
            return {
              content: [
                {
                  type: "text",
                  text: `# ${args.code} ${marketTitleMap[marketType]}行情数据\n\n${formattedData}${indicatorData}`
                }
              ]
            };
          }
          
          // 构建请求参数
          const params: any = {
            token: TUSHARE_API_KEY,
            params: {
              ts_code: args.code,
              start_date: actualStartDate,
              end_date: actualEndDate
            }
            // 不设置fields参数,默认返回所有字段
          };
    
          // 根据不同市场类型设置不同的API名称和参数,默认返回所有字段
          switch(marketType) {
            case 'cn':
              params.api_name = "daily";
              // 不设置fields,返回所有可用字段
              break;
              
            case 'us':
              params.api_name = "us_daily";
              // 不设置fields,返回所有可用字段
              break;
              
            case 'hk':
              params.api_name = "hk_daily";
              // 不设置fields,返回所有可用字段
              break;
              
            case 'fx':
              params.api_name = "fx_daily";
              // 不设置fields,返回所有可用字段
              break;
              
            case 'futures':
              params.api_name = "fut_daily";
              // 不设置fields,返回所有可用字段
              break;
              
            case 'fund':
              params.api_name = "fund_daily";
              // 不设置fields,返回所有可用字段
              break;
              
            case 'repo':
              params.api_name = "repo_daily";
              // 不设置fields,返回所有可用字段
              break;
              
            case 'convertible_bond':
              params.api_name = "cb_daily";
              // 不设置fields,返回所有可用字段
              break;
              
            case 'options':
              params.api_name = "opt_daily";
              // 不设置fields,返回所有可用字段
              // 期权接口优先使用trade_date,如果没有指定则使用end_date作为trade_date
              if (requestedIndicators.length > 0) {
                // 如请求技术指标,必须用区间以便获取足够历史
                params.params = {
                  ts_code: args.code,
                  start_date: actualStartDate,
                  end_date: actualEndDate
                };
                // 统一将 amount 列名标注为(万元)
              } else if (!args.start_date && !args.end_date) {
                // 如果都没指定,使用默认的end_date作为trade_date
                params.params = {
                  trade_date: actualEndDate
                };
              } else if (args.end_date && !args.start_date) {
                // 只指定了end_date,使用作为trade_date
                params.params = {
                  trade_date: actualEndDate
                };
              } else {
                // 如果指定了start_date或日期范围,保持原有逻辑但添加ts_code
                params.params = {
                  ts_code: args.code,
                  start_date: actualStartDate,
                  end_date: actualEndDate
                };
              }
              // 如果指定了具体的期权代码,添加到params中
              if (args.code && args.code.length > 0) {
                params.params.ts_code = args.code;
              }
              break;
          }
          
          console.log(`选择的API接口: ${params.api_name}`);
          console.log(`字段设置: 返回所有可用字段`);
          
          // 设置请求超时
          const controller = new AbortController();
          const timeoutId = setTimeout(() => controller.abort(), TUSHARE_CONFIG.TIMEOUT);
          
          try {
            console.log(`请求Tushare API: ${params.api_name},参数:`, params.params);
            
            // 发送请求
            const response = await fetch(TUSHARE_API_URL, {
              method: "POST",
              headers: {
                "Content-Type": "application/json"
              },
              body: JSON.stringify(params),
              signal: controller.signal
            });
            
            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}`);
            }
            
            // 确保data.data和data.data.items存在
            if (!data.data || !data.data.items || data.data.items.length === 0) {
              throw new Error(`未找到${marketType}市场股票${args.code}的行情数据`);
            }
            
            // 获取字段名
            const fields = data.data.fields;
            
            // 将数据转换为对象数组
            let stockData = data.data.items.map((item: any) => {
              const result: Record<string, any> = {};
              fields.forEach((field: string, index: number) => {
                result[field] = item[index];
              });
              return result;
            });
            
                    console.log(`成功获取到${stockData.length}条${args.code}股票数据记录(扩展数据范围)`);
            
            // 对A股强制应用前复权(qfq):使用最新交易日因子进行归一
            if (marketType === 'cn' && stockData.length > 0) {
              try {
                const afParams = {
                  api_name: 'adj_factor',
                  token: TUSHARE_API_KEY,
                  params: {
                    ts_code: args.code,
                    start_date: actualStartDate,
                    end_date: actualEndDate
                  },
                  fields: 'trade_date,adj_factor'
                } as any;
    
                const afResp = await fetch(TUSHARE_API_URL, {
                  method: 'POST',
                  headers: { 'Content-Type': 'application/json' },
                  body: JSON.stringify(afParams),
                  signal: controller.signal
                });
                if (!afResp.ok) throw new Error(`adj_factor 请求失败: ${afResp.status}`);
                const afJson = await afResp.json();
                if (afJson.code !== 0) throw new Error(`adj_factor 返回错误: ${afJson.msg}`);
                const afFields: string[] = afJson.data?.fields ?? [];
                const afItems: any[] = afJson.data?.items ?? [];
                const idxDate = afFields.indexOf('trade_date');
                const idxFactor = afFields.indexOf('adj_factor');
                const factorMap = new Map<string, number>();
                for (const row of afItems) {
                  const d = String(row[idxDate]);
                  const f = Number(row[idxFactor]);
                  if (!isNaN(f)) factorMap.set(d, f);
                }
    
                // 找到stockData中最新交易日的因子
                const latestDate = stockData
                  .map((r: any) => String(r.trade_date))
                  .sort((a: string, b: string) => b.localeCompare(a))[0];
                const latestFactor = factorMap.get(latestDate);
    
                if (latestFactor && !isNaN(latestFactor)) {
                  stockData = stockData.map((row: any) => {
                    const f = factorMap.get(String(row.trade_date));
                    if (f && !isNaN(f)) {
                      const ratio = f / latestFactor; // 前复权:price * f / f_latest
                      const adj = (v: any) => (v == null || v === '' || isNaN(Number(v))) ? v : Number(v) * ratio;
                      return {
                        ...row,
                        open: adj(row.open),
                        high: adj(row.high),
                        low: adj(row.low),
                        close: adj(row.close)
                      };
                    }
                    return row;
                  });
                  console.log(`已应用前复权(基于最新交易日因子)到 ${args.code} 的OHLC价格`);
                } else {
                  console.warn('未找到最新交易日复权因子,跳过前复权');
                }
              } catch (e) {
                console.warn('应用前复权失败,继续返回未复权数据:', e);
              }
            }
            
            // 计算技术指标
            let indicators: Record<string, any> = {};
            
            if (requestedIndicators.length > 0 && ['cn', 'us', 'hk', 'fund', 'futures', 'convertible_bond', 'options', 'fx', 'crypto'].includes(marketType)) {
              // 对具有可用于OHLC的市场计算技术指标
              // 构建按时间正序的价格序列
              const mid = (a: any, b: any): number => {
                const x = parseFloat(a);
                const y = parseFloat(b);
                if (!isNaN(x) && !isNaN(y)) return (x + y) / 2;
                if (!isNaN(x)) return x;
                if (!isNaN(y)) return y;
                return NaN;
              };
    
              let closes: number[] = [];
              let highs: number[] = [];
              let lows: number[] = [];
    
              if (marketType === 'fx') {
                closes = stockData.map((d: Record<string, any>) => mid(d.bid_close, d.ask_close)).reverse();
                highs = stockData.map((d: Record<string, any>) => mid(d.bid_high, d.ask_high)).reverse();
                lows = stockData.map((d: Record<string, any>) => mid(d.bid_low, d.ask_low)).reverse();
              } else {
                closes = stockData.map((d: Record<string, any>) => parseFloat(d.close)).reverse();
                highs = stockData.map((d: Record<string, any>) => parseFloat(d.high)).reverse();
                lows = stockData.map((d: Record<string, any>) => parseFloat(d.low)).reverse();
              }
              
                                   for (const indicator of requestedIndicators) {
                try {
                  const { name, params } = parseIndicatorParams(indicator);
                  
                  switch (name) {
                    case 'macd':
                      if (params.length !== 3) {
                        throw new Error(`MACD指标需要3个参数,格式:macd(快线,慢线,信号线),如:macd(12,26,9)`);
                      }
                      indicators.macd = calculateMACD(closes, params[0], params[1], params[2]);
                      break;
                    case 'rsi':
                      if (params.length !== 1) {
                        throw new Error(`RSI指标需要1个参数,格式:rsi(周期),如:rsi(14)`);
                      }
                      indicators.rsi = calculateRSI(closes, params[0]);
                      break;
                    case 'kdj':
                      if (params.length !== 3) {
                        throw new Error(`KDJ指标需要3个参数,格式:kdj(K周期,K平滑,D平滑),如:kdj(9,3,3)`);
                      }
                      indicators.kdj = calculateKDJ(highs, lows, closes, params[0], params[1], params[2]);
                      break;
                    case 'boll':
                      if (params.length !== 2) {
                        throw new Error(`布林带指标需要2个参数,格式:boll(周期,标准差倍数),如:boll(20,2)`);
                      }
                      indicators.boll = calculateBOLL(closes, params[0], params[1]);
                      break;
                    case 'ma':
                      if (params.length !== 1) {
                        throw new Error(`移动平均线需要1个参数,格式:ma(周期),如:ma(5)、ma(10)、ma(20)`);
                      }
                      const maPeriod = params[0];
                      indicators[`ma${maPeriod}`] = calculateSMA(closes, maPeriod);
                      break;
                    default:
                      throw new Error(`不支持的技术指标: ${name},支持的指标:macd(12,26,9)、rsi(14)、kdj(9,3,3)、boll(20,2)、ma(周期)`);
                  }
                } catch (error) {
                  console.error(`解析技术指标 ${indicator} 时出错:`, error);
                  throw new Error(`技术指标参数错误: ${indicator}`);
                }
              }
              
                         // 将技术指标数据逆序回来,以匹配原始数据的时间顺序(最新日期在前)
               Object.keys(indicators).forEach(key => {
                 if (typeof indicators[key] === 'object' && indicators[key] !== null) {
                   if (Array.isArray(indicators[key])) {
                     indicators[key] = indicators[key].reverse();
                   } else {
                     // 对于MACD、KDJ、BOLL等对象类型的指标
                     Object.keys(indicators[key]).forEach(subKey => {
                       if (Array.isArray(indicators[key][subKey])) {
                         indicators[key][subKey] = indicators[key][subKey].reverse();
                       }
                     });
                   }
                 }
               });
             }
             
             // 过滤数据到用户请求的时间范围
             if (requestedIndicators.length > 0) {
               stockData = filterDataToUserRange(stockData, userStartDate, userEndDate);
               console.log(`过滤到用户请求时间范围,剩余${stockData.length}条记录`);
             }
            
            // 生成市场类型标题
            const marketTitleMap: Record<string, string> = {
              'cn': 'A股',
              'us': '美股',
              'hk': '港股',
              'fx': '外汇',
              'futures': '期货',
              'fund': '基金',
              'repo': '债券逆回购',
              'convertible_bond': '可转债',
              'options': '期权',
              'crypto': '加密货币'
            };
    
            // 金额(amount)统一以“万元”为单位展示:amount(千) -> amount/10(万)
            const formatAmountWan = (val: any): string => {
              const num = Number(val);
              if (val == null || val === '' || isNaN(num)) return 'N/A';
              return (num / 10).toFixed(2);
            };
            
            // 格式化输出(根据不同市场类型构建表格格式)
            let formattedData = '';
            let indicatorData = '';
            const titleSuffix = marketType === 'cn' ? '(前复权)' : '';
            
            if (marketType === 'fx') {
              // 外汇数据表格展示(追加技术指标列)
              const hasIndicators = Object.keys(indicators).length > 0;
              const indicatorHeaders: string[] = [];
              if (hasIndicators) {
                if (indicators.macd) indicatorHeaders.push('MACD_DIF', 'MACD_DEA', 'MACD');
                if (indicators.rsi) indicatorHeaders.push('RSI');
                if (indicators.kdj) indicatorHeaders.push('KDJ_K', 'KDJ_D', 'KDJ_J');
                if (indicators.boll) indicatorHeaders.push('BOLL_UP', 'BOLL_MID', 'BOLL_LOW');
                const maIndicators = Object.keys(indicators).filter(key => key.startsWith('ma') && key !== 'macd');
                maIndicators.forEach(ma => indicatorHeaders.push(ma.toUpperCase()));
              }
              const baseHeaders = ['交易日期','买入开盘','买入最高','买入最低','买入收盘','卖出开盘','卖出最高','卖出最低','卖出收盘','报价笔数'];
              const headers = [...baseHeaders, ...indicatorHeaders];
              formattedData = `| ${headers.join(' | ')} |\n`;
              formattedData += `|${headers.map(() => '--------').join('|')}|\n`;
              stockData.forEach((data: Record<string, any>, index: number) => {
                const baseRow = [data.trade_date, data.bid_open || 'N/A', data.bid_high || 'N/A', data.bid_low || 'N/A', data.bid_close || 'N/A', data.ask_open || 'N/A', data.ask_high || 'N/A', data.ask_low || 'N/A', data.ask_close || 'N/A', data.tick_qty || 'N/A'];
                const indicatorRow: string[] = [];
                if (hasIndicators) {
                  if (indicators.macd) {
                    indicatorRow.push(
                      isNaN(indicators.macd.dif[index]) ? 'N/A' : indicators.macd.dif[index].toFixed(4),
                      isNaN(indicators.macd.dea[index]) ? 'N/A' : indicators.macd.dea[index].toFixed(4),
                      isNaN(indicators.macd.macd[index]) ? 'N/A' : indicators.macd.macd[index].toFixed(4)
                    );
                  }
                  if (indicators.rsi) indicatorRow.push(isNaN(indicators.rsi[index]) ? 'N/A' : indicators.rsi[index].toFixed(2));
                  if (indicators.kdj) indicatorRow.push(
                    isNaN(indicators.kdj.k[index]) ? 'N/A' : indicators.kdj.k[index].toFixed(2),
                    isNaN(indicators.kdj.d[index]) ? 'N/A' : indicators.kdj.d[index].toFixed(2),
                    isNaN(indicators.kdj.j[index]) ? 'N/A' : indicators.kdj.j[index].toFixed(2)
                  );
                  if (indicators.boll) indicatorRow.push(
                    isNaN(indicators.boll.upper[index]) ? 'N/A' : indicators.boll.upper[index].toFixed(2),
                    isNaN(indicators.boll.middle[index]) ? 'N/A' : indicators.boll.middle[index].toFixed(2),
                    isNaN(indicators.boll.lower[index]) ? 'N/A' : indicators.boll.lower[index].toFixed(2)
                  );
                  const maIndicators = Object.keys(indicators).filter(key => key.startsWith('ma') && key !== 'macd');
                  maIndicators.forEach(ma => {
                    indicatorRow.push(isNaN(indicators[ma][index]) ? 'N/A' : indicators[ma][index].toFixed(2));
                  });
                }
                const row = [...baseRow, ...indicatorRow];
                formattedData += `| ${row.join(' | ')} |\n`;
              });
            } else if (marketType === 'futures') {
              // 期货数据表格展示(追加技术指标列)
              const hasIndicators = Object.keys(indicators).length > 0;
              const indicatorHeaders: string[] = [];
              if (hasIndicators) {
                if (indicators.macd) indicatorHeaders.push('MACD_DIF', 'MACD_DEA', 'MACD');
                if (indicators.rsi) indicatorHeaders.push('RSI');
                if (indicators.kdj) indicatorHeaders.push('KDJ_K', 'KDJ_D', 'KDJ_J');
                if (indicators.boll) indicatorHeaders.push('BOLL_UP', 'BOLL_MID', 'BOLL_LOW');
                const maIndicators = Object.keys(indicators).filter(key => key.startsWith('ma') && key !== 'macd');
                maIndicators.forEach(ma => indicatorHeaders.push(ma.toUpperCase()));
              }
              const baseHeaders = ['交易日期','开盘','最高','最低','收盘','结算','涨跌1','涨跌2','成交量','持仓量'];
              const headers = [...baseHeaders, ...indicatorHeaders];
              formattedData = `| ${headers.join(' | ')} |\n`;
              formattedData += `|${headers.map(() => '--------').join('|')}|\n`;
              stockData.forEach((data: Record<string, any>, index: number) => {
                const baseRow = [data.trade_date, data.open || 'N/A', data.high || 'N/A', data.low || 'N/A', data.close || 'N/A', data.settle || 'N/A', data.change1 || 'N/A', data.change2 || 'N/A', data.vol || 'N/A', data.oi || 'N/A'];
                const indicatorRow: string[] = [];
                if (hasIndicators) {
                  if (indicators.macd) {
                    indicatorRow.push(
                      isNaN(indicators.macd.dif[index]) ? 'N/A' : indicators.macd.dif[index].toFixed(4),
                      isNaN(indicators.macd.dea[index]) ? 'N/A' : indicators.macd.dea[index].toFixed(4),
                      isNaN(indicators.macd.macd[index]) ? 'N/A' : indicators.macd.macd[index].toFixed(4)
                    );
                  }
                  if (indicators.rsi) indicatorRow.push(isNaN(indicators.rsi[index]) ? 'N/A' : indicators.rsi[index].toFixed(2));
                  if (indicators.kdj) indicatorRow.push(
                    isNaN(indicators.kdj.k[index]) ? 'N/A' : indicators.kdj.k[index].toFixed(2),
                    isNaN(indicators.kdj.d[index]) ? 'N/A' : indicators.kdj.d[index].toFixed(2),
                    isNaN(indicators.kdj.j[index]) ? 'N/A' : indicators.kdj.j[index].toFixed(2)
                  );
                  if (indicators.boll) indicatorRow.push(
                    isNaN(indicators.boll.upper[index]) ? 'N/A' : indicators.boll.upper[index].toFixed(2),
                    isNaN(indicators.boll.middle[index]) ? 'N/A' : indicators.boll.middle[index].toFixed(2),
                    isNaN(indicators.boll.lower[index]) ? 'N/A' : indicators.boll.lower[index].toFixed(2)
                  );
                  const maIndicators = Object.keys(indicators).filter(key => key.startsWith('ma') && key !== 'macd');
                  maIndicators.forEach(ma => {
                    indicatorRow.push(isNaN(indicators[ma][index]) ? 'N/A' : indicators[ma][index].toFixed(2));
                  });
                }
                const row = [...baseRow, ...indicatorRow];
                formattedData += `| ${row.join(' | ')} |\n`;
              });
            } else if (marketType === 'repo') {
              // 债券逆回购数据表格展示
              formattedData = `| 交易日期 | 品种名称 | 利率(%) | 成交金额(万元) |\n`;
              formattedData += `|---------|---------|---------|---------------|\n`;
              stockData.forEach((data: Record<string, any>) => {
                const amtWan = formatAmountWan(data.amount);
                formattedData += `| ${data.trade_date} | ${data.name || 'N/A'} | ${data.rate || 'N/A'} | ${amtWan} |\n`;
              });
            } else if (marketType === 'convertible_bond') {
              // 可转债数据表格展示(追加技术指标列)
              const hasIndicators = Object.keys(indicators).length > 0;
              const indicatorHeaders: string[] = [];
              if (hasIndicators) {
                if (indicators.macd) indicatorHeaders.push('MACD_DIF', 'MACD_DEA', 'MACD');
                if (indicators.rsi) indicatorHeaders.push('RSI');
                if (indicators.kdj) indicatorHeaders.push('KDJ_K', 'KDJ_D', 'KDJ_J');
                if (indicators.boll) indicatorHeaders.push('BOLL_UP', 'BOLL_MID', 'BOLL_LOW');
                const maIndicators = Object.keys(indicators).filter(key => key.startsWith('ma') && key !== 'macd');
                maIndicators.forEach(ma => indicatorHeaders.push(ma.toUpperCase()));
              }
              const baseHeaders = ['交易日期','开盘','最高','最低','收盘','涨跌','涨跌幅(%)','成交量(手)','成交金额(万元)','纯债价值','纯债溢价率(%)','转股价值','转股溢价率(%)'];
              const headers = [...baseHeaders, ...indicatorHeaders];
              formattedData = `| ${headers.join(' | ')} |\n`;
              formattedData += `|${headers.map(() => '--------').join('|')}|\n`;
              stockData.forEach((data: Record<string, any>, index: number) => {
                const baseRow = [
                  data.trade_date,
                  data.open || 'N/A',
                  data.high || 'N/A',
                  data.low || 'N/A',
                  data.close || 'N/A',
                  data.change || 'N/A',
                  data.pct_chg || 'N/A',
                  data.vol || 'N/A',
                  formatAmountWan(data.amount),
                  data.bond_value || 'N/A',
                  data.bond_over_rate || 'N/A',
                  data.cb_value || 'N/A',
                  data.cb_over_rate || 'N/A'
                ];
                const indicatorRow: string[] = [];
                if (hasIndicators) {
                  if (indicators.macd) {
                    indicatorRow.push(
                      isNaN(indicators.macd.dif[index]) ? 'N/A' : indicators.macd.dif[index].toFixed(4),
                      isNaN(indicators.macd.dea[index]) ? 'N/A' : indicators.macd.dea[index].toFixed(4),
                      isNaN(indicators.macd.macd[index]) ? 'N/A' : indicators.macd.macd[index].toFixed(4)
                    );
                  }
                  if (indicators.rsi) indicatorRow.push(isNaN(indicators.rsi[index]) ? 'N/A' : indicators.rsi[index].toFixed(2));
                  if (indicators.kdj) indicatorRow.push(
                    isNaN(indicators.kdj.k[index]) ? 'N/A' : indicators.kdj.k[index].toFixed(2),
                    isNaN(indicators.kdj.d[index]) ? 'N/A' : indicators.kdj.d[index].toFixed(2),
                    isNaN(indicators.kdj.j[index]) ? 'N/A' : indicators.kdj.j[index].toFixed(2)
                  );
                  if (indicators.boll) indicatorRow.push(
                    isNaN(indicators.boll.upper[index]) ? 'N/A' : indicators.boll.upper[index].toFixed(2),
                    isNaN(indicators.boll.middle[index]) ? 'N/A' : indicators.boll.middle[index].toFixed(2),
                    isNaN(indicators.boll.lower[index]) ? 'N/A' : indicators.boll.lower[index].toFixed(2)
                  );
                  const maIndicators = Object.keys(indicators).filter(key => key.startsWith('ma') && key !== 'macd');
                  maIndicators.forEach(ma => {
                    indicatorRow.push(isNaN(indicators[ma][index]) ? 'N/A' : indicators[ma][index].toFixed(2));
                  });
                }
                const row = [...baseRow, ...indicatorRow];
                formattedData += `| ${row.join(' | ')} |\n`;
              });
            } else if (marketType === 'options') {
              // 期权数据表格展示(追加技术指标列)
              const hasIndicators = Object.keys(indicators).length > 0;
              const indicatorHeaders: string[] = [];
              if (hasIndicators) {
                if (indicators.macd) indicatorHeaders.push('MACD_DIF', 'MACD_DEA', 'MACD');
                if (indicators.rsi) indicatorHeaders.push('RSI');
                if (indicators.kdj) indicatorHeaders.push('KDJ_K', 'KDJ_D', 'KDJ_J');
                if (indicators.boll) indicatorHeaders.push('BOLL_UP', 'BOLL_MID', 'BOLL_LOW');
                const maIndicators = Object.keys(indicators).filter(key => key.startsWith('ma') && key !== 'macd');
                maIndicators.forEach(ma => indicatorHeaders.push(ma.toUpperCase()));
              }
              const baseHeaders = ['交易日期','交易所','昨结算','前收盘','开盘','最高','最低','收盘','结算','成交量(手)','成交金额(万元)','持仓量(手)'];
              const headers = [...baseHeaders, ...indicatorHeaders];
              formattedData = `| ${headers.join(' | ')} |\n`;
              formattedData += `|${headers.map(() => '--------').join('|')}|\n`;
              stockData.forEach((data: Record<string, any>, index: number) => {
                const baseRow = [
                  data.trade_date,
                  data.exchange || 'N/A',
                  data.pre_settle || 'N/A',
                  data.pre_close || 'N/A',
                  data.open || 'N/A',
                  data.high || 'N/A',
                  data.low || 'N/A',
                  data.close || 'N/A',
                  data.settle || 'N/A',
                  data.vol || 'N/A',
                  formatAmountWan(data.amount),
                  data.oi || 'N/A'
                ];
                const indicatorRow: string[] = [];
                if (hasIndicators) {
                  if (indicators.macd) {
                    indicatorRow.push(
                      isNaN(indicators.macd.dif[index]) ? 'N/A' : indicators.macd.dif[index].toFixed(4),
                      isNaN(indicators.macd.dea[index]) ? 'N/A' : indicators.macd.dea[index].toFixed(4),
                      isNaN(indicators.macd.macd[index]) ? 'N/A' : indicators.macd.macd[index].toFixed(4)
                    );
                  }
                  if (indicators.rsi) indicatorRow.push(isNaN(indicators.rsi[index]) ? 'N/A' : indicators.rsi[index].toFixed(2));
                  if (indicators.kdj) indicatorRow.push(
                    isNaN(indicators.kdj.k[index]) ? 'N/A' : indicators.kdj.k[index].toFixed(2),
                    isNaN(indicators.kdj.d[index]) ? 'N/A' : indicators.kdj.d[index].toFixed(2),
                    isNaN(indicators.kdj.j[index]) ? 'N/A' : indicators.kdj.j[index].toFixed(2)
                  );
                  if (indicators.boll) indicatorRow.push(
                    isNaN(indicators.boll.upper[index]) ? 'N/A' : indicators.boll.upper[index].toFixed(2),
                    isNaN(indicators.boll.middle[index]) ? 'N/A' : indicators.boll.middle[index].toFixed(2),
                    isNaN(indicators.boll.lower[index]) ? 'N/A' : indicators.boll.lower[index].toFixed(2)
                  );
                  const maIndicators = Object.keys(indicators).filter(key => key.startsWith('ma') && key !== 'macd');
                  maIndicators.forEach(ma => {
                    indicatorRow.push(isNaN(indicators[ma][index]) ? 'N/A' : indicators[ma][index].toFixed(2));
                  });
                }
                const row = [...baseRow, ...indicatorRow];
                formattedData += `| ${row.join(' | ')} |\n`;
              });
            } else {
              // 股票数据表格展示(A股、美股、港股、基金等)
              if (stockData.length > 0) {
                // 基础字段
                const coreFields = ['trade_date', 'open', 'close', 'high', 'low', 'vol', 'amount'];
                const availableFields = Object.keys(stockData[0]);
                const displayFields = coreFields.filter(field => availableFields.includes(field));
                
                // 生成字段名映射
                const fieldNameMap: Record<string, string> = {
                  'trade_date': '交易日期',
                  'open': '开盘',
                  'close': '收盘',
                  'high': '最高', 
                  'low': '最低',
                  'vol': '成交量',
                  'amount': '成交额'
                };
                fieldNameMap['amount'] = '成交额(万元)';
                
                // 如果有技术指标,添加技术指标列
                const indicatorHeaders: string[] = [];
                const hasIndicators = Object.keys(indicators).length > 0;
                
                if (hasIndicators) {
                  // 添加技术指标表头
                  if (indicators.macd) {
                    indicatorHeaders.push('MACD_DIF', 'MACD_DEA', 'MACD');
                  }
                  if (indicators.rsi) {
                    indicatorHeaders.push('RSI');
                  }
                  if (indicators.kdj) {
                    indicatorHeaders.push('KDJ_K', 'KDJ_D', 'KDJ_J');
                  }
                  if (indicators.boll) {
                    indicatorHeaders.push('BOLL_UP', 'BOLL_MID', 'BOLL_LOW');
                  }
                  // 添加移动平均线
                  const maIndicators = Object.keys(indicators).filter(key => key.startsWith('ma') && key !== 'macd');
                  maIndicators.forEach(ma => {
                    indicatorHeaders.push(ma.toUpperCase());
                  });
                }
                
                // 组合所有表头
                const allHeaders = [
                  ...displayFields.map(field => field === 'amount' ? '成交额(万元)' : (fieldNameMap[field] || field)),
                  ...indicatorHeaders
                ];
                formattedData = `| ${allHeaders.join(' | ')} |\n`;
                formattedData += `|${allHeaders.map(() => '--------').join('|')}|\n`;
                
                // 生成数据行
                stockData.forEach((data: Record<string, any>, index: number) => {
                  const basicRow = displayFields.map(field => {
                    if (field === 'amount') return formatAmountWan(data.amount);
                    return data[field] || 'N/A';
                  });
                  
                  // 添加技术指标数据
                  const indicatorRow: string[] = [];
                  if (hasIndicators) {
                    if (indicators.macd) {
                      indicatorRow.push(
                        isNaN(indicators.macd.dif[index]) ? 'N/A' : indicators.macd.dif[index].toFixed(4),
                        isNaN(indicators.macd.dea[index]) ? 'N/A' : indicators.macd.dea[index].toFixed(4),
                        isNaN(indicators.macd.macd[index]) ? 'N/A' : indicators.macd.macd[index].toFixed(4)
                      );
                    }
                    if (indicators.rsi) {
                      indicatorRow.push(isNaN(indicators.rsi[index]) ? 'N/A' : indicators.rsi[index].toFixed(2));
                    }
                    if (indicators.kdj) {
                      indicatorRow.push(
                        isNaN(indicators.kdj.k[index]) ? 'N/A' : indicators.kdj.k[index].toFixed(2),
                        isNaN(indicators.kdj.d[index]) ? 'N/A' : indicators.kdj.d[index].toFixed(2),
                        isNaN(indicators.kdj.j[index]) ? 'N/A' : indicators.kdj.j[index].toFixed(2)
                      );
                    }
                    if (indicators.boll) {
                      indicatorRow.push(
                        isNaN(indicators.boll.upper[index]) ? 'N/A' : indicators.boll.upper[index].toFixed(2),
                        isNaN(indicators.boll.middle[index]) ? 'N/A' : indicators.boll.middle[index].toFixed(2),
                        isNaN(indicators.boll.lower[index]) ? 'N/A' : indicators.boll.lower[index].toFixed(2)
                      );
                    }
                    // 添加移动平均线数据
                    const maIndicators = Object.keys(indicators).filter(key => key.startsWith('ma') && key !== 'macd');
                    maIndicators.forEach(ma => {
                      indicatorRow.push(isNaN(indicators[ma][index]) ? 'N/A' : indicators[ma][index].toFixed(2));
                    });
                  }
                  
                  const fullRow = [...basicRow, ...indicatorRow];
                  formattedData += `| ${fullRow.join(' | ')} |\n`;
                });
              }
            }
                    
            // 生成技术指标说明(如果有技术指标)
            if (Object.keys(indicators).length > 0) {
              indicatorData = `\n\n## 📊 技术指标说明\n`;
              
              // 记录实际使用的参数,用于说明中显示
              const indicatorParams: Record<string, string> = {};
              for (const indicator of requestedIndicators) {
                try {
                  const { name, params } = parseIndicatorParams(indicator);
                  indicatorParams[name] = formatIndicatorParams(name, params);
                } catch {
                  // 忽略解析错误,继续处理其他指标
                }
              }
              
              if (indicators.macd) {
                const params = indicatorParams.macd || '(参数未知)';
                indicatorData += `- **MACD${params}**: DIF(快线)、DEA(慢线)、MACD(柱状图)\n`;
              }
              if (indicators.rsi) {
                const params = indicatorParams.rsi || '(参数未知)';
                indicatorData += `- **RSI${params}**: 相对强弱指标,范围0-100,>70超买,<30超卖\n`;
              }
              if (indicators.kdj) {
                const params = indicatorParams.kdj || '(参数未知)';
                indicatorData += `- **KDJ${params}**: 随机指标,K线、D线、J线,>80超买,<20超卖\n`;
              }
              if (indicators.boll) {
                const params = indicatorParams.boll || '(参数未知)';
                indicatorData += `- **BOLL${params}**: 布林带,上轨、中轨、下轨\n`;
              }
              
              // 处理各种MA指标,过滤掉非MA指标
              const maIndicators = Object.keys(indicators).filter(key => key.startsWith('ma') && key !== 'macd');
              if (maIndicators.length > 0) {
                maIndicators.forEach(ma => {
                  const period = ma.replace('ma', '');
                  indicatorData += `- **${ma.toUpperCase()}(${period})**: 移动平均线,常用判断趋势方向\n`;
                });
              }
            }
            
            // 收集股票代码并生成说明(仅对股票市场)
            let stockExplanation = '';
            if (['cn', 'us', 'hk'].includes(marketType)) {
              stockExplanation = await resolveStockCodes([args.code]);
            }
            
            return {
              content: [
                {
                  type: "text",
                  text: `# ${args.code} ${marketTitleMap[marketType]}行情数据${titleSuffix}\n\n${formattedData}${indicatorData}${stockExplanation}`
                }
              ]
            };
          } finally {
            clearTimeout(timeoutId);
          }
        } catch (error) {
          console.error("获取股票数据失败:", error);
          
          return {
            content: [
              {
                type: "text",
                text: `# 获取股票${args.code}数据失败\n\n无法从Tushare API获取数据:${error instanceof Error ? error.message : String(error)}\n\n请检查股票代码和市场类型是否正确:\n- A股格式:"000001.SZ"\n- 美股格式:"AAPL"\n- 港股格式:"00700.HK"\n- 外汇格式:"USDCNH.FXCM"(美元人民币)\n- 期货格式:"CU2501.SHF"\n- 基金格式:"159919.SZ"\n- 债券逆回购格式:"204001.SH"\n- 可转债格式:"113008.SH"\n- 期权格式:"10001313.SH"\n\n技术指标使用说明(必须明确指定参数):\n- **MACD**: macd(快线,慢线,信号线) - 例:macd(12,26,9)\n- **RSI**: rsi(周期) - 例:rsi(14)\n- **KDJ**: kdj(K周期,K平滑,D平滑) - 例:kdj(9,3,3)\n- **布林带**: boll(周期,标准差倍数) - 例:boll(20,2)\n- **移动平均线**: ma(周期) - 例:ma(5)、ma(10)、ma(20)\n\n使用示例:\n- "macd(12,26,9) rsi(14)"\n- "kdj(9,3,3) boll(20,2) ma(30)"\n- "macd(5,10,5) ma(5) ma(10)"`
              }
            ]
          };
        }
      }
    }; 
  • src/index.ts:279-286 (registration)
    Tool call handler registration in the MCP server's CallToolRequestSchema. Dispatches to stockData.run() with parsed arguments when 'stock_data' is called.
    case "stock_data": {
      const code = String(request.params.arguments?.code);
      const market_type = String(request.params.arguments?.market_type);
      const start_date = request.params.arguments?.start_date ? String(request.params.arguments.start_date) : undefined;
      const end_date = request.params.arguments?.end_date ? String(request.params.arguments.end_date) : undefined;
      const indicators = request.params.arguments?.indicators ? String(request.params.arguments.indicators) : undefined;
      return normalizeResult(await stockData.run({ code, market_type, start_date, end_date, indicators }));
    }
  • src/index.ts:179-182 (registration)
    Tool listing registration in ListToolsRequestSchema. Registers 'stock_data' tool's metadata (name, description, schema) for discovery.
    {
      name: stockData.name,
      description: stockData.description,
      inputSchema: stockData.parameters
  • Re-exports all helper functions used by stock_data handler for technical indicator calculations (MACD, KDJ, RSI, BOLL, MA/SMA/EMA), parameter parsing, and data range utilities.
    // 技术指标模块统一导出
    
    export { calculateMACD } from './macd.js';
    export { calculateKDJ } from './kdj.js';
    export { calculateRSI } from './rsi.js';
    export { calculateBOLL } from './boll.js';
    export { calculateSMA, calculateEMA } from './ma.js';
    export { parseIndicatorParams, formatIndicatorParams } from './paramParser.js';
    export { calculateRequiredDays, calculateExtendedStartDate, filterDataToUserRange } from './dataCalculator.js';
    export type { IndicatorParams } from './paramParser.js'; 
  • Input schema definition for the stock_data tool, specifying parameters like code, market_type (required), optional dates and indicators.
    parameters: {
      type: "object",
      properties: {
        code: {
          type: "string",
          description: "股票/合约/加密资产代码。股票示例:'000001.SZ'(A股平安银行)、'AAPL'(美股)、'00700.HK'(港股)、'USDCNH.FXCM'(外汇)、'CU2501.SHF'(期货)、'159919.SZ'(基金)、'204001.SH'(逆回购)、'113008.SH'(可转债)、'10001313.SH'(期权)。加密示例(需 market_type=crypto,Binance):推荐标准写法 'BTCUSDT'、'ETHUSDT'、'USDCUSDT'、'FDUSDUSDT' 等;也兼容 'BTC-USDT' 或 'BTC/USDT'。常见报价币:USDT、USDC、FDUSD、TUSD、BUSD、BTC、ETH。注意:若写 'USD' 会自动映射为 'USDT'(如 'BTC-USD' → 'BTCUSDT')。"
        },
        market_type: {
          type: "string",
          description: "市场类型(必需),可选值:cn(A股),us(美股),hk(港股),fx(外汇),futures(期货),fund(基金),repo(债券逆回购),convertible_bond(可转债),options(期权),crypto(加密货币/CoinGecko)"
        },
        start_date: {
          type: "string",
          description: "起始日期,格式为YYYYMMDD,如'20230101'"
        },
        end_date: {
          type: "string",
          description: "结束日期,格式为YYYYMMDD,如'20230131'"
        },
        indicators: {
          type: "string",
          description: "需要计算的技术指标,多个指标用空格分隔。支持的指标:macd(MACD指标)、rsi(相对强弱指标)、kdj(随机指标)、boll(布林带)、ma(均线指标)。必须明确指定参数,例如:'macd(12,26,9) rsi(14) kdj(9,3,3) boll(20,2) ma(10)'"
        }
      },
      required: ["code", "market_type"]
    },

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