Skip to main content
Glama

Tushare MCP Server

by cloga
server.py26.9 kB
import os from pathlib import Path from dotenv import load_dotenv import tushare as ts import pandas as pd import datetime from mcp.server.fastmcp import FastMCP # Load environment variables from .env file in the same directory env_path = Path(__file__).parent / '.env' load_dotenv(dotenv_path=env_path) # Debug logging import logging logging.basicConfig( filename=str(Path(__file__).parent / 'server.log'), level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logging.info("Server starting...") # Initialize the MCP server mcp = FastMCP("Tushare MCP Server") # Initialize Tushare Pro API # Best practice: Set TUSHARE_TOKEN in your environment variables token = os.environ.get("TUSHARE_TOKEN") if not token: print("Warning: TUSHARE_TOKEN environment variable not set.") pro = ts.pro_api(token) if token else None @mcp.tool() def get_stock_basic( exchange: str = "", list_status: str = "L", fields: str = "ts_code,symbol,name,area,industry,list_date" ) -> str: """ Get basic stock information from Tushare. Args: exchange: Exchange code (SSE, SZSE, BSE). Empty for all. list_status: Listing status (L=Listed, D=Delisted, P=Paused). Default is L. fields: Comma-separated list of fields to return. """ if not pro: return "Error: Tushare token not configured. Please set TUSHARE_TOKEN environment variable." try: # Call Tushare API df = pro.stock_basic(exchange=exchange, list_status=list_status, fields=fields) # Convert DataFrame to JSON string for the LLM return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching data from Tushare: {str(e)}" @mcp.tool() def get_daily_price(ts_code: str, start_date: str, end_date: str) -> str: """ Get daily stock price data. Args: ts_code: Stock code (e.g., '000001.SZ') start_date: Start date in YYYYMMDD format end_date: End date in YYYYMMDD format """ if not pro: return "Error: Tushare token not configured. Please set TUSHARE_TOKEN environment variable." try: df = pro.daily(ts_code=ts_code, start_date=start_date, end_date=end_date) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching daily data: {str(e)}" @mcp.tool() def get_index_daily(ts_code: str, start_date: str, end_date: str) -> str: """ Get daily index data (e.g. 000001.SH for SSE). Args: ts_code: Index code start_date: Start date in YYYYMMDD format end_date: End date in YYYYMMDD format """ if not pro: return "Error: Tushare token not configured." try: df = pro.index_daily(ts_code=ts_code, start_date=start_date, end_date=end_date) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching index daily data: {str(e)}" @mcp.tool() def get_trade_cal(start_date: str, end_date: str, exchange: str = "SSE") -> str: """ Get trade calendar. Args: start_date: Start date in YYYYMMDD format end_date: End date in YYYYMMDD format exchange: Exchange code, default is SSE """ if not pro: return "Error: Tushare token not configured. Please set TUSHARE_TOKEN environment variable." try: df = pro.trade_cal(exchange=exchange, start_date=start_date, end_date=end_date) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching trade calendar: {str(e)}" @mcp.tool() def get_daily_basic(ts_code: str = "", trade_date: str = "", start_date: str = "", end_date: str = "") -> str: """ Get daily basic indicators (PE, PB, turnover, etc.). Args: ts_code: Stock code (e.g., '000001.SZ') trade_date: Trade date (YYYYMMDD) start_date: Start date (YYYYMMDD) end_date: End date (YYYYMMDD) """ if not pro: return "Error: Tushare token not configured." try: df = pro.daily_basic(ts_code=ts_code, trade_date=trade_date, start_date=start_date, end_date=end_date) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching daily basic data: {str(e)}" @mcp.tool() def get_weekly_price(ts_code: str, start_date: str, end_date: str) -> str: """ Get weekly stock price data. Args: ts_code: Stock code start_date: Start date (YYYYMMDD) end_date: End date (YYYYMMDD) """ if not pro: return "Error: Tushare token not configured." try: df = pro.weekly(ts_code=ts_code, start_date=start_date, end_date=end_date) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching weekly data: {str(e)}" @mcp.tool() def get_monthly_price(ts_code: str, start_date: str, end_date: str) -> str: """ Get monthly stock price data. Args: ts_code: Stock code start_date: Start date (YYYYMMDD) end_date: End date (YYYYMMDD) """ if not pro: return "Error: Tushare token not configured." try: df = pro.monthly(ts_code=ts_code, start_date=start_date, end_date=end_date) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching monthly data: {str(e)}" @mcp.tool() def get_income(ts_code: str, start_date: str = "", end_date: str = "", period: str = "") -> str: """ Get income statement data. Args: ts_code: Stock code start_date: Start date (YYYYMMDD) end_date: End date (YYYYMMDD) period: Report period (YYYYMMDD, e.g. 20231231) """ if not pro: return "Error: Tushare token not configured." try: df = pro.income(ts_code=ts_code, start_date=start_date, end_date=end_date, period=period) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching income data: {str(e)}" @mcp.tool() def get_balancesheet(ts_code: str, start_date: str = "", end_date: str = "", period: str = "") -> str: """ Get balance sheet data. Args: ts_code: Stock code start_date: Start date (YYYYMMDD) end_date: End date (YYYYMMDD) period: Report period (YYYYMMDD) """ if not pro: return "Error: Tushare token not configured." try: df = pro.balancesheet(ts_code=ts_code, start_date=start_date, end_date=end_date, period=period) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching balance sheet data: {str(e)}" @mcp.tool() def get_cashflow(ts_code: str, start_date: str = "", end_date: str = "", period: str = "") -> str: """ Get cash flow data. Args: ts_code: Stock code start_date: Start date (YYYYMMDD) end_date: End date (YYYYMMDD) period: Report period (YYYYMMDD) """ if not pro: return "Error: Tushare token not configured." try: df = pro.cashflow(ts_code=ts_code, start_date=start_date, end_date=end_date, period=period) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching cash flow data: {str(e)}" @mcp.tool() def get_forecast(ts_code: str, start_date: str = "", end_date: str = "") -> str: """ Get performance forecast data. Args: ts_code: Stock code start_date: Start date (YYYYMMDD) end_date: End date (YYYYMMDD) """ if not pro: return "Error: Tushare token not configured." try: df = pro.forecast(ts_code=ts_code, start_date=start_date, end_date=end_date) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching forecast data: {str(e)}" @mcp.tool() def get_top10_holders(ts_code: str, start_date: str = "", end_date: str = "") -> str: """ Get top 10 shareholders data. Args: ts_code: Stock code start_date: Start date (YYYYMMDD) end_date: End date (YYYYMMDD) """ if not pro: return "Error: Tushare token not configured." try: df = pro.top10_holders(ts_code=ts_code, start_date=start_date, end_date=end_date) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching top 10 holders data: {str(e)}" @mcp.tool() def get_fina_indicator(ts_code: str, start_date: str = "", end_date: str = "") -> str: """ Get financial indicators data. Args: ts_code: Stock code start_date: Start date (YYYYMMDD) end_date: End date (YYYYMMDD) """ if not pro: return "Error: Tushare token not configured." try: df = pro.fina_indicator(ts_code=ts_code, start_date=start_date, end_date=end_date) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching financial indicators: {str(e)}" @mcp.tool() def get_stock_company(ts_code: str = "", exchange: str = "") -> str: """ Get public company information. Args: ts_code: Stock code exchange: Exchange code """ if not pro: return "Error: Tushare token not configured." try: df = pro.stock_company(ts_code=ts_code, exchange=exchange) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching company info: {str(e)}" @mcp.tool() def get_name_change(ts_code: str = "", start_date: str = "", end_date: str = "") -> str: """ Get stock name change history. Args: ts_code: Stock code start_date: Start date end_date: End date """ if not pro: return "Error: Tushare token not configured." try: df = pro.name_change(ts_code=ts_code, start_date=start_date, end_date=end_date) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching name change history: {str(e)}" @mcp.tool() def get_hs_const(hs_type: str = "SH") -> str: """ Get HSGT (Shanghai-Shenzhen-Hong Kong Stock Connect) constituents. Args: hs_type: Type (SH=Shanghai Connect, SZ=Shenzhen Connect) """ if not pro: return "Error: Tushare token not configured." try: df = pro.hs_const(hs_type=hs_type) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching HSGT constituents: {str(e)}" @mcp.tool() def get_new_share(start_date: str = "", end_date: str = "") -> str: """ Get IPO new share listing info. Args: start_date: Start date end_date: End date """ if not pro: return "Error: Tushare token not configured." try: df = pro.new_share(start_date=start_date, end_date=end_date) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching new share info: {str(e)}" @mcp.tool() def get_adj_factor(ts_code: str = "", trade_date: str = "", start_date: str = "", end_date: str = "") -> str: """ Get adjustment factors. Args: ts_code: Stock code trade_date: Trade date start_date: Start date end_date: End date """ if not pro: return "Error: Tushare token not configured." try: df = pro.adj_factor(ts_code=ts_code, trade_date=trade_date, start_date=start_date, end_date=end_date) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching adjustment factors: {str(e)}" @mcp.tool() def get_suspend_d(ts_code: str = "", suspend_date: str = "", resume_date: str = "", start_date: str = "", end_date: str = "") -> str: """ Get stock suspension/resumption info. Args: ts_code: Stock code suspend_date: Suspension date resume_date: Resumption date start_date: Start date end_date: End date """ if not pro: return "Error: Tushare token not configured." try: df = pro.suspend_d(ts_code=ts_code, suspend_date=suspend_date, resume_date=resume_date, start_date=start_date, end_date=end_date) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching suspension info: {str(e)}" @mcp.tool() def get_express(ts_code: str = "", start_date: str = "", end_date: str = "", period: str = "") -> str: """ Get performance express report. Args: ts_code: Stock code start_date: Start date end_date: End date period: Report period """ if not pro: return "Error: Tushare token not configured." try: df = pro.express(ts_code=ts_code, start_date=start_date, end_date=end_date, period=period) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching express report: {str(e)}" @mcp.tool() def get_dividend(ts_code: str = "", ann_date: str = "", record_date: str = "", ex_date: str = "") -> str: """ Get dividend and bonus share info. Args: ts_code: Stock code ann_date: Announcement date record_date: Record date ex_date: Ex-dividend date """ if not pro: return "Error: Tushare token not configured." try: df = pro.dividend(ts_code=ts_code, ann_date=ann_date, record_date=record_date, ex_date=ex_date) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching dividend info: {str(e)}" @mcp.tool() def get_fina_audit(ts_code: str = "", start_date: str = "", end_date: str = "", period: str = "") -> str: """ Get financial audit opinions. Args: ts_code: Stock code start_date: Start date end_date: End date period: Report period """ if not pro: return "Error: Tushare token not configured." try: df = pro.fina_audit(ts_code=ts_code, start_date=start_date, end_date=end_date, period=period) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching audit opinions: {str(e)}" @mcp.tool() def get_fina_mainbz(ts_code: str = "", period: str = "", type: str = "") -> str: """ Get main business composition. Args: ts_code: Stock code period: Report period type: Type (P=Product, D=Region) """ if not pro: return "Error: Tushare token not configured." try: df = pro.fina_mainbz(ts_code=ts_code, period=period, type=type) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching main business info: {str(e)}" @mcp.tool() def get_index_basic(market: str = "", publisher: str = "", category: str = "") -> str: """ Get index basic information. Args: market: Market (MSE, SZSE, CNI, CSI, etc.) publisher: Publisher category: Index category """ if not pro: return "Error: Tushare token not configured." try: df = pro.index_basic(market=market, publisher=publisher, category=category) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching index basic info: {str(e)}" @mcp.tool() def get_index_weight(index_code: str = "", trade_date: str = "", start_date: str = "", end_date: str = "") -> str: """ Get index constituents and weights. Args: index_code: Index code (e.g. 399300.SZ) trade_date: Trade date start_date: Start date end_date: End date """ if not pro: return "Error: Tushare token not configured." try: df = pro.index_weight(index_code=index_code, trade_date=trade_date, start_date=start_date, end_date=end_date) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching index weights: {str(e)}" @mcp.tool() def get_index_dailybasic(trade_date: str = "", ts_code: str = "", start_date: str = "", end_date: str = "") -> str: """ Get index daily basic indicators (PE, PB, etc.). Args: trade_date: Trade date ts_code: Index code start_date: Start date end_date: End date """ if not pro: return "Error: Tushare token not configured." try: df = pro.index_dailybasic(trade_date=trade_date, ts_code=ts_code, start_date=start_date, end_date=end_date) return df.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error fetching index daily basic info: {str(e)}" @mcp.tool() def scan_market_opportunities( consecutive_monthly_drop: int = 0, ma_trend: str = "", price_below_date: str = "" ) -> str: """ Scan the market for stocks matching specific technical and price criteria. This is a composite tool for complex screening. Args: consecutive_monthly_drop: Number of consecutive months with negative returns (close < open). e.g., 3. ma_trend: Moving Average trend. 'bear' for MA5 < MA10 < MA20, 'bull' for MA5 > MA10 > MA20. price_below_date: Check if current price is lower than the high price on this date (YYYYMMDD). """ if not pro: return "Error: Tushare token not configured." try: candidates = [] # Step 1: Monthly Drop Filter if consecutive_monthly_drop > 0: # Get last N months today = datetime.datetime.now() # We need to find the last N month-end dates # Simplified: Get monthly data for last N+1 months to be safe start_date = (today - datetime.timedelta(days=32 * (consecutive_monthly_drop + 1))).strftime('%Y%m%d') end_date = today.strftime('%Y%m%d') # Fetch monthly data for all stocks # Note: fetching by date range for all stocks might be heavy, but monthly is okay-ish. # Better: fetch by trade_date for the last N months. # Get month ends # Fetch more potential months to handle current incomplete month cal = pro.trade_cal(exchange='SSE', is_open='1', start_date=start_date, end_date=end_date) cal['cal_date'] = pd.to_datetime(cal['cal_date']) cal['ym'] = cal['cal_date'].dt.to_period('M') month_ends = cal.groupby('ym')['cal_date'].max().sort_values(ascending=False) # Take top N+3 candidates to be safe candidate_dates = [d.strftime('%Y%m%d') for d in month_ends.head(consecutive_monthly_drop + 3)] print(f"DEBUG: Candidate months: {candidate_dates}") monthly_dfs = [] valid_dates_count = 0 for d in candidate_dates: if valid_dates_count >= consecutive_monthly_drop: break try: df = pro.monthly(trade_date=d, fields='ts_code,close,open') if not df.empty: monthly_dfs.append(df) valid_dates_count += 1 print(f"DEBUG: Fetched {len(df)} rows for {d}") else: print(f"DEBUG: No data for {d}") except Exception as e: print(f"DEBUG: Error fetching {d}: {e}") pass if len(monthly_dfs) < consecutive_monthly_drop: return f"Error: Not enough monthly data found. Needed {consecutive_monthly_drop}, found {len(monthly_dfs)}." df_monthly = pd.concat(monthly_dfs) # Filter: For each stock, must appear in all N months and close < open # Pivot or Group # Let's just count negatives df_monthly['is_negative'] = df_monthly['close'] < df_monthly['open'] # Group by ts_code stats = df_monthly.groupby('ts_code')['is_negative'].agg(['count', 'sum']) # Debug print(f"DEBUG: Total stocks in monthly data: {len(stats)}") print(f"DEBUG: Target count: {consecutive_monthly_drop}") # count should be N (data exists), sum should be N (all negative) # Note: 'sum' of boolean is count of True. valid_stocks = stats[(stats['count'] == consecutive_monthly_drop) & (stats['sum'] == consecutive_monthly_drop)].index.tolist() candidates = valid_stocks print(f"DEBUG: Candidates after monthly filter: {len(candidates)}") else: # If no monthly filter, start with all listed stocks? Too many. # We require at least one filter to start. # If only price_below_date is set, we fetch all stocks for that date? pass if consecutive_monthly_drop > 0 and not candidates: return "No stocks found matching monthly drop criteria." # Step 2: Price Comparison if price_below_date: # If candidates list is empty (and monthly filter was not used), we need to initialize it # But fetching all stocks is heavy. Let's assume we only run this if candidates exist or we fetch all. # Fetch reference high prices df_ref = pro.daily(trade_date=price_below_date, fields='ts_code,high') if df_ref.empty: # Try next day next_day = (datetime.datetime.strptime(price_below_date, '%Y%m%d') + datetime.timedelta(days=1)).strftime('%Y%m%d') df_ref = pro.daily(trade_date=next_day, fields='ts_code,high') df_ref = df_ref.set_index('ts_code') # Fetch latest prices # Get latest trading date # Robust logic: find the latest date that actually has daily data cal = pro.trade_cal(exchange='SSE', is_open='1', start_date=(datetime.datetime.now() - datetime.timedelta(days=10)).strftime('%Y%m%d'), end_date=datetime.datetime.now().strftime('%Y%m%d')) cal = cal.sort_values('cal_date', ascending=False) latest_date = "" df_latest = pd.DataFrame() for d in cal['cal_date'].tolist(): try: df = pro.daily(trade_date=d, fields='ts_code,close') if not df.empty: latest_date = d df_latest = df print(f"DEBUG: Found latest daily data on {latest_date}") break except: pass if df_latest.empty: return "Error: Could not find any recent daily data." df_latest = df_latest.set_index('ts_code') # Filter current_pool = candidates if candidates else df_ref.index.tolist() new_candidates = [] for code in current_pool: if code in df_ref.index and code in df_latest.index: if df_latest.loc[code, 'close'] < df_ref.loc[code, 'high']: new_candidates.append(code) candidates = new_candidates if (consecutive_monthly_drop > 0 or price_below_date) and not candidates: return "No stocks found matching price criteria." # Step 3: MA Trend if ma_trend: # We need history for candidates if not candidates: return "No candidates to check MA trend for." # Fetch last 25 days for candidates # Optimization: Fetch daily data for ALL stocks for last 25 days is better than 1000 calls if candidates > 50 # But if candidates is small (<50), loop is better. # Let's assume candidates list is moderate. cal = pro.trade_cal(exchange='SSE', is_open='1', start_date=(datetime.datetime.now() - datetime.timedelta(days=60)).strftime('%Y%m%d'), end_date=datetime.datetime.now().strftime('%Y%m%d')) trade_dates = cal.sort_values('cal_date').iloc[-25:]['cal_date'].tolist() final_candidates = [] # To be safe with API limits, let's fetch by date for the whole market (25 calls) and filter in memory # This is safer than N calls where N could be 1000. daily_data = {} for d in trade_dates: df = pro.daily(trade_date=d, fields='ts_code,close') # Filter for candidates df = df[df['ts_code'].isin(candidates)] daily_data[d] = df for code in candidates: closes = [] for d in trade_dates: if d in daily_data: row = daily_data[d][daily_data[d]['ts_code'] == code] if not row.empty: closes.append(row.iloc[0]['close']) if len(closes) >= 20: s = pd.Series(closes) ma5 = s.rolling(5).mean().iloc[-1] ma10 = s.rolling(10).mean().iloc[-1] ma20 = s.rolling(20).mean().iloc[-1] if ma_trend == 'bear': if ma5 < ma10 < ma20: final_candidates.append(code) elif ma_trend == 'bull': if ma5 > ma10 > ma20: final_candidates.append(code) candidates = final_candidates # Return results with names if not candidates: return "No stocks found." # Get names df_basic = pro.stock_basic(exchange='', list_status='L', fields='ts_code,name,industry') df_basic = df_basic[df_basic['ts_code'].isin(candidates)] return df_basic.to_json(orient="records", force_ascii=False) except Exception as e: return f"Error in market scan: {str(e)}" if __name__ == "__main__": # Run the server using the standard stdio transport (default for MCP) mcp.run()

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/cloga/tushare-mcp'

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