"""Function Calling 工具定义和执行器 让 AI 可以主动调用现有 API 获取市场数据: - get_market_dashboard: 市场看板 - get_theme_history: 题材热点历史 - get_active_core_stocks: 核心股追踪 - get_stock_quote: 个股实时行情 - get_fund_flow: 资金流向 """ import json from services.ai_config import AI_API_BASE, AI_API_KEY, AI_MODEL, AI_MAX_TOKENS, AI_TEMPERATURE TOOLS = [ { "type": "function", "function": { "name": "get_market_dashboard", "description": "获取A股市场看板数据,包含主要指数行情、全市场涨跌统计(涨跌家数/涨停/跌停/炸板率/成交额)、市场温度评分与竞价信号、行业强度榜、概念热度、板块主力资金流、两融余额、海外主要指数与国内期货主力合约(globalMarkets字段)、完整连板梯队(limitLadder字段,含涨停原因与封单金额)、事件情报(热门股/龙虎榜/飙升/异动)", "parameters": {"type": "object", "properties": {}, "required": []} } }, { "type": "function", "function": { "name": "get_theme_history", "description": "获取指定交易日的题材涨幅排行前20", "parameters": { "type": "object", "properties": { "date": {"type": "string", "description": "交易日 YYYY-MM-DD"} }, "required": ["date"] } } }, { "type": "function", "function": { "name": "get_active_core_stocks", "description": "获取活跃核心股列表,包含最近10日涨幅矩阵和所属题材", "parameters": {"type": "object", "properties": {}, "required": []} } }, { "type": "function", "function": { "name": "get_consecutive_core_stocks", "description": "获取连续上榜的核心股列表(近3日及以上连续上榜),用于识别持续活跃的热点股", "parameters": { "type": "object", "properties": { "min_days": {"type": "integer", "description": "最小连续天数,默认3"} }, "required": [] } } }, { "type": "function", "function": { "name": "get_stock_quote", "description": "获取单只股票实时行情(价格、涨跌幅、成交量、换手率等)", "parameters": { "type": "object", "properties": { "code": {"type": "string", "description": "6位股票代码,如 600519"} }, "required": ["code"] } } }, { "type": "function", "function": { "name": "get_fund_flow", "description": "获取个股资金流向数据(主力净流入、超大单/大单/中单/小单流入流出)", "parameters": { "type": "object", "properties": { "code": {"type": "string", "description": "6位股票代码"}, "name": {"type": "string", "description": "股票名称"}, "days": {"type": "integer", "description": "获取天数,默认30"} }, "required": ["code", "name"] } } }, { "type": "function", "function": { "name": "get_news", "description": "获取最近的财经快讯(新浪7x24,含宏观/行业/公司/海外动态),用于重要消息面梳理", "parameters": { "type": "object", "properties": { "limit": {"type": "integer", "description": "获取条数,默认30,最大50"} }, "required": [] } } } ] async def execute_tool(tool_name: str, arguments: dict) -> str: """执行工具调用,返回 JSON 字符串""" try: if tool_name == "get_market_dashboard": return await _get_market_dashboard() elif tool_name == "get_theme_history": return await _get_theme_history(arguments.get("date", "")) elif tool_name == "get_active_core_stocks": return await _get_active_core_stocks() elif tool_name == "get_consecutive_core_stocks": return await _get_consecutive_core_stocks(arguments.get("min_days", 3)) elif tool_name == "get_stock_quote": return await _get_stock_quote(arguments.get("code", "")) elif tool_name == "get_fund_flow": return await _get_fund_flow( arguments.get("code", ""), arguments.get("name", ""), arguments.get("days", 30) ) elif tool_name == "get_news": return await _get_news(arguments.get("limit", 30)) else: return json.dumps({"error": f"未知工具: {tool_name}"}) except Exception as e: return json.dumps({"error": str(e)}) async def _get_news(limit: int = 30) -> str: """获取财经快讯""" from services.market_extra import fetch_news news = await fetch_news(limit) if not news: return json.dumps({"error": "快讯获取失败或暂无数据"}, ensure_ascii=False) return json.dumps({"count": len(news), "items": news}, ensure_ascii=False) async def _get_market_dashboard() -> str: """获取市场看板数据""" from routes.market_dashboard import _build_dashboard data = await _build_dashboard() return json.dumps(data, ensure_ascii=False, default=str) async def _get_theme_history(date: str) -> str: """获取指定交易日题材涨幅前20""" from database import get_connection conn = get_connection() try: rows = conn.execute( "SELECT * FROM daily_top_themes WHERE trade_date = ? ORDER BY rank ASC", (date,) ).fetchall() items = [dict(r) for r in rows] return json.dumps({"date": date, "items": items}, ensure_ascii=False) finally: conn.close() async def _get_active_core_stocks() -> str: """获取活跃核心股列表""" from database import get_connection from datetime import date as date_cls conn = get_connection() try: # 最近10个有数据的交易日 rows = conn.execute( "SELECT DISTINCT trade_date FROM daily_core_stocks ORDER BY trade_date DESC LIMIT 10" ).fetchall() dates = [r["trade_date"] for r in reversed(rows)] if not dates: return json.dumps({"dates": [], "stocks": []}, ensure_ascii=False) placeholders = ",".join("?" * len(dates)) rows = conn.execute( f"""SELECT trade_date, stock_code, stock_name, f3, cover_count FROM daily_core_stocks WHERE trade_date IN ({placeholders}) ORDER BY trade_date DESC, rank ASC""", dates, ).fetchall() stock_days = {} for r in rows: code = r["stock_code"] s = stock_days.setdefault(code, { "stockCode": code, "stockName": r["stock_name"], "coverCount": r["cover_count"], "dailyGains": {}, "appearCount": 0, "lastAppear": None, }) s["dailyGains"][r["trade_date"]] = r["f3"] s["appearCount"] += 1 if s["lastAppear"] is None or r["trade_date"] > s["lastAppear"]: s["lastAppear"] = r["trade_date"] # 只保留最近10日中出现次数最多的前25只(全量可达145KB,会把上下文撑爆) stocks = sorted(stock_days.values(), key=lambda x: -x["appearCount"])[:25] keep_codes = {s["stockCode"] for s in stocks} themes_rows = conn.execute( f"""SELECT stock_code, theme_code, theme_name FROM daily_core_stock_themes WHERE trade_date IN ({placeholders})""", dates, ).fetchall() themes_by_stock = {} for t in themes_rows: if t["stock_code"] not in keep_codes: continue per = themes_by_stock.setdefault(t["stock_code"], {}) per.setdefault(t["theme_code"], {"theme_code": t["theme_code"], "theme_name": t["theme_name"]}) for s in stocks: s["themes"] = list(themes_by_stock.get(s["stockCode"], {}).values())[:5] latest = dates[-1] if dates else None for s in stocks: if s.get("lastAppear") and latest: try: d1 = date_cls.fromisoformat(latest) d2 = date_cls.fromisoformat(s["lastAppear"]) s["daysSinceLastAppear"] = (d1 - d2).days except ValueError: s["daysSinceLastAppear"] = 0 else: s["daysSinceLastAppear"] = 0 return json.dumps({"dates": dates, "stocks": stocks}, ensure_ascii=False) finally: conn.close() async def _get_consecutive_core_stocks(min_days: int = 3) -> str: """获取连续上榜的核心股列表""" from database import get_connection conn = get_connection() try: # 最近30个有数据的交易日 rows = conn.execute( "SELECT DISTINCT trade_date FROM daily_core_stocks ORDER BY trade_date DESC LIMIT 30" ).fetchall() all_dates = [r["trade_date"] for r in reversed(rows)] if not all_dates: return json.dumps({"dates": [], "stocks": []}, ensure_ascii=False) # 查询所有上榜记录 placeholders = ",".join("?" * len(all_dates)) rows = conn.execute( f"""SELECT trade_date, stock_code, stock_name, f3, cover_count FROM daily_core_stocks WHERE trade_date IN ({placeholders}) ORDER BY stock_code, trade_date""", all_dates, ).fetchall() # 按股票分组,记录上榜日期 stock_dates: dict[str, dict] = {} for r in rows: code = r["stock_code"] s = stock_dates.setdefault(code, { "stockCode": code, "stockName": r["stock_name"], "dates": [], "dailyGains": {}, }) s["dates"].append(r["trade_date"]) s["dailyGains"][r["trade_date"]] = r["f3"] # 计算连续上榜天数(从最新交易日往回数) results = [] for code, s in stock_dates.items(): dates_list = sorted(s["dates"]) # 从最新的日期往回数连续天数 consecutive = 0 last_n = [] for i in range(len(dates_list) - 1, -1, -1): if i == len(dates_list) - 1: consecutive = 1 last_n.append(dates_list[i]) else: # 检查是否连续(允许间隔1天非交易日) prev_idx = all_dates.index(dates_list[i + 1]) if dates_list[i + 1] in all_dates else -1 curr_idx = all_dates.index(dates_list[i]) if dates_list[i] in all_dates else -1 if prev_idx - curr_idx <= 2: # 允许间隔1天 consecutive += 1 last_n.append(dates_list[i]) else: break if consecutive >= min_days: results.append({ "stockCode": code, "stockName": s["stockName"], "consecutiveDays": consecutive, "recentDates": last_n, "dailyGains": {d: s["dailyGains"].get(d) for d in last_n}, }) # 按连续天数降序排列 results.sort(key=lambda x: -x["consecutiveDays"]) return json.dumps({ "minDays": min_days, "stocks": results[:30], }, ensure_ascii=False) finally: conn.close() async def _get_stock_quote(code: str) -> str: """获取个股实时行情""" from services.tencent import fetch_quote data = await fetch_quote(code) if not data: return json.dumps({"error": f"未找到股票 {code} 的数据"}) return json.dumps(data, ensure_ascii=False) async def _get_fund_flow(code: str, name: str, days: int) -> str: """获取个股资金流向""" from services.tencent import fetch_quote, get_market_prefix from database import get_connection # 先尝试从本地数据库获取历史资金流向 conn = get_connection() try: rows = conn.execute( "SELECT * FROM cache WHERE key LIKE ? AND expires_at > datetime('now','localtime')", (f"fund_flow:{code}%",) ).fetchall() if rows: return rows[0]["value"] finally: conn.close() # 降级:通过腾讯获取基础行情数据 quote = await fetch_quote(code) if not quote: return json.dumps({"error": f"未找到股票 {code} 的数据"}) return json.dumps({ "code": code, "name": name or quote.get("name", ""), "currentPrice": quote.get("currentPrice", 0), "changePercent": quote.get("changePercent", 0), "volume": quote.get("volume", 0), "amount": quote.get("amount", 0), "note": "资金流向详细数据需通过东方财富API获取" }, ensure_ascii=False)