diff --git a/app.py b/app.py index 4fbd0cba..8352a59e 100755 --- a/app.py +++ b/app.py @@ -17583,6 +17583,217 @@ def get_market_heatmap(): }), 500 +@app.route('/api/market/realtime-stats', methods=['GET']) +def get_market_realtime_stats(): + """ + 获取市场实时统计数据(从 ClickHouse 实时行情表) + 返回:大盘涨跌幅、涨停/跌停数、多空对比、成交额等 + """ + from clickhouse_driver import Client as CHClient + + try: + ch_client = CHClient( + host=os.environ.get('CLICKHOUSE_HOST', 'localhost'), + port=int(os.environ.get('CLICKHOUSE_PORT', 9000)), + user=os.environ.get('CLICKHOUSE_USER', 'default'), + password=os.environ.get('CLICKHOUSE_PASSWORD', ''), + database='stock' + ) + + result = { + 'success': True, + 'data': { + 'index': None, # 大盘指数 + 'limit_up_count': 0, # 涨停数 + 'limit_down_count': 0, # 跌停数 + 'rising_count': 0, # 上涨家数 + 'falling_count': 0, # 下跌家数 + 'flat_count': 0, # 平盘家数 + 'total_amount': 0, # 总成交额 + 'continuous_limit': [], # 连板股 + 'update_time': None + } + } + + # 1. 获取上证指数实时数据 + try: + index_query = """ + SELECT + security_id, + current_index, + prev_close, + open_index, + high_index, + low_index, + amount, + trade_time + FROM stock.szse_index_realtime + WHERE trade_date = today() + AND security_id = '399001' + ORDER BY trade_time DESC + LIMIT 1 + """ + index_data = ch_client.execute(index_query) + if index_data: + row = index_data[0] + prev_close = float(row[2]) if row[2] else 0 + current = float(row[1]) if row[1] else 0 + change_pct = ((current - prev_close) / prev_close * 100) if prev_close else 0 + result['data']['index'] = { + 'code': '399001.SZ', + 'name': '深证成指', + 'current': current, + 'prev_close': prev_close, + 'change_pct': round(change_pct, 2), + 'open': float(row[3]) if row[3] else 0, + 'high': float(row[4]) if row[4] else 0, + 'low': float(row[5]) if row[5] else 0, + } + result['data']['update_time'] = str(row[7]) if row[7] else None + except Exception as e: + app.logger.warning(f"获取指数实时数据失败: {e}") + + # 2. 统计深交所股票涨跌情况 + try: + szse_stats_query = """ + SELECT + security_id, + last_price, + prev_close, + upper_limit_price, + lower_limit_price, + amount + FROM stock.szse_stock_realtime + WHERE trade_date = today() + AND last_price > 0 + AND prev_close > 0 + ORDER BY security_id, trade_time DESC + LIMIT 1 BY security_id + """ + szse_data = ch_client.execute(szse_stats_query) + + for row in szse_data: + last_price = float(row[1]) + prev_close = float(row[2]) + upper_limit = float(row[3]) if row[3] else None + lower_limit = float(row[4]) if row[4] else None + amount = float(row[5]) if row[5] else 0 + + change_pct = (last_price - prev_close) / prev_close * 100 + + # 统计涨跌 + if change_pct > 0.01: + result['data']['rising_count'] += 1 + elif change_pct < -0.01: + result['data']['falling_count'] += 1 + else: + result['data']['flat_count'] += 1 + + # 判断涨停/跌停 + if upper_limit and abs(last_price - upper_limit) < 0.01: + result['data']['limit_up_count'] += 1 + elif lower_limit and abs(last_price - lower_limit) < 0.01: + result['data']['limit_down_count'] += 1 + + result['data']['total_amount'] += amount + + except Exception as e: + app.logger.warning(f"统计深交所数据失败: {e}") + + # 3. 统计上交所股票涨跌情况 + try: + sse_stats_query = """ + SELECT + security_id, + last_price, + prev_close, + upper_limit_price, + lower_limit_price, + amount + FROM stock.sse_stock_realtime + WHERE trade_date = today() + AND last_price > 0 + AND prev_close > 0 + ORDER BY security_id, trade_time DESC + LIMIT 1 BY security_id + """ + sse_data = ch_client.execute(sse_stats_query) + + for row in sse_data: + last_price = float(row[1]) + prev_close = float(row[2]) + upper_limit = float(row[3]) if row[3] else None + lower_limit = float(row[4]) if row[4] else None + amount = float(row[5]) if row[5] else 0 + + change_pct = (last_price - prev_close) / prev_close * 100 + + if change_pct > 0.01: + result['data']['rising_count'] += 1 + elif change_pct < -0.01: + result['data']['falling_count'] += 1 + else: + result['data']['flat_count'] += 1 + + if upper_limit and abs(last_price - upper_limit) < 0.01: + result['data']['limit_up_count'] += 1 + elif lower_limit and abs(last_price - lower_limit) < 0.01: + result['data']['limit_down_count'] += 1 + + result['data']['total_amount'] += amount + + except Exception as e: + app.logger.warning(f"统计上交所数据失败: {e}") + + # 4. 尝试获取上证指数(补充) + try: + sh_index_query = """ + SELECT + seccode, + close as current_price, + pre_close, + open, + high, + low, + amount, + tradetime + FROM stock.index_minute + WHERE tradetime >= today() + AND seccode = '000001.SH' + ORDER BY tradetime DESC + LIMIT 1 + """ + sh_data = ch_client.execute(sh_index_query) + if sh_data: + row = sh_data[0] + prev_close = float(row[2]) if row[2] else 0 + current = float(row[1]) if row[1] else 0 + change_pct = ((current - prev_close) / prev_close * 100) if prev_close else 0 + result['data']['index'] = { + 'code': '000001.SH', + 'name': '上证指数', + 'current': current, + 'prev_close': prev_close, + 'change_pct': round(change_pct, 2), + 'open': float(row[3]) if row[3] else 0, + 'high': float(row[4]) if row[4] else 0, + 'low': float(row[5]) if row[5] else 0, + } + if row[7]: + result['data']['update_time'] = str(row[7]) + except Exception as e: + app.logger.warning(f"获取上证指数失败: {e}") + + return jsonify(result) + + except Exception as e: + app.logger.error(f"获取实时市场统计失败: {e}") + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + @app.route('/api/market/statistics', methods=['GET']) def get_market_statistics(): """获取市场统计数据(从ea_blocktrading表)""" diff --git a/mcp_server.py b/mcp_server.py index 9cfd65fd..59182b3a 100644 --- a/mcp_server.py +++ b/mcp_server.py @@ -4967,10 +4967,17 @@ async def shutdown_event(): if __name__ == "__main__": import uvicorn + import os + # 生产环境禁用 reload,开发环境限制监控目录 + is_dev = os.environ.get("ENV", "production") == "development" + uvicorn.run( "mcp_server:app", host="0.0.0.0", port=8900, - reload=True, + reload=is_dev, + reload_dirs=["."] if is_dev else None, # 只监控当前目录的 py 文件 + reload_includes=["*.py"] if is_dev else None, + reload_excludes=["node_modules/*", ".git/*", "build/*", "data/*"] if is_dev else None, log_level="info" ) diff --git a/src/views/StockOverview/index.tsx b/src/views/StockOverview/index.tsx index 489fdb12..4b952a4c 100644 --- a/src/views/StockOverview/index.tsx +++ b/src/views/StockOverview/index.tsx @@ -199,7 +199,7 @@ const StockOverview: React.FC = () => { } }, []); - // 获取市场统计数据 + // 获取市场统计数据(历史数据,用于非实时场景) const fetchMarketStats = useCallback( async (date: string | null = null) => { try { @@ -238,6 +238,57 @@ const StockOverview: React.FC = () => { [trackMarketStatsViewed] ); + // 获取实时市场统计数据(盘中实时刷新) + const fetchRealtimeStats = useCallback(async () => { + try { + const response = await fetch(`${getApiBase()}/api/market/realtime-stats`); + const data = await response.json(); + + if (data.success && data.data) { + const realtimeData = data.data; + + // 更新涨停/跌停统计 + setLimitStats((prev) => ({ + ...prev, + limitUpCount: realtimeData.limit_up_count || 0, + limitDownCount: realtimeData.limit_down_count || 0, + })); + + // 更新市场统计(涨跌家数、成交额) + setMarketStats((prev) => ({ + ...(prev || {}), + rising_count: realtimeData.rising_count || 0, + falling_count: realtimeData.falling_count || 0, + total_amount: realtimeData.total_amount || prev?.total_amount || 0, + })); + + // 更新大盘指数数据 + if (realtimeData.index) { + setHotspotData((prev) => ({ + ...(prev || {}), + index: { + code: realtimeData.index.code, + name: realtimeData.index.name, + current: realtimeData.index.current, + change_pct: realtimeData.index.change_pct, + prev_close: realtimeData.index.prev_close, + }, + })); + } + + logger.debug('StockOverview', '实时统计数据更新', { + limitUp: realtimeData.limit_up_count, + limitDown: realtimeData.limit_down_count, + rising: realtimeData.rising_count, + falling: realtimeData.falling_count, + updateTime: realtimeData.update_time, + }); + } + } catch (error) { + logger.error('StockOverview', 'fetchRealtimeStats', error); + } + }, []); + // 查看概念详情 const handleConceptClick = useCallback( (concept: Concept, rank: number = 0) => { @@ -284,7 +335,62 @@ const StockOverview: React.FC = () => { fetchTopConcepts(); fetchHeatmapData(); fetchMarketStats(); - }, [fetchTopConcepts, fetchHeatmapData, fetchMarketStats]); + fetchRealtimeStats(); // 初始获取实时数据 + }, [fetchTopConcepts, fetchHeatmapData, fetchMarketStats, fetchRealtimeStats]); + + // 盘中定时刷新实时数据(每30秒) + useEffect(() => { + // 判断是否在交易时间内 + const isTradeTime = () => { + const now = new Date(); + const day = now.getDay(); + const hour = now.getHours(); + const minute = now.getMinutes(); + const timeNum = hour * 100 + minute; + + // 周一到周五,9:15-11:30 或 13:00-15:00 + if (day >= 1 && day <= 5) { + if ((timeNum >= 915 && timeNum <= 1130) || (timeNum >= 1300 && timeNum <= 1500)) { + return true; + } + } + return false; + }; + + // 只在交易时间内刷新 + let intervalId: NodeJS.Timeout | null = null; + + const startRefresh = () => { + if (isTradeTime()) { + intervalId = setInterval(() => { + if (isTradeTime()) { + fetchRealtimeStats(); + } else { + // 非交易时间,停止刷新 + if (intervalId) { + clearInterval(intervalId); + intervalId = null; + } + } + }, 30000); // 30秒刷新一次 + } + }; + + startRefresh(); + + // 每分钟检查是否进入交易时间 + const checkInterval = setInterval(() => { + if (isTradeTime() && !intervalId) { + fetchRealtimeStats(); + startRefresh(); + } + }, 60000); + + return () => { + if (intervalId) clearInterval(intervalId); + clearInterval(checkInterval); + }; + }, [fetchRealtimeStats]); return (