diff --git a/mcp_database.py b/mcp_database.py index 00024b9a..1b099bc7 100644 --- a/mcp_database.py +++ b/mcp_database.py @@ -781,3 +781,246 @@ async def remove_favorite_event(user_id: str, event_id: int) -> Dict[str, Any]: return {"success": True, "message": "删除自选事件成功"} else: return {"success": False, "message": "未找到该自选事件"} + + +# ==================== ClickHouse 分钟频数据查询 ==================== + +from clickhouse_driver import Client as ClickHouseClient + +# ClickHouse 连接配置 +CLICKHOUSE_CONFIG = { + 'host': '222.128.1.157', + 'port': 18000, + 'user': 'default', + 'password': 'Zzl33818!', + 'database': 'stock' +} + +# ClickHouse 客户端(懒加载) +_clickhouse_client = None + + +def get_clickhouse_client(): + """获取 ClickHouse 客户端(单例模式)""" + global _clickhouse_client + if _clickhouse_client is None: + _clickhouse_client = ClickHouseClient( + host=CLICKHOUSE_CONFIG['host'], + port=CLICKHOUSE_CONFIG['port'], + user=CLICKHOUSE_CONFIG['user'], + password=CLICKHOUSE_CONFIG['password'], + database=CLICKHOUSE_CONFIG['database'] + ) + logger.info("ClickHouse client created") + return _clickhouse_client + + +async def get_stock_minute_data( + code: str, + start_time: Optional[str] = None, + end_time: Optional[str] = None, + limit: int = 240 +) -> List[Dict[str, Any]]: + """ + 获取股票分钟频数据 + + Args: + code: 股票代码(例如:'600519' 或 '600519.SH') + start_time: 开始时间,格式:YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD + end_time: 结束时间,格式:YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD + limit: 返回条数,默认240(一个交易日的分钟数据) + + Returns: + 分钟频数据列表 + """ + try: + client = get_clickhouse_client() + + # 标准化股票代码(去除后缀) + stock_code = code.split('.')[0] if '.' in code else code + + # 构建查询 + query = """ + SELECT + code, + timestamp, + open, + high, + low, + close, + volume, + amt + FROM stock_minute + WHERE code = %(code)s + """ + + params = {'code': stock_code} + + if start_time: + query += " AND timestamp >= %(start_time)s" + params['start_time'] = start_time + + if end_time: + query += " AND timestamp <= %(end_time)s" + params['end_time'] = end_time + + query += " ORDER BY timestamp DESC LIMIT %(limit)s" + params['limit'] = limit + + # 执行查询 + result = client.execute(query, params, with_column_types=True) + + rows = result[0] + columns = [col[0] for col in result[1]] + + # 转换为字典列表 + data = [] + for row in rows: + record = {} + for i, col in enumerate(columns): + value = row[i] + # 处理 datetime 类型 + if hasattr(value, 'isoformat'): + record[col] = value.isoformat() + else: + record[col] = value + data.append(record) + + logger.info(f"[ClickHouse] 查询分钟数据: code={stock_code}, 返回 {len(data)} 条记录") + return data + + except Exception as e: + logger.error(f"[ClickHouse] 查询分钟数据失败: {e}", exc_info=True) + return [] + + +async def get_stock_minute_aggregation( + code: str, + date: str, + interval: int = 5 +) -> List[Dict[str, Any]]: + """ + 获取股票分钟频数据的聚合(按指定分钟间隔) + + Args: + code: 股票代码 + date: 日期,格式:YYYY-MM-DD + interval: 聚合间隔(分钟),默认5分钟 + + Returns: + 聚合后的K线数据 + """ + try: + client = get_clickhouse_client() + + # 标准化股票代码 + stock_code = code.split('.')[0] if '.' in code else code + + # 使用 ClickHouse 的时间函数进行聚合 + query = f""" + SELECT + code, + toStartOfInterval(timestamp, INTERVAL {interval} MINUTE) as interval_start, + argMin(open, timestamp) as open, + max(high) as high, + min(low) as low, + argMax(close, timestamp) as close, + sum(volume) as volume, + sum(amt) as amt + FROM stock_minute + WHERE code = %(code)s + AND toDate(timestamp) = %(date)s + GROUP BY code, interval_start + ORDER BY interval_start + """ + + params = {'code': stock_code, 'date': date} + + result = client.execute(query, params, with_column_types=True) + + rows = result[0] + columns = [col[0] for col in result[1]] + + data = [] + for row in rows: + record = {} + for i, col in enumerate(columns): + value = row[i] + if hasattr(value, 'isoformat'): + record[col] = value.isoformat() + else: + record[col] = value + data.append(record) + + logger.info(f"[ClickHouse] 聚合分钟数据: code={stock_code}, date={date}, interval={interval}min, 返回 {len(data)} 条记录") + return data + + except Exception as e: + logger.error(f"[ClickHouse] 聚合分钟数据失败: {e}", exc_info=True) + return [] + + +async def get_stock_intraday_statistics( + code: str, + date: str +) -> Dict[str, Any]: + """ + 获取股票日内统计数据 + + Args: + code: 股票代码 + date: 日期,格式:YYYY-MM-DD + + Returns: + 日内统计数据(开盘价、最高价、最低价、收盘价、成交量、成交额、波动率等) + """ + try: + client = get_clickhouse_client() + + stock_code = code.split('.')[0] if '.' in code else code + + query = """ + SELECT + code, + toDate(timestamp) as trade_date, + argMin(open, timestamp) as open, + max(high) as high, + min(low) as low, + argMax(close, timestamp) as close, + sum(volume) as total_volume, + sum(amt) as total_amount, + count(*) as data_points, + min(timestamp) as first_time, + max(timestamp) as last_time, + (max(high) - min(low)) / min(low) * 100 as intraday_range_pct, + stddevPop(close) as price_volatility + FROM stock_minute + WHERE code = %(code)s + AND toDate(timestamp) = %(date)s + GROUP BY code, trade_date + """ + + params = {'code': stock_code, 'date': date} + + result = client.execute(query, params, with_column_types=True) + + if not result[0]: + return {"success": False, "error": f"未找到 {stock_code} 在 {date} 的分钟数据"} + + row = result[0][0] + columns = [col[0] for col in result[1]] + + data = {} + for i, col in enumerate(columns): + value = row[i] + if hasattr(value, 'isoformat'): + data[col] = value.isoformat() + else: + data[col] = float(value) if isinstance(value, (int, float)) else value + + logger.info(f"[ClickHouse] 日内统计: code={stock_code}, date={date}") + return {"success": True, "data": data} + + except Exception as e: + logger.error(f"[ClickHouse] 日内统计失败: {e}", exc_info=True) + return {"success": False, "error": str(e)} diff --git a/mcp_server.py b/mcp_server.py index 7e14a763..9256d8b8 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -701,6 +701,75 @@ TOOLS: List[ToolDefinition] = [ "required": [] } ), + # ==================== 分钟频数据工具 ==================== + ToolDefinition( + name="get_stock_minute_data", + description="获取股票分钟频K线数据。适用于分析日内走势、寻找交易时机、技术分析等场景。", + parameters={ + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "股票代码,例如:'600519' 或 '600519.SH'" + }, + "start_time": { + "type": "string", + "description": "开始时间,格式:YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD" + }, + "end_time": { + "type": "string", + "description": "结束时间,格式:YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD" + }, + "limit": { + "type": "integer", + "description": "返回条数,默认240(约一个交易日)", + "default": 240 + } + }, + "required": ["code"] + } + ), + ToolDefinition( + name="get_stock_minute_aggregation", + description="获取股票分钟频数据的聚合K线(5分钟、15分钟、30分钟等周期)。适用于中短期技术分析。", + parameters={ + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "股票代码" + }, + "date": { + "type": "string", + "description": "交易日期,格式:YYYY-MM-DD" + }, + "interval": { + "type": "integer", + "description": "聚合间隔(分钟),可选:5、15、30、60", + "default": 5 + } + }, + "required": ["code", "date"] + } + ), + ToolDefinition( + name="get_stock_intraday_statistics", + description="获取股票日内统计数据,包括开高低收、成交量、成交额、日内波动率等汇总指标。", + parameters={ + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "股票代码" + }, + "date": { + "type": "string", + "description": "交易日期,格式:YYYY-MM-DD" + } + }, + "required": ["code", "date"] + } + ), ] # ==================== MCP协议端点 ==================== @@ -1114,6 +1183,48 @@ async def handle_get_user_following_events(args: Dict[str, Any]) -> Any: "data": [] } + +# ==================== 分钟频数据处理函数 ==================== + +async def handle_get_stock_minute_data(args: Dict[str, Any]) -> Any: + """处理股票分钟频数据查询""" + code = args["code"] + start_time = args.get("start_time") + end_time = args.get("end_time") + limit = args.get("limit", 240) + + result = await db.get_stock_minute_data(code, start_time, end_time, limit) + return { + "success": True, + "data": result, + "count": len(result) + } + + +async def handle_get_stock_minute_aggregation(args: Dict[str, Any]) -> Any: + """处理股票分钟频数据聚合查询""" + code = args["code"] + date = args["date"] + interval = args.get("interval", 5) + + result = await db.get_stock_minute_aggregation(code, date, interval) + return { + "success": True, + "data": result, + "count": len(result), + "interval": f"{interval}分钟" + } + + +async def handle_get_stock_intraday_statistics(args: Dict[str, Any]) -> Any: + """处理股票日内统计数据查询""" + code = args["code"] + date = args["date"] + + result = await db.get_stock_intraday_statistics(code, date) + return result + + # 工具处理函数映射 TOOL_HANDLERS = { "search_news": handle_search_news, @@ -1136,6 +1247,10 @@ TOOL_HANDLERS = { "get_stock_comparison": handle_get_stock_comparison, "get_user_watchlist": handle_get_user_watchlist, "get_user_following_events": handle_get_user_following_events, + # 分钟频数据工具 + "get_stock_minute_data": handle_get_stock_minute_data, + "get_stock_minute_aggregation": handle_get_stock_minute_aggregation, + "get_stock_intraday_statistics": handle_get_stock_intraday_statistics, } # ==================== Agent系统实现 ==================== diff --git a/package.json b/package.json index f2adc836..c0210101 100755 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "react-to-print": "^3.0.3", "react-tsparticles": "^2.12.2", "recharts": "^3.1.2", + "remark-gfm": "^4.0.1", "sass": "^1.49.9", "socket.io-client": "^4.7.4", "styled-components": "^5.3.11", diff --git a/src/components/ChatBot/MarkdownWithCharts.js b/src/components/ChatBot/MarkdownWithCharts.js index e322f437..11b93a68 100644 --- a/src/components/ChatBot/MarkdownWithCharts.js +++ b/src/components/ChatBot/MarkdownWithCharts.js @@ -2,8 +2,9 @@ // 支持 ECharts 图表的 Markdown 渲染组件 import React from 'react'; -import { Box, Alert, AlertIcon, Text, VStack, Code, useColorModeValue } from '@chakra-ui/react'; +import { Box, Alert, AlertIcon, Text, VStack, Code, useColorModeValue, Table, Thead, Tbody, Tr, Th, Td, TableContainer } from '@chakra-ui/react'; import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; import { EChartsRenderer } from './EChartsRenderer'; import { logger } from '@utils/logger'; @@ -83,6 +84,7 @@ export const MarkdownWithCharts = ({ content, variant = 'auto' }) => { return ( ( @@ -142,6 +144,55 @@ export const MarkdownWithCharts = ({ content, variant = 'auto' }) => { {children} ), + // 表格渲染 + table: ({ children }) => ( + + + {children} +
+
+ ), + thead: ({ children }) => ( + + {children} + + ), + tbody: ({ children }) => {children}, + tr: ({ children }) => ( + + {children} + + ), + th: ({ children }) => ( + + {children} + + ), + td: ({ children }) => ( + + {children} + + ), }} > {part.content} diff --git a/src/views/AgentChat/hooks/useAgentChat.ts b/src/views/AgentChat/hooks/useAgentChat.ts index 1e38d98a..c1564532 100644 --- a/src/views/AgentChat/hooks/useAgentChat.ts +++ b/src/views/AgentChat/hooks/useAgentChat.ts @@ -190,6 +190,9 @@ export const useAgentChat = ({ setCurrentSessionId(data.session_id); } + // 获取执行步骤(后端返回 step_results 字段) + const stepResults = data.step_results || data.steps || []; + // 显示执行计划(如果有) if (data.plan) { addMessage({ @@ -200,24 +203,24 @@ export const useAgentChat = ({ } // 显示执行步骤(如果有) - if (data.steps && data.steps.length > 0) { + if (stepResults.length > 0) { addMessage({ type: MessageTypes.AGENT_EXECUTING, content: '正在执行步骤...', plan: data.plan, - stepResults: data.steps, + stepResults: stepResults, }); } // 移除 "执行中" 消息 setMessages((prev) => prev.filter((m) => m.type !== MessageTypes.AGENT_EXECUTING)); - // 显示最终回复 + // 显示最终回复(使用 final_summary 或 final_answer 或 message) addMessage({ type: MessageTypes.AGENT_RESPONSE, - content: data.final_answer || data.message || '处理完成', + content: data.final_summary || data.final_answer || data.message || '处理完成', plan: data.plan, - stepResults: data.steps, + stepResults: stepResults, metadata: data.metadata, });