From bebe58c99f3b9992d28444a1a82b9d9173af8710 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Tue, 9 Dec 2025 17:07:47 +0800 Subject: [PATCH] update pay ui --- app.py | 118 +++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 81 insertions(+), 37 deletions(-) diff --git a/app.py b/app.py index b231ea6c..e1d71850 100755 --- a/app.py +++ b/app.py @@ -12465,6 +12465,10 @@ def get_hotspot_overview(): """ 获取热点概览数据(用于个股中心的热点概览图表) 返回:指数分时数据 + 概念异动标注 + + 数据来源: + - 指数分时:ClickHouse index_minute 表 + - 概念异动:MySQL concept_anomaly_hybrid 表(来自 realtime_detector.py) """ try: trade_date = request.args.get('date') @@ -12532,61 +12536,100 @@ def get_hotspot_overview(): 'change_pct': change_pct }) - # 2. 获取概念异动数据 + # 2. 获取概念异动数据(从 concept_anomaly_hybrid 表) alerts = [] with engine.connect() as conn: + # 查询 concept_anomaly_hybrid 表 alert_result = conn.execute(text(""" SELECT - concept_id, concept_name, alert_time, alert_type, - change_pct, change_delta, limit_up_count, limit_up_delta, - rank_position, index_price, index_change_pct, - stock_count, concept_type, extra_info, - prev_change_pct, zscore, importance_score - FROM concept_minute_alert - WHERE trade_date = :trade_date - ORDER BY alert_time + a.concept_id, + a.alert_time, + a.trade_date, + a.alert_type, + a.final_score, + a.rule_score, + a.ml_score, + a.trigger_reason, + a.alpha, + a.alpha_delta, + a.amt_ratio, + a.amt_delta, + a.rank_pct, + a.limit_up_ratio, + a.stock_count, + a.total_amt, + a.triggered_rules + FROM concept_anomaly_hybrid a + WHERE a.trade_date = :trade_date + ORDER BY a.alert_time """), {'trade_date': trade_date}) + # 获取概念名称映射(从 ES 或缓存) + concept_names = {} + for row in alert_result: - alert_time = row[2] - extra_info = None - if row[13]: + concept_id = row[0] + alert_time = row[1] + triggered_rules = None + if row[16]: try: - extra_info = json.loads(row[13]) if isinstance(row[13], str) else row[13] + triggered_rules = json.loads(row[16]) if isinstance(row[16], str) else row[16] except: pass - # 从 extra_info 提取 zscore 和 importance_score(兼容旧数据) - zscore = None - importance_score = None - if len(row) > 15: - zscore = float(row[15]) if row[15] else None - importance_score = float(row[16]) if row[16] else None - if extra_info: - zscore = zscore or extra_info.get('zscore') - importance_score = importance_score or extra_info.get('importance_score') + # 获取概念名称(优先从缓存,否则使用 concept_id) + concept_name = concept_names.get(concept_id) or concept_id + + # 计算涨停数量(从 limit_up_ratio 和 stock_count 估算) + limit_up_ratio = float(row[13]) if row[13] else 0 + stock_count = int(row[14]) if row[14] else 0 + limit_up_count = int(limit_up_ratio * stock_count) if stock_count > 0 else 0 alerts.append({ - 'concept_id': row[0], - 'concept_name': row[1], + 'concept_id': concept_id, + 'concept_name': concept_name, 'time': alert_time.strftime('%H:%M') if alert_time else None, 'timestamp': alert_time.isoformat() if alert_time else None, 'alert_type': row[3], - 'change_pct': float(row[4]) if row[4] else None, - 'change_delta': float(row[5]) if row[5] else None, - 'limit_up_count': row[6], - 'limit_up_delta': row[7], - 'rank_position': row[8], - 'index_price': float(row[9]) if row[9] else None, - 'index_change_pct': float(row[10]) if row[10] else None, - 'stock_count': row[11], - 'concept_type': row[12], - 'extra_info': extra_info, - 'prev_change_pct': float(row[14]) if len(row) > 14 and row[14] else None, - 'zscore': zscore, - 'importance_score': importance_score + 'final_score': float(row[4]) if row[4] else None, + 'rule_score': float(row[5]) if row[5] else None, + 'ml_score': float(row[6]) if row[6] else None, + 'trigger_reason': row[7], + 'alpha': float(row[8]) if row[8] else None, + 'alpha_delta': float(row[9]) if row[9] else None, + 'amt_ratio': float(row[10]) if row[10] else None, + 'amt_delta': float(row[11]) if row[11] else None, + 'rank_pct': float(row[12]) if row[12] else None, + 'limit_up_ratio': limit_up_ratio, + 'limit_up_count': limit_up_count, + 'stock_count': stock_count, + 'total_amt': float(row[15]) if row[15] else None, + 'triggered_rules': triggered_rules, + # 兼容旧字段 + 'importance_score': float(row[4]) / 100 if row[4] else None, }) + # 尝试批量获取概念名称 + if alerts: + concept_ids = list(set(a['concept_id'] for a in alerts)) + try: + from elasticsearch import Elasticsearch + es_client = Elasticsearch(['http://127.0.0.1:9200']) + es_result = es_client.mget( + index='concept_library_v3', + body={'ids': concept_ids}, + _source=['concept'] + ) + for doc in es_result.get('docs', []): + if doc.get('found') and doc.get('_source'): + concept_names[doc['_id']] = doc['_source'].get('concept', doc['_id']) + # 更新 alerts 中的概念名称 + for alert in alerts: + if alert['concept_id'] in concept_names: + alert['concept_name'] = concept_names[alert['concept_id']] + except Exception as e: + logger.warning(f"获取概念名称失败: {e}") + # 计算统计信息 day_high = max([d['price'] for d in index_timeline if d['price']], default=None) day_low = min([d['price'] for d in index_timeline if d['price']], default=None) @@ -12614,6 +12657,7 @@ def get_hotspot_overview(): 'surge_up': len([a for a in alerts if a['alert_type'] == 'surge_up']), 'surge_down': len([a for a in alerts if a['alert_type'] == 'surge_down']), 'limit_up': len([a for a in alerts if a['alert_type'] == 'limit_up']), + 'volume_spike': len([a for a in alerts if a['alert_type'] == 'volume_spike']), 'rank_jump': len([a for a in alerts if a['alert_type'] == 'rank_jump']) } }