Skip to main content
Glama

Taiwan Stock Real-Time Analysis MCP Server

by ravenyeh
MIT License
  • Apple
taiwan_stock_mcp.py23.5 kB
""" 台灣股票即時分析 MCP 服務器 Taiwan Stock Real-Time Analysis MCP Server 提供台灣股票市場的即時報價、技術分析、買賣建議等功能。 """ from mcp.server.fastmcp import FastMCP from pydantic import BaseModel, Field, field_validator, ConfigDict from typing import Optional, List, Dict, Any, Literal from enum import Enum import httpx import json from datetime import datetime import asyncio # 初始化 MCP 服務器 mcp = FastMCP("taiwan_stock_mcp") # 常數定義 CHARACTER_LIMIT = 25000 TWSE_API_BASE = "https://mis.twse.com.tw/stock/api" REQUEST_DELAY = 3.0 # 證交所限制:每5秒最多3個請求 # ==================== 回應格式 ==================== class ResponseFormat(str, Enum): """輸出格式選項""" MARKDOWN = "markdown" JSON = "json" # ==================== 輸入模型 ==================== class StockQueryInput(BaseModel): """股票查詢輸入模型""" model_config = ConfigDict( str_strip_whitespace=True, validate_assignment=True, extra='forbid' ) stock_code: str = Field( ..., description="台灣股票代碼,例如:'2330'(台積電), '2317'(鴻海), '0050'(元大台灣50)", min_length=4, max_length=6, pattern=r'^[0-9]{4,6}$' ) response_format: ResponseFormat = Field( default=ResponseFormat.MARKDOWN, description="輸出格式:'markdown' 適合人類閱讀,'json' 適合程式處理" ) @field_validator('stock_code') @classmethod def validate_stock_code(cls, v: str) -> str: """驗證股票代碼格式""" if not v.isdigit(): raise ValueError("股票代碼必須是純數字") return v class MultiStockQueryInput(BaseModel): """多支股票查詢輸入模型""" model_config = ConfigDict( str_strip_whitespace=True, validate_assignment=True, extra='forbid' ) stock_codes: List[str] = Field( ..., description="股票代碼列表,例如:['2330', '2317', '0050']", min_items=1, max_items=20 ) response_format: ResponseFormat = Field( default=ResponseFormat.MARKDOWN, description="輸出格式:'markdown' 或 'json'" ) class TechnicalAnalysisInput(BaseModel): """技術分析輸入模型""" model_config = ConfigDict( str_strip_whitespace=True, validate_assignment=True, extra='forbid' ) stock_code: str = Field( ..., description="股票代碼,例如:'2330'", pattern=r'^[0-9]{4,6}$' ) analysis_type: Literal["basic", "advanced", "full"] = Field( default="basic", description="分析類型:'basic'(基本分析), 'advanced'(進階分析), 'full'(完整分析)" ) response_format: ResponseFormat = Field( default=ResponseFormat.MARKDOWN, description="輸出格式" ) # ==================== 工具函數 ==================== async def fetch_stock_data(stock_codes: List[str], exchange: str = "tse") -> Dict[str, Any]: """ 從證交所 API 獲取股票即時資料 Args: stock_codes: 股票代碼列表 exchange: 交易所類型,'tse' 為上市,'otc' 為上櫃 Returns: API 回應的 JSON 資料 """ # 構建股票代碼字串 stock_list = '|'.join(f'{exchange}_{code}.tw' for code in stock_codes) url = f"{TWSE_API_BASE}/getStockInfo.jsp" params = { 'ex_ch': stock_list, 'json': '1', 'delay': '0' } async with httpx.AsyncClient(timeout=10.0, verify=False) as client: try: response = await client.get(url, params=params) response.raise_for_status() data = response.json() if data.get('rtcode') != '0000': raise ValueError(f"API 回應錯誤: {data.get('rtmessage', 'Unknown error')}") return data except httpx.HTTPError as e: raise ValueError(f"無法連接證交所 API: {str(e)}") except json.JSONDecodeError: raise ValueError("API 回應格式錯誤") def calculate_price_change(current: float, previous: float) -> tuple[float, str]: """ 計算價格變動 Returns: (變動金額, 變動百分比字串) """ change = current - previous change_percent = (change / previous * 100) if previous > 0 else 0 sign = "+" if change >= 0 else "" return change, f"{sign}{change_percent:.2f}%" def format_timestamp(timestamp: str) -> str: """格式化時間戳記""" try: ts = int(timestamp) / 1000 dt = datetime.fromtimestamp(ts) return dt.strftime('%Y-%m-%d %H:%M:%S') except: return timestamp def analyze_technical_indicators(stock_data: Dict[str, Any]) -> Dict[str, Any]: """ 技術指標分析 分析當前價格與開盤、最高、最低、昨收的關係 """ try: # 安全地轉換數值,處理 '-' 和空值 def safe_float(value, default=0.0): if value in ['', '-', None]: return default try: return float(value) except (ValueError, TypeError): return default def safe_int(value, default=0): if value in ['', '-', None]: return default try: return int(value) except (ValueError, TypeError): return default current = safe_float(stock_data.get('z')) # 成交價 open_price = safe_float(stock_data.get('o')) # 開盤 high = safe_float(stock_data.get('h')) # 最高 low = safe_float(stock_data.get('l')) # 最低 prev_close = safe_float(stock_data.get('y')) # 昨收 volume = safe_int(stock_data.get('v')) # 成交量 # 計算技術指標 price_position = (current - low) / (high - low) * 100 if high > low else 50 # 判斷趨勢 if current > prev_close: trend = "上漲" trend_strength = "強勢" if current > open_price else "震盪上漲" elif current < prev_close: trend = "下跌" trend_strength = "弱勢" if current < open_price else "震盪下跌" else: trend = "平盤" trend_strength = "盤整" # 價格位置分析 if price_position >= 80: position_desc = "高檔區(接近今日最高)" elif price_position >= 60: position_desc = "中高檔區" elif price_position >= 40: position_desc = "中檔區" elif price_position >= 20: position_desc = "中低檔區" else: position_desc = "低檔區(接近今日最低)" return { 'trend': trend, 'trend_strength': trend_strength, 'price_position': price_position, 'position_desc': position_desc, 'volume': volume, 'high': high, 'low': low, 'open': open_price, 'current': current, 'prev_close': prev_close } except Exception as e: return {'error': f"技術分析錯誤: {str(e)}"} def generate_trading_suggestion(analysis: Dict[str, Any], stock_data: Dict[str, Any]) -> Dict[str, str]: """ 生成買賣建議 基於技術分析結果提供交易建議 """ if 'error' in analysis: return { 'action': '無法分析', 'reason': analysis['error'], 'risk_level': '未知' } current = analysis['current'] prev_close = analysis['prev_close'] price_position = analysis['price_position'] trend = analysis['trend'] # 買賣五檔分析 best_bid = stock_data.get('b', '').split('_')[0] if stock_data.get('b') else '' best_ask = stock_data.get('a', '').split('_')[0] if stock_data.get('a') else '' try: bid_price = float(best_bid) if best_bid else 0 ask_price = float(best_ask) if best_ask else 0 spread = ask_price - bid_price if bid_price and ask_price else 0 except: spread = 0 # 決策邏輯 if trend == "上漲": if price_position < 40: action = "買進" reason = f"股價處於{analysis['position_desc']}且呈上漲趨勢,具備向上動能" risk_level = "中等" elif price_position < 70: action = "觀望或小量買進" reason = f"股價已上漲至{analysis['position_desc']},可等回檔再進場" risk_level = "中高" else: action = "觀望" reason = f"股價已在{analysis['position_desc']},追高風險較大" risk_level = "高" elif trend == "下跌": if price_position > 60: action = "賣出或減碼" reason = f"股價雖處{analysis['position_desc']}但呈下跌趨勢,建議減碼" risk_level = "中高" elif price_position > 30: action = "觀望" reason = f"股價在{analysis['position_desc']}且下跌中,等待止跌訊號" risk_level = "中等" else: action = "觀望或小量買進" reason = f"股價已在{analysis['position_desc']},可能接近短期支撐" risk_level = "中等" else: # 平盤 if price_position < 30: action = "可考慮買進" reason = f"股價在{analysis['position_desc']},風險相對較低" risk_level = "中低" elif price_position > 70: action = "觀望" reason = f"股價在{analysis['position_desc']},等待回檔" risk_level = "中等" else: action = "觀望" reason = "股價盤整中,等待明確趨勢" risk_level = "中等" return { 'action': action, 'reason': reason, 'risk_level': risk_level, 'spread': f"{spread:.2f}" if spread else "N/A" } def format_stock_markdown(stock_data: Dict[str, Any], include_analysis: bool = False) -> str: """將股票資料格式化為 Markdown""" try: def safe_float(value, default=0.0): if value in ['', '-', None]: return default try: return float(value) except (ValueError, TypeError): return default code = stock_data.get('c', 'N/A') name = stock_data.get('n', 'N/A') current = safe_float(stock_data.get('z')) prev_close = safe_float(stock_data.get('y')) open_price = stock_data.get('o', '-') high = stock_data.get('h', '-') low = stock_data.get('l', '-') volume = stock_data.get('v', '-') time = format_timestamp(stock_data.get('tlong', '0')) # 判斷是否為盤後(成交價為0或為'-') is_after_hours = (stock_data.get('z') in ['', '-', None] or current == 0) if is_after_hours: # 盤後時段,顯示昨收價 price_display = f"{prev_close:.2f} (昨收,盤後)" change_display = "-" else: # 盤中時段,正常顯示 change, change_pct = calculate_price_change(current, prev_close) price_display = f"{current:.2f} ({change:+.2f}, {change_pct})" change_display = f"{change:+.2f} ({change_pct})" # 買賣五檔 bid_prices = stock_data.get('b', '').split('_')[:5] bid_volumes = stock_data.get('g', '').split('_')[:5] ask_prices = stock_data.get('a', '').split('_')[:5] ask_volumes = stock_data.get('f', '').split('_')[:5] status_note = "\n\n⏰ **盤後時段** - 以下為昨日收盤資訊,盤中時段將顯示即時報價。\n" if is_after_hours else "" markdown = f"""## 📊 {name} ({code}){status_note} ### {'收盤資訊' if is_after_hours else '即時報價'} - **{'收盤價' if is_after_hours else '成交價'}**: {price_display} - **開盤**: {open_price} - **最高**: {high} - **最低**: {low} - **昨收**: {prev_close:.2f} - **成交量**: {volume} 張 - **更新時間**: {time} ### 買賣五檔 """ # 買賣五檔表格 markdown += "\n| 委買量 | 委買價 | 委賣價 | 委賣量 |\n" markdown += "|--------|--------|--------|--------|\n" for i in range(5): bid_vol = bid_volumes[i] if i < len(bid_volumes) else '-' bid_price = bid_prices[i] if i < len(bid_prices) else '-' ask_price = ask_prices[i] if i < len(ask_prices) else '-' ask_vol = ask_volumes[i] if i < len(ask_volumes) else '-' markdown += f"| {bid_vol} | {bid_price} | {ask_price} | {ask_vol} |\n" # 技術分析和建議 if include_analysis: analysis = analyze_technical_indicators(stock_data) suggestion = generate_trading_suggestion(analysis, stock_data) markdown += f""" ### 📈 技術分析 - **趨勢**: {analysis.get('trend', 'N/A')} ({analysis.get('trend_strength', 'N/A')}) - **價格位置**: {analysis.get('position_desc', 'N/A')} ({analysis.get('price_position', 0):.1f}%) ### 💡 交易建議 - **建議動作**: {suggestion['action']} - **理由**: {suggestion['reason']} - **風險等級**: {suggestion['risk_level']} - **買賣價差**: {suggestion['spread']} --- ⚠️ **免責聲明**: 以上分析僅供參考,不構成投資建議。投資有風險,請謹慎評估。 """ return markdown except Exception as e: return f"❌ 格式化股票資料時發生錯誤: {str(e)}" # ==================== MCP 工具 ==================== @mcp.tool(name="get_stock_realtime_quote") async def get_stock_realtime_quote(params: StockQueryInput) -> str: """取得台灣股票即時報價資訊。 此工具從台灣證券交易所獲取指定股票的即時交易資料,包括成交價、漲跌幅、 成交量、買賣五檔等資訊。適合快速查詢單一股票的當前狀態。 Args: params (StockQueryInput): 包含以下欄位: - stock_code (str): 股票代碼 (4-6位數字) - response_format (str): 輸出格式 ('markdown' 或 'json') Returns: str: 格式化的股票即時報價資訊 Examples: 查詢台積電(2330)即時報價: - stock_code: "2330" - response_format: "markdown" Note: - 資料來源為台灣證券交易所,約每5秒更新一次 - 請遵守 API 使用限制,避免過於頻繁請求 """ try: # 嘗試上市股票 data = await fetch_stock_data([params.stock_code], "tse") if not data.get('msgArray'): # 嘗試上櫃股票 data = await fetch_stock_data([params.stock_code], "otc") if not data.get('msgArray'): return f"❌ 找不到股票代碼 {params.stock_code} 的資料,請確認代碼是否正確" stock_info = data['msgArray'][0] if params.response_format == ResponseFormat.JSON: return json.dumps(stock_info, ensure_ascii=False, indent=2) else: return format_stock_markdown(stock_info, include_analysis=False) except Exception as e: return f"❌ 查詢失敗: {str(e)}" @mcp.tool(name="get_multiple_stocks_quotes") async def get_multiple_stocks_quotes(params: MultiStockQueryInput) -> str: """同時取得多支台灣股票的即時報價資訊。 此工具可一次查詢多支股票的即時交易資料,適合用於投資組合監控、 類股比較等場景。 Args: params (MultiStockQueryInput): 包含以下欄位: - stock_codes (List[str]): 股票代碼列表,最多20支 - response_format (str): 輸出格式 Returns: str: 所有股票的即時報價資訊 Examples: 查詢台積電、鴻海、聯發科: - stock_codes: ["2330", "2317", "2454"] - response_format: "markdown" Note: - 建議一次查詢不超過10支股票以提升效能 - 會自動識別上市/上櫃股票 """ try: results = [] # 分批處理股票代碼(上市和上櫃分開) for code in params.stock_codes: try: data = await fetch_stock_data([code], "tse") if not data.get('msgArray'): data = await fetch_stock_data([code], "otc") if data.get('msgArray'): results.append(data['msgArray'][0]) else: results.append({'c': code, 'error': '查無資料'}) # 避免請求過於頻繁 await asyncio.sleep(0.5) except Exception as e: results.append({'c': code, 'error': str(e)}) if params.response_format == ResponseFormat.JSON: return json.dumps(results, ensure_ascii=False, indent=2) else: markdown = "# 📊 多支股票即時報價\n\n" for stock in results: if 'error' in stock: markdown += f"## ❌ {stock['c']}: {stock['error']}\n\n" else: markdown += format_stock_markdown(stock, include_analysis=False) markdown += "\n---\n\n" return markdown except Exception as e: return f"❌ 查詢失敗: {str(e)}" @mcp.tool(name="analyze_stock_with_suggestion") async def analyze_stock_with_suggestion(params: TechnicalAnalysisInput) -> str: """進行股票技術分析並提供買賣建議。 此工具結合即時報價資料進行技術面分析,包括趨勢判斷、價格位置分析, 並基於分析結果提供交易建議。適合需要交易決策參考的場景。 Args: params (TechnicalAnalysisInput): 包含以下欄位: - stock_code (str): 股票代碼 - analysis_type (str): 分析類型 ('basic', 'advanced', 'full') - response_format (str): 輸出格式 Returns: str: 包含技術分析和交易建議的完整報告 Analysis Types: - basic: 基本趨勢和建議 - advanced: 包含更多技術指標 - full: 完整分析報告 Examples: 分析台積電並取得買賣建議: - stock_code: "2330" - analysis_type: "full" - response_format: "markdown" Warning: 分析結果僅供參考,不構成實際投資建議。 投資前請務必進行完整評估並考慮個人風險承受能力。 """ try: # 獲取即時資料 data = await fetch_stock_data([params.stock_code], "tse") if not data.get('msgArray'): data = await fetch_stock_data([params.stock_code], "otc") if not data.get('msgArray'): return f"❌ 找不到股票代碼 {params.stock_code} 的資料" stock_info = data['msgArray'][0] # 進行技術分析 analysis = analyze_technical_indicators(stock_info) suggestion = generate_trading_suggestion(analysis, stock_info) if params.response_format == ResponseFormat.JSON: result = { 'stock_info': stock_info, 'technical_analysis': analysis, 'trading_suggestion': suggestion } return json.dumps(result, ensure_ascii=False, indent=2) else: # 使用增強版的 Markdown 格式 return format_stock_markdown(stock_info, include_analysis=True) except Exception as e: return f"❌ 分析失敗: {str(e)}" @mcp.tool(name="compare_stocks") async def compare_stocks(params: MultiStockQueryInput) -> str: """比較多支股票的表現和技術面。 此工具同時分析多支股票,並以表格形式比較各股票的關鍵指標, 幫助投資人快速了解不同標的的相對強弱。 Args: params (MultiStockQueryInput): 包含股票代碼列表和輸出格式 Returns: str: 股票比較分析結果 Examples: 比較台積電、聯電、力積電: - stock_codes: ["2330", "2303", "6770"] - response_format: "markdown" """ try: comparisons = [] for code in params.stock_codes: try: data = await fetch_stock_data([code], "tse") if not data.get('msgArray'): data = await fetch_stock_data([code], "otc") if data.get('msgArray'): stock_info = data['msgArray'][0] analysis = analyze_technical_indicators(stock_info) suggestion = generate_trading_suggestion(analysis, stock_info) comparisons.append({ 'code': code, 'name': stock_info.get('n', 'N/A'), 'price': float(stock_info.get('z', 0)), 'change': calculate_price_change( float(stock_info.get('z', 0)), float(stock_info.get('y', 0)) )[1], 'trend': analysis.get('trend', 'N/A'), 'position': f"{analysis.get('price_position', 0):.1f}%", 'suggestion': suggestion['action'], 'risk': suggestion['risk_level'] }) await asyncio.sleep(0.5) except Exception as e: comparisons.append({ 'code': code, 'error': str(e) }) if params.response_format == ResponseFormat.JSON: return json.dumps(comparisons, ensure_ascii=False, indent=2) else: markdown = "# 📊 股票比較分析\n\n" markdown += "| 代碼 | 名稱 | 現價 | 漲跌幅 | 趨勢 | 價格位置 | 建議 | 風險 |\n" markdown += "|------|------|------|--------|------|----------|------|------|\n" for comp in comparisons: if 'error' in comp: markdown += f"| {comp['code']} | - | - | - | ❌ {comp['error']} | - | - | - |\n" else: markdown += f"| {comp['code']} | {comp['name']} | {comp['price']:.2f} | {comp['change']} | {comp['trend']} | {comp['position']} | {comp['suggestion']} | {comp['risk']} |\n" markdown += "\n---\n⚠️ **免責聲明**: 以上分析僅供參考,不構成投資建議。" return markdown except Exception as e: return f"❌ 比較分析失敗: {str(e)}" # ==================== 主程式 ==================== if __name__ == "__main__": # 啟動 MCP 服務器 mcp.run()

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/ravenyeh/multi_market_stock_mcp'

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