diff --git a/app.py b/app.py index 617f508e..b231ea6c 100755 --- a/app.py +++ b/app.py @@ -12458,6 +12458,279 @@ def get_daily_top_concepts(): }), 500 +# ==================== 热点概览 API ==================== + +@app.route('/api/market/hotspot-overview', methods=['GET']) +def get_hotspot_overview(): + """ + 获取热点概览数据(用于个股中心的热点概览图表) + 返回:指数分时数据 + 概念异动标注 + """ + try: + trade_date = request.args.get('date') + index_code = request.args.get('index', '000001.SH') + + # 如果没有指定日期,使用最新交易日 + if not trade_date: + today = date.today() + if today in trading_days_set: + trade_date = today.strftime('%Y-%m-%d') + else: + target_date = get_trading_day_near_date(today) + trade_date = target_date.strftime('%Y-%m-%d') if target_date else today.strftime('%Y-%m-%d') + + # 1. 获取指数分时数据 + client = get_clickhouse_client() + target_date_obj = datetime.strptime(trade_date, '%Y-%m-%d').date() + + index_data = client.execute( + """ + SELECT timestamp, open, high, low, close, volume + FROM index_minute + WHERE code = %(code)s + AND toDate(timestamp) = %(date)s + ORDER BY timestamp + """, + { + 'code': index_code, + 'date': target_date_obj + } + ) + + # 获取昨收价 + code_no_suffix = index_code.split('.')[0] + prev_close = None + with engine.connect() as conn: + prev_result = conn.execute(text(""" + SELECT F006N FROM ea_exchangetrade + WHERE INDEXCODE = :code + AND TRADEDATE < :today + ORDER BY TRADEDATE DESC LIMIT 1 + """), { + 'code': code_no_suffix, + 'today': target_date_obj + }).fetchone() + if prev_result and prev_result[0]: + prev_close = float(prev_result[0]) + + # 格式化指数数据 + index_timeline = [] + for row in index_data: + ts, open_p, high_p, low_p, close_p, vol = row + change_pct = None + if prev_close and close_p: + change_pct = round((float(close_p) - prev_close) / prev_close * 100, 4) + + index_timeline.append({ + 'time': ts.strftime('%H:%M'), + 'timestamp': ts.isoformat(), + 'price': float(close_p) if close_p else None, + 'open': float(open_p) if open_p else None, + 'high': float(high_p) if high_p else None, + 'low': float(low_p) if low_p else None, + 'volume': int(vol) if vol else 0, + 'change_pct': change_pct + }) + + # 2. 获取概念异动数据 + alerts = [] + with engine.connect() as conn: + 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 + """), {'trade_date': trade_date}) + + for row in alert_result: + alert_time = row[2] + extra_info = None + if row[13]: + try: + extra_info = json.loads(row[13]) if isinstance(row[13], str) else row[13] + 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') + + alerts.append({ + 'concept_id': row[0], + 'concept_name': row[1], + '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 + }) + + # 计算统计信息 + 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) + latest_price = index_timeline[-1]['price'] if index_timeline else None + latest_change_pct = index_timeline[-1]['change_pct'] if index_timeline else None + + return jsonify({ + 'success': True, + 'data': { + 'trade_date': trade_date, + 'index': { + 'code': index_code, + 'name': '上证指数' if index_code == '000001.SH' else index_code, + 'prev_close': prev_close, + 'latest_price': latest_price, + 'change_pct': latest_change_pct, + 'high': day_high, + 'low': day_low, + 'timeline': index_timeline + }, + 'alerts': alerts, + 'alert_count': len(alerts), + 'alert_summary': { + 'surge': len([a for a in alerts if a['alert_type'] == 'surge']), + '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']), + 'rank_jump': len([a for a in alerts if a['alert_type'] == 'rank_jump']) + } + } + }) + + except Exception as e: + import traceback + logger.error(f"获取热点概览数据失败: {traceback.format_exc()}") + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + +@app.route('/api/market/concept-alerts', methods=['GET']) +def get_concept_alerts(): + """ + 获取概念异动列表(支持分页和筛选) + """ + try: + trade_date = request.args.get('date') + alert_type = request.args.get('type') # surge/limit_up/rank_jump + concept_type = request.args.get('concept_type') # leaf/lv1/lv2/lv3 + limit = request.args.get('limit', 50, type=int) + offset = request.args.get('offset', 0, type=int) + + # 构建查询条件 + conditions = [] + params = {'limit': limit, 'offset': offset} + + if trade_date: + conditions.append("trade_date = :trade_date") + params['trade_date'] = trade_date + else: + conditions.append("trade_date = CURDATE()") + + if alert_type: + conditions.append("alert_type = :alert_type") + params['alert_type'] = alert_type + + if concept_type: + conditions.append("concept_type = :concept_type") + params['concept_type'] = concept_type + + where_clause = " AND ".join(conditions) if conditions else "1=1" + + with engine.connect() as conn: + # 获取总数 + count_sql = text(f"SELECT COUNT(*) FROM concept_minute_alert WHERE {where_clause}") + total = conn.execute(count_sql, params).scalar() + + # 获取数据 + query_sql = text(f""" + SELECT + id, concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_price, index_change_pct, + stock_count, concept_type, extra_info + FROM concept_minute_alert + WHERE {where_clause} + ORDER BY alert_time DESC + LIMIT :limit OFFSET :offset + """) + + result = conn.execute(query_sql, params) + + alerts = [] + for row in result: + extra_info = None + if row[19]: + try: + extra_info = json.loads(row[19]) if isinstance(row[19], str) else row[19] + except: + pass + + alerts.append({ + 'id': row[0], + 'concept_id': row[1], + 'concept_name': row[2], + 'alert_time': row[3].isoformat() if row[3] else None, + 'alert_type': row[4], + 'trade_date': row[5].isoformat() if row[5] else None, + 'change_pct': float(row[6]) if row[6] else None, + 'prev_change_pct': float(row[7]) if row[7] else None, + 'change_delta': float(row[8]) if row[8] else None, + 'limit_up_count': row[9], + 'prev_limit_up_count': row[10], + 'limit_up_delta': row[11], + 'rank_position': row[12], + 'prev_rank_position': row[13], + 'rank_delta': row[14], + 'index_price': float(row[15]) if row[15] else None, + 'index_change_pct': float(row[16]) if row[16] else None, + 'stock_count': row[17], + 'concept_type': row[18], + 'extra_info': extra_info + }) + + return jsonify({ + 'success': True, + 'data': alerts, + 'total': total, + 'limit': limit, + 'offset': offset + }) + + except Exception as e: + import traceback + logger.error(f"获取概念异动列表失败: {traceback.format_exc()}") + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + @app.route('/api/market/rise-analysis/', methods=['GET']) def get_rise_analysis(seccode): """获取股票涨幅分析数据(从 Elasticsearch 获取)""" diff --git a/concept_alert_20251208.log b/concept_alert_20251208.log new file mode 100644 index 00000000..dffe4ef2 --- /dev/null +++ b/concept_alert_20251208.log @@ -0,0 +1,2823 @@ +2025-12-08 15:43:31,991 - INFO - ============================================================ +2025-12-08 15:43:31,991 - INFO - 🔄 开始回测: 2025-12-08 +2025-12-08 15:43:31,992 - INFO - ============================================================ +2025-12-08 15:43:57,527 - INFO - ============================================================ +2025-12-08 15:43:57,528 - INFO - 🔄 开始回测: 2025-12-08 +2025-12-08 15:43:57,528 - INFO - ============================================================ +2025-12-08 15:47:59,701 - INFO - ============================================================ +2025-12-08 15:47:59,701 - INFO - 🔄 开始回测: 2025-12-08 +2025-12-08 15:47:59,703 - INFO - ============================================================ +2025-12-08 15:47:59,803 - INFO - 已清除 2025-12-08 的已有数据 +2025-12-08 15:47:59,803 - INFO - 加载概念数据... +2025-12-08 15:48:00,021 - INFO - POST http://222.128.1.157:19200/concept_library_v3/_search?scroll=2m [status:200 duration:0.216s] +2025-12-08 15:48:00,218 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.192s] +2025-12-08 15:48:00,442 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.175s] +2025-12-08 15:48:00,656 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.210s] +2025-12-08 15:48:00,845 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.185s] +2025-12-08 15:48:01,025 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.177s] +2025-12-08 15:48:01,209 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.180s] +2025-12-08 15:48:01,377 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.164s] +2025-12-08 15:48:01,486 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.105s] +2025-12-08 15:48:01,496 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.007s] +2025-12-08 15:48:01,505 - INFO - DELETE http://222.128.1.157:19200/_search/scroll [status:200 duration:0.008s] +2025-12-08 15:48:01,505 - INFO - 获取到 865 个叶子概念 +2025-12-08 15:48:01,513 - INFO - 生成了 103 个母概念 +2025-12-08 15:48:01,513 - INFO - 总计 968 个概念 +2025-12-08 15:48:01,516 - INFO - 监控 5938 只股票 +2025-12-08 15:48:02,488 - INFO - 获取到 5132 个基准价格 +2025-12-08 15:48:02,498 - INFO - 指数昨收价: 3902.8076 +2025-12-08 15:48:02,667 - INFO - 找到 241 个分钟时间点 +2025-12-08 15:48:03,435 - ERROR - 保存异动失败: SpaceX - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '54d3a21ee5b8a15d2004b7350bf67e6d', 'concept_name': 'SpaceX', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 11.1678, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 10, 'prev_limit_up_count': 9, 'limit_up_delta': 1, 'rank_position': 2, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 19, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["002149", "688102", "603601", "002792", "300136", "000547", "000901", "688053", "605598", "301568"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,442 - ERROR - 保存异动失败: 光纤列阵单元FAU - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '7e7a80554a69b2ff892349f661a0e158', 'concept_name': '光纤列阵单元FAU', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 9.2138, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 3, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 10, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["688025", "688143", "300570"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,449 - ERROR - 保存异动失败: 海峡两岸福建 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'c0e5202a076100676c901fb6a6fe70fa', 'concept_name': '海峡两岸福建', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 7.3406, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 13, 'prev_limit_up_count': 11, 'limit_up_delta': 2, 'rank_position': 5, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 45, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300300", "000905", "000701", "000632", "000797", "002578", "002961", "300102", "600734", "002679", "002682", "002752", "603122"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,454 - ERROR - 保存异动失败: 蓝箭航天朱雀三号 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'd4e69baa804e9adf57bc8fb26807299f', 'concept_name': '蓝箭航天朱雀三号', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 7.0298, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 8, 'prev_limit_up_count': 7, 'limit_up_delta': 1, 'rank_position': 7, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 31, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["688102", "002792", "688333", "301005", "688539", "600343", "688270", "000547"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,465 - ERROR - 保存异动失败: 卫星互联网 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '9814b44dd4e4a09363dcfe5c374618f9', 'concept_name': '卫星互联网', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 6.8333, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 11, 'prev_limit_up_count': 8, 'limit_up_delta': 3, 'rank_position': 8, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 48, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300762", "688270", "003031", "300123", "301117", "688102", "688539", "688418", "688311", "300136", "000901"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,469 - ERROR - 保存异动失败: 商业航天卫星通信 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '2b570e6ae2e96a7b249dcb0ae68f512d', 'concept_name': '商业航天卫星通信', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 6.7523, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 18, 'prev_limit_up_count': 14, 'limit_up_delta': 4, 'rank_position': 9, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 83, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["600343", "688539", "688102", "301005", "688333", "688270", "300123", "300762", "301117", "002792", "300136", "688418", "688311", "002512", "300102", "000547", "000901", "920665"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,480 - ERROR - 保存异动失败: 星网 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '01bf110e2146b364400869c107c83451', 'concept_name': '星网', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 6.5314, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 17, 'prev_limit_up_count': 13, 'limit_up_delta': 4, 'rank_position': 10, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 83, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["600343", "688102", "301005", "688333", "688270", "300123", "300762", "301117", "002792", "300136", "688418", "688311", "002512", "300102", "000547", "000901", "920665"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,486 - ERROR - 保存异动失败: 光纤 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '56944fc3bac2fbf44215db12ad2b94b2', 'concept_name': '光纤', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 6.4413, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 11, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 14, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["600105", "688143", "300570"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,498 - ERROR - 保存异动失败: 商业航天 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '3be4e01f6112192e002c26b6a648e799', 'concept_name': '商业航天', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 6.3055, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 20, 'prev_limit_up_count': 16, 'limit_up_delta': 4, 'rank_position': 12, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 110, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["301005", "688539", "688102", "300900", "000547", "300762", "002977", "600343", "688333", "688270", "003031", "300123", "301117", "688418", "688311", "300136", "002512", "000901", "920665", "600151"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,503 - ERROR - 保存异动失败: 空芯光纤 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '973f2ac5c5a1a8770b83e34a8683659c', 'concept_name': '空芯光纤', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 6.2376, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 13, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 11, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["000070", "688143", "300570"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,551 - ERROR - 保存异动失败: 远程火力 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '271c4f3a775d854954c1a8ea24104821', 'concept_name': '远程火力', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 6.1789, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 11, 'prev_limit_up_count': 6, 'limit_up_delta': 5, 'rank_position': 14, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 57, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["688333", "688282", "688143", "688311", "300123", "002977", "688270", "300427", "603122", "002149", "600302"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,577 - ERROR - 保存异动失败: [二级] 陆军装备 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'e78c88c7728af7aa', 'concept_name': '[二级] 陆军装备', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 6.1789, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 11, 'prev_limit_up_count': 6, 'limit_up_delta': 5, 'rank_position': 15, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 57, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["002977", "688311", "688333", "300427", "688143", "600302", "603122", "688270", "002149", "300123", "688282"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,601 - ERROR - 保存异动失败: 手机直连卫星 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '2e8d597a0e6bd633695b947e683af5a7', 'concept_name': '手机直连卫星', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 6.1785, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 6, 'prev_limit_up_count': 4, 'limit_up_delta': 2, 'rank_position': 16, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 21, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300762", "002977", "688311", "001270", "688270", "688418"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,624 - ERROR - 保存异动失败: 水下军工 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'ddf3e3a0c99aa84b88024fd5e0a2fa6b', 'concept_name': '水下军工', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 5.7954, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 2, 'limit_up_delta': 2, 'rank_position': 19, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 27, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["002149", "688143", "688282", "300123"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,630 - ERROR - 保存异动失败: 军工水面水下作战 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '26870549d3469d8b016d58878a4a1694', 'concept_name': '军工水面水下作战', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 5.4306, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 22, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 28, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["002149", "688143", "688282"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,636 - ERROR - 保存异动失败: 核聚变超导 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '9e0ad5f3de553a074aca639d13094d76', 'concept_name': '核聚变超导', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 5.2595, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 6, 'prev_limit_up_count': 5, 'limit_up_delta': 1, 'rank_position': 26, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 43, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["600105", "920576", "601106", "601399", "002149", "603015"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,646 - ERROR - 保存异动失败: 福建国资 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'b59679d54c060fe313b16d6917d9ca93', 'concept_name': '福建国资', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 5.1542, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 8, 'prev_limit_up_count': 6, 'limit_up_delta': 2, 'rank_position': 27, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 50, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["000905", "600734", "000797", "000701", "002682", "002679", "300300", "000632"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,654 - ERROR - 保存异动失败: 光通信CPO - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '0b20095da20380417829e5484feac90b', 'concept_name': '光通信CPO', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 5.1014, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 3, 'limit_up_delta': 1, 'rank_position': 28, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 32, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["600105", "688048", "300570", "000070"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,660 - ERROR - 保存异动失败: 军工信息化 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '66bd549e8504be635ff0bbad8ce930e0', 'concept_name': '军工信息化', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 4.9328, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 15, 'prev_limit_up_count': 12, 'limit_up_delta': 3, 'rank_position': 33, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 135, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["002977", "300427", "300123", "001270", "000547", "688311", "300762", "688081", "000070", "688270", "301117", "688539", "600973", "300900", "300560"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,666 - ERROR - 保存异动失败: 可控核聚变 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'f35622ab5c0d533e0c8da487df982c05', 'concept_name': '可控核聚变', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 4.8832, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 9, 'prev_limit_up_count': 8, 'limit_up_delta': 1, 'rank_position': 35, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 73, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["600105", "600973", "688167", "603015", "601399", "688102", "920576", "601106", "002149"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,690 - ERROR - 保存异动失败: 光通信 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'dd05cdaa409ff3dafb3fe19bedbeaa87', 'concept_name': '光通信', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 4.6474, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 3, 'limit_up_delta': 1, 'rank_position': 39, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 36, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300570", "600105", "688048", "000070"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,712 - ERROR - 保存异动失败: 北斗信使 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '1808e46d4965b2f8272846d77025716e', 'concept_name': '北斗信使', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 4.5627, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 41, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 15, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["000901", "002512"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,717 - ERROR - 保存异动失败: [二级] 无人作战与信息化 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'ab0a7a074639080b', 'concept_name': '[二级] 无人作战与信息化', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 4.4116, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 17, 'prev_limit_up_count': 14, 'limit_up_delta': 3, 'rank_position': 44, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 200, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["688311", "301117", "300560", "002977", "000901", "300900", "688270", "002149", "600973", "001270", "300123", "688539", "300427", "000070", "000547", "688081", "300762"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,722 - ERROR - 保存异动失败: [二级] 商业航天 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '3342079af14780e7', 'concept_name': '[二级] 商业航天', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 4.2974, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 27, 'prev_limit_up_count': 23, 'limit_up_delta': 4, 'rank_position': 50, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 238, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["688333", "003031", "300136", "600343", "002977", "000901", "300900", "688270", "001270", "300123", "688539", "688102", "300102", "002512", "002792", "300751", "688311", "301117", "002565", "688418", "301005", "688282", "605598", "000547", "300762", "920665", "600151"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,732 - ERROR - 保存异动失败: 信息支援概念整理 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '42e3093f9fc99b4a86374ac455873b9b', 'concept_name': '信息支援概念整理', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 4.1763, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 1, 'prev_limit_up_count': 0, 'limit_up_delta': 1, 'rank_position': 54, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 24, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["688311"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,737 - ERROR - 保存异动失败: 军工 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '58ac1fb9ce375d4849893453f9615b49', 'concept_name': '军工', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 4.1592, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 10, 'prev_limit_up_count': 8, 'limit_up_delta': 2, 'rank_position': 56, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 168, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["002977", "000547", "000901", "600343", "688143", "002149", "688333", "688311", "300337", "300123"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,745 - ERROR - 保存异动失败: 珠海航展 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'd02ad5be74f6a9c53fb28291a961621c', 'concept_name': '珠海航展', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 4.1462, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 5, 'prev_limit_up_count': 3, 'limit_up_delta': 2, 'rank_position': 57, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 44, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["600343", "000901", "300900", "688311", "300123"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,750 - ERROR - 保存异动失败: 6G概念 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '863500a9c8b544d099618a6f355ccfff', 'concept_name': '6G概念', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 4.1245, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 9, 'prev_limit_up_count': 5, 'limit_up_delta': 4, 'rank_position': 59, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 80, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["002093", "002792", "300136", "688270", "300123", "300570", "000547", "300560", "002512"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,770 - ERROR - 保存异动失败: 磁悬浮压缩机 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '6ae1f6de3a42693cb613896ba7ce8173', 'concept_name': '磁悬浮压缩机', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 4.1143, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 61, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 14, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["002158", "000811"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,775 - ERROR - 保存异动失败: [二级] 海军装备 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '36d8fd5f5f85dffb', 'concept_name': '[二级] 海军装备', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 4.0227, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 9, 'prev_limit_up_count': 6, 'limit_up_delta': 3, 'rank_position': 62, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 99, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["300337", "688143", "688282", "601696", "002512", "000547", "601106", "002149", "300123"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,780 - ERROR - 保存异动失败: [一级] 国防军工 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'b2cd2902316a116f', 'concept_name': '[一级] 国防军工', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 4.0186, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 30, 'prev_limit_up_count': 24, 'limit_up_delta': 6, 'rank_position': 63, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 373, 'concept_type': 'lv1', 'extra_info': '{"limit_up_stocks": ["688333", "600343", "300560", "002977", "000901", "300900", "600302", "002682", "688270", "002149", "600973", "001270", "300123" ... (23 characters truncated) ... 300337", "002512", "688311", "301117", "688143", "601696", "603122", "603601", "688282", "000070", "000547", "601106", "688081", "300762", "600151"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,786 - ERROR - 保存异动失败: 军用无人机反无人机 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '388136e7bee429da7f0026a26acc953f', 'concept_name': '军用无人机反无人机', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 4.0027, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 67, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 38, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300900", "300123"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,814 - ERROR - 保存异动失败: 核电产业链 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '41e18cd109f4f15cd0716e3c6d9955dd', 'concept_name': '核电产业链', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.9503, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 7, 'prev_limit_up_count': 6, 'limit_up_delta': 1, 'rank_position': 69, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 86, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["600973", "600105", "601399", "603015", "920576", "601106", "688167"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,845 - ERROR - 保存异动失败: [二级] 综合与主题 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '8dd8fe507c554405', 'concept_name': '[二级] 综合与主题', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.936, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 13, 'prev_limit_up_count': 11, 'limit_up_delta': 2, 'rank_position': 71, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 204, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["688311", "301117", "688333", "688143", "600343", "300560", "002977", "000901", "002149", "300123", "300337", "000547", "600151"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,851 - ERROR - 保存异动失败: 航母福建舰240430 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '67d2e2df29052090e9954abeb00d517d', 'concept_name': '航母福建舰240430', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.835, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 3, 'limit_up_delta': 1, 'rank_position': 74, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 40, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300337", "601106", "000547", "002512"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,886 - ERROR - 保存异动失败: 铜连接 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '56e10d2abdf5ceb6f13e200e85fef36e', 'concept_name': '铜连接', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.7676, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 79, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 23, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["002792", "600973", "300570"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,915 - ERROR - 保存异动失败: 博通交换机 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '6f778df3439781376d8c908f8d2b2276', 'concept_name': '博通交换机', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.7567, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 80, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 22, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300570", "301486"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,937 - ERROR - 保存异动失败: 卫星出海 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '69d1d811ac734aa07561db06a141bc16', 'concept_name': '卫星出海', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.6151, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 2, 'limit_up_delta': 2, 'rank_position': 91, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 48, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["002792", "300762", "300123", "002512"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,942 - ERROR - 保存异动失败: 量子科技产业链 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'f7daaa03d8415d499ca82a4d70ead074', 'concept_name': '量子科技产业链', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.5094, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 2, 'limit_up_delta': 2, 'rank_position': 100, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 63, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["301117", "300123", "002512", "688418"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,949 - ERROR - 保存异动失败: 无人机蜂群 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '34f096164ed77a917364457c58fb7b18', 'concept_name': '无人机蜂群', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.484, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 102, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 24, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["688311", "301117", "000901"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,955 - ERROR - 保存异动失败: 超聚变 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '78a0c55f302a623ae81994e671989229', 'concept_name': '超聚变', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.3262, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 112, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 36, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300290", "600105"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,962 - ERROR - 保存异动失败: 对日反制 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'f989d4184984a32d90fa74385399c3eb', 'concept_name': '对日反制', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.3087, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 12, 'prev_limit_up_count': 11, 'limit_up_delta': 1, 'rank_position': 113, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 155, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["688323", "000859", "003031", "002585", "000547", "002682", "300560", "002083", "603122", "603067", "300427", "300123"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,970 - ERROR - 保存异动失败: 庭院割草机器人 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '7e531a981bb00d2127f93503ebd29036', 'concept_name': '庭院割草机器人', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.2773, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 119, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 13, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["301125", "002779"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:03,995 - ERROR - 保存异动失败: 华为 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'd63e7ea059dc68e97e2ef16531e47e6c', 'concept_name': '华为', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.2412, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 1, 'limit_up_delta': 2, 'rank_position': 122, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 40, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300290", "301125", "688031"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,001 - ERROR - 保存异动失败: 北斗导航 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'e1cb2d2097ad5505e52914eea10fb93f', 'concept_name': '北斗导航', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.2333, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 123, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 15, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["688282", "000901"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,007 - ERROR - 保存异动失败: 深海经济 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '62c1ff95b594873a91665479c74fa5d5', 'concept_name': '深海经济', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.2094, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 130, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 57, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["920576", "002149"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,014 - ERROR - 保存异动失败: 低空经济亿航智能 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '9e56e5451682848c3c8a440306122ea7', 'concept_name': '低空经济亿航智能', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.1804, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 3, 'limit_up_delta': 1, 'rank_position': 133, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 81, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300900", "002660", "002682", "000701"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,021 - ERROR - 保存异动失败: 海军 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '10bb72970f8ef3c8801898e342d47942', 'concept_name': '海军', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.1182, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 1, 'prev_limit_up_count': 0, 'limit_up_delta': 1, 'rank_position': 140, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 26, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300123"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,034 - ERROR - 保存异动失败: IPV6 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '3fbcd320695e3a01273994471d09cc36', 'concept_name': 'IPV6', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.1161, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 3, 'limit_up_delta': 1, 'rank_position': 142, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 35, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["002885", "688282", "300560", "301529"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,043 - ERROR - 保存异动失败: 国产航母 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '41a3d2f7a25c41eaa2c2e48a520bc380', 'concept_name': '国产航母', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.1147, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 143, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 25, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["601696", "300123"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,056 - ERROR - 保存异动失败: 台资企业 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '814666d2762595328dfddbd3546a1fbf', 'concept_name': '台资企业', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.109, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 6, 'prev_limit_up_count': 5, 'limit_up_delta': 1, 'rank_position': 145, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 54, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["603122", "603886", "603015", "002158", "002578", "002752"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,062 - ERROR - 保存异动失败: 机器人零部件加工设备 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '480f22ef7a1876fb952bbb51afc87210', 'concept_name': '机器人零部件加工设备', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.1047, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 147, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 28, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["688577", "301125", "002779"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,071 - ERROR - 保存异动失败: 量子科技 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '41381b4a238daead45abe7866364e44e', 'concept_name': '量子科技', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.0729, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 2, 'limit_up_delta': 2, 'rank_position': 151, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 70, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["688418", "002512", "300123", "301117"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,078 - ERROR - 保存异动失败: 超聚变借壳预期 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'e3f9eb598bd782998ef4d63abf7f914b', 'concept_name': '超聚变借壳预期', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.0691, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 0, 'limit_up_delta': 2, 'rank_position': 152, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 15, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300290", "600302"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,101 - ERROR - 保存异动失败: 5G毫米波 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '331c00a813c599fef1ad0ad65cf90631', 'concept_name': '5G毫米波', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.0402, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 5, 'prev_limit_up_count': 4, 'limit_up_delta': 1, 'rank_position': 155, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 40, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["002792", "300136", "003031", "688270", "002977"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,117 - ERROR - 保存异动失败: [二级] 量子科技 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '24435b1fd612ff3d', 'concept_name': '[二级] 量子科技', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.0325, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 5, 'prev_limit_up_count': 3, 'limit_up_delta': 2, 'rank_position': 158, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 95, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["301117", "300123", "002512", "688418", "002149"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,124 - ERROR - 保存异动失败: [一级] 前沿科技 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '87da2b6fbbcb3e33', 'concept_name': '[一级] 前沿科技', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 3.0325, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 5, 'prev_limit_up_count': 3, 'limit_up_delta': 2, 'rank_position': 159, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 95, 'concept_type': 'lv1', 'extra_info': '{"limit_up_stocks": ["301117", "300123", "002512", "688418", "002149"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,133 - ERROR - 保存异动失败: 低空经济产业链汇集 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '68aa3c7715416e1ffa19f237d0f95961', 'concept_name': '低空经济产业链汇集', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.9994, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 167, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 122, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["000701", "000547", "002792"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,139 - ERROR - 保存异动失败: [一级] 空天经济 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'd1f5d74529f14e29', 'concept_name': '[一级] 空天经济', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.9293, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 37, 'prev_limit_up_count': 31, 'limit_up_delta': 6, 'rank_position': 177, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 690, 'concept_type': 'lv1', 'extra_info': '{"limit_up_stocks": ["300136", "000901", "300900", "002682", "688539", "300102", "002512", "002660", "603601", "301005", "605598", "300570", "920665" ... (93 characters truncated) ... 688102", "002792", "300751", "688311", "301117", "002565", "688418", "600105", "002779", "002093", "688282", "000070", "000547", "600151", "300762"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,147 - ERROR - 保存异动失败: [三级] 核能 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'b44eaa65d1c2425d', 'concept_name': '[三级] 核能', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.91, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 12, 'prev_limit_up_count': 9, 'limit_up_delta': 3, 'rank_position': 178, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 232, 'concept_type': 'lv3', 'extra_info': '{"limit_up_stocks": ["002158", "920576", "600973", "002149", "688102", "600105", "601399", "300290", "000811", "603015", "601106", "688167"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,161 - ERROR - 保存异动失败: [二级] 通信技术 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '22330358ccd65aeb', 'concept_name': '[二级] 通信技术', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.9016, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 14, 'prev_limit_up_count': 10, 'limit_up_delta': 4, 'rank_position': 182, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 189, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["003031", "300136", "300560", "002977", "688270", "300123", "600105", "002093", "000070", "002512", "002792", "000547", "300762", "300570"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,184 - ERROR - 保存异动失败: AEBS - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '4cd10b5ab4805603cd2d7a34f6f6e6b2', 'concept_name': 'AEBS', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.8962, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 1, 'prev_limit_up_count': 0, 'limit_up_delta': 1, 'rank_position': 184, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 38, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300123"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,189 - ERROR - 保存异动失败: 低空经济 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '6c5f0e25b757e8286f7cb93330b36dc8', 'concept_name': '低空经济', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.765, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 3, 'limit_up_delta': 1, 'rank_position': 199, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 120, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300900", "002660", "002682", "000701"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,210 - ERROR - 保存异动失败: 冰雪经济 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '1cf3a25cd1c90c27310c4610d4a3743f', 'concept_name': '冰雪经济', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.7636, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 200, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 44, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["000811", "002158", "605299"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,227 - ERROR - 保存异动失败: 英伟达概念 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '650c2a8d881ff609d809cab6379d1593', 'concept_name': '英伟达概念', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.7475, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 202, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 76, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300570", "301117"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,259 - ERROR - 保存异动失败: 养老机器人 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '30af5b5e3a1ec3a15d21497e9b0399e4', 'concept_name': '养老机器人', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.7411, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 1, 'prev_limit_up_count': 0, 'limit_up_delta': 1, 'rank_position': 203, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 28, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300290"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,267 - ERROR - 保存异动失败: 信创概念 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'aeac02717442e5a225bcae32103ee6a4', 'concept_name': '信创概念', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.7404, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 204, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 72, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["301218", "688031"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,294 - ERROR - 保存异动失败: [三级] AI关键组件 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'd125ffc194b42ee4', 'concept_name': '[三级] AI关键组件', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.7179, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 19, 'prev_limit_up_count': 18, 'limit_up_delta': 1, 'rank_position': 212, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 360, 'concept_type': 'lv3', 'extra_info': '{"limit_up_stocks": ["688048", "600973", "603386", "300102", "688025", "002792", "300751", "688143", "002565", "301526", "603122", "301183", "600366", "301486", "688383", "600105", "000070", "300570", "688167"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,300 - ERROR - 保存异动失败: 关键软件 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '7a7fd726a54670b2b3c8bff3b778acf4', 'concept_name': '关键软件', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.705, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 214, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 103, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["688031", "301218"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,314 - ERROR - 保存异动失败: 数据中心液冷 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'f8bbd66624a99da406ceb891aeb6eb3a', 'concept_name': '数据中心液冷', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.6955, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 217, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 65, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["000811", "002158"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,325 - ERROR - 保存异动失败: 国产信创概览 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '22a45db024b3e7d118d2810c2e7bc5bd', 'concept_name': '国产信创概览', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.6884, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 219, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 101, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["688031", "301218"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,336 - ERROR - 保存异动失败: AI-细分延伸更新 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'c6796e52492170dfc6234c299cbc095b', 'concept_name': 'AI-细分延伸更新', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.6801, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 220, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 63, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["301526", "002158", "000811"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,341 - ERROR - 保存异动失败: [三级] AI配套设施 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '986593c763801dcc', 'concept_name': '[三级] AI配套设施', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.6794, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 7, 'prev_limit_up_count': 6, 'limit_up_delta': 1, 'rank_position': 222, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 297, 'concept_type': 'lv3', 'extra_info': '{"limit_up_stocks": ["688333", "002158", "002885", "688102", "000811", "002660", "600218"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,347 - ERROR - 保存异动失败: [三级] 制造工艺与设备 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '67341e0ae4ef7dac', 'concept_name': '[三级] 制造工艺与设备', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.6481, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 3, 'limit_up_delta': 1, 'rank_position': 228, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 70, 'concept_type': 'lv3', 'extra_info': '{"limit_up_stocks": ["300136", "002779", "688577", "301125"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,366 - ERROR - 保存异动失败: Minimax - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '380c18e239f03cf9c0a846c7defd029b', 'concept_name': 'Minimax', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.6426, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 229, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 28, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["688383", "605303"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,372 - ERROR - 保存异动失败: 章盟主概念股 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '0ebd312797bf29853df2fb0f54ab9b72', 'concept_name': '章盟主概念股', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.6389, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 1, 'prev_limit_up_count': 0, 'limit_up_delta': 1, 'rank_position': 230, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 22, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["688311"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,377 - ERROR - 保存异动失败: 马斯克Grok3大模型 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'df7573bd0405ab31be0762125a92fdec', 'concept_name': '马斯克Grok3大模型', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.6051, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 243, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 37, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300570", "688661"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,399 - ERROR - 保存异动失败: 华为Pura70 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '7b30055f26aa31e887125520d038e8c9', 'concept_name': '华为Pura70', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.5615, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 3, 'limit_up_delta': 1, 'rank_position': 248, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 68, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300136", "688311", "301183", "688048"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,404 - ERROR - 保存异动失败: 次新股 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '50371a2c5078b757a8f8c75b8877e815', 'concept_name': '次新股', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.5459, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 249, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 51, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["301526", "301568"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,410 - ERROR - 保存异动失败: 华为昇腾 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '1287f5d7e9279690c27706d91bf3000b', 'concept_name': '华为昇腾', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.5455, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 250, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 44, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["301125", "688031"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,415 - ERROR - 保存异动失败: 天太机器人 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '60bd1b38447575a0469438b78c702219', 'concept_name': '天太机器人', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.5369, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 251, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 29, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300503", "688160", "300570"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,421 - ERROR - 保存异动失败: 中俄贸易 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '58e58336a55c82ef6e6917be068b2892', 'concept_name': '中俄贸易', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.515, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 3, 'limit_up_delta': 1, 'rank_position': 254, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 72, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300900", "000901", "601106", "301125"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,433 - ERROR - 保存异动失败: [二级] 信创与自主可控 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'df470e2b63c29363', 'concept_name': '[二级] 信创与自主可控', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.4885, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 9, 'prev_limit_up_count': 8, 'limit_up_delta': 1, 'rank_position': 262, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 286, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["301218", "003031", "300136", "688292", "688031", "002792", "301117", "688418", "688081"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,438 - ERROR - 保存异动失败: 整车央企重组 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'c70db13b77d825b0fb1b49cbceae1884', 'concept_name': '整车央企重组', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.4477, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 268, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 34, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["601399", "600302"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,463 - ERROR - 保存异动失败: [二级] 低空经济 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '77979791fa471afe', 'concept_name': '[二级] 低空经济', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.4077, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 7, 'prev_limit_up_count': 6, 'limit_up_delta': 1, 'rank_position': 279, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 349, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["300900", "002682", "000701", "002792", "002660", "002779", "000547"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,469 - ERROR - 保存异动失败: 玻璃基板 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '6a20715a0e59c87740a6bacf5dcdd797', 'concept_name': '玻璃基板', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.4031, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 1, 'prev_limit_up_count': 0, 'limit_up_delta': 1, 'rank_position': 281, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 49, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["301568"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,476 - ERROR - 保存异动失败: [二级] 机器人产业链 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '2e2d410b95c1d2af', 'concept_name': '[二级] 机器人产业链', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.3695, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 11, 'prev_limit_up_count': 10, 'limit_up_delta': 1, 'rank_position': 292, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 311, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["300136", "688160", "301125", "300421", "301526", "301529", "301005", "002779", "300503", "603015", "688577"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,482 - ERROR - 保存异动失败: 设备更新 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'e3f7d1e5f5ea7749d173f9c3d50b494a', 'concept_name': '设备更新', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.3619, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 294, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 70, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["600302", "301125", "603122"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,487 - ERROR - 保存异动失败: [二级] AI基础设施 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'a143fdd34018ba6f', 'concept_name': '[二级] AI基础设施', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.3613, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 30, 'prev_limit_up_count': 28, 'limit_up_delta': 2, 'rank_position': 295, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 875, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["002158", "688031", "600973", "300102", "688025", "688143", "002660", "301486", "300290", "605598", "300570", "688167", "688333" ... (23 characters truncated) ... 603386", "688102", "000811", "002792", "300751", "002565", "301526", "600218", "603122", "301183", "600366", "688383", "600105", "000070", "000547"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,497 - ERROR - 保存异动失败: 神经网络 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '40850ccab8cdabc6868888fd85808d0e', 'concept_name': '神经网络', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.3377, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 304, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 30, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["301568", "688081"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,504 - ERROR - 保存异动失败: [二级] 华为产业链 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'a7e8f7a5548a002c', 'concept_name': '[二级] 华为产业链', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.3065, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 17, 'prev_limit_up_count': 16, 'limit_up_delta': 1, 'rank_position': 312, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 498, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["300136", "920592", "688031", "300337", "600734", "300290", "605299", "003031", "688048", "688292", "002885", "002792", "301125", "688311", "301183", "600366", "688383"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,525 - ERROR - 保存异动失败: 工业母机 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '5374db0880e238ea30333257c7707b1f', 'concept_name': '工业母机', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.2962, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 315, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 47, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300503", "600302"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,533 - ERROR - 保存异动失败: [三级] 技术与生态 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '13c15b670f35e095', 'concept_name': '[三级] 技术与生态', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.2884, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 12, 'prev_limit_up_count': 11, 'limit_up_delta': 1, 'rank_position': 317, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 368, 'concept_type': 'lv3', 'extra_info': '{"limit_up_stocks": ["605299", "003031", "300136", "688292", "920592", "688031", "002885", "002792", "301125", "600734", "688383", "300290"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,559 - ERROR - 保存异动失败: [二级] 清洁能源 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '9c928eb8da4b6b99', 'concept_name': '[二级] 清洁能源', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.2863, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 15, 'prev_limit_up_count': 12, 'limit_up_delta': 3, 'rank_position': 318, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 486, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["002158", "920576", "600973", "002149", "688102", "000811", "688025", "300751", "600105", "601399", "300290", "605598", "603015", "601106", "688167"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,564 - ERROR - 保存异动失败: 谷歌概念 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'b0af7bb745342ddb825256f2e6dc07cc', 'concept_name': '谷歌概念', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.2787, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 320, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 74, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["688167", "300570", "000070"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,589 - ERROR - 保存异动失败: 机器人-神经网络 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'f550f0183f520fd786dca9c73ae3932d', 'concept_name': '机器人-神经网络', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.275, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 322, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 28, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["301568", "002779", "300503"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,594 - ERROR - 保存异动失败: 电子束光刻机“羲之” - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '27e92f2be93742878116b1099c6b9c84', 'concept_name': '电子束光刻机“羲之”', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.2563, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 1, 'prev_limit_up_count': 0, 'limit_up_delta': 1, 'rank_position': 329, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 22, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["301568"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,612 - ERROR - 保存异动失败: 华为Mate70手表 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '0370e22b8849e09f85840b850ff43ff9', 'concept_name': '华为Mate70手表', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.2508, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 3, 'limit_up_delta': 1, 'rank_position': 332, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 95, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300136", "300337", "301183", "688311"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,619 - ERROR - 保存异动失败: [一级] 新能源与电力 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'cedd5e74f7572b73', 'concept_name': '[一级] 新能源与电力', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.2458, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 27, 'prev_limit_up_count': 25, 'limit_up_delta': 2, 'rank_position': 334, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 1054, 'concept_type': 'lv1', 'extra_info': '{"limit_up_stocks": ["300136", "002158", "002585", "600973", "002149", "300427", "688025", "300982", "002660", "603067", "688499", "300062", "300290", "605598", "688167", "301468", "920576", "002885", "688102", "000811", "002083", "300751", "002300", "600105", "601399", "603015", "601106"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,625 - ERROR - 保存异动失败: [一级] 数字经济与金融科技 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '4a5f6ee0da8e5802', 'concept_name': '[一级] 数字经济与金融科技', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.2312, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 11, 'prev_limit_up_count': 10, 'limit_up_delta': 1, 'rank_position': 343, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 440, 'concept_type': 'lv1', 'extra_info': '{"limit_up_stocks": ["301218", "688292", "920592", "688031", "002885", "300377", "002961", "600734", "688282", "688081", "300762"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,635 - ERROR - 保存异动失败: [一级] 机器人 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'a21131132b7ae4ba', 'concept_name': '[一级] 机器人', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.2178, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 20, 'prev_limit_up_count': 16, 'limit_up_delta': 4, 'rank_position': 351, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 671, 'concept_type': 'lv1', 'extra_info': '{"limit_up_stocks": ["300136", "688160", "002682", "300421", "301005", "300503", "688577", "300570", "002589", "002885", "301125", "605287", "301526", "301529", "600366", "600105", "002779", "603015", "301568", "600151"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,642 - ERROR - 保存异动失败: [一级] 消费电子 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '540bff395b82f2c9', 'concept_name': '[一级] 消费电子', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.1646, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 27, 'prev_limit_up_count': 26, 'limit_up_delta': 1, 'rank_position': 370, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 804, 'concept_type': 'lv1', 'extra_info': '{"limit_up_stocks": ["300136", "920592", "688031", "300337", "688025", "600734", "002660", "301486", "300290", "688323", "688167", "605299", "688333", "003031", "688048", "688292", "002885", "000859", "002792", "301125", "688311", "301529", "301183", "600366", "688383", "002779", "688661"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,651 - ERROR - 保存异动失败: [二级] 产业升级与制造 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '5bd863c58a5c878e', 'concept_name': '[二级] 产业升级与制造', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.15, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 3, 'limit_up_delta': 1, 'rank_position': 378, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 175, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["600302", "603122", "300503", "301125"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,655 - ERROR - 保存异动失败: 新型离岸贸易 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'c4fa4f91f9fb625419f317690ea59800', 'concept_name': '新型离岸贸易', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.1436, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 380, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 40, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["000701", "000797"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,687 - ERROR - 保存异动失败: 跨境数据数据要素 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '209d102f174e89a82bfcabc05fbc7a94', 'concept_name': '跨境数据数据要素', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.141, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 5, 'prev_limit_up_count': 4, 'limit_up_delta': 1, 'rank_position': 381, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 173, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["600734", "600734", "688031", "688292", "301218"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,694 - ERROR - 保存异动失败: 杭州六小龙-群核科技 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '43db71cecaa96d2bd95738daa1ea5b29', 'concept_name': '杭州六小龙-群核科技', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.1323, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 388, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 84, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300749", "688539", "301218"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,700 - ERROR - 保存异动失败: [三级] 通用生态 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '858106800942746c', 'concept_name': '[三级] 通用生态', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.1289, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 6, 'prev_limit_up_count': 5, 'limit_up_delta': 1, 'rank_position': 390, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 292, 'concept_type': 'lv3', 'extra_info': '{"limit_up_stocks": ["301117", "300503", "000070", "688661", "300570", "688167"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,711 - ERROR - 保存异动失败: [二级] 人形机器人整机 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '5cec0ba414c30f8c', 'concept_name': '[二级] 人形机器人整机', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.1162, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 11, 'prev_limit_up_count': 9, 'limit_up_delta': 2, 'rank_position': 396, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 405, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["688160", "002682", "605287", "300421", "301529", "600105", "301005", "002779", "300503", "688577", "300570"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,744 - ERROR - 保存异动失败: [二级] 机器人软件与AI - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '2c291cfeba422359', 'concept_name': '[二级] 机器人软件与AI', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.1084, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 397, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 42, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["002779", "300503", "301568"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,767 - ERROR - 保存异动失败: 并购重组 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '91dd748e27516c02c3f85115558c0cfb', 'concept_name': '并购重组', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.0698, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 5, 'prev_limit_up_count': 4, 'limit_up_delta': 1, 'rank_position': 411, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 149, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["000905", "300102", "300290", "002779", "003031"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,792 - ERROR - 保存异动失败: 外贸出口 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '3f3c9e9578d4a4189f8846eb48f1e0a0', 'concept_name': '外贸出口', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.0469, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 424, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 40, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["002083", "605598", "000701"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,800 - ERROR - 保存异动失败: 松延动力机器人 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'a84916ad3c4a624071692dc8d59f0c33', 'concept_name': '松延动力机器人', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.0434, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 1, 'prev_limit_up_count': 0, 'limit_up_delta': 1, 'rank_position': 426, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 31, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["605287"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,807 - ERROR - 保存异动失败: 化工有色元素周期表 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'e2e105e80fa3fe99d2d18494ae8dad9c', 'concept_name': '化工有色元素周期表', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.0119, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 443, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 122, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["002578", "603067"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,813 - ERROR - 保存异动失败: [二级] 并购重组 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '128a4a614ab175f9', 'concept_name': '[二级] 并购重组', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 2.002, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 13, 'prev_limit_up_count': 11, 'limit_up_delta': 2, 'rank_position': 446, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 555, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["300136", "688160", "300102", "601696", "000035", "301486", "300290", "003031", "600302", "000905", "301183", "002779", "688661"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,840 - ERROR - 保存异动失败: 华为鸿蒙 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '70c532af1fb28f41d335e8ed8fab7039', 'concept_name': '华为鸿蒙', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.9991, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 1, 'prev_limit_up_count': 0, 'limit_up_delta': 1, 'rank_position': 450, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 65, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300290"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,858 - ERROR - 保存异动失败: 央国企AI一张图 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '5072e741fffe4d28ad32bd51710390de', 'concept_name': '央国企AI一张图', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.9947, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 1, 'prev_limit_up_count': 0, 'limit_up_delta': 1, 'rank_position': 452, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 90, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300290"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,875 - ERROR - 保存异动失败: [二级] 数据要素 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '8cac77fc45376080', 'concept_name': '[二级] 数据要素', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.985, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 6, 'prev_limit_up_count': 5, 'limit_up_delta': 1, 'rank_position': 459, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 305, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["301218", "688292", "688031", "002885", "600734", "688282"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,882 - ERROR - 保存异动失败: [二级] 国企改革与市值管理 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '31eaecf0a6fa349b', 'concept_name': '[二级] 国企改革与市值管理', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.9848, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 21, 'prev_limit_up_count': 16, 'limit_up_delta': 5, 'rank_position': 460, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 821, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["000901", "002682", "000663", "000632", "600734", "300290", "002679", "003031", "600343", "000797", "600302", "000701", "000859", "300300", "000905", "600218", "601399", "000070", "000547", "601106", "600151"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,899 - ERROR - 保存异动失败: [二级] AI综合与趋势 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '91ac773adb0a2fc8', 'concept_name': '[二级] AI综合与趋势', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.9603, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 3, 'limit_up_delta': 1, 'rank_position': 471, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 190, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["301526", "002158", "688031", "000811"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,905 - ERROR - 保存异动失败: DeepSeek - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '264d9bb481d458fdda428e70a2bfe2dd', 'concept_name': 'DeepSeek', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.9386, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 477, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 53, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300290", "603122", "688031"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,914 - ERROR - 保存异动失败: 政务云政务IT - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'dcd9ab454e81d760bec327f3af236501', 'concept_name': '政务云政务IT', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.9367, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 1, 'prev_limit_up_count': 0, 'limit_up_delta': 1, 'rank_position': 478, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 55, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["301218"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,934 - ERROR - 保存异动失败: [一级] 政策与主题 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'c69c28ca187d7096', 'concept_name': '[一级] 政策与主题', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.9353, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 54, 'prev_limit_up_count': 48, 'limit_up_delta': 6, 'rank_position': 480, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 2332, 'concept_type': 'lv1', 'extra_info': '{"limit_up_stocks": ["301218", "300102", "600734", "003031", "301125", "002565", "000905", "301183", "601399", "002779", "688160", "002682", "688025" ... (263 characters truncated) ... 002149", "001360", "000632", "002679", "688292", "002589", "920576", "000859", "002702", "301117", "603122", "603696", "601106", "688661", "600151"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,940 - ERROR - 保存异动失败: [二级] 国家战略 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '176e364167a853eb', 'concept_name': '[二级] 国家战略', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.9087, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 9, 'prev_limit_up_count': 8, 'limit_up_delta': 1, 'rank_position': 489, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 369, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["920576", "002149", "688102", "001360", "688025", "002565", "002300", "603696", "688081"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,964 - ERROR - 保存异动失败: [二级] 区域发展与自贸区 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '4153baee9169db33', 'concept_name': '[二级] 区域发展与自贸区', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.9039, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 6, 'prev_limit_up_count': 5, 'limit_up_delta': 1, 'rank_position': 492, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 254, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["000632", "688292", "000797", "000701", "603696", "600693"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,970 - ERROR - 保存异动失败: 上海自贸区 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '7619efd113c2722b97fc339a576056ec', 'concept_name': '上海自贸区', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.8776, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 1, 'prev_limit_up_count': 0, 'limit_up_delta': 1, 'rank_position': 507, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 62, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["000701"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:04,997 - ERROR - 保存异动失败: [二级] AI模型与软件 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '2c814fef42bd1255', 'concept_name': '[二级] AI模型与软件', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.8513, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 12, 'prev_limit_up_count': 11, 'limit_up_delta': 1, 'rank_position': 519, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 341, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["688031", "605303", "603122", "300940", "301486", "688383", "002093", "300290", "000070", "688081", "688661", "300570"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,002 - ERROR - 保存异动失败: 云手机 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '226e2f919636374374b636ca926974ee', 'concept_name': '云手机', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.8501, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 1, 'prev_limit_up_count': 0, 'limit_up_delta': 1, 'rank_position': 520, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 21, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["600302"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,009 - ERROR - 保存异动失败: 科技重组 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '94715a411814ca5d1cba1553ea302fe8', 'concept_name': '科技重组', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.8428, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 521, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 53, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300290", "301486"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,029 - ERROR - 保存异动失败: 央国企 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '65ce42095ba531cf3afd04abb20868c2', 'concept_name': '央国企', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.8052, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 0, 'limit_up_delta': 2, 'rank_position': 535, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 149, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["000797", "000663"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,043 - ERROR - 保存异动失败: 半导体设备 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '7cf6b644fb26f80f3941b0cbcebb4e07', 'concept_name': '半导体设备', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.8025, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 1, 'limit_up_delta': 2, 'rank_position': 536, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 72, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["688661", "688383", "002158"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,069 - ERROR - 保存异动失败: 功率半导体 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'c2354cda068646013b9f8ecc8daf955a', 'concept_name': '功率半导体', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.7791, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 547, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 30, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["600302", "301183"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,081 - ERROR - 保存异动失败: [二级] 贸易政策与关系 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'cb524ffba0dd36d0', 'concept_name': '[二级] 贸易政策与关系', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.7596, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 12, 'prev_limit_up_count': 11, 'limit_up_delta': 1, 'rank_position': 554, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 614, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["002158", "000901", "300900", "600973", "000632", "603067", "688323", "300377", "301125", "300751", "601106", "688661"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,105 - ERROR - 保存异动失败: 华为海思星闪 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'b6ed4f1111b7800992b9b5614047109e', 'concept_name': '华为海思星闪', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.759, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 556, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 65, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["688383", "688031"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,122 - ERROR - 保存异动失败: 杭州算力大会 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'ee44bc8a1b4434d98631bda794220959', 'concept_name': '杭州算力大会', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.7345, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 1, 'prev_limit_up_count': 0, 'limit_up_delta': 1, 'rank_position': 564, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 55, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300290"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,131 - ERROR - 保存异动失败: [一级] 全球宏观与贸易 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '8904f63b21322564', 'concept_name': '[一级] 全球宏观与贸易', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.6868, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 16, 'prev_limit_up_count': 15, 'limit_up_delta': 1, 'rank_position': 587, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 886, 'concept_type': 'lv1', 'extra_info': '{"limit_up_stocks": ["002158", "000901", "300900", "002682", "600973", "000632", "603067", "688323", "300123", "600227", "300377", "301125", "300751", "000905", "601106", "688661"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,153 - ERROR - 保存异动失败: [二级] 半导体设备 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '2c5cc752e1e328ce', 'concept_name': '[二级] 半导体设备', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.67, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 5, 'prev_limit_up_count': 2, 'limit_up_delta': 3, 'rank_position': 592, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 170, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["002158", "688383", "688661", "688167", "301568"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,160 - ERROR - 保存异动失败: [二级] 芯片设计与制造 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'cad6c2087d1d396d', 'concept_name': '[二级] 芯片设计与制造', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.6532, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 6, 'prev_limit_up_count': 5, 'limit_up_delta': 1, 'rank_position': 600, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 244, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["300751", "003031", "300136", "600302", "603122", "301183"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,167 - ERROR - 保存异动失败: 高温概念 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '7cb8e6347cdfdc1e834d60887ef30db4', 'concept_name': '高温概念', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.6528, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 3, 'prev_limit_up_count': 2, 'limit_up_delta': 1, 'rank_position': 601, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 74, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["000811", "002158", "600105"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,174 - ERROR - 保存异动失败: [一级] 半导体 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '1d2cfd648c370f3e', 'concept_name': '[一级] 半导体', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.6494, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 18, 'prev_limit_up_count': 14, 'limit_up_delta': 4, 'rank_position': 604, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 725, 'concept_type': 'lv1', 'extra_info': '{"limit_up_stocks": ["300136", "002158", "688031", "001360", "688143", "688323", "688167", "003031", "688048", "600302", "000859", "300751", "301117", "603122", "301183", "688383", "688661", "301568"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,193 - ERROR - 保存异动失败: [二级] 先进封装 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'c9bee33f0408abfd', 'concept_name': '[二级] 先进封装', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.6381, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 4, 'prev_limit_up_count': 2, 'limit_up_delta': 2, 'rank_position': 613, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 123, 'concept_type': 'lv2', 'extra_info': '{"limit_up_stocks": ["300751", "688383", "688661", "301568"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,206 - ERROR - 保存异动失败: 城市旧改 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '93da0e46ef34f13e0cc170b6e7d195b7', 'concept_name': '城市旧改', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.5557, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 655, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 24, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["605287", "000632"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,211 - ERROR - 保存异动失败: 海事反制 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': '47c91f00b40c8a204ca4e693ac52e009', 'concept_name': '海事反制', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.5077, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 1, 'prev_limit_up_count': 0, 'limit_up_delta': 1, 'rank_position': 675, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 53, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300123"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,218 - ERROR - 保存异动失败: 医保DRGDIP - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'a52dfb0f6f112392b3ef1ee62fc3950e', 'concept_name': '医保DRGDIP', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.4841, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 688, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 33, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["300290", "301117"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,225 - ERROR - 保存异动失败: 国产半导体 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'e51800fd2ea5a195005e4af9f0c9b36d', 'concept_name': '国产半导体', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.4012, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 5, 'prev_limit_up_count': 4, 'limit_up_delta': 1, 'rank_position': 731, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 137, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["003031", "688661", "688048", "002158", "000859"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:48:05,230 - ERROR - 保存异动失败: 房地产产业链 - (pymysql.err.OperationalError) (1054, "Unknown column 'limit_up_delta' in 'field list'") +[SQL: + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (%(concept_id)s, %(concept_name)s, %(alert_time)s, %(alert_type)s, %(trade_date)s, + %(change_pct)s, %(prev_change_pct)s, %(change_delta)s, + %(limit_up_count)s, %(prev_limit_up_count)s, %(limit_up_delta)s, + %(rank_position)s, %(prev_rank_position)s, %(rank_delta)s, + %(index_code)s, %(index_price)s, %(index_change_pct)s, + %(stock_count)s, %(concept_type)s, %(extra_info)s) + ] +[parameters: {'concept_id': 'cf102e739fa64e34ccf2c412cbd67b50', 'concept_name': '房地产产业链', 'alert_time': datetime.datetime(2025, 12, 8, 9, 31, tzinfo=), 'alert_type': 'limit_up', 'trade_date': '2025-12-08', 'change_pct': 1.3825, 'prev_change_pct': None, 'change_delta': None, 'limit_up_count': 2, 'prev_limit_up_count': 1, 'limit_up_delta': 1, 'rank_position': 735, 'prev_rank_position': None, 'rank_delta': None, 'index_code': '000001.SH', 'index_price': 3912.76, 'index_change_pct': 0.255, 'stock_count': 143, 'concept_type': 'leaf', 'extra_info': '{"limit_up_stocks": ["000797", "301526"]}'}] +(Background on this error at: https://sqlalche.me/e/14/e3q8) +2025-12-08 15:49:46,800 - INFO - ============================================================ +2025-12-08 15:49:46,800 - INFO - 🔄 开始回测: 2025-12-08 +2025-12-08 15:49:46,802 - INFO - ============================================================ +2025-12-08 15:49:46,898 - INFO - 已清除 2025-12-08 的已有数据 +2025-12-08 15:49:46,898 - INFO - 加载概念数据... +2025-12-08 15:49:47,114 - INFO - POST http://222.128.1.157:19200/concept_library_v3/_search?scroll=2m [status:200 duration:0.215s] +2025-12-08 15:49:47,322 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.204s] +2025-12-08 15:49:47,547 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.188s] +2025-12-08 15:49:47,752 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.202s] +2025-12-08 15:49:47,944 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.189s] +2025-12-08 15:49:48,125 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.176s] +2025-12-08 15:49:48,304 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.176s] +2025-12-08 15:49:48,474 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.166s] +2025-12-08 15:49:48,601 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.123s] +2025-12-08 15:49:48,610 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.006s] +2025-12-08 15:49:48,618 - INFO - DELETE http://222.128.1.157:19200/_search/scroll [status:200 duration:0.007s] +2025-12-08 15:49:48,618 - INFO - 获取到 865 个叶子概念 +2025-12-08 15:49:48,624 - INFO - 生成了 103 个母概念 +2025-12-08 15:49:48,625 - INFO - 总计 968 个概念 +2025-12-08 15:49:48,627 - INFO - 监控 5938 只股票 +2025-12-08 15:49:48,724 - INFO - 获取到 5132 个基准价格 +2025-12-08 15:49:48,733 - INFO - 指数昨收价: 3902.8076 +2025-12-08 15:49:48,844 - INFO - 找到 241 个分钟时间点 +2025-12-08 15:50:04,980 - INFO - 进度: 30/241 (12%), 已检测到 980 条异动 +2025-12-08 15:50:19,301 - INFO - 进度: 60/241 (24%), 已检测到 1644 条异动 +2025-12-08 15:50:33,414 - INFO - 进度: 90/241 (37%), 已检测到 2142 条异动 +2025-12-08 15:50:48,181 - INFO - 进度: 120/241 (49%), 已检测到 3331 条异动 +2025-12-08 15:50:59,775 - INFO - 进度: 150/241 (62%), 已检测到 4678 条异动 +2025-12-08 15:51:08,681 - INFO - 进度: 180/241 (74%), 已检测到 5498 条异动 +2025-12-08 15:51:22,318 - INFO - 进度: 210/241 (87%), 已检测到 7315 条异动 +2025-12-08 15:51:30,566 - INFO - 进度: 240/241 (99%), 已检测到 7969 条异动 +2025-12-08 15:51:31,051 - INFO - ============================================================ +2025-12-08 15:51:31,052 - INFO - ✅ 回测完成! +2025-12-08 15:51:31,053 - INFO - 处理分钟数: 241 +2025-12-08 15:51:31,058 - INFO - 检测到异动: 8043 条 +2025-12-08 15:51:31,058 - INFO - ============================================================ +2025-12-08 15:55:59,803 - INFO - ============================================================ +2025-12-08 15:55:59,803 - INFO - 🔄 开始回测: 2025-12-08 +2025-12-08 15:55:59,805 - INFO - ============================================================ diff --git a/concept_alert_alpha.py b/concept_alert_alpha.py new file mode 100644 index 00000000..0e8c8f54 --- /dev/null +++ b/concept_alert_alpha.py @@ -0,0 +1,1078 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +概念异动检测 - 基于超额收益(Alpha)的方法 + +核心思想: +- 异动 = 概念表现与市场整体的显著偏离 +- Alpha = 概念涨幅 - 大盘涨幅(超额收益) +- 用 Z-Score 衡量 Alpha 是否显著偏离历史正常范围 + +优点: +- 自适应:不同概念有不同波动率,自动适应 +- 相对比较:不看绝对涨跌,看相对表现 +- 无需规则维护:统计方法自动学习"正常范围" +- 通用:可迁移到其他市场 +""" + +import pandas as pd +import numpy as np +from datetime import datetime, timedelta +from sqlalchemy import create_engine, text +from elasticsearch import Elasticsearch +from clickhouse_driver import Client +from collections import deque, defaultdict +import time +import logging +import json +import os +import hashlib +import argparse +from typing import Dict, List, Optional, Tuple +from dataclasses import dataclass, field + +# ==================== 配置 ==================== + +MYSQL_ENGINE = create_engine( + "mysql+pymysql://root:Zzl5588161!@222.128.1.157:33060/stock", + echo=False +) + +ES_CLIENT = Elasticsearch(['http://222.128.1.157:19200']) +INDEX_NAME = 'concept_library_v3' + +CLICKHOUSE_CONFIG = { + 'host': '222.128.1.157', + 'port': 18000, + 'user': 'default', + 'password': 'Zzl33818!', + 'database': 'stock' +} + +HIERARCHY_FILE = 'concept_hierarchy_v3.json' +REFERENCE_INDEX = '000001.SH' + +# ==================== Alpha 异动检测配置 ==================== + +ALPHA_CONFIG = { + # Z-Score 阈值 + 'zscore_threshold': 2.0, # |Z| > 2.0 视为异动(降低阈值) + + # 绝对 Alpha 阈值(当历史数据不足时使用) + 'absolute_alpha_threshold': 1.5, # |Alpha| > 1.5% 视为异动 + + # 历史窗口 + 'history_window': 60, # 保留最近60个数据点用于计算均值/标准差 + 'min_history': 5, # 最少需要5个数据点才用Z-Score(降低要求) + + # 冷却时间 + 'cooldown_minutes': 8, # 同一概念触发异动后的冷却时间 + + # 显示控制 + 'max_alerts_per_minute': 15, # 每分钟最多显示的异动数 + 'min_alpha_abs': 0.5, # 最小超额收益绝对值(过滤微小波动) + + # 重要性评分权重 + 'importance_weights': { + 'alpha_zscore': 0.35, # Alpha Z-Score 绝对值 + 'alpha_abs': 0.25, # 超额收益绝对值 + 'rank_in_minute': 0.20, # 当分钟内的排名 + 'limit_up_count': 0.20, # 涨停数 + } +} + +# ==================== 日志配置 ==================== + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(f'concept_alert_alpha_{datetime.now().strftime("%Y%m%d")}.log', encoding='utf-8'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# ==================== 数据结构 ==================== + +@dataclass +class AlphaStats: + """概念的Alpha统计信息""" + history: deque = field(default_factory=lambda: deque(maxlen=ALPHA_CONFIG['history_window'])) + mean: float = 0.0 + std: float = 1.0 + last_update: datetime = None + + def update(self, alpha: float, timestamp: datetime): + """更新统计""" + self.history.append(alpha) + self.last_update = timestamp + + if len(self.history) >= 2: + self.mean = np.mean(self.history) + self.std = max(np.std(self.history), 0.1) # 避免除零 + + def get_zscore(self, alpha: float) -> float: + """计算Z-Score""" + if len(self.history) < ALPHA_CONFIG['min_history']: + return 0.0 # 数据不足,不触发 + return (alpha - self.mean) / self.std + + def is_ready(self) -> bool: + """是否有足够数据进行检测""" + return len(self.history) >= ALPHA_CONFIG['min_history'] + + +# ==================== 全局变量 ==================== + +ch_client = None + +# 每个概念的Alpha统计 +alpha_stats: Dict[str, AlphaStats] = defaultdict(AlphaStats) + +# 冷却记录 +cooldown_cache: Dict[str, datetime] = {} + +# 当前分钟的数据缓存(用于计算排名) +current_minute_data: List[dict] = [] + + +def get_ch_client(): + global ch_client + if ch_client is None: + ch_client = Client(**CLICKHOUSE_CONFIG) + return ch_client + + +def generate_id(name: str) -> str: + return hashlib.md5(name.encode('utf-8')).hexdigest()[:16] + + +def code_to_ch_format(code: str) -> str: + if not code or len(code) != 6 or not code.isdigit(): + return None + if code.startswith('6'): + return f"{code}.SH" + elif code.startswith('0') or code.startswith('3'): + return f"{code}.SZ" + else: + return f"{code}.BJ" + + +# ==================== 概念数据获取 ==================== + +def get_all_concepts(): + """从ES获取所有叶子概念""" + concepts = [] + + query = { + "query": {"match_all": {}}, + "size": 100, + "_source": ["concept_id", "concept", "stocks"] + } + + resp = ES_CLIENT.search(index=INDEX_NAME, body=query, scroll='2m') + scroll_id = resp['_scroll_id'] + hits = resp['hits']['hits'] + + while len(hits) > 0: + for hit in hits: + source = hit['_source'] + concept_info = { + 'concept_id': source.get('concept_id'), + 'concept_name': source.get('concept'), + 'stocks': [], + 'concept_type': 'leaf' + } + + if 'stocks' in source and isinstance(source['stocks'], list): + for stock in source['stocks']: + if isinstance(stock, dict) and 'code' in stock and stock['code']: + concept_info['stocks'].append(stock['code']) + + if concept_info['stocks']: + concepts.append(concept_info) + + resp = ES_CLIENT.scroll(scroll_id=scroll_id, scroll='2m') + scroll_id = resp['_scroll_id'] + hits = resp['hits']['hits'] + + ES_CLIENT.clear_scroll(scroll_id=scroll_id) + return concepts + + +def load_hierarchy_concepts(leaf_concepts: list) -> list: + """加载层级结构,生成母概念""" + hierarchy_path = os.path.join(os.path.dirname(__file__), HIERARCHY_FILE) + if not os.path.exists(hierarchy_path): + return [] + + with open(hierarchy_path, 'r', encoding='utf-8') as f: + hierarchy_data = json.load(f) + + concept_to_stocks = {c['concept_name']: set(c['stocks']) for c in leaf_concepts} + parent_concepts = [] + + for lv1 in hierarchy_data.get('hierarchy', []): + lv1_name = lv1.get('lv1', '') + lv1_stocks = set() + + for child in lv1.get('children', []): + lv2_name = child.get('lv2', '') + lv2_stocks = set() + + if 'children' in child: + for lv3_child in child.get('children', []): + lv3_name = lv3_child.get('lv3', '') + lv3_stocks = set() + + for concept_name in lv3_child.get('concepts', []): + if concept_name in concept_to_stocks: + lv3_stocks.update(concept_to_stocks[concept_name]) + + if lv3_stocks: + parent_concepts.append({ + 'concept_id': generate_id(f"lv3_{lv3_name}"), + 'concept_name': f"[三级] {lv3_name}", + 'stocks': list(lv3_stocks), + 'concept_type': 'lv3' + }) + lv2_stocks.update(lv3_stocks) + else: + for concept_name in child.get('concepts', []): + if concept_name in concept_to_stocks: + lv2_stocks.update(concept_to_stocks[concept_name]) + + if lv2_stocks: + parent_concepts.append({ + 'concept_id': generate_id(f"lv2_{lv2_name}"), + 'concept_name': f"[二级] {lv2_name}", + 'stocks': list(lv2_stocks), + 'concept_type': 'lv2' + }) + lv1_stocks.update(lv2_stocks) + + if lv1_stocks: + parent_concepts.append({ + 'concept_id': generate_id(f"lv1_{lv1_name}"), + 'concept_name': f"[一级] {lv1_name}", + 'stocks': list(lv1_stocks), + 'concept_type': 'lv1' + }) + + return parent_concepts + + +# ==================== 价格数据获取 ==================== + +def get_base_prices(stock_codes: list, current_date: str) -> dict: + """获取昨收价""" + if not stock_codes: + return {} + + valid_codes = [code for code in stock_codes if code and len(code) == 6 and code.isdigit()] + if not valid_codes: + return {} + + stock_codes_str = "','".join(valid_codes) + + query = f""" + SELECT SECCODE, F002N + FROM ea_trade + WHERE SECCODE IN ('{stock_codes_str}') + AND TRADEDATE = ( + SELECT MAX(TRADEDATE) FROM ea_trade WHERE TRADEDATE < '{current_date}' + ) + AND F002N IS NOT NULL AND F002N > 0 + """ + + try: + with MYSQL_ENGINE.connect() as conn: + result = conn.execute(text(query)) + return {row[0]: float(row[1]) for row in result if row[1] and float(row[1]) > 0} + except Exception as e: + logger.error(f"获取基准价格失败: {e}") + return {} + + +def get_latest_prices(stock_codes: list) -> dict: + """获取最新价格""" + if not stock_codes: + return {} + + client = get_ch_client() + + ch_codes = [] + code_mapping = {} + for code in stock_codes: + ch_code = code_to_ch_format(code) + if ch_code: + ch_codes.append(ch_code) + code_mapping[ch_code] = code + + if not ch_codes: + return {} + + ch_codes_str = "','".join(ch_codes) + + query = f""" + SELECT code, close, timestamp + FROM ( + SELECT code, close, timestamp, + ROW_NUMBER() OVER (PARTITION BY code ORDER BY timestamp DESC) as rn + FROM stock_minute + WHERE code IN ('{ch_codes_str}') + AND toDate(timestamp) = today() + ) + WHERE rn = 1 + """ + + try: + result = client.execute(query) + prices = {} + for row in result: + ch_code, close, ts = row + if close and close > 0: + pure_code = code_mapping.get(ch_code) + if pure_code: + prices[pure_code] = {'close': float(close), 'timestamp': ts} + return prices + except Exception as e: + logger.error(f"获取最新价格失败: {e}") + return {} + + +def get_prices_at_time(stock_codes: list, timestamp: datetime) -> dict: + """获取指定时间的价格""" + if not stock_codes: + return {} + + client = get_ch_client() + + ch_codes = [] + code_mapping = {} + for code in stock_codes: + ch_code = code_to_ch_format(code) + if ch_code: + ch_codes.append(ch_code) + code_mapping[ch_code] = code + + if not ch_codes: + return {} + + ch_codes_str = "','".join(ch_codes) + ts_str = timestamp.strftime('%Y-%m-%d %H:%M:%S') + + query = f""" + SELECT code, close, timestamp + FROM stock_minute + WHERE code IN ('{ch_codes_str}') + AND timestamp = '{ts_str}' + """ + + try: + result = client.execute(query) + prices = {} + for row in result: + ch_code, close, ts = row + if close and close > 0: + pure_code = code_mapping.get(ch_code) + if pure_code: + prices[pure_code] = {'close': float(close), 'timestamp': ts} + return prices + except Exception as e: + logger.error(f"获取历史价格失败: {e}") + return {} + + +def get_index_change(index_code: str, trade_date: str, timestamp: datetime = None) -> Optional[float]: + """获取指数涨跌幅""" + client = get_ch_client() + + try: + # 获取昨收 + code_no_suffix = index_code.split('.')[0] + with MYSQL_ENGINE.connect() as conn: + prev_result = conn.execute(text(""" + SELECT F006N FROM ea_exchangetrade + WHERE INDEXCODE = :code AND TRADEDATE < :today + ORDER BY TRADEDATE DESC LIMIT 1 + """), {'code': code_no_suffix, 'today': trade_date}).fetchone() + + if not prev_result or not prev_result[0]: + return None + prev_close = float(prev_result[0]) + + # 获取当前价格 + if timestamp: + ts_str = timestamp.strftime('%Y-%m-%d %H:%M:%S') + query = f""" + SELECT close FROM index_minute + WHERE code = '{index_code}' AND timestamp = '{ts_str}' + LIMIT 1 + """ + else: + query = f""" + SELECT close FROM index_minute + WHERE code = '{index_code}' AND toDate(timestamp) = today() + ORDER BY timestamp DESC LIMIT 1 + """ + + result = client.execute(query) + if not result: + return None + + current_price = float(result[0][0]) + return (current_price - prev_close) / prev_close * 100 + + except Exception as e: + logger.error(f"获取指数涨跌幅失败: {e}") + return None + + +def get_index_data(index_code: str, trade_date: str, timestamp: datetime = None) -> Optional[dict]: + """获取指数完整数据""" + client = get_ch_client() + + try: + code_no_suffix = index_code.split('.')[0] + with MYSQL_ENGINE.connect() as conn: + prev_result = conn.execute(text(""" + SELECT F006N FROM ea_exchangetrade + WHERE INDEXCODE = :code AND TRADEDATE < :today + ORDER BY TRADEDATE DESC LIMIT 1 + """), {'code': code_no_suffix, 'today': trade_date}).fetchone() + + if not prev_result or not prev_result[0]: + return None + prev_close = float(prev_result[0]) + + if timestamp: + ts_str = timestamp.strftime('%Y-%m-%d %H:%M:%S') + query = f""" + SELECT close, timestamp FROM index_minute + WHERE code = '{index_code}' AND timestamp = '{ts_str}' + LIMIT 1 + """ + else: + query = f""" + SELECT close, timestamp FROM index_minute + WHERE code = '{index_code}' AND toDate(timestamp) = today() + ORDER BY timestamp DESC LIMIT 1 + """ + + result = client.execute(query) + if not result: + return None + + current_price, ts = result[0] + change_pct = (float(current_price) - prev_close) / prev_close * 100 + + return { + 'code': index_code, + 'price': float(current_price), + 'prev_close': prev_close, + 'change_pct': round(change_pct, 4), + 'timestamp': ts + } + + except Exception as e: + logger.error(f"获取指数数据失败: {e}") + return None + + +# ==================== 概念统计计算 ==================== + +def calculate_concept_stats(concepts: list, stock_changes: dict, index_change: float) -> list: + """ + 计算概念统计,包含超额收益(Alpha) + Alpha = 概念涨幅 - 指数涨幅 + """ + stats = [] + + for concept in concepts: + concept_id = concept['concept_id'] + concept_name = concept['concept_name'] + stock_codes = concept['stocks'] + concept_type = concept.get('concept_type', 'leaf') + + changes = [] + limit_up_count = 0 + limit_down_count = 0 + + for code in stock_codes: + if code in stock_changes: + change_pct = stock_changes[code] + changes.append(change_pct) + + if change_pct >= 9.8: + limit_up_count += 1 + elif change_pct <= -9.8: + limit_down_count += 1 + + if not changes: + continue + + avg_change = np.mean(changes) + + # 核心:计算超额收益 Alpha + alpha = avg_change - index_change + + stats.append({ + 'concept_id': concept_id, + 'concept_name': concept_name, + 'change_pct': round(avg_change, 4), + 'alpha': round(alpha, 4), # 超额收益 + 'index_change': round(index_change, 4), + 'stock_count': len(changes), + 'concept_type': concept_type, + 'limit_up_count': limit_up_count, + 'limit_down_count': limit_down_count, + }) + + # 按Alpha排序 + stats.sort(key=lambda x: x['alpha'], reverse=True) + for i, item in enumerate(stats): + item['alpha_rank'] = i + 1 + + return stats + + +# ==================== Alpha 异动检测 ==================== + +def check_cooldown(concept_id: str, current_time: datetime) -> bool: + """检查冷却""" + if concept_id in cooldown_cache: + last_alert = cooldown_cache[concept_id] + if current_time - last_alert < timedelta(minutes=ALPHA_CONFIG['cooldown_minutes']): + return True + return False + + +def set_cooldown(concept_id: str, current_time: datetime): + """设置冷却""" + cooldown_cache[concept_id] = current_time + + +def calculate_importance( + alpha_zscore: float, + alpha: float, + alpha_rank: int, + total_concepts: int, + limit_up_count: int +) -> float: + """计算重要性评分""" + weights = ALPHA_CONFIG['importance_weights'] + + # Z-Score 分数(归一化到0-1) + zscore_score = min(abs(alpha_zscore) / 5.0, 1.0) + + # Alpha 绝对值分数 + alpha_score = min(abs(alpha) / 3.0, 1.0) + + # 排名分数(越靠前/越靠后越重要) + # alpha_rank 小表示强势,alpha_rank 大表示弱势 + rank_ratio = alpha_rank / max(total_concepts, 1) + rank_score = max(1 - rank_ratio, rank_ratio) # 两端都重要 + + # 涨停分数 + limit_score = min(limit_up_count / 3.0, 1.0) + + total = ( + weights['alpha_zscore'] * zscore_score + + weights['alpha_abs'] * alpha_score + + weights['rank_in_minute'] * rank_score + + weights['limit_up_count'] * limit_score + ) + + return round(total, 4) + + +def detect_alpha_alerts( + stats: list, + index_data: dict, + trade_date: str, + current_time: datetime +) -> list: + """ + 基于Alpha Z-Score的异动检测 + + 检测逻辑: + 1. 如果有足够历史数据(>= min_history):用 Z-Score 判断 + 2. 如果历史数据不足:用绝对 Alpha 阈值判断 + """ + global alpha_stats + + alerts = [] + total_concepts = len(stats) + + for stat in stats: + concept_id = stat['concept_id'] + concept_name = stat['concept_name'] + alpha = stat['alpha'] + change_pct = stat['change_pct'] + alpha_rank = stat['alpha_rank'] + limit_up_count = stat['limit_up_count'] + limit_down_count = stat['limit_down_count'] + + # 更新该概念的Alpha统计 + concept_stats = alpha_stats[concept_id] + + # 计算Z-Score(在更新前计算,用历史数据) + alpha_zscore = concept_stats.get_zscore(alpha) + + # 更新统计 + concept_stats.update(alpha, current_time) + + # Alpha 太小,不值得关注 + if abs(alpha) < ALPHA_CONFIG['min_alpha_abs']: + continue + + # 检查冷却 + if check_cooldown(concept_id, current_time): + continue + + # 判断是否触发异动 + is_alert = False + trigger_reason = "" + + if concept_stats.is_ready(): + # 方式1:有足够历史数据,用 Z-Score + if abs(alpha_zscore) >= ALPHA_CONFIG['zscore_threshold']: + is_alert = True + trigger_reason = f"Z={alpha_zscore:.2f}" + else: + # 方式2:历史数据不足,用绝对 Alpha 阈值 + if abs(alpha) >= ALPHA_CONFIG['absolute_alpha_threshold']: + is_alert = True + trigger_reason = f"Alpha={alpha:+.2f}%" + alpha_zscore = alpha / 0.5 # 估算一个Z值(假设std=0.5) + + if not is_alert: + continue + + # 判断异动类型 + if alpha > 0: + alert_type = 'alpha_surge' # 超额上涨 + else: + alert_type = 'alpha_drop' # 超额下跌 + + # 计算重要性 + importance = calculate_importance( + alpha_zscore, alpha, alpha_rank, total_concepts, limit_up_count + ) + + alert = { + 'concept_id': concept_id, + 'concept_name': concept_name, + 'alert_type': alert_type, + 'alert_time': current_time, + 'trade_date': trade_date, + 'change_pct': change_pct, + 'alpha': alpha, + 'alpha_zscore': round(alpha_zscore, 4), + 'alpha_mean': round(concept_stats.mean, 4), + 'alpha_std': round(concept_stats.std, 4), + 'index_change_pct': stat['index_change'], + 'alpha_rank': alpha_rank, + 'limit_up_count': limit_up_count, + 'limit_down_count': limit_down_count, + 'stock_count': stat['stock_count'], + 'concept_type': stat['concept_type'], + 'importance_score': importance, + 'index_code': REFERENCE_INDEX, + 'index_price': index_data['price'] if index_data else None, + } + + alerts.append(alert) + set_cooldown(concept_id, current_time) + + # 日志 + direction = "📈 超额上涨" if alert_type == 'alpha_surge' else "📉 超额下跌" + logger.info( + f"{direction}: {concept_name} " + f"Alpha={alpha:+.2f}% ({trigger_reason}) " + f"概念{change_pct:+.2f}% vs 大盘{stat['index_change']:+.2f}%" + ) + + # 按重要性排序,限制数量 + alerts.sort(key=lambda x: x['importance_score'], reverse=True) + return alerts[:ALPHA_CONFIG['max_alerts_per_minute']] + + +# ==================== 数据持久化 ==================== + +def save_alerts_to_mysql(alerts: list): + """保存异动到MySQL""" + if not alerts: + return 0 + + saved = 0 + with MYSQL_ENGINE.begin() as conn: + for alert in alerts: + try: + # 映射 alert_type 到数据库格式 + db_alert_type = 'surge_up' if alert['alert_type'] == 'alpha_surge' else 'surge_down' + + insert_sql = text(""" + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, zscore, importance_score, extra_info) + VALUES + (:concept_id, :concept_name, :alert_time, :alert_type, :trade_date, + :change_pct, :prev_change_pct, :change_delta, + :limit_up_count, 0, 0, + :rank_position, NULL, NULL, + :index_code, :index_price, :index_change_pct, + :stock_count, :concept_type, :zscore, :importance_score, :extra_info) + """) + + params = { + 'concept_id': alert['concept_id'], + 'concept_name': alert['concept_name'], + 'alert_time': alert['alert_time'], + 'alert_type': db_alert_type, + 'trade_date': alert['trade_date'], + 'change_pct': alert['change_pct'], + 'prev_change_pct': alert['index_change_pct'], # 用大盘涨幅作为参照 + 'change_delta': alert['alpha'], # Alpha 作为变化量 + 'limit_up_count': alert['limit_up_count'], + 'rank_position': alert['alpha_rank'], + 'index_code': alert['index_code'], + 'index_price': alert['index_price'], + 'index_change_pct': alert['index_change_pct'], + 'stock_count': alert['stock_count'], + 'concept_type': alert['concept_type'], + 'zscore': alert['alpha_zscore'], + 'importance_score': alert['importance_score'], + 'extra_info': json.dumps({ + 'alpha': alert['alpha'], + 'alpha_zscore': alert['alpha_zscore'], + 'alpha_mean': alert['alpha_mean'], + 'alpha_std': alert['alpha_std'], + 'limit_down_count': alert['limit_down_count'], + }, ensure_ascii=False) + } + + conn.execute(insert_sql, params) + saved += 1 + + except Exception as e: + logger.error(f"保存异动失败: {alert['concept_name']} - {e}") + + return saved + + +def save_index_snapshot(index_data: dict, trade_date: str): + """保存指数快照""" + if not index_data: + return + + try: + with MYSQL_ENGINE.begin() as conn: + conn.execute(text(""" + REPLACE INTO index_minute_snapshot + (index_code, trade_date, snapshot_time, price, prev_close, change_pct) + VALUES (:index_code, :trade_date, :snapshot_time, :price, :prev_close, :change_pct) + """), { + 'index_code': index_data['code'], + 'trade_date': trade_date, + 'snapshot_time': index_data['timestamp'], + 'price': index_data['price'], + 'prev_close': index_data.get('prev_close'), + 'change_pct': index_data.get('change_pct') + }) + except Exception as e: + logger.error(f"保存指数快照失败: {e}") + + +# ==================== 交易时间 ==================== + +def is_trading_time() -> bool: + now = datetime.now() + if now.weekday() >= 5: + return False + + current_time = now.hour * 60 + now.minute + morning = (9 * 60 + 30 <= current_time <= 11 * 60 + 30) + afternoon = (13 * 60 <= current_time <= 15 * 60) + return morning or afternoon + + +def get_next_update_time() -> int: + now = datetime.now() + if is_trading_time(): + return 60 - now.second + + hour, minute = now.hour, now.minute + if hour < 9 or (hour == 9 and minute < 30): + target = now.replace(hour=9, minute=30, second=0) + elif (hour == 11 and minute >= 30) or hour == 12: + target = now.replace(hour=13, minute=0, second=0) + elif hour >= 15: + target = (now + timedelta(days=1)).replace(hour=9, minute=30, second=0) + else: + target = now + timedelta(minutes=1) + + return max(60, int((target - now).total_seconds())) + + +# ==================== 主运行逻辑 ==================== + +def run_once(concepts: list, all_stocks: list, trade_date: str, timestamp: datetime = None) -> Tuple[int, int]: + """执行一次检测""" + timestamp = timestamp or datetime.now() + + # 获取基准价格 + base_prices = get_base_prices(all_stocks, trade_date) + if not base_prices: + logger.warning("无法获取基准价格") + return 0, 0 + + # 获取当前价格 + if timestamp.date() == datetime.now().date(): + latest_prices = get_latest_prices(all_stocks) + else: + latest_prices = get_prices_at_time(all_stocks, timestamp) + + if not latest_prices: + logger.warning("无法获取最新价格") + return 0, 0 + + # 计算股票涨跌幅 + stock_changes = {} + for code, price_data in latest_prices.items(): + if code in base_prices and base_prices[code] > 0: + change = (price_data['close'] - base_prices[code]) / base_prices[code] * 100 + stock_changes[code] = round(change, 4) + + if not stock_changes: + return 0, 0 + + # 获取指数数据 + index_data = get_index_data(REFERENCE_INDEX, trade_date, timestamp) + if not index_data: + logger.warning("无法获取指数数据") + return 0, 0 + + index_change = index_data['change_pct'] + save_index_snapshot(index_data, trade_date) + + # 计算概念统计(包含Alpha) + stats = calculate_concept_stats(concepts, stock_changes, index_change) + + # 检测异动 + alerts = detect_alpha_alerts(stats, index_data, trade_date, timestamp) + + # 保存 + if alerts: + saved = save_alerts_to_mysql(alerts) + logger.info(f"💾 保存了 {saved} 条异动") + + return len(stats), len(alerts) + + +def run_realtime(): + """实时运行""" + logger.info("=" * 60) + logger.info("🚀 启动概念异动检测服务(Alpha Z-Score 方法)") + logger.info("=" * 60) + logger.info(f"配置: Z-Score阈值={ALPHA_CONFIG['zscore_threshold']}, 冷却={ALPHA_CONFIG['cooldown_minutes']}分钟") + + # 加载概念 + leaf_concepts = get_all_concepts() + parent_concepts = load_hierarchy_concepts(leaf_concepts) + all_concepts = leaf_concepts + parent_concepts + + all_stocks = set() + for c in all_concepts: + all_stocks.update(c['stocks']) + all_stocks = list(all_stocks) + + logger.info(f"监控 {len(all_concepts)} 个概念, {len(all_stocks)} 只股票") + + total_alerts = 0 + last_reload = datetime.now() + + while True: + try: + now = datetime.now() + trade_date = now.strftime('%Y-%m-%d') + + # 每小时重载概念 + if (now - last_reload).total_seconds() > 3600: + leaf_concepts = get_all_concepts() + parent_concepts = load_hierarchy_concepts(leaf_concepts) + all_concepts = leaf_concepts + parent_concepts + all_stocks = list(set(s for c in all_concepts for s in c['stocks'])) + last_reload = now + logger.info(f"重载概念: {len(all_concepts)} 个") + + if not is_trading_time(): + wait = get_next_update_time() + logger.info(f"⏰ 非交易时间,等待 {wait//60} 分钟") + time.sleep(min(wait, 300)) + continue + + updated, alert_count = run_once(all_concepts, all_stocks, trade_date) + total_alerts += alert_count + + if alert_count > 0: + logger.info(f"📊 本次 {alert_count} 条异动,累计 {total_alerts} 条") + + time.sleep(60 - datetime.now().second) + + except KeyboardInterrupt: + logger.info("\n停止服务") + break + except Exception as e: + logger.error(f"错误: {e}") + import traceback + traceback.print_exc() + time.sleep(60) + + +def run_backtest(trade_date: str, clear_existing: bool = True): + """回测""" + global alpha_stats, cooldown_cache + + logger.info("=" * 60) + logger.info(f"🔄 回测: {trade_date} (Alpha Z-Score 方法)") + logger.info("=" * 60) + + # 清空状态 + alpha_stats.clear() + cooldown_cache.clear() + + if clear_existing: + with MYSQL_ENGINE.begin() as conn: + conn.execute(text("DELETE FROM concept_minute_alert WHERE trade_date = :date"), {'date': trade_date}) + conn.execute(text("DELETE FROM index_minute_snapshot WHERE trade_date = :date"), {'date': trade_date}) + logger.info(f"已清除 {trade_date} 的数据") + + # 加载概念 + leaf_concepts = get_all_concepts() + parent_concepts = load_hierarchy_concepts(leaf_concepts) + all_concepts = leaf_concepts + parent_concepts + + all_stocks = list(set(s for c in all_concepts for s in c['stocks'])) + logger.info(f"概念: {len(all_concepts)}, 股票: {len(all_stocks)}") + + # 获取分钟时间戳 + client = get_ch_client() + result = client.execute(f""" + SELECT DISTINCT timestamp FROM stock_minute + WHERE toDate(timestamp) = '{trade_date}' + ORDER BY timestamp + """) + timestamps = [row[0] for row in result] + + if not timestamps: + logger.error(f"未找到 {trade_date} 的数据") + return + + logger.info(f"时间点: {len(timestamps)}") + + total_alerts = 0 + + for i, ts in enumerate(timestamps): + updated, alerts = run_once(all_concepts, all_stocks, trade_date, ts) + total_alerts += alerts + + if (i + 1) % 30 == 0: + logger.info(f"进度: {i+1}/{len(timestamps)} ({(i+1)*100//len(timestamps)}%), 异动: {total_alerts}") + + logger.info("=" * 60) + logger.info(f"✅ 回测完成! 检测到 {total_alerts} 条异动") + logger.info("=" * 60) + + +def show_status(): + """显示状态""" + print("\n" + "=" * 60) + print("概念异动检测服务 - Alpha Z-Score 方法") + print("=" * 60) + + print(f"\n当前时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print(f"交易时间: {'是' if is_trading_time() else '否'}") + print(f"Z-Score阈值: {ALPHA_CONFIG['zscore_threshold']}") + + try: + with MYSQL_ENGINE.connect() as conn: + result = conn.execute(text(""" + SELECT alert_type, COUNT(*), AVG(change_delta) + FROM concept_minute_alert + WHERE trade_date = CURDATE() + GROUP BY alert_type + """)) + + print("\n今日异动统计:") + rows = list(result) + if rows: + for row in rows: + type_name = {'surge_up': '超额上涨', 'surge_down': '超额下跌'}.get(row[0], row[0]) + avg_alpha = f"{row[2]:+.2f}%" if row[2] else "-" + print(f" {type_name}: {row[1]} 条 (平均Alpha: {avg_alpha})") + else: + print(" 暂无异动") + + print("\n最新异动 (前10条):") + result = conn.execute(text(""" + SELECT concept_name, alert_type, alert_time, change_pct, + change_delta, index_change_pct, zscore + FROM concept_minute_alert + WHERE trade_date = CURDATE() + ORDER BY alert_time DESC + LIMIT 10 + """)) + + rows = list(result) + if rows: + print(f" {'概念':<18} | {'类型':<8} | {'时间':<5} | {'涨幅':>6} | {'Alpha':>6} | {'大盘':>5} | {'Z':>5}") + print(" " + "-" * 75) + for row in rows: + name = (row[0][:16] + '..') if len(row[0]) > 18 else row[0] + t = {'surge_up': '超额涨', 'surge_down': '超额跌'}.get(row[1], row[1][:6]) + time_s = row[2].strftime('%H:%M') if row[2] else '-' + chg = f"{row[3]:+.2f}%" if row[3] else '-' + alpha = f"{row[4]:+.2f}%" if row[4] else '-' + idx = f"{row[5]:+.1f}%" if row[5] else '-' + z = f"{row[6]:.1f}" if row[6] else '-' + print(f" {name:<18} | {t:<8} | {time_s:<5} | {chg:>6} | {alpha:>6} | {idx:>5} | {z:>5}") + + except Exception as e: + print(f" 查询失败: {e}") + + +def main(): + parser = argparse.ArgumentParser(description='概念异动检测 - Alpha Z-Score 方法') + parser.add_argument('command', nargs='?', default='realtime', + choices=['realtime', 'once', 'status', 'backtest'], + help='命令') + parser.add_argument('--date', '-d', type=str, default=None, help='回测日期') + parser.add_argument('--keep', '-k', action='store_true', help='保留已有数据') + + args = parser.parse_args() + + if args.command == 'realtime': + run_realtime() + elif args.command == 'once': + leaf_concepts = get_all_concepts() + parent_concepts = load_hierarchy_concepts(leaf_concepts) + all_concepts = leaf_concepts + parent_concepts + all_stocks = list(set(s for c in all_concepts for s in c['stocks'])) + trade_date = datetime.now().strftime('%Y-%m-%d') + run_once(all_concepts, all_stocks, trade_date) + elif args.command == 'status': + show_status() + elif args.command == 'backtest': + trade_date = args.date or datetime.now().strftime('%Y-%m-%d') + run_backtest(trade_date, not args.keep) + + +if __name__ == "__main__": + main() diff --git a/concept_alert_alpha_20251208.log b/concept_alert_alpha_20251208.log new file mode 100644 index 00000000..656199bf --- /dev/null +++ b/concept_alert_alpha_20251208.log @@ -0,0 +1,28 @@ +2025-12-08 16:40:41,567 - INFO - ============================================================ +2025-12-08 16:40:41,567 - INFO - 🔄 回测: 2025-12-08 (Alpha Z-Score 方法) +2025-12-08 16:40:41,569 - INFO - ============================================================ +2025-12-08 16:40:41,679 - INFO - 已清除 2025-12-08 的数据 +2025-12-08 16:40:41,903 - INFO - POST http://222.128.1.157:19200/concept_library_v3/_search?scroll=2m [status:200 duration:0.224s] +2025-12-08 16:40:42,105 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.197s] +2025-12-08 16:40:42,330 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.178s] +2025-12-08 16:40:42,518 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.183s] +2025-12-08 16:40:42,704 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.182s] +2025-12-08 16:40:42,894 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.186s] +2025-12-08 16:40:43,060 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.162s] +2025-12-08 16:40:43,234 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.171s] +2025-12-08 16:40:43,383 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.145s] +2025-12-08 16:40:43,394 - INFO - POST http://222.128.1.157:19200/_search/scroll [status:200 duration:0.008s] +2025-12-08 16:40:43,399 - INFO - DELETE http://222.128.1.157:19200/_search/scroll [status:200 duration:0.005s] +2025-12-08 16:40:43,409 - INFO - 概念: 968, 股票: 5938 +2025-12-08 16:40:43,505 - INFO - 时间点: 241 +2025-12-08 16:41:02,028 - INFO - 进度: 30/241 (12%), 异动: 0 +2025-12-08 16:41:20,851 - INFO - 进度: 60/241 (24%), 异动: 0 +2025-12-08 16:41:39,396 - INFO - 进度: 90/241 (37%), 异动: 0 +2025-12-08 16:41:58,687 - INFO - 进度: 120/241 (49%), 异动: 0 +2025-12-08 16:43:08,124 - INFO - 进度: 150/241 (62%), 异动: 0 +2025-12-08 16:43:26,973 - INFO - 进度: 180/241 (74%), 异动: 0 +2025-12-08 16:43:45,746 - INFO - 进度: 210/241 (87%), 异动: 0 +2025-12-08 16:44:04,479 - INFO - 进度: 240/241 (99%), 异动: 0 +2025-12-08 16:44:05,123 - INFO - ============================================================ +2025-12-08 16:44:05,123 - INFO - ✅ 回测完成! 检测到 0 条异动 +2025-12-08 16:44:05,125 - INFO - ============================================================ diff --git a/concept_alert_ml.py b/concept_alert_ml.py new file mode 100644 index 00000000..dbe50ea0 --- /dev/null +++ b/concept_alert_ml.py @@ -0,0 +1,1625 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +概念异动智能检测服务 - 基于 Z-Score + SVM +- Z-Score: 动态阈值,根据历史波动率判断异常 +- SVM: 多特征分类,综合判断是否为有意义的异动 +- 支持暴涨和暴跌检测 +- 异动重要性评分,避免图表过于密集 +""" + +import pandas as pd +import numpy as np +from datetime import datetime, timedelta +from sqlalchemy import create_engine, text +from elasticsearch import Elasticsearch +from clickhouse_driver import Client +from collections import deque +import time +import logging +import json +import os +import hashlib +import argparse +import pickle +from typing import Dict, List, Optional, Tuple + +# 尝试导入sklearn,如果不存在则提示安装 +try: + from sklearn.svm import OneClassSVM + from sklearn.preprocessing import StandardScaler + from sklearn.ensemble import IsolationForest + SKLEARN_AVAILABLE = True +except ImportError: + SKLEARN_AVAILABLE = False + print("警告: sklearn 未安装,将使用纯 Z-Score 方法") + print("安装命令: pip install scikit-learn") + +# ==================== 配置 ==================== + +# MySQL配置 +MYSQL_ENGINE = create_engine( + "mysql+pymysql://root:Zzl5588161!@222.128.1.157:33060/stock", + echo=False +) + +# Elasticsearch配置 +ES_CLIENT = Elasticsearch(['http://222.128.1.157:19200']) +INDEX_NAME = 'concept_library_v3' + +# ClickHouse配置 +CLICKHOUSE_CONFIG = { + 'host': '222.128.1.157', + 'port': 18000, + 'user': 'default', + 'password': 'Zzl33818!', + 'database': 'stock' +} + +# 层级结构文件 +HIERARCHY_FILE = 'concept_hierarchy_v3.json' + +# 模型保存路径 +MODEL_DIR = 'models' +os.makedirs(MODEL_DIR, exist_ok=True) + +# ==================== 智能异动检测配置 ==================== + +SMART_ALERT_CONFIG = { + # Z-Score 配置 + 'zscore': { + 'enabled': True, + 'lookback_days': 20, # 历史数据回看天数 + 'threshold_up': 2.5, # 上涨异动阈值(标准差倍数) + 'threshold_down': -2.5, # 下跌异动阈值(标准差倍数) + 'min_data_points': 10, # 最少数据点数 + }, + + # SVM 异常检测配置 + 'svm': { + 'enabled': SKLEARN_AVAILABLE, + 'nu': 0.05, # 异常比例(预期5%为异常) + 'kernel': 'rbf', # 核函数 + 'gamma': 'auto', + 'retrain_days': 7, # 每N天重新训练 + }, + + # 特征配置(用于SVM) + 'features': [ + 'change_pct', # 当前涨跌幅 + 'change_delta_5min', # 5分钟涨跌幅变化 + 'change_delta_10min', # 10分钟涨跌幅变化 + 'rank_delta_5min', # 5分钟排名变化 + 'limit_up_ratio', # 涨停股占比 + 'volume_ratio', # 成交量比率(预留) + 'index_correlation', # 与指数相关性 + ], + + # 重要性评分权重 + 'importance_weights': { + 'zscore_abs': 0.3, # Z-Score 绝对值 + 'rank_position': 0.2, # 排名位置(越靠前越重要) + 'limit_up_count': 0.2, # 涨停数 + 'stock_count': 0.1, # 概念股票数 + 'change_magnitude': 0.2, # 涨跌幅度 + }, + + # 显示控制 + 'display': { + 'max_alerts_per_hour': 20, # 每小时最多显示异动数 + 'min_importance_score': 0.3, # 最低重要性分数 + 'cooldown_minutes': 15, # 同一概念冷却时间 + }, +} + +# 参考指数 +REFERENCE_INDEX = '000001.SH' + +# ==================== 日志配置 ==================== + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(f'concept_alert_ml_{datetime.now().strftime("%Y%m%d")}.log', encoding='utf-8'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# ==================== 全局变量 ==================== + +ch_client = None + +# 历史统计数据缓存 +# 结构: {concept_id: {'mean': float, 'std': float, 'history': deque}} +stats_cache: Dict[str, dict] = {} + +# 分钟级历史缓存(用于计算变化率) +minute_cache: Dict[str, deque] = {} +MINUTE_WINDOW = 15 # 保留15分钟数据 + +# 冷却记录 +cooldown_cache: Dict[Tuple[str, str], datetime] = {} + +# SVM 模型 +svm_model = None +svm_scaler = None +svm_last_train = None + + +def get_ch_client(): + """获取ClickHouse客户端""" + global ch_client + if ch_client is None: + ch_client = Client(**CLICKHOUSE_CONFIG) + return ch_client + + +def generate_id(name: str) -> str: + """生成概念ID""" + return hashlib.md5(name.encode('utf-8')).hexdigest()[:16] + + +def code_to_ch_format(code: str) -> str: + """将6位股票代码转换为ClickHouse格式""" + if not code or len(code) != 6 or not code.isdigit(): + return None + if code.startswith('6'): + return f"{code}.SH" + elif code.startswith('0') or code.startswith('3'): + return f"{code}.SZ" + else: + return f"{code}.BJ" + + +# ==================== 概念数据获取(复用原有代码)==================== + +def get_all_concepts(): + """从ES获取所有叶子概念及其股票列表""" + concepts = [] + + query = { + "query": {"match_all": {}}, + "size": 100, + "_source": ["concept_id", "concept", "stocks"] + } + + resp = ES_CLIENT.search(index=INDEX_NAME, body=query, scroll='2m') + scroll_id = resp['_scroll_id'] + hits = resp['hits']['hits'] + + while len(hits) > 0: + for hit in hits: + source = hit['_source'] + concept_info = { + 'concept_id': source.get('concept_id'), + 'concept_name': source.get('concept'), + 'stocks': [], + 'concept_type': 'leaf' + } + + if 'stocks' in source and isinstance(source['stocks'], list): + for stock in source['stocks']: + if isinstance(stock, dict) and 'code' in stock and stock['code']: + concept_info['stocks'].append(stock['code']) + + if concept_info['stocks']: + concepts.append(concept_info) + + resp = ES_CLIENT.scroll(scroll_id=scroll_id, scroll='2m') + scroll_id = resp['_scroll_id'] + hits = resp['hits']['hits'] + + ES_CLIENT.clear_scroll(scroll_id=scroll_id) + return concepts + + +def load_hierarchy_concepts(leaf_concepts: list) -> list: + """加载层级结构,生成母概念""" + hierarchy_path = os.path.join(os.path.dirname(__file__), HIERARCHY_FILE) + if not os.path.exists(hierarchy_path): + logger.warning(f"层级文件不存在: {hierarchy_path}") + return [] + + with open(hierarchy_path, 'r', encoding='utf-8') as f: + hierarchy_data = json.load(f) + + concept_to_stocks = {} + for c in leaf_concepts: + concept_to_stocks[c['concept_name']] = set(c['stocks']) + + parent_concepts = [] + + for lv1 in hierarchy_data.get('hierarchy', []): + lv1_name = lv1.get('lv1', '') + lv1_stocks = set() + + for child in lv1.get('children', []): + lv2_name = child.get('lv2', '') + lv2_stocks = set() + + if 'children' in child: + for lv3_child in child.get('children', []): + lv3_name = lv3_child.get('lv3', '') + lv3_stocks = set() + + for concept_name in lv3_child.get('concepts', []): + if concept_name in concept_to_stocks: + lv3_stocks.update(concept_to_stocks[concept_name]) + + if lv3_stocks: + parent_concepts.append({ + 'concept_id': generate_id(f"lv3_{lv3_name}"), + 'concept_name': f"[三级] {lv3_name}", + 'stocks': list(lv3_stocks), + 'concept_type': 'lv3' + }) + + lv2_stocks.update(lv3_stocks) + else: + for concept_name in child.get('concepts', []): + if concept_name in concept_to_stocks: + lv2_stocks.update(concept_to_stocks[concept_name]) + + if lv2_stocks: + parent_concepts.append({ + 'concept_id': generate_id(f"lv2_{lv2_name}"), + 'concept_name': f"[二级] {lv2_name}", + 'stocks': list(lv2_stocks), + 'concept_type': 'lv2' + }) + + lv1_stocks.update(lv2_stocks) + + if lv1_stocks: + parent_concepts.append({ + 'concept_id': generate_id(f"lv1_{lv1_name}"), + 'concept_name': f"[一级] {lv1_name}", + 'stocks': list(lv1_stocks), + 'concept_type': 'lv1' + }) + + return parent_concepts + + +# ==================== 价格数据获取 ==================== + +def get_base_prices(stock_codes: list, current_date: str) -> dict: + """获取昨收价作为基准""" + if not stock_codes: + return {} + + valid_codes = [code for code in stock_codes if code and len(code) == 6 and code.isdigit()] + if not valid_codes: + return {} + + stock_codes_str = "','".join(valid_codes) + + query = f""" + SELECT SECCODE, F002N + FROM ea_trade + WHERE SECCODE IN ('{stock_codes_str}') + AND TRADEDATE = ( + SELECT MAX(TRADEDATE) + FROM ea_trade + WHERE TRADEDATE < '{current_date}' + ) + AND F002N IS NOT NULL AND F002N > 0 + """ + + try: + with MYSQL_ENGINE.connect() as conn: + result = conn.execute(text(query)) + base_prices = {row[0]: float(row[1]) for row in result if row[1] and float(row[1]) > 0} + return base_prices + except Exception as e: + logger.error(f"获取基准价格失败: {e}") + return {} + + +def get_latest_prices(stock_codes: list) -> dict: + """从ClickHouse获取最新价格""" + if not stock_codes: + return {} + + client = get_ch_client() + + ch_codes = [] + code_mapping = {} + for code in stock_codes: + ch_code = code_to_ch_format(code) + if ch_code: + ch_codes.append(ch_code) + code_mapping[ch_code] = code + + if not ch_codes: + return {} + + ch_codes_str = "','".join(ch_codes) + + query = f""" + SELECT code, close, timestamp + FROM ( + SELECT code, close, timestamp, + ROW_NUMBER() OVER (PARTITION BY code ORDER BY timestamp DESC) as rn + FROM stock_minute + WHERE code IN ('{ch_codes_str}') + AND toDate(timestamp) = today() + ) + WHERE rn = 1 + """ + + try: + result = client.execute(query) + if not result: + return {} + + latest_prices = {} + for row in result: + ch_code, close, ts = row + if close and close > 0: + pure_code = code_mapping.get(ch_code) + if pure_code: + latest_prices[pure_code] = { + 'close': float(close), + 'timestamp': ts + } + + return latest_prices + except Exception as e: + logger.error(f"获取最新价格失败: {e}") + return {} + + +def get_prices_at_time(stock_codes: list, timestamp: datetime) -> dict: + """获取指定时间点的股票价格""" + if not stock_codes: + return {} + + client = get_ch_client() + + ch_codes = [] + code_mapping = {} + for code in stock_codes: + ch_code = code_to_ch_format(code) + if ch_code: + ch_codes.append(ch_code) + code_mapping[ch_code] = code + + if not ch_codes: + return {} + + ch_codes_str = "','".join(ch_codes) + + query = f""" + SELECT code, close, timestamp + FROM stock_minute + WHERE code IN ('{ch_codes_str}') + AND timestamp = '{timestamp.strftime('%Y-%m-%d %H:%M:%S')}' + """ + + try: + result = client.execute(query) + prices = {} + for row in result: + ch_code, close, ts = row + if close and close > 0: + pure_code = code_mapping.get(ch_code) + if pure_code: + prices[pure_code] = { + 'close': float(close), + 'timestamp': ts + } + return prices + except Exception as e: + logger.error(f"获取历史价格失败: {e}") + return {} + + +def get_index_realtime(index_code: str = REFERENCE_INDEX) -> dict: + """获取指数实时数据""" + client = get_ch_client() + + try: + query = f""" + SELECT close, timestamp + FROM index_minute + WHERE code = '{index_code}' + AND toDate(timestamp) = today() + ORDER BY timestamp DESC + LIMIT 1 + """ + result = client.execute(query) + + if not result: + return None + + close, ts = result[0] + + # 获取昨收价 + prev_close = get_index_prev_close(index_code, datetime.now().strftime('%Y-%m-%d')) + + change_pct = None + if close and prev_close and prev_close > 0: + change_pct = (float(close) - prev_close) / prev_close * 100 + + return { + 'code': index_code, + 'price': float(close), + 'prev_close': prev_close, + 'change_pct': round(change_pct, 4) if change_pct else None, + 'timestamp': ts + } + + except Exception as e: + logger.error(f"获取指数数据失败: {e}") + return None + + +def get_index_at_time(index_code: str, timestamp: datetime, prev_close: float) -> dict: + """获取指定时间点的指数数据""" + client = get_ch_client() + + query = f""" + SELECT close, timestamp + FROM index_minute + WHERE code = '{index_code}' + AND timestamp = '{timestamp.strftime('%Y-%m-%d %H:%M:%S')}' + LIMIT 1 + """ + + try: + result = client.execute(query) + if not result: + return None + + close, ts = result[0] + change_pct = None + if close and prev_close and prev_close > 0: + change_pct = (float(close) - prev_close) / prev_close * 100 + + return { + 'code': index_code, + 'price': float(close), + 'prev_close': prev_close, + 'change_pct': round(change_pct, 4) if change_pct else None, + 'timestamp': ts + } + except Exception as e: + logger.error(f"获取指数数据失败: {e}") + return None + + +def get_index_prev_close(index_code: str, trade_date: str) -> float: + """获取指数昨收价""" + code_no_suffix = index_code.split('.')[0] + + with MYSQL_ENGINE.connect() as conn: + result = conn.execute(text(""" + SELECT F006N FROM ea_exchangetrade + WHERE INDEXCODE = :code + AND TRADEDATE < :today + ORDER BY TRADEDATE DESC LIMIT 1 + """), { + 'code': code_no_suffix, + 'today': trade_date + }).fetchone() + + if result and result[0]: + return float(result[0]) + return None + + +# ==================== 涨跌幅计算 ==================== + +def calculate_change_pct(base_prices: dict, latest_prices: dict) -> dict: + """计算涨跌幅""" + changes = {} + for code, latest in latest_prices.items(): + if code in base_prices and base_prices[code] > 0: + base = base_prices[code] + close = latest['close'] + change_pct = (close - base) / base * 100 + changes[code] = { + 'change_pct': round(change_pct, 4), + 'close': close, + 'base': base + } + return changes + + +def calculate_concept_stats(concepts: list, stock_changes: dict) -> list: + """计算概念统计""" + stats = [] + + for concept in concepts: + concept_id = concept['concept_id'] + concept_name = concept['concept_name'] + stock_codes = concept['stocks'] + concept_type = concept.get('concept_type', 'leaf') + + changes = [] + limit_up_count = 0 + limit_down_count = 0 + limit_up_stocks = [] + limit_down_stocks = [] + + for code in stock_codes: + if code in stock_changes: + change_info = stock_changes[code] + change_pct = change_info['change_pct'] + changes.append(change_pct) + + # 涨停判断(涨幅 >= 9.8%) + if change_pct >= 9.8: + limit_up_count += 1 + limit_up_stocks.append(code) + # 跌停判断(跌幅 <= -9.8%) + elif change_pct <= -9.8: + limit_down_count += 1 + limit_down_stocks.append(code) + + if not changes: + continue + + avg_change_pct = round(np.mean(changes), 4) + std_change_pct = round(np.std(changes), 4) if len(changes) > 1 else 0 + + stats.append({ + 'concept_id': concept_id, + 'concept_name': concept_name, + 'avg_change_pct': avg_change_pct, + 'std_change_pct': std_change_pct, + 'stock_count': len(changes), + 'concept_type': concept_type, + 'limit_up_count': limit_up_count, + 'limit_down_count': limit_down_count, + 'limit_up_stocks': limit_up_stocks, + 'limit_down_stocks': limit_down_stocks, + 'limit_up_ratio': limit_up_count / len(changes) if changes else 0, + 'limit_down_ratio': limit_down_count / len(changes) if changes else 0, + }) + + # 按涨幅排序并添加排名 + stats.sort(key=lambda x: x['avg_change_pct'], reverse=True) + for i, item in enumerate(stats): + item['rank'] = i + 1 + + return stats + + +# ==================== Z-Score 计算 ==================== + +def load_historical_stats(concept_id: str, lookback_days: int = 20) -> dict: + """ + 加载概念的历史统计数据(用于计算Z-Score) + 从历史分钟数据中计算每日的平均涨跌幅变化 + """ + if concept_id in stats_cache and stats_cache[concept_id].get('loaded'): + return stats_cache[concept_id] + + # 查询历史异动记录计算统计 + try: + with MYSQL_ENGINE.connect() as conn: + # 获取历史每日变化统计 + result = conn.execute(text(""" + SELECT + trade_date, + AVG(change_delta) as avg_delta, + MAX(change_delta) as max_delta, + MIN(change_delta) as min_delta + FROM concept_minute_alert + WHERE concept_id = :concept_id + AND trade_date >= DATE_SUB(CURDATE(), INTERVAL :days DAY) + AND alert_type = 'surge' + GROUP BY trade_date + """), {'concept_id': concept_id, 'days': lookback_days}) + + rows = list(result) + + if len(rows) >= SMART_ALERT_CONFIG['zscore']['min_data_points']: + deltas = [float(r[1]) for r in rows if r[1]] + stats_cache[concept_id] = { + 'mean': np.mean(deltas), + 'std': np.std(deltas) if len(deltas) > 1 else 1.0, + 'count': len(deltas), + 'loaded': True + } + else: + # 数据不足,使用默认值 + stats_cache[concept_id] = { + 'mean': 0.5, # 默认平均变化0.5% + 'std': 0.8, # 默认标准差0.8% + 'count': 0, + 'loaded': True + } + except Exception as e: + logger.error(f"加载历史统计失败: {e}") + stats_cache[concept_id] = { + 'mean': 0.5, + 'std': 0.8, + 'count': 0, + 'loaded': True + } + + return stats_cache[concept_id] + + +def calculate_zscore(concept_id: str, change_delta: float) -> float: + """ + 计算涨跌幅变化的Z-Score + Z = (X - μ) / σ + """ + stats = load_historical_stats(concept_id) + + mean = stats['mean'] + std = stats['std'] + + # 避免除零 + if std < 0.1: + std = 0.1 + + zscore = (change_delta - mean) / std + return round(zscore, 4) + + +def update_minute_cache(concept_id: str, timestamp: datetime, data: dict): + """更新分钟级缓存""" + if concept_id not in minute_cache: + minute_cache[concept_id] = deque(maxlen=MINUTE_WINDOW) + + minute_cache[concept_id].append({ + 'timestamp': timestamp, + **data + }) + + +def get_minute_history(concept_id: str, minutes_ago: int) -> Optional[dict]: + """获取N分钟前的数据""" + if concept_id not in minute_cache: + return None + + history = minute_cache[concept_id] + if not history: + return None + + # 获取当前最新时间 + current_time = history[-1]['timestamp'] if history else datetime.now() + target_time = current_time - timedelta(minutes=minutes_ago) + + # 找到最接近目标时间的记录 + for record in reversed(list(history)): + if record['timestamp'] <= target_time: + return record + + return None + + +# ==================== SVM 异常检测 ==================== + +def extract_features(stat: dict, index_data: dict) -> np.ndarray: + """ + 提取用于SVM的特征向量 + """ + concept_id = stat['concept_id'] + + # 获取历史数据 + prev_5min = get_minute_history(concept_id, 5) + prev_10min = get_minute_history(concept_id, 10) + + features = [] + + # 1. 当前涨跌幅 + features.append(stat['avg_change_pct']) + + # 2. 5分钟涨跌幅变化 + if prev_5min: + features.append(stat['avg_change_pct'] - prev_5min.get('change_pct', 0)) + else: + features.append(0) + + # 3. 10分钟涨跌幅变化 + if prev_10min: + features.append(stat['avg_change_pct'] - prev_10min.get('change_pct', 0)) + else: + features.append(0) + + # 4. 5分钟排名变化 + if prev_5min: + features.append(prev_5min.get('rank', stat['rank']) - stat['rank']) + else: + features.append(0) + + # 5. 涨停股占比 + features.append(stat.get('limit_up_ratio', 0) * 100) + + # 6. 成交量比率(预留,暂用0) + features.append(0) + + # 7. 与指数相关性(简化:涨跌方向一致性) + if index_data and index_data.get('change_pct'): + index_change = index_data['change_pct'] + concept_change = stat['avg_change_pct'] + # 同向为正,反向为负 + correlation = 1 if (index_change * concept_change > 0) else -1 + features.append(correlation * abs(concept_change - index_change)) + else: + features.append(0) + + return np.array(features) + + +def train_svm_model(training_data: List[np.ndarray]): + """ + 训练 OneClass SVM 模型 + 用于检测异常模式 + """ + global svm_model, svm_scaler, svm_last_train + + if not SKLEARN_AVAILABLE: + return False + + if len(training_data) < 100: + logger.warning(f"训练数据不足: {len(training_data)} 条") + return False + + try: + X = np.array(training_data) + + # 标准化 + svm_scaler = StandardScaler() + X_scaled = svm_scaler.fit_transform(X) + + # 训练 OneClass SVM + svm_model = OneClassSVM( + nu=SMART_ALERT_CONFIG['svm']['nu'], + kernel=SMART_ALERT_CONFIG['svm']['kernel'], + gamma=SMART_ALERT_CONFIG['svm']['gamma'] + ) + svm_model.fit(X_scaled) + + svm_last_train = datetime.now() + + # 保存模型 + model_path = os.path.join(MODEL_DIR, 'svm_model.pkl') + scaler_path = os.path.join(MODEL_DIR, 'svm_scaler.pkl') + + with open(model_path, 'wb') as f: + pickle.dump(svm_model, f) + with open(scaler_path, 'wb') as f: + pickle.dump(svm_scaler, f) + + logger.info(f"SVM模型训练完成,使用 {len(training_data)} 条数据") + return True + + except Exception as e: + logger.error(f"SVM模型训练失败: {e}") + return False + + +def load_svm_model(): + """加载已保存的SVM模型""" + global svm_model, svm_scaler + + if not SKLEARN_AVAILABLE: + return False + + model_path = os.path.join(MODEL_DIR, 'svm_model.pkl') + scaler_path = os.path.join(MODEL_DIR, 'svm_scaler.pkl') + + if os.path.exists(model_path) and os.path.exists(scaler_path): + try: + with open(model_path, 'rb') as f: + svm_model = pickle.load(f) + with open(scaler_path, 'rb') as f: + svm_scaler = pickle.load(f) + logger.info("SVM模型加载成功") + return True + except Exception as e: + logger.error(f"SVM模型加载失败: {e}") + + return False + + +def predict_anomaly(features: np.ndarray) -> Tuple[bool, float]: + """ + 使用SVM预测是否为异常 + 返回: (是否异常, 异常分数) + """ + global svm_model, svm_scaler + + if svm_model is None or svm_scaler is None: + return False, 0.0 + + try: + X_scaled = svm_scaler.transform(features.reshape(1, -1)) + prediction = svm_model.predict(X_scaled)[0] + score = svm_model.decision_function(X_scaled)[0] + + # prediction: 1 = 正常, -1 = 异常 + is_anomaly = prediction == -1 + + return is_anomaly, float(score) + except Exception as e: + logger.error(f"SVM预测失败: {e}") + return False, 0.0 + + +# ==================== 重要性评分 ==================== + +def calculate_importance_score( + zscore: float, + rank: int, + limit_up_count: int, + stock_count: int, + change_pct: float, + total_concepts: int +) -> float: + """ + 计算异动的重要性分数(0-1) + 综合多个因素判断这条异动是否值得显示 + """ + weights = SMART_ALERT_CONFIG['importance_weights'] + + scores = {} + + # 1. Z-Score 绝对值(越大越重要) + zscore_score = min(abs(zscore) / 5.0, 1.0) # 5倍标准差为满分 + scores['zscore_abs'] = zscore_score + + # 2. 排名位置(越靠前越重要) + rank_score = max(0, 1 - (rank - 1) / min(100, total_concepts)) + scores['rank_position'] = rank_score + + # 3. 涨停数(越多越重要) + limit_score = min(limit_up_count / 5.0, 1.0) # 5个涨停为满分 + scores['limit_up_count'] = limit_score + + # 4. 概念股票数(适中最好) + if stock_count < 10: + stock_score = stock_count / 10.0 + elif stock_count > 100: + stock_score = max(0.5, 1 - (stock_count - 100) / 200) + else: + stock_score = 1.0 + scores['stock_count'] = stock_score + + # 5. 涨跌幅度 + change_score = min(abs(change_pct) / 5.0, 1.0) # 5%为满分 + scores['change_magnitude'] = change_score + + # 加权求和 + total_score = sum(scores[k] * weights[k] for k in weights) + + return round(total_score, 4) + + +# ==================== 智能异动检测 ==================== + +def check_cooldown(concept_id: str, alert_type: str) -> bool: + """检查是否在冷却期""" + key = (concept_id, alert_type) + cooldown_minutes = SMART_ALERT_CONFIG['display']['cooldown_minutes'] + + if key in cooldown_cache: + last_alert = cooldown_cache[key] + if datetime.now() - last_alert < timedelta(minutes=cooldown_minutes): + return True + return False + + +def set_cooldown(concept_id: str, alert_type: str, alert_time: datetime = None): + """设置冷却""" + cooldown_cache[(concept_id, alert_type)] = alert_time or datetime.now() + + +def detect_smart_alerts( + current_stats: list, + index_data: dict, + trade_date: str, + current_time: datetime = None +) -> list: + """ + 智能异动检测 + 结合 Z-Score + SVM 进行检测 + """ + alerts = [] + current_time = current_time or datetime.now() + total_concepts = len(current_stats) + + for stat in current_stats: + concept_id = stat['concept_id'] + concept_name = stat['concept_name'] + change_pct = stat['avg_change_pct'] + rank = stat['rank'] + limit_up_count = stat['limit_up_count'] + limit_down_count = stat['limit_down_count'] + stock_count = stat['stock_count'] + concept_type = stat['concept_type'] + + # 更新分钟缓存 + update_minute_cache(concept_id, current_time, { + 'change_pct': change_pct, + 'rank': rank, + 'limit_up_count': limit_up_count, + 'limit_down_count': limit_down_count + }) + + # 获取历史数据计算变化 + prev_5min = get_minute_history(concept_id, 5) + if not prev_5min: + continue + + change_delta = change_pct - prev_5min.get('change_pct', 0) + + # ========== Z-Score 检测 ========== + zscore = calculate_zscore(concept_id, abs(change_delta)) + + # 判断是涨还是跌 + is_surge_up = change_delta > 0 and zscore >= SMART_ALERT_CONFIG['zscore']['threshold_up'] + is_surge_down = change_delta < 0 and zscore >= abs(SMART_ALERT_CONFIG['zscore']['threshold_down']) + + if not (is_surge_up or is_surge_down): + continue + + alert_type = 'surge_up' if is_surge_up else 'surge_down' + + # 检查冷却 + if check_cooldown(concept_id, alert_type): + continue + + # ========== SVM 验证(可选)========== + svm_is_anomaly = False + svm_score = 0.0 + + if SMART_ALERT_CONFIG['svm']['enabled'] and svm_model is not None: + features = extract_features(stat, index_data) + svm_is_anomaly, svm_score = predict_anomaly(features) + + # 如果SVM认为不是异常,降低Z-Score要求 + if not svm_is_anomaly and abs(zscore) < 3.5: + continue + + # ========== 计算重要性分数 ========== + importance = calculate_importance_score( + zscore=zscore, + rank=rank, + limit_up_count=limit_up_count if is_surge_up else limit_down_count, + stock_count=stock_count, + change_pct=change_pct, + total_concepts=total_concepts + ) + + # 过滤低重要性 + if importance < SMART_ALERT_CONFIG['display']['min_importance_score']: + continue + + # ========== 创建异动记录 ========== + alert = { + 'concept_id': concept_id, + 'concept_name': concept_name, + 'alert_type': alert_type, + 'alert_time': current_time, + 'trade_date': trade_date, + 'change_pct': change_pct, + 'prev_change_pct': prev_5min.get('change_pct'), + 'change_delta': round(change_delta, 4), + 'zscore': zscore, + 'svm_score': svm_score, + 'importance_score': importance, + 'limit_up_count': limit_up_count, + 'limit_down_count': limit_down_count, + 'prev_limit_up_count': prev_5min.get('limit_up_count', 0), + 'rank_position': rank, + 'prev_rank_position': prev_5min.get('rank'), + 'rank_delta': (prev_5min.get('rank', rank) - rank) if prev_5min else 0, + 'stock_count': stock_count, + 'concept_type': concept_type, + 'index_code': REFERENCE_INDEX, + 'index_price': index_data['price'] if index_data else None, + 'index_change_pct': index_data['change_pct'] if index_data else None, + 'extra_info': { + 'limit_up_stocks': stat.get('limit_up_stocks', []), + 'limit_down_stocks': stat.get('limit_down_stocks', []), + } + } + + alerts.append(alert) + set_cooldown(concept_id, alert_type, current_time) + + # 日志 + direction = "🔥 暴涨" if is_surge_up else "💧 暴跌" + logger.info( + f"{direction}: {concept_name} " + f"涨幅 {prev_5min.get('change_pct', 0):.2f}% -> {change_pct:.2f}% " + f"(Δ{change_delta:+.2f}%, Z={zscore:.2f}, 重要性={importance:.2f})" + ) + + # 按重要性排序,限制数量 + alerts.sort(key=lambda x: x['importance_score'], reverse=True) + max_alerts = SMART_ALERT_CONFIG['display']['max_alerts_per_hour'] + + return alerts[:max_alerts] + + +# ==================== 数据持久化 ==================== + +def save_alerts_to_mysql(alerts: list): + """保存异动数据到MySQL""" + if not alerts: + return 0 + + saved = 0 + with MYSQL_ENGINE.begin() as conn: + for alert in alerts: + try: + insert_sql = text(""" + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (:concept_id, :concept_name, :alert_time, :alert_type, :trade_date, + :change_pct, :prev_change_pct, :change_delta, + :limit_up_count, :prev_limit_up_count, :limit_up_delta, + :rank_position, :prev_rank_position, :rank_delta, + :index_code, :index_price, :index_change_pct, + :stock_count, :concept_type, :extra_info) + """) + + # 计算 limit_up_delta + limit_up_delta = alert.get('limit_up_count', 0) - alert.get('prev_limit_up_count', 0) + + params = { + 'concept_id': alert['concept_id'], + 'concept_name': alert['concept_name'], + 'alert_time': alert['alert_time'], + 'alert_type': alert['alert_type'], + 'trade_date': alert['trade_date'], + 'change_pct': alert.get('change_pct'), + 'prev_change_pct': alert.get('prev_change_pct'), + 'change_delta': alert.get('change_delta'), + 'limit_up_count': alert.get('limit_up_count', 0), + 'prev_limit_up_count': alert.get('prev_limit_up_count', 0), + 'limit_up_delta': limit_up_delta, + 'rank_position': alert.get('rank_position'), + 'prev_rank_position': alert.get('prev_rank_position'), + 'rank_delta': alert.get('rank_delta'), + 'index_code': alert.get('index_code', REFERENCE_INDEX), + 'index_price': alert.get('index_price'), + 'index_change_pct': alert.get('index_change_pct'), + 'stock_count': alert.get('stock_count'), + 'concept_type': alert.get('concept_type', 'leaf'), + 'extra_info': json.dumps({ + **alert.get('extra_info', {}), + 'zscore': alert.get('zscore'), + 'svm_score': alert.get('svm_score'), + 'importance_score': alert.get('importance_score'), + }, ensure_ascii=False) if alert.get('extra_info') else None + } + + conn.execute(insert_sql, params) + saved += 1 + + except Exception as e: + logger.error(f"保存异动失败: {alert['concept_name']} - {e}") + + return saved + + +def save_index_snapshot(index_data: dict, trade_date: str): + """保存指数快照""" + if not index_data: + return + + try: + with MYSQL_ENGINE.begin() as conn: + upsert_sql = text(""" + REPLACE INTO index_minute_snapshot + (index_code, trade_date, snapshot_time, price, prev_close, change_pct) + VALUES (:index_code, :trade_date, :snapshot_time, :price, :prev_close, :change_pct) + """) + + conn.execute(upsert_sql, { + 'index_code': index_data['code'], + 'trade_date': trade_date, + 'snapshot_time': index_data['timestamp'], + 'price': index_data['price'], + 'prev_close': index_data.get('prev_close'), + 'change_pct': index_data.get('change_pct') + }) + except Exception as e: + logger.error(f"保存指数快照失败: {e}") + + +# ==================== 交易时间判断 ==================== + +def is_trading_time() -> bool: + """判断当前是否为交易时间""" + now = datetime.now() + weekday = now.weekday() + + if weekday >= 5: + return False + + hour, minute = now.hour, now.minute + current_time = hour * 60 + minute + + morning_start = 9 * 60 + 30 + morning_end = 11 * 60 + 30 + afternoon_start = 13 * 60 + afternoon_end = 15 * 60 + + return (morning_start <= current_time <= morning_end) or \ + (afternoon_start <= current_time <= afternoon_end) + + +def get_next_update_time() -> int: + """获取距离下次更新的秒数""" + now = datetime.now() + + if is_trading_time(): + return 60 - now.second + else: + hour, minute = now.hour, now.minute + + if hour < 9 or (hour == 9 and minute < 30): + target = now.replace(hour=9, minute=30, second=0, microsecond=0) + elif (hour == 11 and minute >= 30) or hour == 12: + target = now.replace(hour=13, minute=0, second=0, microsecond=0) + elif hour >= 15: + target = (now + timedelta(days=1)).replace(hour=9, minute=30, second=0, microsecond=0) + else: + target = now + timedelta(minutes=1) + + wait_seconds = (target - now).total_seconds() + return max(60, int(wait_seconds)) + + +# ==================== 主运行逻辑 ==================== + +def run_once(concepts: list, all_stocks: list) -> tuple: + """执行一次检测""" + now = datetime.now() + trade_date = now.strftime('%Y-%m-%d') + + # 获取基准价格 + base_prices = get_base_prices(all_stocks, trade_date) + if not base_prices: + logger.warning("无法获取基准价格") + return 0, 0 + + # 获取最新价格 + latest_prices = get_latest_prices(all_stocks) + if not latest_prices: + logger.warning("无法获取最新价格") + return 0, 0 + + # 获取指数数据 + index_data = get_index_realtime(REFERENCE_INDEX) + if index_data: + save_index_snapshot(index_data, trade_date) + + # 计算涨跌幅 + stock_changes = calculate_change_pct(base_prices, latest_prices) + if not stock_changes: + logger.warning("无涨跌幅数据") + return 0, 0 + + logger.info(f"获取到 {len(stock_changes)} 只股票的涨跌幅") + + # 计算概念统计 + stats = calculate_concept_stats(concepts, stock_changes) + logger.info(f"计算了 {len(stats)} 个概念的涨跌幅") + + # 智能异动检测 + alerts = detect_smart_alerts(stats, index_data, trade_date, now) + + # 保存异动 + if alerts: + saved = save_alerts_to_mysql(alerts) + logger.info(f"💾 保存了 {saved} 条异动记录") + + return len(stats), len(alerts) + + +def run_realtime(): + """实时检测主循环""" + logger.info("=" * 60) + logger.info("🚀 启动智能概念异动检测服务 (Z-Score + SVM)") + logger.info("=" * 60) + logger.info(f"配置: {json.dumps(SMART_ALERT_CONFIG, indent=2, ensure_ascii=False, default=str)}") + + # 尝试加载SVM模型 + if SKLEARN_AVAILABLE: + load_svm_model() + + # 加载概念数据 + logger.info("加载概念数据...") + leaf_concepts = get_all_concepts() + logger.info(f"获取到 {len(leaf_concepts)} 个叶子概念") + + parent_concepts = load_hierarchy_concepts(leaf_concepts) + logger.info(f"生成了 {len(parent_concepts)} 个母概念") + + all_concepts = leaf_concepts + parent_concepts + logger.info(f"总计 {len(all_concepts)} 个概念") + + # 收集所有股票代码 + all_stocks = set() + for c in all_concepts: + all_stocks.update(c['stocks']) + all_stocks = list(all_stocks) + logger.info(f"监控 {len(all_stocks)} 只股票") + + last_concept_update = datetime.now() + total_alerts = 0 + + while True: + try: + now = datetime.now() + + # 每小时重新加载概念数据 + if (now - last_concept_update).total_seconds() > 3600: + logger.info("重新加载概念数据...") + leaf_concepts = get_all_concepts() + parent_concepts = load_hierarchy_concepts(leaf_concepts) + all_concepts = leaf_concepts + parent_concepts + all_stocks = set() + for c in all_concepts: + all_stocks.update(c['stocks']) + all_stocks = list(all_stocks) + last_concept_update = now + logger.info(f"更新完成: {len(all_concepts)} 个概念, {len(all_stocks)} 只股票") + + # 检查是否交易时间 + if not is_trading_time(): + wait_sec = get_next_update_time() + wait_min = wait_sec // 60 + logger.info(f"⏰ 非交易时间,等待 {wait_min} 分钟后重试...") + time.sleep(min(wait_sec, 300)) + continue + + # 执行检测 + logger.info(f"\n{'=' * 40}") + logger.info(f"🔍 检测时间: {now.strftime('%Y-%m-%d %H:%M:%S')}") + updated, alert_count = run_once(all_concepts, all_stocks) + total_alerts += alert_count + + if alert_count > 0: + logger.info(f"📊 本次检测到 {alert_count} 条异动,累计 {total_alerts} 条") + + # 等待下一分钟 + sleep_sec = 60 - datetime.now().second + logger.info(f"⏳ 等待 {sleep_sec} 秒后继续...") + time.sleep(sleep_sec) + + except KeyboardInterrupt: + logger.info("\n收到退出信号,停止服务...") + break + except Exception as e: + logger.error(f"发生错误: {e}") + import traceback + traceback.print_exc() + time.sleep(60) + + +def run_single(): + """单次运行""" + logger.info("单次检测模式") + + if SKLEARN_AVAILABLE: + load_svm_model() + + leaf_concepts = get_all_concepts() + parent_concepts = load_hierarchy_concepts(leaf_concepts) + all_concepts = leaf_concepts + parent_concepts + + all_stocks = set() + for c in all_concepts: + all_stocks.update(c['stocks']) + all_stocks = list(all_stocks) + + logger.info(f"概念数: {len(all_concepts)}, 股票数: {len(all_stocks)}") + + updated, alerts = run_once(all_concepts, all_stocks) + logger.info(f"检测完成: {updated} 个概念, {alerts} 条异动") + + +# ==================== 回测功能 ==================== + +def get_minute_timestamps(trade_date: str) -> list: + """获取指定交易日的所有分钟时间戳""" + client = get_ch_client() + + query = f""" + SELECT DISTINCT timestamp + FROM stock_minute + WHERE toDate(timestamp) = '{trade_date}' + ORDER BY timestamp + """ + + result = client.execute(query) + return [row[0] for row in result] + + +def run_backtest(trade_date: str, clear_existing: bool = True): + """ + 回测指定日期的异动检测 + """ + global minute_cache, cooldown_cache, stats_cache + + logger.info("=" * 60) + logger.info(f"🔄 开始智能回测: {trade_date}") + logger.info("=" * 60) + + # 清空缓存 + minute_cache = {} + cooldown_cache = {} + stats_cache = {} + + # 清除已有数据 + if clear_existing: + with MYSQL_ENGINE.begin() as conn: + conn.execute(text("DELETE FROM concept_minute_alert WHERE trade_date = :date"), {'date': trade_date}) + conn.execute(text("DELETE FROM index_minute_snapshot WHERE trade_date = :date"), {'date': trade_date}) + logger.info(f"已清除 {trade_date} 的已有数据") + + # 加载概念数据 + logger.info("加载概念数据...") + leaf_concepts = get_all_concepts() + logger.info(f"获取到 {len(leaf_concepts)} 个叶子概念") + + parent_concepts = load_hierarchy_concepts(leaf_concepts) + logger.info(f"生成了 {len(parent_concepts)} 个母概念") + + all_concepts = leaf_concepts + parent_concepts + logger.info(f"总计 {len(all_concepts)} 个概念") + + # 收集所有股票代码 + all_stocks = set() + for c in all_concepts: + all_stocks.update(c['stocks']) + all_stocks = list(all_stocks) + logger.info(f"监控 {len(all_stocks)} 只股票") + + # 获取基准价格(昨收价) + base_prices = get_base_prices(all_stocks, trade_date) + if not base_prices: + logger.error("无法获取基准价格,退出回测") + return + logger.info(f"获取到 {len(base_prices)} 个基准价格") + + # 获取指数昨收价 + index_prev_close = get_index_prev_close(REFERENCE_INDEX, trade_date) + logger.info(f"指数昨收价: {index_prev_close}") + + # 获取所有分钟时间戳 + timestamps = get_minute_timestamps(trade_date) + if not timestamps: + logger.error(f"未找到 {trade_date} 的分钟数据") + return + logger.info(f"找到 {len(timestamps)} 个分钟时间点") + + total_alerts = 0 + processed = 0 + + # 逐分钟处理 + for ts in timestamps: + processed += 1 + + # 获取该时间点的价格 + latest_prices = get_prices_at_time(all_stocks, ts) + if not latest_prices: + continue + + # 获取指数数据 + index_data = get_index_at_time(REFERENCE_INDEX, ts, index_prev_close) + if index_data: + save_index_snapshot(index_data, trade_date) + + # 计算涨跌幅 + stock_changes = calculate_change_pct(base_prices, latest_prices) + if not stock_changes: + continue + + # 计算概念统计 + stats = calculate_concept_stats(all_concepts, stock_changes) + + # 智能异动检测 + alerts = detect_smart_alerts(stats, index_data, trade_date, ts) + + # 保存异动 + if alerts: + saved = save_alerts_to_mysql(alerts) + total_alerts += saved + + # 进度显示 + if processed % 30 == 0: + logger.info(f"进度: {processed}/{len(timestamps)} ({processed*100//len(timestamps)}%), 已检测到 {total_alerts} 条异动") + + logger.info("=" * 60) + logger.info(f"✅ 回测完成!") + logger.info(f" 处理分钟数: {processed}") + logger.info(f" 检测到异动: {total_alerts} 条") + logger.info("=" * 60) + + +def show_status(): + """显示状态""" + print("\n" + "=" * 60) + print("智能概念异动检测服务 (Z-Score + SVM) - 状态") + print("=" * 60) + + now = datetime.now() + print(f"\n当前时间: {now.strftime('%Y-%m-%d %H:%M:%S')}") + print(f"是否交易时间: {'是' if is_trading_time() else '否'}") + print(f"sklearn可用: {'是' if SKLEARN_AVAILABLE else '否'}") + + # 模型状态 + model_path = os.path.join(MODEL_DIR, 'svm_model.pkl') + print(f"SVM模型: {'已加载' if os.path.exists(model_path) else '未训练'}") + + # 今日异动统计 + print("\n今日异动统计:") + try: + with MYSQL_ENGINE.connect() as conn: + result = conn.execute(text(""" + SELECT alert_type, COUNT(*) as cnt, AVG(change_delta) as avg_delta + FROM concept_minute_alert + WHERE trade_date = CURDATE() + GROUP BY alert_type + """)) + rows = list(result) + if rows: + for row in rows: + alert_type_name = { + 'surge_up': '暴涨', + 'surge_down': '暴跌', + 'surge': '急涨(旧)', + 'limit_up': '涨停增加', + 'rank_jump': '排名跃升' + }.get(row[0], row[0]) + avg_delta = f"{row[2]:.2f}%" if row[2] else "-" + print(f" {alert_type_name}: {row[1]} 条 (平均变化: {avg_delta})") + else: + print(" 今日暂无异动") + + # 最新异动 + print("\n最新异动 (前10条):") + result = conn.execute(text(""" + SELECT concept_name, alert_type, alert_time, change_pct, change_delta, extra_info + FROM concept_minute_alert + WHERE trade_date = CURDATE() + ORDER BY alert_time DESC + LIMIT 10 + """)) + rows = list(result) + if rows: + print(f" {'概念':<20} | {'类型':<6} | {'时间':<8} | {'涨幅':>6} | {'变化':>6} | {'Z分':>5}") + print(" " + "-" * 70) + for row in rows: + name = row[0][:18] if len(row[0]) > 18 else row[0] + alert_type = {'surge_up': '暴涨', 'surge_down': '暴跌'}.get(row[1], row[1][:4]) + time_str = row[2].strftime('%H:%M') if row[2] else '-' + change = f"{row[3]:.2f}%" if row[3] else '-' + delta = f"{row[4]:+.2f}%" if row[4] else '-' + + # 解析extra_info获取zscore + zscore = '-' + if row[5]: + try: + extra = json.loads(row[5]) if isinstance(row[5], str) else row[5] + zscore = f"{extra.get('zscore', 0):.1f}" + except: + pass + + print(f" {name:<20} | {alert_type:<6} | {time_str:<8} | {change:>6} | {delta:>6} | {zscore:>5}") + else: + print(" 暂无异动记录") + + except Exception as e: + print(f" 查询失败: {e}") + + +def train_model(): + """训练SVM模型""" + if not SKLEARN_AVAILABLE: + print("错误: sklearn未安装,无法训练模型") + print("安装命令: pip install scikit-learn") + return + + logger.info("=" * 60) + logger.info("🎓 开始训练SVM模型") + logger.info("=" * 60) + + # 加载概念数据 + leaf_concepts = get_all_concepts() + parent_concepts = load_hierarchy_concepts(leaf_concepts) + all_concepts = leaf_concepts + parent_concepts + + all_stocks = set() + for c in all_concepts: + all_stocks.update(c['stocks']) + all_stocks = list(all_stocks) + + logger.info(f"概念数: {len(all_concepts)}, 股票数: {len(all_stocks)}") + + # 收集训练数据(使用历史异动数据) + training_features = [] + + # 从最近N天的数据中提取特征 + lookback_days = 30 + + try: + with MYSQL_ENGINE.connect() as conn: + result = conn.execute(text(""" + SELECT change_pct, change_delta, rank_position, limit_up_count, + stock_count, index_change_pct, extra_info + FROM concept_minute_alert + WHERE trade_date >= DATE_SUB(CURDATE(), INTERVAL :days DAY) + """), {'days': lookback_days}) + + for row in result: + features = [ + float(row[0]) if row[0] else 0, # change_pct + float(row[1]) if row[1] else 0, # change_delta + 0, # change_delta_10min (not available) + -float(row[2]) if row[2] else 0, # rank_delta (approximation) + float(row[3]) / max(1, float(row[4])) * 100 if row[3] and row[4] else 0, # limit_up_ratio + 0, # volume_ratio + float(row[0]) - float(row[5]) if row[0] and row[5] else 0, # index_correlation + ] + training_features.append(features) + + logger.info(f"收集到 {len(training_features)} 条训练数据") + + if len(training_features) >= 100: + success = train_svm_model(training_features) + if success: + logger.info("✅ 模型训练成功!") + else: + logger.error("❌ 模型训练失败") + else: + logger.warning("训练数据不足100条,跳过训练") + + except Exception as e: + logger.error(f"训练失败: {e}") + import traceback + traceback.print_exc() + + +# ==================== 主函数 ==================== + +def main(): + parser = argparse.ArgumentParser(description='智能概念异动检测服务 (Z-Score + SVM)') + parser.add_argument('command', nargs='?', default='realtime', + choices=['realtime', 'once', 'status', 'backtest', 'train'], + help='命令: realtime(实时运行), once(单次运行), status(状态), backtest(回测), train(训练模型)') + parser.add_argument('--date', '-d', type=str, default=None, + help='回测日期,格式: YYYY-MM-DD,默认为今天') + parser.add_argument('--keep', '-k', action='store_true', + help='回测时保留已有数据(默认会清除)') + + args = parser.parse_args() + + if args.command == 'realtime': + run_realtime() + elif args.command == 'once': + run_single() + elif args.command == 'status': + show_status() + elif args.command == 'backtest': + trade_date = args.date or datetime.now().strftime('%Y-%m-%d') + clear_existing = not args.keep + run_backtest(trade_date, clear_existing) + elif args.command == 'train': + train_model() + + +if __name__ == "__main__": + main() diff --git a/concept_alert_realtime.py b/concept_alert_realtime.py new file mode 100644 index 00000000..6e931a91 --- /dev/null +++ b/concept_alert_realtime.py @@ -0,0 +1,1366 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +概念异动实时检测服务 +- 基于 concept_quota_realtime.py 扩展 +- 检测概念板块的异动(急涨、涨停增加、排名跃升) +- 记录异动时的指数位置,用于热点概览图表展示 +""" + +import pandas as pd +import numpy as np +from datetime import datetime, timedelta +from sqlalchemy import create_engine, text +from elasticsearch import Elasticsearch +from clickhouse_driver import Client +from collections import deque +import time +import logging +import json +import os +import hashlib +import argparse + +# ==================== 配置 ==================== + +# MySQL配置 +MYSQL_ENGINE = create_engine( + "mysql+pymysql://root:Zzl5588161!@222.128.1.157:33060/stock", + echo=False +) + +# Elasticsearch配置 +ES_CLIENT = Elasticsearch(['http://222.128.1.157:19200']) +INDEX_NAME = 'concept_library_v3' + +# ClickHouse配置 +CLICKHOUSE_CONFIG = { + 'host': '222.128.1.157', + 'port': 18000, + 'user': 'default', + 'password': 'Zzl33818!', + 'database': 'stock' +} + +# 层级结构文件 +HIERARCHY_FILE = 'concept_hierarchy_v3.json' + +# ==================== 异动检测阈值配置 ==================== + +ALERT_CONFIG = { + # 急涨检测:N分钟内涨幅变化超过阈值 + 'surge': { + 'enabled': True, + 'window_minutes': 5, # 检测窗口(分钟) + 'threshold_pct': 1.0, # 涨幅变化阈值(%) + 'min_change_pct': 0.5, # 最低涨幅要求(避免负涨幅的噪音) + 'cooldown_minutes': 10, # 同一概念冷却时间(避免重复报警) + }, + # 涨停数增加检测 + 'limit_up': { + 'enabled': True, + 'threshold_count': 1, # 涨停数增加阈值 + 'cooldown_minutes': 15, # 冷却时间 + }, + # 排名跃升检测 + 'rank_jump': { + 'enabled': True, + 'window_minutes': 5, # 检测窗口 + 'threshold_rank': 15, # 排名上升阈值 + 'max_rank': 50, # 只关注前N名的变化 + 'cooldown_minutes': 15, + }, +} + +# 参考指数 +REFERENCE_INDEX = '000001.SH' # 上证指数 + +# ==================== 日志配置 ==================== + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(f'concept_alert_{datetime.now().strftime("%Y%m%d")}.log', encoding='utf-8'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# ==================== 全局变量 ==================== + +ch_client = None + +# 历史数据缓存(用于异动检测) +# 结构: {concept_id: deque([(timestamp, change_pct, rank, limit_up_count), ...])} +history_cache = {} +HISTORY_WINDOW = 10 # 保留最近10分钟的数据 + +# 冷却记录(避免重复报警) +# 结构: {(concept_id, alert_type): last_alert_time} +cooldown_cache = {} + +# 当前排名缓存 +current_rankings = {} + + +def get_ch_client(): + """获取ClickHouse客户端""" + global ch_client + if ch_client is None: + ch_client = Client(**CLICKHOUSE_CONFIG) + return ch_client + + +def generate_id(name: str) -> str: + """生成概念ID""" + return hashlib.md5(name.encode('utf-8')).hexdigest()[:16] + + +def code_to_ch_format(code: str) -> str: + """将6位股票代码转换为ClickHouse格式""" + if not code or len(code) != 6 or not code.isdigit(): + return None + if code.startswith('6'): + return f"{code}.SH" + elif code.startswith('0') or code.startswith('3'): + return f"{code}.SZ" + else: + return f"{code}.BJ" + + +# ==================== 概念数据获取 ==================== + +def get_all_concepts(): + """从ES获取所有叶子概念及其股票列表""" + concepts = [] + + query = { + "query": {"match_all": {}}, + "size": 100, + "_source": ["concept_id", "concept", "stocks"] + } + + resp = ES_CLIENT.search(index=INDEX_NAME, body=query, scroll='2m') + scroll_id = resp['_scroll_id'] + hits = resp['hits']['hits'] + + while len(hits) > 0: + for hit in hits: + source = hit['_source'] + concept_info = { + 'concept_id': source.get('concept_id'), + 'concept_name': source.get('concept'), + 'stocks': [], + 'concept_type': 'leaf' + } + + if 'stocks' in source and isinstance(source['stocks'], list): + for stock in source['stocks']: + if isinstance(stock, dict) and 'code' in stock and stock['code']: + concept_info['stocks'].append(stock['code']) + + if concept_info['stocks']: + concepts.append(concept_info) + + resp = ES_CLIENT.scroll(scroll_id=scroll_id, scroll='2m') + scroll_id = resp['_scroll_id'] + hits = resp['hits']['hits'] + + ES_CLIENT.clear_scroll(scroll_id=scroll_id) + return concepts + + +def load_hierarchy_concepts(leaf_concepts: list) -> list: + """加载层级结构,生成母概念""" + hierarchy_path = os.path.join(os.path.dirname(__file__), HIERARCHY_FILE) + if not os.path.exists(hierarchy_path): + logger.warning(f"层级文件不存在: {hierarchy_path}") + return [] + + with open(hierarchy_path, 'r', encoding='utf-8') as f: + hierarchy_data = json.load(f) + + concept_to_stocks = {} + for c in leaf_concepts: + concept_to_stocks[c['concept_name']] = set(c['stocks']) + + parent_concepts = [] + + for lv1 in hierarchy_data.get('hierarchy', []): + lv1_name = lv1.get('lv1', '') + lv1_stocks = set() + + for child in lv1.get('children', []): + lv2_name = child.get('lv2', '') + lv2_stocks = set() + + if 'children' in child: + for lv3_child in child.get('children', []): + lv3_name = lv3_child.get('lv3', '') + lv3_stocks = set() + + for concept_name in lv3_child.get('concepts', []): + if concept_name in concept_to_stocks: + lv3_stocks.update(concept_to_stocks[concept_name]) + + if lv3_stocks: + parent_concepts.append({ + 'concept_id': generate_id(f"lv3_{lv3_name}"), + 'concept_name': f"[三级] {lv3_name}", + 'stocks': list(lv3_stocks), + 'concept_type': 'lv3' + }) + + lv2_stocks.update(lv3_stocks) + else: + for concept_name in child.get('concepts', []): + if concept_name in concept_to_stocks: + lv2_stocks.update(concept_to_stocks[concept_name]) + + if lv2_stocks: + parent_concepts.append({ + 'concept_id': generate_id(f"lv2_{lv2_name}"), + 'concept_name': f"[二级] {lv2_name}", + 'stocks': list(lv2_stocks), + 'concept_type': 'lv2' + }) + + lv1_stocks.update(lv2_stocks) + + if lv1_stocks: + parent_concepts.append({ + 'concept_id': generate_id(f"lv1_{lv1_name}"), + 'concept_name': f"[一级] {lv1_name}", + 'stocks': list(lv1_stocks), + 'concept_type': 'lv1' + }) + + return parent_concepts + + +# ==================== 价格数据获取 ==================== + +def get_base_prices(stock_codes: list, current_date: str) -> dict: + """获取昨收价作为基准""" + if not stock_codes: + return {} + + valid_codes = [code for code in stock_codes if code and len(code) == 6 and code.isdigit()] + if not valid_codes: + return {} + + stock_codes_str = "','".join(valid_codes) + + query = f""" + SELECT SECCODE, F002N + FROM ea_trade + WHERE SECCODE IN ('{stock_codes_str}') + AND TRADEDATE = ( + SELECT MAX(TRADEDATE) + FROM ea_trade + WHERE TRADEDATE <= '{current_date}' + ) + AND F002N IS NOT NULL AND F002N > 0 + """ + + try: + with MYSQL_ENGINE.connect() as conn: + result = conn.execute(text(query)) + base_prices = {row[0]: float(row[1]) for row in result if row[1] and float(row[1]) > 0} + return base_prices + except Exception as e: + logger.error(f"获取基准价格失败: {e}") + return {} + + +def get_latest_prices(stock_codes: list) -> dict: + """从ClickHouse获取最新价格""" + if not stock_codes: + return {} + + client = get_ch_client() + + ch_codes = [] + code_mapping = {} + for code in stock_codes: + ch_code = code_to_ch_format(code) + if ch_code: + ch_codes.append(ch_code) + code_mapping[ch_code] = code + + if not ch_codes: + return {} + + ch_codes_str = "','".join(ch_codes) + + query = f""" + SELECT code, close, timestamp + FROM ( + SELECT code, close, timestamp, + ROW_NUMBER() OVER (PARTITION BY code ORDER BY timestamp DESC) as rn + FROM stock_minute + WHERE code IN ('{ch_codes_str}') + AND toDate(timestamp) = today() + ) + WHERE rn = 1 + """ + + try: + result = client.execute(query) + if not result: + return {} + + latest_prices = {} + for row in result: + ch_code, close, ts = row + if close and close > 0: + pure_code = code_mapping.get(ch_code) + if pure_code: + latest_prices[pure_code] = { + 'close': float(close), + 'timestamp': ts + } + + return latest_prices + except Exception as e: + logger.error(f"获取最新价格失败: {e}") + return {} + + +def get_index_realtime(index_code: str = REFERENCE_INDEX) -> dict: + """获取指数实时数据""" + client = get_ch_client() + + try: + # 从 index_minute 表获取最新数据 + query = f""" + SELECT close, timestamp + FROM index_minute + WHERE code = '{index_code}' + AND toDate(timestamp) = today() + ORDER BY timestamp DESC + LIMIT 1 + """ + result = client.execute(query) + + if not result: + return None + + close, ts = result[0] + + # 获取昨收价 + prev_close = None + code_no_suffix = index_code.split('.')[0] + + with MYSQL_ENGINE.connect() as conn: + prev_result = conn.execute(text(""" + SELECT F006N FROM ea_exchangetrade + WHERE INDEXCODE = :code + AND TRADEDATE < CURDATE() + ORDER BY TRADEDATE DESC LIMIT 1 + """), {'code': code_no_suffix}).fetchone() + + if prev_result and prev_result[0]: + prev_close = float(prev_result[0]) + + change_pct = None + if close and prev_close and prev_close > 0: + change_pct = (float(close) - prev_close) / prev_close * 100 + + return { + 'code': index_code, + 'price': float(close), + 'prev_close': prev_close, + 'change_pct': round(change_pct, 4) if change_pct else None, + 'timestamp': ts + } + + except Exception as e: + logger.error(f"获取指数数据失败: {e}") + return None + + +# ==================== 涨跌幅计算 ==================== + +def calculate_change_pct(base_prices: dict, latest_prices: dict) -> dict: + """计算涨跌幅""" + changes = {} + for code, latest in latest_prices.items(): + if code in base_prices and base_prices[code] > 0: + base = base_prices[code] + close = latest['close'] + change_pct = (close - base) / base * 100 + changes[code] = { + 'change_pct': round(change_pct, 4), + 'close': close, + 'base': base + } + return changes + + +def calculate_concept_stats(concepts: list, stock_changes: dict) -> list: + """计算概念统计(包含涨停数)""" + stats = [] + + for concept in concepts: + concept_id = concept['concept_id'] + concept_name = concept['concept_name'] + stock_codes = concept['stocks'] + concept_type = concept.get('concept_type', 'leaf') + + changes = [] + limit_up_count = 0 + limit_up_stocks = [] + + for code in stock_codes: + if code in stock_changes: + change_info = stock_changes[code] + change_pct = change_info['change_pct'] + changes.append(change_pct) + + # 涨停判断(涨幅 >= 9.8%) + if change_pct >= 9.8: + limit_up_count += 1 + limit_up_stocks.append(code) + + if not changes: + continue + + avg_change_pct = round(np.mean(changes), 4) + + stats.append({ + 'concept_id': concept_id, + 'concept_name': concept_name, + 'avg_change_pct': avg_change_pct, + 'stock_count': len(changes), + 'concept_type': concept_type, + 'limit_up_count': limit_up_count, + 'limit_up_stocks': limit_up_stocks + }) + + # 按涨幅排序并添加排名 + stats.sort(key=lambda x: x['avg_change_pct'], reverse=True) + for i, item in enumerate(stats): + item['rank'] = i + 1 + + return stats + + +# ==================== 异动检测 ==================== + +def check_cooldown(concept_id: str, alert_type: str, cooldown_minutes: int) -> bool: + """检查是否在冷却期内""" + key = (concept_id, alert_type) + if key in cooldown_cache: + last_alert = cooldown_cache[key] + if datetime.now() - last_alert < timedelta(minutes=cooldown_minutes): + return True + return False + + +def set_cooldown(concept_id: str, alert_type: str): + """设置冷却""" + cooldown_cache[(concept_id, alert_type)] = datetime.now() + + +def update_history(concept_id: str, timestamp: datetime, change_pct: float, rank: int, limit_up_count: int): + """更新历史缓存""" + if concept_id not in history_cache: + history_cache[concept_id] = deque(maxlen=HISTORY_WINDOW) + + history_cache[concept_id].append({ + 'timestamp': timestamp, + 'change_pct': change_pct, + 'rank': rank, + 'limit_up_count': limit_up_count + }) + + +def get_history(concept_id: str, minutes_ago: int) -> dict: + """获取N分钟前的历史数据""" + if concept_id not in history_cache: + return None + + history = history_cache[concept_id] + if not history: + return None + + target_time = datetime.now() - timedelta(minutes=minutes_ago) + + # 找到最接近目标时间的记录 + for record in history: + if record['timestamp'] <= target_time: + return record + + # 如果没有足够早的数据,返回最早的记录 + return history[0] if history else None + + +def detect_alerts(current_stats: list, index_data: dict, trade_date: str) -> list: + """检测异动""" + alerts = [] + now = datetime.now() + + for stat in current_stats: + concept_id = stat['concept_id'] + concept_name = stat['concept_name'] + change_pct = stat['avg_change_pct'] + rank = stat['rank'] + limit_up_count = stat['limit_up_count'] + stock_count = stat['stock_count'] + concept_type = stat['concept_type'] + + # 更新历史 + update_history(concept_id, now, change_pct, rank, limit_up_count) + + # 1. 急涨检测 + if ALERT_CONFIG['surge']['enabled']: + cfg = ALERT_CONFIG['surge'] + if change_pct >= cfg['min_change_pct']: # 最低涨幅要求 + if not check_cooldown(concept_id, 'surge', cfg['cooldown_minutes']): + prev_data = get_history(concept_id, cfg['window_minutes']) + if prev_data: + change_delta = change_pct - prev_data['change_pct'] + if change_delta >= cfg['threshold_pct']: + alerts.append({ + 'concept_id': concept_id, + 'concept_name': concept_name, + 'alert_type': 'surge', + 'alert_time': now, + 'trade_date': trade_date, + 'change_pct': change_pct, + 'prev_change_pct': prev_data['change_pct'], + 'change_delta': round(change_delta, 4), + 'limit_up_count': limit_up_count, + 'rank_position': rank, + 'stock_count': stock_count, + 'concept_type': concept_type, + 'index_price': index_data['price'] if index_data else None, + 'index_change_pct': index_data['change_pct'] if index_data else None, + }) + set_cooldown(concept_id, 'surge') + logger.info(f"🔥 急涨异动: {concept_name} 涨幅 {prev_data['change_pct']:.2f}% -> {change_pct:.2f}% (+{change_delta:.2f}%)") + + # 2. 涨停数增加检测 + if ALERT_CONFIG['limit_up']['enabled']: + cfg = ALERT_CONFIG['limit_up'] + if limit_up_count > 0: + if not check_cooldown(concept_id, 'limit_up', cfg['cooldown_minutes']): + prev_data = get_history(concept_id, 1) # 对比上一分钟 + if prev_data: + limit_up_delta = limit_up_count - prev_data['limit_up_count'] + if limit_up_delta >= cfg['threshold_count']: + alerts.append({ + 'concept_id': concept_id, + 'concept_name': concept_name, + 'alert_type': 'limit_up', + 'alert_time': now, + 'trade_date': trade_date, + 'change_pct': change_pct, + 'limit_up_count': limit_up_count, + 'prev_limit_up_count': prev_data['limit_up_count'], + 'limit_up_delta': limit_up_delta, + 'rank_position': rank, + 'stock_count': stock_count, + 'concept_type': concept_type, + 'index_price': index_data['price'] if index_data else None, + 'index_change_pct': index_data['change_pct'] if index_data else None, + 'extra_info': {'limit_up_stocks': stat.get('limit_up_stocks', [])} + }) + set_cooldown(concept_id, 'limit_up') + logger.info(f"🚀 涨停异动: {concept_name} 涨停数 {prev_data['limit_up_count']} -> {limit_up_count} (+{limit_up_delta})") + + # 3. 排名跃升检测 + if ALERT_CONFIG['rank_jump']['enabled']: + cfg = ALERT_CONFIG['rank_jump'] + if rank <= cfg['max_rank']: # 只关注前N名 + if not check_cooldown(concept_id, 'rank_jump', cfg['cooldown_minutes']): + prev_data = get_history(concept_id, cfg['window_minutes']) + if prev_data and prev_data['rank'] > cfg['max_rank']: # 从榜外进入前N + rank_delta = prev_data['rank'] - rank # 正数表示上升 + if rank_delta >= cfg['threshold_rank']: + alerts.append({ + 'concept_id': concept_id, + 'concept_name': concept_name, + 'alert_type': 'rank_jump', + 'alert_time': now, + 'trade_date': trade_date, + 'change_pct': change_pct, + 'rank_position': rank, + 'prev_rank_position': prev_data['rank'], + 'rank_delta': -rank_delta, # 负数表示上升 + 'limit_up_count': limit_up_count, + 'stock_count': stock_count, + 'concept_type': concept_type, + 'index_price': index_data['price'] if index_data else None, + 'index_change_pct': index_data['change_pct'] if index_data else None, + }) + set_cooldown(concept_id, 'rank_jump') + logger.info(f"📈 排名跃升: {concept_name} 排名 {prev_data['rank']} -> {rank} (上升{rank_delta}名)") + + return alerts + + +# ==================== 数据持久化 ==================== + +def save_alerts_to_mysql(alerts: list): + """保存异动数据到MySQL""" + if not alerts: + return 0 + + saved = 0 + with MYSQL_ENGINE.begin() as conn: + for alert in alerts: + try: + insert_sql = text(""" + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, prev_change_pct, change_delta, + limit_up_count, prev_limit_up_count, limit_up_delta, + rank_position, prev_rank_position, rank_delta, + index_code, index_price, index_change_pct, + stock_count, concept_type, extra_info) + VALUES + (:concept_id, :concept_name, :alert_time, :alert_type, :trade_date, + :change_pct, :prev_change_pct, :change_delta, + :limit_up_count, :prev_limit_up_count, :limit_up_delta, + :rank_position, :prev_rank_position, :rank_delta, + :index_code, :index_price, :index_change_pct, + :stock_count, :concept_type, :extra_info) + """) + + params = { + 'concept_id': alert['concept_id'], + 'concept_name': alert['concept_name'], + 'alert_time': alert['alert_time'], + 'alert_type': alert['alert_type'], + 'trade_date': alert['trade_date'], + 'change_pct': alert.get('change_pct'), + 'prev_change_pct': alert.get('prev_change_pct'), + 'change_delta': alert.get('change_delta'), + 'limit_up_count': alert.get('limit_up_count', 0), + 'prev_limit_up_count': alert.get('prev_limit_up_count', 0), + 'limit_up_delta': alert.get('limit_up_delta', 0), + 'rank_position': alert.get('rank_position'), + 'prev_rank_position': alert.get('prev_rank_position'), + 'rank_delta': alert.get('rank_delta'), + 'index_code': REFERENCE_INDEX, + 'index_price': alert.get('index_price'), + 'index_change_pct': alert.get('index_change_pct'), + 'stock_count': alert.get('stock_count'), + 'concept_type': alert.get('concept_type', 'leaf'), + 'extra_info': json.dumps(alert.get('extra_info')) if alert.get('extra_info') else None + } + + conn.execute(insert_sql, params) + saved += 1 + + except Exception as e: + logger.error(f"保存异动失败: {alert['concept_name']} - {e}") + + return saved + + +def save_index_snapshot(index_data: dict, trade_date: str): + """保存指数快照""" + if not index_data: + return + + try: + with MYSQL_ENGINE.begin() as conn: + upsert_sql = text(""" + REPLACE INTO index_minute_snapshot + (index_code, trade_date, snapshot_time, price, prev_close, change_pct) + VALUES (:index_code, :trade_date, :snapshot_time, :price, :prev_close, :change_pct) + """) + + conn.execute(upsert_sql, { + 'index_code': index_data['code'], + 'trade_date': trade_date, + 'snapshot_time': index_data['timestamp'], + 'price': index_data['price'], + 'prev_close': index_data.get('prev_close'), + 'change_pct': index_data.get('change_pct') + }) + except Exception as e: + logger.error(f"保存指数快照失败: {e}") + + +# ==================== 交易时间判断 ==================== + +def is_trading_time() -> bool: + """判断当前是否为交易时间""" + now = datetime.now() + weekday = now.weekday() + + if weekday >= 5: + return False + + hour, minute = now.hour, now.minute + current_time = hour * 60 + minute + + morning_start = 9 * 60 + 30 + morning_end = 11 * 60 + 30 + afternoon_start = 13 * 60 + afternoon_end = 15 * 60 + + return (morning_start <= current_time <= morning_end) or \ + (afternoon_start <= current_time <= afternoon_end) + + +def get_next_update_time() -> int: + """获取距离下次更新的秒数""" + now = datetime.now() + + if is_trading_time(): + return 60 - now.second + else: + hour, minute = now.hour, now.minute + + if hour < 9 or (hour == 9 and minute < 30): + target = now.replace(hour=9, minute=30, second=0, microsecond=0) + elif (hour == 11 and minute >= 30) or hour == 12: + target = now.replace(hour=13, minute=0, second=0, microsecond=0) + elif hour >= 15: + target = (now + timedelta(days=1)).replace(hour=9, minute=30, second=0, microsecond=0) + else: + target = now + timedelta(minutes=1) + + wait_seconds = (target - now).total_seconds() + return max(60, int(wait_seconds)) + + +# ==================== 主运行逻辑 ==================== + +def run_once(concepts: list, all_stocks: list) -> tuple: + """执行一次检测,返回 (更新数, 异动数)""" + now = datetime.now() + trade_date = now.strftime('%Y-%m-%d') + + # 获取基准价格 + base_prices = get_base_prices(all_stocks, trade_date) + if not base_prices: + logger.warning("无法获取基准价格") + return 0, 0 + + # 获取最新价格 + latest_prices = get_latest_prices(all_stocks) + if not latest_prices: + logger.warning("无法获取最新价格") + return 0, 0 + + # 获取指数数据 + index_data = get_index_realtime(REFERENCE_INDEX) + if index_data: + save_index_snapshot(index_data, trade_date) + + # 计算涨跌幅 + stock_changes = calculate_change_pct(base_prices, latest_prices) + if not stock_changes: + logger.warning("无涨跌幅数据") + return 0, 0 + + logger.info(f"获取到 {len(stock_changes)} 只股票的涨跌幅") + + # 计算概念统计 + stats = calculate_concept_stats(concepts, stock_changes) + logger.info(f"计算了 {len(stats)} 个概念的涨跌幅") + + # 检测异动 + alerts = detect_alerts(stats, index_data, trade_date) + + # 保存异动 + if alerts: + saved = save_alerts_to_mysql(alerts) + logger.info(f"💾 保存了 {saved} 条异动记录") + + return len(stats), len(alerts) + + +def run_realtime(): + """实时检测主循环""" + logger.info("=" * 60) + logger.info("🚀 启动概念异动实时检测服务") + logger.info("=" * 60) + logger.info(f"异动配置: {json.dumps(ALERT_CONFIG, indent=2, ensure_ascii=False)}") + + # 加载概念数据 + logger.info("加载概念数据...") + leaf_concepts = get_all_concepts() + logger.info(f"获取到 {len(leaf_concepts)} 个叶子概念") + + parent_concepts = load_hierarchy_concepts(leaf_concepts) + logger.info(f"生成了 {len(parent_concepts)} 个母概念") + + all_concepts = leaf_concepts + parent_concepts + logger.info(f"总计 {len(all_concepts)} 个概念") + + # 收集所有股票代码 + all_stocks = set() + for c in all_concepts: + all_stocks.update(c['stocks']) + all_stocks = list(all_stocks) + logger.info(f"监控 {len(all_stocks)} 只股票") + + last_concept_update = datetime.now() + total_alerts = 0 + + while True: + try: + now = datetime.now() + + # 每小时重新加载概念数据 + if (now - last_concept_update).total_seconds() > 3600: + logger.info("重新加载概念数据...") + leaf_concepts = get_all_concepts() + parent_concepts = load_hierarchy_concepts(leaf_concepts) + all_concepts = leaf_concepts + parent_concepts + all_stocks = set() + for c in all_concepts: + all_stocks.update(c['stocks']) + all_stocks = list(all_stocks) + last_concept_update = now + logger.info(f"更新完成: {len(all_concepts)} 个概念, {len(all_stocks)} 只股票") + + # 检查是否交易时间 + if not is_trading_time(): + wait_sec = get_next_update_time() + wait_min = wait_sec // 60 + logger.info(f"⏰ 非交易时间,等待 {wait_min} 分钟后重试...") + time.sleep(min(wait_sec, 300)) + continue + + # 执行检测 + logger.info(f"\n{'=' * 40}") + logger.info(f"🔍 检测时间: {now.strftime('%Y-%m-%d %H:%M:%S')}") + updated, alert_count = run_once(all_concepts, all_stocks) + total_alerts += alert_count + + if alert_count > 0: + logger.info(f"📊 本次检测到 {alert_count} 条异动,累计 {total_alerts} 条") + + # 等待下一分钟 + sleep_sec = 60 - datetime.now().second + logger.info(f"⏳ 等待 {sleep_sec} 秒后继续...") + time.sleep(sleep_sec) + + except KeyboardInterrupt: + logger.info("\n收到退出信号,停止服务...") + break + except Exception as e: + logger.error(f"发生错误: {e}") + import traceback + traceback.print_exc() + time.sleep(60) + + +def run_single(): + """单次运行""" + logger.info("单次检测模式") + + leaf_concepts = get_all_concepts() + parent_concepts = load_hierarchy_concepts(leaf_concepts) + all_concepts = leaf_concepts + parent_concepts + + all_stocks = set() + for c in all_concepts: + all_stocks.update(c['stocks']) + all_stocks = list(all_stocks) + + logger.info(f"概念数: {len(all_concepts)}, 股票数: {len(all_stocks)}") + + updated, alerts = run_once(all_concepts, all_stocks) + logger.info(f"检测完成: {updated} 个概念, {alerts} 条异动") + + +def show_status(): + """显示状态""" + print("\n" + "=" * 60) + print("概念异动实时检测服务 - 状态") + print("=" * 60) + + now = datetime.now() + print(f"\n当前时间: {now.strftime('%Y-%m-%d %H:%M:%S')}") + print(f"是否交易时间: {'是' if is_trading_time() else '否'}") + + # 今日异动统计 + print("\n今日异动统计:") + try: + with MYSQL_ENGINE.connect() as conn: + result = conn.execute(text(""" + SELECT alert_type, COUNT(*) as cnt + FROM concept_minute_alert + WHERE trade_date = CURDATE() + GROUP BY alert_type + """)) + rows = list(result) + if rows: + for row in rows: + alert_type_name = { + 'surge': '急涨', + 'limit_up': '涨停增加', + 'rank_jump': '排名跃升' + }.get(row[0], row[0]) + print(f" {alert_type_name}: {row[1]} 条") + else: + print(" 今日暂无异动") + + # 最新异动 + print("\n最新异动 (前10条):") + result = conn.execute(text(""" + SELECT concept_name, alert_type, alert_time, change_pct, limit_up_count, index_price + FROM concept_minute_alert + WHERE trade_date = CURDATE() + ORDER BY alert_time DESC + LIMIT 10 + """)) + rows = list(result) + if rows: + print(f" {'概念':<20} | {'类型':<8} | {'时间':<8} | {'涨幅':>6} | {'涨停':>4} | {'指数':>8}") + print(" " + "-" * 70) + for row in rows: + name = row[0][:18] if len(row[0]) > 18 else row[0] + alert_type = {'surge': '急涨', 'limit_up': '涨停', 'rank_jump': '排名'}.get(row[1], row[1]) + time_str = row[2].strftime('%H:%M') if row[2] else '-' + change = f"{row[3]:.2f}%" if row[3] else '-' + limit_up = str(row[4]) if row[4] else '-' + index_p = f"{row[5]:.2f}" if row[5] else '-' + print(f" {name:<20} | {alert_type:<8} | {time_str:<8} | {change:>6} | {limit_up:>4} | {index_p:>8}") + else: + print(" 暂无异动记录") + + except Exception as e: + print(f" 查询失败: {e}") + + +def init_tables(): + """初始化数据库表""" + print("初始化数据库表...") + + sql_file = os.path.join(os.path.dirname(__file__), 'sql', 'concept_minute_alert.sql') + + if not os.path.exists(sql_file): + print(f"SQL文件不存在: {sql_file}") + return + + with open(sql_file, 'r', encoding='utf-8') as f: + sql_content = f.read() + + # 分割多个语句 + statements = [s.strip() for s in sql_content.split(';') if s.strip() and not s.strip().startswith('--')] + + with MYSQL_ENGINE.begin() as conn: + for stmt in statements: + if stmt: + try: + conn.execute(text(stmt)) + print(f"✅ 执行成功") + except Exception as e: + print(f"❌ 执行失败: {e}") + + print("初始化完成") + + +# ==================== 回测功能 ==================== + +def get_minute_timestamps(trade_date: str) -> list: + """获取指定交易日的所有分钟时间戳""" + client = get_ch_client() + + query = f""" + SELECT DISTINCT timestamp + FROM stock_minute + WHERE toDate(timestamp) = '{trade_date}' + ORDER BY timestamp + """ + + result = client.execute(query) + return [row[0] for row in result] + + +def get_prices_at_time(stock_codes: list, timestamp: datetime) -> dict: + """获取指定时间点的股票价格 + + Args: + stock_codes: 纯6位股票代码列表 + timestamp: 指定的时间点 + + Returns: + dict: {纯6位代码: {'close': 价格, 'timestamp': 时间}} + """ + if not stock_codes: + return {} + + client = get_ch_client() + + # 转换为ClickHouse格式 + ch_codes = [] + code_mapping = {} + for code in stock_codes: + ch_code = code_to_ch_format(code) + if ch_code: + ch_codes.append(ch_code) + code_mapping[ch_code] = code + + if not ch_codes: + return {} + + ch_codes_str = "','".join(ch_codes) + + # 获取指定时间点的数据 + query = f""" + SELECT code, close, timestamp + FROM stock_minute + WHERE code IN ('{ch_codes_str}') + AND timestamp = '{timestamp.strftime('%Y-%m-%d %H:%M:%S')}' + """ + + try: + result = client.execute(query) + prices = {} + for row in result: + ch_code, close, ts = row + if close and close > 0: + pure_code = code_mapping.get(ch_code) + if pure_code: + prices[pure_code] = { + 'close': float(close), + 'timestamp': ts + } + return prices + except Exception as e: + logger.error(f"获取历史价格失败: {e}") + return {} + + +def get_index_at_time(index_code: str, timestamp: datetime, prev_close: float) -> dict: + """获取指定时间点的指数数据""" + client = get_ch_client() + + query = f""" + SELECT close, timestamp + FROM index_minute + WHERE code = '{index_code}' + AND timestamp = '{timestamp.strftime('%Y-%m-%d %H:%M:%S')}' + LIMIT 1 + """ + + try: + result = client.execute(query) + if not result: + return None + + close, ts = result[0] + change_pct = None + if close and prev_close and prev_close > 0: + change_pct = (float(close) - prev_close) / prev_close * 100 + + return { + 'code': index_code, + 'price': float(close), + 'prev_close': prev_close, + 'change_pct': round(change_pct, 4) if change_pct else None, + 'timestamp': ts + } + except Exception as e: + logger.error(f"获取指数数据失败: {e}") + return None + + +def get_index_prev_close(index_code: str, trade_date: str) -> float: + """获取指数昨收价""" + code_no_suffix = index_code.split('.')[0] + + with MYSQL_ENGINE.connect() as conn: + result = conn.execute(text(""" + SELECT F006N FROM ea_exchangetrade + WHERE INDEXCODE = :code + AND TRADEDATE < :today + ORDER BY TRADEDATE DESC LIMIT 1 + """), { + 'code': code_no_suffix, + 'today': trade_date + }).fetchone() + + if result and result[0]: + return float(result[0]) + return None + + +def run_backtest(trade_date: str, clear_existing: bool = True): + """ + 回测指定日期的异动检测 + + Args: + trade_date: 交易日期,格式 'YYYY-MM-DD' + clear_existing: 是否清除该日期已有的异动数据 + """ + global history_cache, cooldown_cache + + logger.info("=" * 60) + logger.info(f"🔄 开始回测: {trade_date}") + logger.info("=" * 60) + + # 清空缓存 + history_cache = {} + cooldown_cache = {} + + # 清除已有数据 + if clear_existing: + with MYSQL_ENGINE.begin() as conn: + conn.execute(text("DELETE FROM concept_minute_alert WHERE trade_date = :date"), {'date': trade_date}) + conn.execute(text("DELETE FROM index_minute_snapshot WHERE trade_date = :date"), {'date': trade_date}) + logger.info(f"已清除 {trade_date} 的已有数据") + + # 加载概念数据 + logger.info("加载概念数据...") + leaf_concepts = get_all_concepts() + logger.info(f"获取到 {len(leaf_concepts)} 个叶子概念") + + parent_concepts = load_hierarchy_concepts(leaf_concepts) + logger.info(f"生成了 {len(parent_concepts)} 个母概念") + + all_concepts = leaf_concepts + parent_concepts + logger.info(f"总计 {len(all_concepts)} 个概念") + + # 收集所有股票代码 + all_stocks = set() + for c in all_concepts: + all_stocks.update(c['stocks']) + all_stocks = list(all_stocks) + logger.info(f"监控 {len(all_stocks)} 只股票") + + # 获取基准价格(昨收价) + base_prices = get_base_prices(all_stocks, trade_date) + if not base_prices: + logger.error("无法获取基准价格,退出回测") + return + logger.info(f"获取到 {len(base_prices)} 个基准价格") + + # 获取指数昨收价 + index_prev_close = get_index_prev_close(REFERENCE_INDEX, trade_date) + logger.info(f"指数昨收价: {index_prev_close}") + + # 获取所有分钟时间戳 + timestamps = get_minute_timestamps(trade_date) + if not timestamps: + logger.error(f"未找到 {trade_date} 的分钟数据") + return + logger.info(f"找到 {len(timestamps)} 个分钟时间点") + + total_alerts = 0 + processed = 0 + + # 逐分钟处理 + for ts in timestamps: + processed += 1 + + # 获取该时间点的价格 + latest_prices = get_prices_at_time(all_stocks, ts) + if not latest_prices: + continue + + # 获取指数数据 + index_data = get_index_at_time(REFERENCE_INDEX, ts, index_prev_close) + if index_data: + save_index_snapshot(index_data, trade_date) + + # 计算涨跌幅 + stock_changes = calculate_change_pct(base_prices, latest_prices) + if not stock_changes: + continue + + # 计算概念统计 + stats = calculate_concept_stats(all_concepts, stock_changes) + + # 检测异动(使用回测专用函数) + alerts = detect_alerts_backtest(stats, index_data, trade_date, ts) + + # 保存异动 + if alerts: + saved = save_alerts_to_mysql(alerts) + total_alerts += saved + + # 进度显示 + if processed % 30 == 0: + logger.info(f"进度: {processed}/{len(timestamps)} ({processed*100//len(timestamps)}%), 已检测到 {total_alerts} 条异动") + + logger.info("=" * 60) + logger.info(f"✅ 回测完成!") + logger.info(f" 处理分钟数: {processed}") + logger.info(f" 检测到异动: {total_alerts} 条") + logger.info("=" * 60) + + +def detect_alerts_backtest(current_stats: list, index_data: dict, trade_date: str, current_time: datetime) -> list: + """ + 回测模式的异动检测(使用指定时间而非当前时间) + """ + alerts = [] + + for stat in current_stats: + concept_id = stat['concept_id'] + concept_name = stat['concept_name'] + change_pct = stat['avg_change_pct'] + rank = stat['rank'] + limit_up_count = stat['limit_up_count'] + stock_count = stat['stock_count'] + concept_type = stat['concept_type'] + + # 更新历史(使用指定时间) + if concept_id not in history_cache: + history_cache[concept_id] = deque(maxlen=HISTORY_WINDOW) + + history_cache[concept_id].append({ + 'timestamp': current_time, + 'change_pct': change_pct, + 'rank': rank, + 'limit_up_count': limit_up_count + }) + + # 获取历史数据的辅助函数(回测专用) + def get_history_backtest(concept_id: str, minutes_ago: int): + if concept_id not in history_cache: + return None + history = history_cache[concept_id] + if not history: + return None + + target_time = current_time - timedelta(minutes=minutes_ago) + for record in reversed(list(history)): + if record['timestamp'] <= target_time: + return record + return None + + # 检查冷却(回测专用) + def check_cooldown_backtest(concept_id: str, alert_type: str, cooldown_minutes: int) -> bool: + key = (concept_id, alert_type) + if key in cooldown_cache: + last_alert = cooldown_cache[key] + if current_time - last_alert < timedelta(minutes=cooldown_minutes): + return True + return False + + def set_cooldown_backtest(concept_id: str, alert_type: str): + cooldown_cache[(concept_id, alert_type)] = current_time + + # 1. 急涨检测 + if ALERT_CONFIG['surge']['enabled']: + cfg = ALERT_CONFIG['surge'] + if change_pct >= cfg['min_change_pct']: + if not check_cooldown_backtest(concept_id, 'surge', cfg['cooldown_minutes']): + prev_data = get_history_backtest(concept_id, cfg['window_minutes']) + if prev_data: + change_delta = change_pct - prev_data['change_pct'] + if change_delta >= cfg['threshold_pct']: + alerts.append({ + 'concept_id': concept_id, + 'concept_name': concept_name, + 'alert_type': 'surge', + 'alert_time': current_time, + 'trade_date': trade_date, + 'change_pct': change_pct, + 'prev_change_pct': prev_data['change_pct'], + 'change_delta': round(change_delta, 4), + 'limit_up_count': limit_up_count, + 'rank_position': rank, + 'stock_count': stock_count, + 'concept_type': concept_type, + 'index_price': index_data['price'] if index_data else None, + 'index_change_pct': index_data['change_pct'] if index_data else None, + }) + set_cooldown_backtest(concept_id, 'surge') + logger.debug(f"🔥 急涨: {concept_name} {prev_data['change_pct']:.2f}% -> {change_pct:.2f}%") + + # 2. 涨停数增加检测 + if ALERT_CONFIG['limit_up']['enabled']: + cfg = ALERT_CONFIG['limit_up'] + if limit_up_count > 0: + if not check_cooldown_backtest(concept_id, 'limit_up', cfg['cooldown_minutes']): + prev_data = get_history_backtest(concept_id, 1) + if prev_data: + limit_up_delta = limit_up_count - prev_data['limit_up_count'] + if limit_up_delta >= cfg['threshold_count']: + alerts.append({ + 'concept_id': concept_id, + 'concept_name': concept_name, + 'alert_type': 'limit_up', + 'alert_time': current_time, + 'trade_date': trade_date, + 'change_pct': change_pct, + 'limit_up_count': limit_up_count, + 'prev_limit_up_count': prev_data['limit_up_count'], + 'limit_up_delta': limit_up_delta, + 'rank_position': rank, + 'stock_count': stock_count, + 'concept_type': concept_type, + 'index_price': index_data['price'] if index_data else None, + 'index_change_pct': index_data['change_pct'] if index_data else None, + 'extra_info': {'limit_up_stocks': stat.get('limit_up_stocks', [])} + }) + set_cooldown_backtest(concept_id, 'limit_up') + logger.debug(f"🚀 涨停: {concept_name} 涨停数 +{limit_up_delta}") + + # 3. 排名跃升检测 + if ALERT_CONFIG['rank_jump']['enabled']: + cfg = ALERT_CONFIG['rank_jump'] + if rank <= cfg['max_rank']: + if not check_cooldown_backtest(concept_id, 'rank_jump', cfg['cooldown_minutes']): + prev_data = get_history_backtest(concept_id, cfg['window_minutes']) + if prev_data and prev_data['rank'] > cfg['max_rank']: + rank_delta = prev_data['rank'] - rank + if rank_delta >= cfg['threshold_rank']: + alerts.append({ + 'concept_id': concept_id, + 'concept_name': concept_name, + 'alert_type': 'rank_jump', + 'alert_time': current_time, + 'trade_date': trade_date, + 'change_pct': change_pct, + 'rank_position': rank, + 'prev_rank_position': prev_data['rank'], + 'rank_delta': -rank_delta, + 'limit_up_count': limit_up_count, + 'stock_count': stock_count, + 'concept_type': concept_type, + 'index_price': index_data['price'] if index_data else None, + 'index_change_pct': index_data['change_pct'] if index_data else None, + }) + set_cooldown_backtest(concept_id, 'rank_jump') + logger.debug(f"📈 排名跃升: {concept_name} 排名 {prev_data['rank']} -> {rank}") + + return alerts + + +# ==================== 主函数 ==================== + +def main(): + parser = argparse.ArgumentParser(description='概念异动实时检测服务') + parser.add_argument('command', nargs='?', default='realtime', + choices=['realtime', 'once', 'status', 'init', 'backtest'], + help='命令: realtime(实时运行), once(单次运行), status(状态查看), init(初始化表), backtest(回测历史)') + parser.add_argument('--date', '-d', type=str, default=None, + help='回测日期,格式: YYYY-MM-DD,默认为今天') + parser.add_argument('--keep', '-k', action='store_true', + help='回测时保留已有数据(默认会清除)') + + args = parser.parse_args() + + if args.command == 'realtime': + run_realtime() + elif args.command == 'once': + run_single() + elif args.command == 'status': + show_status() + elif args.command == 'init': + init_tables() + elif args.command == 'backtest': + # 回测模式 + trade_date = args.date or datetime.now().strftime('%Y-%m-%d') + clear_existing = not args.keep + run_backtest(trade_date, clear_existing) + + +if __name__ == "__main__": + main() diff --git a/concept_hierarchy_v3.json b/concept_hierarchy_v3.json index a7eb2200..e5b74263 100644 --- a/concept_hierarchy_v3.json +++ b/concept_hierarchy_v3.json @@ -2,435 +2,602 @@ "hierarchy": [ { "lv1": "人工智能", - "lv1_id": "lv1_1", + "lv1_id": "ai", "children": [ { - "lv2": "AI基础设施", - "lv2_id": "lv2_1_1", + "lv2": "AI芯片与存储", + "lv2_id": "ai_chips_memory", + "concepts": [ + "AI算力芯片", + "AI芯片", + "GPU概念股", + "HBM", + "SRAM存储", + "RISC-V", + "TPU芯片", + "中昊芯英概念股", + "国产算力芯片", + "国产GPU", + "国产芯片参股公司", + "摩尔线程", + "摩尔线程IPO", + "沐曦集成", + "超威半导体AMD", + "英伟达概念", + "英伟达H20", + "芯片替代", + "谷歌", + "谷歌概念", + "阿里AI芯片", + "利基型存储DDR4", + "存储芯片产业", + "存储", + "存储芯片" + ] + }, + { + "lv2": "AI硬件与基础设施", + "lv2_id": "ai_hardware_infra", "children": [ { - "lv3": "AI算力硬件", - "lv3_id": "lv3_1_1_1", - "concepts": [ - "AI一体机", - "AI算力芯片", - "AI芯片", - "DeepSeek智算一体机", - "GPU概念股", - "TPU芯片", - "一体机核心标的弹性测算", - "中昊芯英概念股", - "国产算力芯片", - "国产GPU", - "服务器", - "昇腾推理一体机", - "摩尔线程", - "摩尔线程IPO", - "沐曦集成", - "阿里AI芯片" - ] - }, - { - "lv3": "AI关键组件", - "lv3_id": "lv3_1_1_2", + "lv3": "AI PCB与封装", + "lv3_id": "ai_pcb_packaging", "concepts": [ "AI PCB", "AI PCB英伟达M9", - "AI服务器钽电容", - "HBM", - "OCS光电路交换机", "PCB", "PCB设备及耗材", - "SRAM存储", - "光纤", - "光通信CPO", - "光通信", - "光芯片", - "光纤列阵单元FAU", - "博通交换机", - "改良型半加成工艺mSAP", - "服务器零部件", - "硅光技术", - "空芯光纤", - "薄膜铌酸锂", - "存储", - "存储芯片", - "存储芯片产业", - "铜互连", - "铜连接", - "钽电容", - "忆阻器", - "利基型存储DDR4" + "改良型半加成工艺mSAP" ] }, { - "lv3": "AI配套设施", - "lv3_id": "lv3_1_1_3", + "lv3": "AI一体机", + "lv3_id": "ai_integrated_machines", + "concepts": [ + "AI一体机", + "DeepSeek智算一体机", + "一体机核心标的弹性测算", + "昇腾推理一体机" + ] + }, + { + "lv3": "AI服务器组件", + "lv3_id": "ai_server_components", + "concepts": [ + "AI服务器钽电容", + "钽电容", + "功率半导体", + "电池备份单元", + "超级电容器" + ] + }, + { + "lv3": "AI数据中心与电力", + "lv3_id": "ai_data_center_power", "concepts": [ "AIDC供配电设备弹性", "数据中心", - "数据中心液冷", "数据中心电力设备", - "微泵液冷", - "微通道水冷板", - "液冷", - "液冷数据中心", - "液态金属散热", - "电池备份单元", - "电磁屏蔽", - "超级电容器", "柴油发电机", - "钻石散热", "英伟达电源方案" ] }, { - "lv3": "算力网络与服务", - "lv3_id": "lv3_1_1_4", + "lv3": "AI散热", + "lv3_id": "ai_cooling", "concepts": [ - "中国星际之门芜湖", - "上海算力", - "四川算力", - "大厂算力订单", - "字节算力", - "星际之门概念", - "杭州算力大会", - "核心城市智算算力", - "毫秒用算", - "算力" + "微泵液冷", + "微通道水冷板", + "数据中心液冷", + "液态金属散热", + "液冷", + "钻石散热", + "电磁屏蔽" + ] + }, + { + "lv3": "AI网络设备", + "lv3_id": "ai_network_equipment", + "concepts": [ + "博通交换机", + "铜连接", + "铜互连", + "铜互联" ] } ] }, { - "lv2": "AI模型与软件", - "lv2_id": "lv2_1_2", + "lv2": "AI光通信", + "lv2_id": "ai_optical_comm", + "concepts": [ + "光纤", + "光通信CPO", + "光通信", + "光芯片", + "光纤列阵单元FAU", + "OCS光电路交换机", + "硅光技术", + "空芯光纤", + "薄膜铌酸锂" + ] + }, + { + "lv2": "AI算力基础设施", + "lv2_id": "ai_computing_power_infra", + "concepts": [ + "中国星际之门芜湖", + "上海算力", + "四川算力", + "大厂算力订单", + "字节算力", + "核心城市智算算力", + "毫秒用算", + "星际之门概念", + "杭州算力大会", + "甲骨文概念股", + "算力" + ] + }, + { + "lv2": "AI模型与算法", + "lv2_id": "ai_models_algorithms", "concepts": [ "DeepSeek FP8", "DeepSeek", "DeepSeek、国产算力", - "KIMI", "MOE模型", "Minimax", - "Nano Banana", "SORA概念", "国产大模型", + "阶跃星辰" + ] + }, + { + "lv2": "AI应用与服务", + "lv2_id": "ai_applications_services", + "concepts": [ + "AI4S", + "AI应用AI语料", + "AI编程", + "Nano Banana", + "KIMI", + "低代码", + "内容审核概念", "文生视频", - "秘塔AI", - "阶跃星辰", + "智象未来", + "版权", + "秘塔AI" + ] + }, + { + "lv2": "AI智能体与陪伴", + "lv2_id": "ai_agents_companions", + "concepts": [ + "AI伴侣", + "AI成人陪伴", + "AI应用陪伴智能体", + "AI智能体", + "AI应用智能体", + "AI智能体AI应用", + "AI语音助手", + "AI陪伴", + "Manus", + "字节AI陪伴", + "开发智能体" + ] + }, + { + "lv2": "AI通用概念", + "lv2_id": "ai_general_concepts", + "concepts": [ + "AI-细分延伸更新", + "AI合集", + "物理AI", "马斯克Grok3大模型" ] }, { - "lv2": "AI应用", - "lv2_id": "lv2_1_3", - "children": [ - { - "lv3": "智能体与陪伴", - "lv3_id": "lv3_1_3_1", - "concepts": [ - "AI伴侣", - "AI成人陪伴", - "AI应用陪伴智能体", - "AI智能体", - "AI应用智能体", - "AI智能体AI应用", - "AI语音助手", - "AI陪伴", - "Manus", - "字节AI陪伴", - "开发智能体" - ] - }, - { - "lv3": "行业应用", - "lv3_id": "lv3_1_3_2", - "concepts": [ - "AI4S", - "AI应用AI语料", - "AI编程", - "低代码", - "内容审核概念", - "物理AI" - ] - } - ] - }, - { - "lv2": "AI生态系统", - "lv2_id": "lv2_1_4", - "children": [ - { - "lv3": "通用生态", - "lv3_id": "lv3_1_4_1", - "concepts": [ - "云计算各厂商云", - "微软Azure云平台", - "甲骨文概念股", - "英伟达代理", - "英伟达概念", - "英伟达H20", - "超威半导体AMD", - "谷歌", - "谷歌概念" - ] - }, - { - "lv3": "阿里生态", - "lv3_id": "lv3_1_4_2", - "concepts": [ - "通义千问阿里云", - "阿里云", - "阿里云通义千问", - "阿里AI千问、灵光", - "阿里“千问”项目", - "阿里AI来听" - ] - }, - { - "lv3": "腾讯生态", - "lv3_id": "lv3_1_4_3", - "concepts": [ - "腾讯元宝", - "腾讯大模型", - "腾讯混元大模型", - "腾讯云及大模型合作公司" - ] - }, - { - "lv3": "字节跳动生态", - "lv3_id": "lv3_1_4_4", - "concepts": [ - "努比亚手机", - "华为抖音支付", - "字节概念豆包AI手机", - "字节豆包概念股", - "抖音概念", - "豆包大模型" - ] - } - ] - }, - { - "lv2": "AI综合与趋势", - "lv2_id": "lv2_1_5", + "lv2": "云平台与大模型", + "lv2_id": "cloud_platforms_large_models", "concepts": [ - "AI-细分延伸更新", - "AI合集" + "云手机", + "云计算各厂商云", + "微软Azure云平台", + "腾讯元宝", + "腾讯大模型", + "腾讯混元大模型", + "腾讯云及大模型合作公司", + "通义千问阿里云", + "阿里云通义千问", + "阿里AI千问、灵光", + "阿里“千问”项目", + "阿里AI来听", + "阿里云" ] } ] }, { "lv1": "半导体", - "lv1_id": "lv1_2", + "lv1_id": "semiconductor", "children": [ { - "lv2": "半导体设备", - "lv2_id": "lv2_2_1", + "lv2": "半导体制造与材料", + "lv2_id": "semiconductor_mfg_materials", "concepts": [ "EDA", + "GAA晶体管", + "PSPI", "上海微电子", - "光刻机", + "先进陶瓷", + "光刻胶", "光刻机宇量昇", + "光刻机", + "半导体抛光液", + "半导体封测", + "半导体设计", + "半导体材料", + "半导体产业链", "半导体设备", + "半导体混合键合技术", + "国产光刻胶", + "国产半导体", "大湾区芯片展览会-新凯莱", "新凯来概念股", "新凯来示波器", - "电子束光刻机“羲之”" - ] - }, - { - "lv2": "半导体材料", - "lv2_id": "lv2_2_2", - "concepts": [ - "PSPI", - "先进陶瓷", - "光刻胶", - "半导体抛光液", - "半导体材料", - "国产光刻胶", - "石英砂", - "磷化铟" - ] - }, - { - "lv2": "芯片设计与制造", - "lv2_id": "lv2_2_3", - "concepts": [ - "ASIC", - "GAA晶体管", - "ISP视觉", - "RISC-V", - "功率半导体", - "半导体设计", - "第三代半导体", - "端侧AI芯片", - "碳化硅", - "英诺赛科概念股" - ] - }, - { - "lv2": "先进封装", - "lv2_id": "lv2_2_4", - "concepts": [ - "半导体封测", - "半导体混合键合技术", + "电子束光刻机“羲之”", "玻璃基板", + "石英砂", + "磷化铟", + "盛合晶微概念股", "盛合晶微", - "盛合晶微概念股" - ] - }, - { - "lv2": "重点企业与IPO", - "lv2_id": "lv2_2_5", - "concepts": [ - "地平线", - "地平线概念", + "第三代半导体", "紫光展锐IPO", + "碳化硅", + "科特估半导体", + "英诺赛科概念股", + "超硬材料", "长鑫存储", - "长鑫、长江产业链", - "高通概念" + "长鑫、长江产业链" ] }, { - "lv2": "综合与政策", - "lv2_id": "lv2_2_6", + "lv2": "半导体政策与地缘", + "lv2_id": "semiconductor_policy_geopolitics", "concepts": [ - "半导体产业链", - "国产半导体", - "国产芯片参股公司", - "科特估半导体", - "芯片替代", - "英特尔概念股" + "中美关系", + "出口管制" ] } ] }, { - "lv1": "机器人", - "lv1_id": "lv1_3", + "lv1": "机器人与智能制造", + "lv1_id": "robotics_smart_mfg", "children": [ { - "lv2": "人形机器人整机", - "lv2_id": "lv2_3_1", + "lv2": "人形机器人整机与生态", + "lv2_id": "humanoid_robot_systems_ecosystems", "concepts": [ "Optimus特斯拉机器人", "乐聚机器人", - "人形机器人", + "人形机器产业链", "人形机器人Figure", + "人形机器人", "云深处", "优必选", + "人形机器人万向节", + "人形机器人核心标的概览", + "人形机器人核心标的估值弹性测算", "优必选机器人", "华为人形机器人", "各厂商机器人", "奇瑞机器人潜在产业链", "天太机器人", - "天工机器人", "宇树人形机器人", "宇树机器人", + "小米智元机器人产业链机构机构版", + "天工机器人", "小鹏机器人", "小米机器人", - "小米智元机器人产业链机构版", "开普勒机器人", - "松延动力机器人", - "特斯拉人形机器人", + "工业机器人", "智元机器人", + "松延动力机器人", + "机器狗四足机器人", + "特斯拉人形机器人价值量", + "特斯拉人形机器人", + "特斯拉产业链", + "特斯拉人形机器人弹性测算", "荣耀华为人形机器人", + "美的库卡机器人", "赛力斯机器人" ] }, { - "lv2": "机器人核心零部件", - "lv2_id": "lv2_3_2", + "lv2": "机器人核心部件与材料", + "lv2_id": "robotics_core_components_materials", "concepts": [ + "MIM概念", "PCB轴向磁通电机", "人形机器人-滚柱丝杆丝杠", - "人形机器人万向节", + "人形机器人轻量化-PEEK材料", "人形机器人腱绳", - "机电", - "机器人皮肤仿生皮肤", + "冷锻产业链", + "机器人轻量化-PEEK", + "机器人轻量化—碳纤维", + "机器人零部件加工设备", + "机器人轻量化-镁铝合金", "摆线减速器", - "电子皮肤", - "轴向磁通电机" + "金属粉末注射成形MIM", + "超高分子量聚乙烯纤维", + "机器人氮化镓", + "轴向磁通电机", + "机电" ] }, { - "lv2": "机器人产业链", - "lv2_id": "lv2_3_3", - "children": [ - { - "lv3": "综合与价值链", - "lv3_id": "lv3_3_3_1", - "concepts": [ - "人形机器产业链", - "人形机器人核心标的估值弹性测算", - "人形机器人核心标的概览", - "特斯拉人形机器人价值量", - "特斯拉人形机器人弹性测算", - "特斯拉产业链" - ] - }, - { - "lv3": "轻量化材料", - "lv3_id": "lv3_3_3_2", - "concepts": [ - "人形机器人轻量化-PEEK", - "人形机器人轻量化-PEEK材料", - "机器人轻量化-镁铝合金", - "机器人轻量化—碳纤维", - "超高分子量聚乙烯纤维" - ] - }, - { - "lv3": "制造工艺与设备", - "lv3_id": "lv3_3_3_3", - "concepts": [ - "MIM概念", - "冷锻产业链", - "机器人零部件加工设备", - "金属粉末注射成形MIM" - ] - } + "lv2": "机器人感知与交互", + "lv2_id": "robotics_perception_interaction", + "concepts": [ + "机器人动作捕捉", + "机器人皮肤仿生皮肤", + "电子皮肤" ] }, { - "lv2": "机器人软件与AI", - "lv2_id": "lv2_3_4", + "lv2": "机器人AI与控制", + "lv2_id": "robotics_ai_control", "concepts": [ "机器人-神经网络", - "机器人动作捕捉" + "神经网络" ] }, { - "lv2": "其他类型机器人", - "lv2_id": "lv2_3_5", + "lv2": "信创与工业自动化", + "lv2_id": "xinchuang_industrial_automation", "concepts": [ - "外骨骼机器人", - "工业机器人", - "机器狗四足机器人", - "美的库卡机器人" + "信创概念", + "关键软件", + "国产信创概览", + "工业设备更新", + "工业软件", + "工业母机", + "灯塔工厂", + "自主可控", + "软件自主可控", + "设备更新" ] } ] }, { - "lv1": "消费电子", - "lv1_id": "lv1_4", + "lv1": "新能源与电力", + "lv1_id": "new_energy_power", "children": [ { - "lv2": "智能终端(端侧AI)", - "lv2_id": "lv2_4_1", + "lv2": "光伏产业", + "lv2_id": "photovoltaic_industry", "concepts": [ - "2025CES参展公司", - "AI PC", - "AIPC", - "AI手机" + "N型产业链", + "光伏", + "光伏行业兼并重组", + "光伏产业链", + "反内卷光伏", + "叠层钙钛矿", + "钙钛矿电池" ] }, { - "lv2": "XR与空间计算", - "lv2_id": "lv2_4_2", + "lv2": "先进电池技术", + "lv2_id": "advanced_battery_tech", + "concepts": [ + "固态电池-硅基负极", + "固态电池-硫化物", + "固态电池", + "固态电池设备", + "固态电池负极集流体材料-铜箔", + "固态电池产业链", + "复合集流体", + "富锂锰基材料", + "硅基负极材料", + "钠离子电池", + "隔膜", + "陶瓷隔膜骨架膜" + ] + }, + { + "lv2": "充电与换电", + "lv2_id": "charging_battery_swapping", + "concepts": [ + "充电桩", + "华为智能充电", + "华为智能充电网络", + "换电", + "换电重卡", + "比亚迪兆瓦闪充" + ] + }, + { + "lv2": "电力基础设施与AI能源", + "lv2_id": "power_infra_ai_energy", + "concepts": [ + "北美缺电AI电力", + "变压器出海", + "固体氧化物燃料电池-SOFC", + "固态变压器SST", + "燃料电池", + "燃气设备", + "电力产业链", + "燃气轮机HRSG", + "电力电网", + "电力设备", + "电力" + ] + }, + { + "lv2": "核能技术与应用", + "lv2_id": "nuclear_energy_applications", + "concepts": [ + "可控核聚变", + "微型核电", + "核电钍基熔盐堆", + "核聚变超导", + "核电产业链", + "超聚变" + ] + }, + { + "lv2": "核能通用概念", + "lv2_id": "nuclear_energy_general_concepts", + "concepts": [ + "高温概念" + ] + } + ] + }, + { + "lv1": "智能出行与交通", + "lv1_id": "smart_mobility_trans", + "children": [ + { + "lv2": "智能驾驶", + "lv2_id": "autonomous_driving", + "concepts": [ + "Robotaxi", + "小马智行", + "无人驾驶公交", + "文远知行", + "智能驾驶产业链", + "无人驾驶-线控转向", + "无人驾驶", + "特斯拉FSD", + "特斯拉RoboTaxi概念", + "矿山智驾", + "特斯拉无人驾驶出租车Robotaxi", + "自动驾驶" + ] + }, + { + "lv2": "无人物流", + "lv2_id": "unmanned_logistics", + "concepts": [ + "京东物流Robovan", + "无人物流", + "无人环卫车", + "无人物流车九识智能", + "菜鸟无人物流车" + ] + }, + { + "lv2": "智能驾驶基础设施", + "lv2_id": "autonomous_driving_infra", + "concepts": [ + "机器人充电", + "汽车无线充电" + ] + }, + { + "lv2": "智能出行服务", + "lv2_id": "smart_mobility_services", + "concepts": [ + "网约车" + ] + }, + { + "lv2": "智能驾驶与整车", + "lv2_id": "smart_driving_oems", + "concepts": [ + "mobileye替代概念", + "地平线概念", + "地平线", + "比亚迪产业链", + "比亚迪智驾", + "理想汽车" + ] + }, + { + "lv2": "低空经济", + "lv2_id": "low_altitude_economy", + "concepts": [ + "eVTOL材料", + "亿航智能订单量", + "低空经济&飞行汽车", + "低空经济", + "低空管控", + "低空物流", + "低空经济亿航智能", + "低空设计", + "低空经济产业链汇集", + "小鹏汇天", + "小鹏汇天供应商", + "空中成像", + "长安飞行汽车机器人概念", + "飞行汽车eVTOL" + ] + } + ] + }, + { + "lv1": "通信与航天", + "lv1_id": "comm_aerospace", + "children": [ + { + "lv2": "5G与未来通信", + "lv2_id": "5g_future_comm", + "concepts": [ + "5G毫米波", + "5G-A", + "5.5G", + "eSIM概念", + "通感一体" + ] + }, + { + "lv2": "卫星通信与应用", + "lv2_id": "satellite_comm_applications", + "concepts": [ + "6G概念", + "低轨卫星通信华为", + "北斗信使", + "北斗导航", + "商业航天卫星通信", + "卫星出海", + "卫星互联网", + "商业航天", + "太空行走", + "太空旅行", + "太空算力", + "手机直连卫星", + "星河动力", + "星网", + "蓝箭航天朱雀三号", + "长征十二号甲", + "SpaceX", + "卫星能源太阳翼" + ] + }, + { + "lv2": "航空航天", + "lv2_id": "aviation_space", + "concepts": [ + "九天无人机", + "凌空天行", + "大飞机", + "珠海航展", + "空客合作" + ] + } + ] + }, + { + "lv1": "消费电子与XR", + "lv1_id": "consumer_electronics_xr", + "children": [ + { + "lv2": "智能穿戴与XR", + "lv2_id": "smart_wearables_xr", "concepts": [ "AI手势识别", "AI眼镜", @@ -440,665 +607,56 @@ "META智能眼镜", "MR", "Rokid AR", - "消费电子-玄玑感知系统", + "小米眼镜", "智能穿戴", "智能眼镜", - "小米眼镜", - "阿里夸克AI眼镜", - "谷歌AI眼镜-合作XREAL" - ] - }, - { - "lv2": "华为产业链", - "lv2_id": "lv2_4_3", - "children": [ - { - "lv3": "终端产品", - "lv3_id": "lv3_4_3_1", - "concepts": [ - "华为P70", - "华为Mate80", - "华为Pura70", - "华为Mate70手表", - "华为MATE70" - ] - }, - { - "lv3": "技术与生态", - "lv3_id": "lv3_4_3_2", - "concepts": [ - "华为", - "华为5G", - "华为AI容器", - "华为云", - "华为昇腾", - "华为昇腾超节点", - "华为通信大模型", - "华为海思星闪", - "华为鸿蒙", - "华为鸿蒙甄选与支付", - "华字辈", - "昇腾异构计算架构-CANN", - "鸿蒙PC" - ] - }, - { - "lv3": "芯片与算力", - "lv3_id": "lv3_4_3_3", - "concepts": [ - "华为910C", - "华为AI存储", - "华为存储OceanStor", - "华为麒麟芯片", - "磁电存储" - ] - } - ] - }, - { - "lv2": "苹果产业链", - "lv2_id": "lv2_4_4", - "concepts": [ - "果链OPEN AI复用", + "消费电子-玄玑感知系统", "苹果MR产业链", + "谷歌AI眼镜-合作XREAL", + "阿里夸克AI眼镜", + "雷鸟创新光波导" + ] + }, + { + "lv2": "AI终端与芯片", + "lv2_id": "ai_devices_chips", + "concepts": [ + "2025CES参展公司", + "AI PC", + "AIPC", + "AI手机", + "ASIC", + "ISP视觉", + "果链OPEN AI复用", + "端侧AI芯片", "苹果供应商核心公司", "苹果机器人", - "苹果手机产业链" + "苹果手机产业链", + "高通概念" ] }, { "lv2": "新型显示技术", - "lv2_id": "lv2_4_5", + "lv2_id": "new_display_technologies", "concepts": [ "华为三折叠屏", "折叠屏", "显影液及硅基OLED", "苹果OLED潜在受益", "苹果折叠屏", - "面板" + "面板", + "TV面板LCD" ] } ] }, { - "lv1": "智能驾驶与汽车", - "lv1_id": "lv1_5", + "lv1": "文化传媒与娱乐", + "lv1_id": "culture_media_ent", "children": [ { - "lv2": "自动驾驶解决方案", - "lv2_id": "lv2_5_1", - "concepts": [ - "Robotaxi", - "京东物流Robovan", - "小马智行", - "文远知行", - "无人物流", - "无人物流车九识智能", - "无人驾驶", - "特斯拉FSD", - "特斯拉RoboTaxi概念", - "特斯拉无人驾驶出租车Robotaxi", - "自动驾驶", - "菜鸟无人物流车" - ] - }, - { - "lv2": "智能汽车产业链", - "lv2_id": "lv2_5_2", - "concepts": [ - "比亚迪产业链", - "比亚迪智驾", - "孙潇雅团队概念股", - "小米YU7供应链弹性测算", - "小米大模型", - "小米概念", - "小米算力AI互联", - "小米汽车产业链", - "小米汽车产业链弹性", - "小米汽车产业链弹性测算", - "小鹏产业链", - "理想汽车" - ] - }, - { - "lv2": "车路协同与特定场景", - "lv2_id": "lv2_5_3", - "concepts": [ - "无人驾驶-线控转向", - "无人驾驶公交", - "无人环卫车", - "矿山智驾", - "车路云-车路协同运营建设", - "车路云一体化", - "车路协同" - ] - }, - { - "lv2": "产业链综合", - "lv2_id": "lv2_5_4", - "concepts": [ - "mobileye替代概念", - "智能驾驶产业链", - "汽车安全" - ] - }, - { - "lv2": "出行服务", - "lv2_id": "lv2_5_5", - "concepts": [ - "网约车" - ] - }, - { - "lv2": "智能座舱", - "lv2_id": "lv2_5_6", - "concepts": [ - "空中成像" - ] - } - ] - }, - { - "lv1": "新能源与电力", - "lv1_id": "lv1_6", - "children": [ - { - "lv2": "新型电池技术", - "lv2_id": "lv2_6_1", - "children": [ - { - "lv3": "固态电池", - "lv3_id": "lv3_6_1_1", - "concepts": [ - "固态电池", - "固态电池-硅基负极", - "固态电池-硫化物", - "固态电池产业链", - "固态电池设备", - "固态电池负极集流体材料-铜箔", - "陶瓷隔膜骨架膜" - ] - }, - { - "lv3": "其他材料与技术", - "lv3_id": "lv3_6_1_2", - "concepts": [ - "复合集流体", - "富锂锰基材料", - "硅基负极材料", - "钠离子电池", - "隔膜" - ] - } - ] - }, - { - "lv2": "电力设备与电网", - "lv2_id": "lv2_6_2", - "concepts": [ - "北美缺电AI电力", - "变压器出海", - "固体氧化物燃料电池-SOFC", - "固态变压器SST", - "燃料电池", - "燃气设备", - "燃气轮机HRSG", - "电力", - "电力产业链", - "电力电网", - "电力设备" - ] - }, - { - "lv2": "清洁能源", - "lv2_id": "lv2_6_3", - "children": [ - { - "lv3": "光伏", - "lv3_id": "lv3_6_3_1", - "concepts": [ - "N型产业链", - "光伏", - "光伏行业兼并重组", - "光伏产业链", - "反内卷光伏", - "叠层钙钛矿", - "钙钛矿电池" - ] - }, - { - "lv3": "核能", - "lv3_id": "lv3_6_3_2", - "concepts": [ - "可控核聚变", - "微型核电", - "核电产业链", - "核电钍基熔盐堆", - "核聚变超导", - "超聚变", - "高温概念" - ] - } - ] - }, - { - "lv2": "充电桩与补能", - "lv2_id": "lv2_6_4", - "concepts": [ - "充电桩", - "华为智能充电", - "华为智能充电网络", - "换电", - "换电重卡", - "机器人充电", - "汽车无线充电", - "比亚迪兆瓦闪充" - ] - } - ] - }, - { - "lv1": "空天经济", - "lv1_id": "lv1_7", - "children": [ - { - "lv2": "低空经济", - "lv2_id": "lv2_7_1", - "concepts": [ - "eVTOL材料", - "九天无人机", - "亿航智能订单量", - "低空经济", - "低空经济&飞行汽车", - "低空经济产业链汇集", - "低空经济亿航智能", - "低空管控", - "低空物流", - "低空设计", - "小鹏汇天", - "小鹏汇天供应商", - "飞行汽车eVTOL", - "长安飞行汽车机器人概念" - ] - }, - { - "lv2": "商业航天", - "lv2_id": "lv2_7_2", - "concepts": [ - "凌空天行", - "北斗信使", - "北斗导航", - "商业航天", - "商业航天卫星通信", - "卫星出海", - "卫星互联网", - "太空行走", - "太空旅行", - "太空算力", - "手机直连卫星", - "星河动力", - "星网", - "蓝箭航天朱雀三号" - ] - }, - { - "lv2": "通信技术", - "lv2_id": "lv2_7_3", - "concepts": [ - "5.5G", - "5G-A", - "5G毫米波", - "6G概念", - "eSIM概念", - "通感一体" - ] - }, - { - "lv2": "民用航空", - "lv2_id": "lv2_7_4", - "concepts": [ - "大飞机", - "空客合作" - ] - }, - { - "lv2": "综合与主题", - "lv2_id": "lv2_7_5", - "concepts": [ - "珠海航展" - ] - } - ] - }, - { - "lv1": "国防军工", - "lv1_id": "lv1_8", - "children": [ - { - "lv2": "无人作战与信息化", - "lv2_id": "lv2_8_1", - "concepts": [ - "AI军工", - "AI无人机军工信息化", - "信息支援概念整理", - "军工信息化", - "军用无人机反无人机", - "无人机蜂群" - ] - }, - { - "lv2": "海军装备", - "lv2_id": "lv2_8_2", - "concepts": [ - "军工水面水下作战", - "国产航母", - "水下军工", - "海军", - "电磁弹射概念股", - "电磁发射设备", - "航母福建舰240430" - ] - }, - { - "lv2": "空军装备", - "lv2_id": "lv2_8_3", - "concepts": [ - "军机" - ] - }, - { - "lv2": "陆军装备", - "lv2_id": "lv2_8_4", - "concepts": [ - "远程火力" - ] - }, - { - "lv2": "军贸出海", - "lv2_id": "lv2_8_5", - "concepts": [ - "军贸", - "巴印军贸", - "巴黎航展" - ] - }, - { - "lv2": "综合与主题", - "lv2_id": "lv2_8_6", - "concepts": [ - "军工", - "军工-阅兵", - "国防军工" - ] - } - ] - }, - { - "lv1": "政策与主题", - "lv1_id": "lv1_9", - "children": [ - { - "lv2": "国企改革与市值管理", - "lv2_id": "lv2_9_1", - "concepts": [ - "中兵集团并购重组", - "中字头", - "中字头央企", - "中船合并", - "央企市值管理", - "央国企", - "央国企地产", - "央国企重组", - "安徽国资", - "地面兵装", - "整车央企重组", - "河南国资能源集团重组", - "市值管理16条-破净股", - "珠海国资", - "破净央国企", - "破净股合集", - "福建国资", - "湖北三资改革", - "国资高息股", - "高送转概念股", - "央国企AI一张图" - ] - }, - { - "lv2": "并购重组", - "lv2_id": "lv2_9_2", - "concepts": [ - "IPO终止相关企业重组预期", - "上海并购重组", - "券商合并预期", - "宝德计算机", - "并购重组", - "并购重组预期", - "消费医疗重组预期", - "湘财合并大智慧", - "科创板并购重组", - "秦淮数据", - "科技重组", - "超聚变借壳预期", - "证券", - "荣耀股改", - "重组-中科院系&海光系" - ] - }, - { - "lv2": "信创与自主可控", - "lv2_id": "lv2_9_3", - "concepts": [ - "信创概念", - "关键软件", - "国产信创概览", - "工业软件", - "自主可控", - "软件自主可控", - "信息安全", - "安全概念股", - "网络安全", - "通信设备", - "通信安全" - ] - }, - { - "lv2": "重大基建", - "lv2_id": "lv2_9_4", - "concepts": [ - "三峡水运新通道", - "新藏铁路", - "新疆概念", - "水利", - "水利工程", - "混凝土减水剂、砂石设备", - "节水产业240423", - "西部大开发", - "西部大开发240424", - "西南水电", - "西南水电站", - "西南水电站-机构测算", - "隧洞设备盾构机", - "雅下水电对电力设备增量测算-机构", - "雅下水电站", - "雅下水电站大件物流" - ] - }, - { - "lv2": "供给侧改革", - "lv2_id": "lv2_9_5", - "concepts": [ - "反内卷", - "反内卷造纸", - "反内卷食用盐", - "反内卷快递", - "反内卷合集", - "生猪", - "物流", - "牛肉", - "统一大市场", - "钢铁" - ] - }, - { - "lv2": "产业升级与制造", - "lv2_id": "lv2_9_6", - "concepts": [ - "工业设备更新", - "工业母机", - "设备更新", - "灯塔工厂" - ] - }, - { - "lv2": "国家战略", - "lv2_id": "lv2_9_7", - "concepts": [ - "2025年政府工作报告利好行业及个股", - "新型经济", - "新质生产力", - "深海数智化", - "深海经济", - "深地经济", - "首发经济" - ] - }, - { - "lv2": "区域发展与自贸区", - "lv2_id": "lv2_9_8", - "concepts": [ - "上海自贸区", - "免税离境退税", - "新型离岸贸易", - "海南", - "海南自贸区", - "海南自贸港", - "零售消费免税" - ] - }, - { - "lv2": "行业监管与规范", - "lv2_id": "lv2_9_9", - "concepts": [ - "充电宝", - "农药证件厂家", - "食品安全", - "食品安全全链条", - "预制菜" - ] - } - ] - }, - { - "lv1": "周期与材料", - "lv1_id": "lv1_10", - "children": [ - { - "lv2": "化工", - "lv2_id": "lv2_10_1", - "children": [ - { - "lv3": "行业趋势", - "lv3_id": "lv3_10_1_1", - "concepts": [ - "化工", - "化工概念", - "化工品涨价", - "涨价概念" - ] - }, - { - "lv3": "具体品种", - "lv3_id": "lv3_10_1_2", - "concepts": [ - "TMA偏苯三酸酐", - "乙烷", - "光引发剂", - "六氟磷酸锂", - "农药杀虫剂-氯虫苯甲酰胺", - "双季戊四醇", - "己内酰胺", - "有机硅", - "正丙醇", - "烧碱", - "涤纶长丝", - "电解液添加剂", - "甲苯二异氰酸酯-TDI", - "环氧丙烷", - "纯碱", - "磷化工", - "磷化工六氟磷酸锂", - "聚酯产业", - "维生素", - "苯酚丙酮", - "超硬材料", - "除草剂-烯草酮" - ] - }, - { - "lv3": "产业链", - "lv3_id": "lv3_10_1_3", - "concepts": [ - "有机硅产业链", - "电解液产业链" - ] - } - ] - }, - { - "lv2": "有色金属", - "lv2_id": "lv2_10_2", - "concepts": [ - "化工有色元素周期表", - "有色金属", - "电解铝", - "稀土", - "白银", - "铅酸电池", - "铜", - "铜产业", - "钨金属", - "钴", - "钴金属", - "钼金属", - "锡矿" - ] - } - ] - }, - { - "lv1": "大消费", - "lv1_id": "lv1_11", - "children": [ - { - "lv2": "文化传媒", - "lv2_id": "lv2_11_1", - "concepts": [ - "AI游戏", - "乙游", - "传媒出海", - "出版传媒", - "国产游戏黑神话", - "周杰伦概念股", - "幻兽帕鲁", - "影视", - "影视IP", - "影视传媒", - "影视院线", - "春节档重点影片(哪吒2)", - "智象未来", - "漫剧", - "游戏", - "游戏出海", - "疯狂动物城2", - "短剧", - "诡秘之主", - "腾讯短剧重点名单" - ] - }, - { - "lv2": "新消费", - "lv2_id": "lv2_11_2", + "lv2": "潮玩与IP衍生", + "lv2_id": "trendy_play_ip_derivatives", "concepts": [ "上市潮玩盲盒公司", "卡游文创玩具", @@ -1112,8 +670,263 @@ ] }, { - "lv2": "人口与社会", - "lv2_id": "lv2_11_3", + "lv2": "影视与IP内容", + "lv2_id": "film_tv_ip_content", + "concepts": [ + "出版传媒", + "周杰伦概念股", + "影视院线", + "影视IP", + "影视", + "影视传媒", + "春节档重点影片(哪吒2)", + "疯狂动物城2", + "诡秘之主" + ] + }, + { + "lv2": "体育赛事与经济", + "lv2_id": "sports_events_economy", + "concepts": [ + "体育", + "体育产业", + "冰雪经济", + "川超联赛", + "第十五届全运会", + "足球-苏超联赛、体彩", + "足球" + ] + }, + { + "lv2": "游戏与AI赋能", + "lv2_id": "gaming_ai_empowerment", + "concepts": [ + "AI游戏", + "乙游", + "国产游戏黑神话", + "幻兽帕鲁", + "游戏" + ] + }, + { + "lv2": "内容出海与新业态", + "lv2_id": "content_export_new_formats", + "concepts": [ + "传媒出海", + "漫剧", + "游戏出海", + "短剧", + "腾讯短剧重点名单" + ] + }, + { + "lv2": "社交媒体与跨境电商", + "lv2_id": "social_media_cross_border_ecomm", + "concepts": [ + "TikTok", + "小红书概念", + "小红书概念股", + "敦煌网跨境电商" + ] + } + ] + }, + { + "lv1": "宏观经济与政策", + "lv1_id": "macro_policy", + "children": [ + { + "lv2": "国家战略与新经济", + "lv2_id": "national_strategy_new_economy", + "concepts": [ + "2025年政府工作报告利好行业及个股", + "新型经济", + "新质生产力", + "深海数智化", + "深海经济", + "深地经济", + "首发经济" + ] + }, + { + "lv2": "国企改革与市值管理", + "lv2_id": "soe_reform_market_value", + "concepts": [ + "中兵集团并购重组", + "中字头", + "中字头央企", + "中船合并", + "国资高息股", + "央企市值管理", + "央国企", + "地面兵装", + "央国企地产", + "央国企重组", + "安徽国资", + "市值管理16条-破净股", + "整车央企重组", + "河南国资能源集团重组", + "珠海国资", + "湖北三资改革", + "破净央国企", + "破净股合集", + "福建国资" + ] + }, + { + "lv2": "并购重组", + "lv2_id": "mergers_acquisitions", + "concepts": [ + "IPO终止相关企业重组预期", + "上海并购重组", + "券商合并预期", + "宝德计算机", + "并购重组预期", + "并购重组", + "消费医疗重组预期", + "湘财合并大智慧", + "科创板并购重组", + "秦淮数据", + "科技重组", + "超聚变借壳预期", + "证券", + "荣耀股改", + "重组-中科院系&海光系" + ] + }, + { + "lv2": "供给侧改革与周期", + "lv2_id": "supply_side_reform_cycles", + "concepts": [ + "反内卷", + "反内卷造纸", + "反内卷食用盐", + "反内卷快递", + "反内卷合集", + "己内酰胺", + "涤纶长丝", + "生猪", + "牛肉", + "聚酯产业", + "钢铁", + "BOPET膜" + ] + }, + { + "lv2": "区域经济与自贸区", + "lv2_id": "regional_economy_free_trade_zones", + "concepts": [ + "上海自贸区", + "免税离境退税", + "新型离岸贸易", + "海南", + "海南自贸区", + "海南自贸港", + "零售消费免税" + ] + } + ] + }, + { + "lv1": "地缘政治与国际关系", + "lv1_id": "geopolitics_intl", + "children": [ + { + "lv2": "贸易政策与供应链", + "lv2_id": "trade_policy_supply_chain", + "concepts": [ + "中俄贸易", + "乙烷", + "中欧贸易", + "二轮车全地形车", + "关税豁免", + "关税减免出口链", + "后关税战受益", + "反制关税涨价预期", + "墨西哥汽车零部件", + "绒毛浆", + "芬太尼管制", + "转口贸易出口转内销", + "越南工厂" + ] + }, + { + "lv2": "国际冲突与重建", + "lv2_id": "intl_conflicts_reconstruction", + "concepts": [ + "乌克兰战后重建概念", + "乌克兰重建", + "俄乌重建", + "柬泰战争", + "黄岩岛概念股" + ] + }, + { + "lv2": "两岸关系与区域发展", + "lv2_id": "cross_strait_regional_dev", + "concepts": [ + "两岸融合", + "台资企业", + "海峡两岸福建", + "厦门“十五五规划”" + ] + } + ] + }, + { + "lv1": "数字经济与数据要素", + "lv1_id": "digital_economy_data", + "children": [ + { + "lv2": "数字金融与Web3", + "lv2_id": "digital_finance_web3", + "concepts": [ + "上海浦江数链", + "互联网金融", + "复星稳定币", + "数字货币", + "树图链概念", + "稳定币RWA概念股", + "稳定币-蚂蚁国际", + "稳定币一体机", + "蚂蚁金服", + "香港金融牌照" + ] + }, + { + "lv2": "数据要素与基础设施", + "lv2_id": "data_elements_infra", + "concepts": [ + "RWA上链— IoT设备数据采集", + "RDA概念股", + "数据可信", + "数据交易所", + "数据要素", + "跨境数据数据要素" + ] + }, + { + "lv2": "网络与数据安全", + "lv2_id": "network_data_security", + "concepts": [ + "信息安全", + "地理信息", + "安全概念股", + "汽车安全", + "网络安全", + "通信设备", + "通信安全" + ] + } + ] + }, + { + "lv1": "社会民生", + "lv1_id": "social_livelihood", + "children": [ + { + "lv2": "生育与教育", + "lv2_id": "fertility_education", "concepts": [ "三胎", "多胎", @@ -1124,148 +937,274 @@ ] }, { - "lv2": "体育产业", - "lv2_id": "lv2_11_4", + "lv2": "行业监管与安全", + "lv2_id": "industry_regulation_safety", "concepts": [ - "体育", - "体育产业", - "冰雪经济", - "川超联赛", - "第十五届全运会", - "足球", - "足球-苏超联赛、体彩" + "充电宝", + "农药证件厂家", + "食品安全", + "食品安全全链条", + "预制菜" ] } ] }, { - "lv1": "数字经济与金融科技", - "lv1_id": "lv1_12", + "lv1": "医药与生物科技", + "lv1_id": "pharma_biotech", "children": [ { - "lv2": "数据要素", - "lv2_id": "lv2_12_1", - "concepts": [ - "RDA概念股", - "RWA上链— IoT设备数据采集", - "版权", - "地理信息", - "数据可信", - "数据交易所", - "数据要素", - "政务云政务IT", - "跨境数据数据要素" - ] - }, - { - "lv2": "数字金融", - "lv2_id": "lv2_12_2", - "concepts": [ - "上海浦江数链", - "互联网金融", - "复星稳定币", - "数字货币", - "树图链概念", - "稳定币-蚂蚁国际", - "稳定币RWA概念股", - "稳定币一体机", - "蚂蚁金服", - "香港金融牌照" - ] - } - ] - }, - { - "lv1": "全球宏观与贸易", - "lv1_id": "lv1_13", - "children": [ - { - "lv2": "地缘政治与冲突", - "lv2_id": "lv2_13_1", - "concepts": [ - "以伊冲突-天然气", - "以伊冲突-油运仓储", - "以伊冲突-航运", - "以伊冲突-资源化工", - "乌克兰战后重建概念", - "乌克兰重建", - "俄乌重建", - "油气", - "海外港口", - "海事反制", - "石油", - "航运", - "远洋航运", - "柬泰战争", - "黄岩岛概念股" - ] - }, - { - "lv2": "贸易政策与关系", - "lv2_id": "lv2_13_2", - "concepts": [ - "中美关系", - "中俄贸易", - "中欧贸易", - "出口管制", - "反制关税涨价预期", - "后关税战受益", - "关税减免出口链", - "关税豁免", - "绒毛浆", - "芬太尼管制", - "转口贸易出口转内销" - ] - }, - { - "lv2": "供应链重构", - "lv2_id": "lv2_13_3", - "concepts": [ - "二轮车全地形车", - "墨西哥汽车零部件", - "越南工厂" - ] - } - ] - }, - { - "lv1": "医药健康", - "lv1_id": "lv1_14", - "children": [ - { - "lv2": "创新药", - "lv2_id": "lv2_14_1", + "lv2": "创新药与医疗器械", + "lv2_id": "innovative_drugs_medical_devices", "concepts": [ "AI制药", - "创新药", - "创新药双抗", "创新药相关", - "医药" + "创新药双抗", + "医疗器械", + "创新药", + "医药", + "医药外包CXO" + ] + } + ] + }, + { + "lv1": "基础材料与化工", + "lv1_id": "basic_materials_chem", + "children": [ + { + "lv2": "化工产品与周期", + "lv2_id": "chemical_products_cycles", + "concepts": [ + "TMA偏苯三酸酐", + "光引发剂", + "六氟磷酸锂", + "农药杀虫剂-氯虫苯甲酰胺", + "化工品涨价", + "化工概念", + "化工", + "双季戊四醇", + "正丙醇", + "有机硅", + "有机硅产业链", + "烧碱", + "涨价概念", + "电解液添加剂", + "电解液产业链", + "甲苯二异氰酸酯-TDI", + "环氧丙烷", + "电解铝", + "纯碱", + "磷化工六氟磷酸锂", + "磷化工", + "维生素", + "苯酚丙酮", + "铅酸电池", + "除草剂-烯草酮" ] }, { - "lv2": "细胞治疗", - "lv2_id": "lv2_14_2", + "lv2": "有色金属与稀有矿产", + "lv2_id": "non_ferrous_rare_minerals", "concepts": [ - "干细胞", - "干细胞概念股" + "化工有色元素周期表", + "有色金属", + "稀土", + "白银", + "铜产业", + "钨金属", + "钴", + "铜", + "钼金属", + "钴金属", + "锡矿" + ] + } + ] + }, + { + "lv1": "基础设施与工程", + "lv1_id": "infra_engineering", + "children": [ + { + "lv2": "重大基建与水利", + "lv2_id": "major_infra_water", + "concepts": [ + "三峡水运新通道", + "新藏铁路", + "新疆概念", + "水利", + "水利工程", + "混凝土减水剂、砂石设备", + "节水产业240423", + "西部大开发", + "西南水电站", + "西部大开发240424", + "西南水电站-机构测算", + "西南水电", + "隧洞设备盾构机", + "雅下水电对电力设备增量测算-机构", + "雅下水电站", + "雅下水电站大件物流" ] } ] }, { "lv1": "前沿科技", - "lv1_id": "lv1_15", + "lv1_id": "frontier_tech", "children": [ { - "lv2": "量子科技", - "lv2_id": "lv2_15_1", + "lv2": "量子技术与应用", + "lv2_id": "quantum_tech_applications", "concepts": [ "量子材料钛酸锶", "量子科技", "量子科技产业链", - "量子科技参股公司", - "量子计算" + "量子计算", + "量子科技参股公司" + ] + } + ] + }, + { + "lv1": "市场情绪与概念", + "lv1_id": "market_sentiment", + "children": [ + { + "lv2": "叙事与情绪驱动", + "lv2_id": "narrative_emotion_driven", + "concepts": [ + "“马”字辈", + "人造肉", + "长安的荔枝", + "韦神概念股" + ] + } + ] + }, + { + "lv1": "华为生态", + "lv1_id": "huawei_eco", + "children": [ + { + "lv2": "华为终端与芯片", + "lv2_id": "huawei_devices_chips", + "concepts": [ + "华为P70", + "华为Mate80", + "华为Pura70", + "华为Mate70手表", + "华为MATE70", + "华为麒麟芯片" + ] + }, + { + "lv2": "华为通信与软件", + "lv2_id": "huawei_comm_software", + "concepts": [ + "华为5G", + "华为通信大模型", + "华为海思星闪", + "华为鸿蒙", + "华为鸿蒙甄选与支付", + "鸿蒙PC" + ] + }, + { + "lv2": "华为AI与云计算", + "lv2_id": "huawei_ai_cloud", + "concepts": [ + "华为存储OceanStor", + "华为910C", + "华为云", + "华为AI存储", + "华为AI容器", + "华为昇腾", + "华为昇腾超节点", + "昇腾异构计算架构-CANN", + "磁电存储" + ] + }, + { + "lv2": "华为通用概念", + "lv2_id": "huawei_general_concepts", + "concepts": [ + "华为", + "华字辈" + ] + } + ] + }, + { + "lv1": "小米生态", + "lv1_id": "xiaomi_eco", + "children": [ + { + "lv2": "小米生态与智能汽车", + "lv2_id": "xiaomi_eco_smart_vehicles", + "concepts": [ + "小米算力AI互联", + "孙潇雅团队概念股", + "小米YU7供应链弹性测算", + "小米大模型", + "小鹏产业链", + "小米概念", + "小米汽车产业链弹性", + "小米汽车产业链", + "小米汽车产业链弹性测算" + ] + } + ] + }, + { + "lv1": "字节跳动生态", + "lv1_id": "bytedance_eco", + "children": [ + { + "lv2": "字节生态与AI终端", + "lv2_id": "bytedance_eco_ai_devices", + "concepts": [ + "努比亚手机", + "华为抖音支付", + "字节概念豆包AI手机", + "字节豆包概念股", + "抖音概念", + "豆包大模型" + ] + } + ] + }, + { + "lv1": "国防军工", + "lv1_id": "national_defense_military", + "children": [ + { + "lv2": "军工装备与信息化", + "lv2_id": "military_equipment_informatization", + "concepts": [ + "AI军工", + "AI无人机军工信息化", + "信息支援概念整理", + "军工-阅兵", + "军工信息化", + "军工", + "军工水面水下作战", + "军贸", + "军用无人机反无人机", + "军机", + "国防军工", + "国产航母", + "巴印军贸", + "巴黎航展", + "无人机蜂群", + "水下军工", + "海军", + "电磁弹射概念股", + "电磁发射设备", + "航母福建舰240430", + "远程火力", + "福建-军工" ] } ] diff --git a/concept_quota_realtime.py b/concept_quota_realtime.py new file mode 100644 index 00000000..103b586e --- /dev/null +++ b/concept_quota_realtime.py @@ -0,0 +1,681 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +概念涨跌幅实时更新服务 +- 在交易时间段每分钟从ClickHouse获取最新分钟数据 +- 计算涨跌幅后更新MySQL的concept_daily_stats表 +- 支持叶子概念和母概念(lv1/lv2/lv3) +""" + +import pandas as pd +import numpy as np +from datetime import datetime, timedelta +from sqlalchemy import create_engine, text +from elasticsearch import Elasticsearch +from clickhouse_driver import Client +import time +import logging +import json +import os +import hashlib +import argparse + +# ==================== 配置 ==================== + +# MySQL配置 +MYSQL_ENGINE = create_engine( + "mysql+pymysql://root:Zzl5588161!@222.128.1.157:33060/stock", + echo=False +) + +# Elasticsearch配置 +ES_CLIENT = Elasticsearch(['http://222.128.1.157:19200']) +INDEX_NAME = 'concept_library_v3' + +# ClickHouse配置 +CLICKHOUSE_CONFIG = { + 'host': '222.128.1.157', + 'port': 18000, + 'user': 'default', + 'password': 'Zzl33818!', + 'database': 'stock' +} + +# 层级结构文件 +HIERARCHY_FILE = 'concept_hierarchy_v3.json' + +# 交易时间配置 +TRADING_HOURS = { + 'morning_start': (9, 30), + 'morning_end': (11, 30), + 'afternoon_start': (13, 0), + 'afternoon_end': (15, 0), +} + +# ==================== 日志配置 ==================== + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(f'concept_realtime_{datetime.now().strftime("%Y%m%d")}.log', encoding='utf-8'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# ClickHouse客户端 +ch_client = None + + +def get_ch_client(): + """获取ClickHouse客户端""" + global ch_client + if ch_client is None: + ch_client = Client(**CLICKHOUSE_CONFIG) + return ch_client + + +def generate_id(name: str) -> str: + """生成概念ID""" + return hashlib.md5(name.encode('utf-8')).hexdigest()[:16] + + +def code_to_ch_format(code: str) -> str: + """将6位股票代码转换为ClickHouse格式(带后缀) + + 规则: + - 6开头 -> .SH(上海) + - 0或3开头 -> .SZ(深圳) + - 其他 -> .BJ(北京) + - 非6位数字的忽略(可能是港股) + """ + if not code or len(code) != 6 or not code.isdigit(): + return None + + if code.startswith('6'): + return f"{code}.SH" + elif code.startswith('0') or code.startswith('3'): + return f"{code}.SZ" + else: + return f"{code}.BJ" + + +def ch_code_to_pure(ch_code: str) -> str: + """将ClickHouse格式的股票代码转回纯6位代码""" + if not ch_code: + return None + return ch_code.split('.')[0] + + +# ==================== 概念数据获取 ==================== + +def get_all_concepts(): + """从ES获取所有叶子概念及其股票列表""" + concepts = [] + + query = { + "query": {"match_all": {}}, + "size": 100, + "_source": ["concept_id", "concept", "stocks"] + } + + resp = ES_CLIENT.search(index=INDEX_NAME, body=query, scroll='2m') + scroll_id = resp['_scroll_id'] + hits = resp['hits']['hits'] + + while len(hits) > 0: + for hit in hits: + source = hit['_source'] + concept_info = { + 'concept_id': source.get('concept_id'), + 'concept_name': source.get('concept'), + 'stocks': [], + 'concept_type': 'leaf' + } + + # v3索引的stocks字段是 [{name, code}, ...] + if 'stocks' in source and isinstance(source['stocks'], list): + for stock in source['stocks']: + if isinstance(stock, dict) and 'code' in stock and stock['code']: + concept_info['stocks'].append(stock['code']) + + if concept_info['stocks']: + concepts.append(concept_info) + + resp = ES_CLIENT.scroll(scroll_id=scroll_id, scroll='2m') + scroll_id = resp['_scroll_id'] + hits = resp['hits']['hits'] + + ES_CLIENT.clear_scroll(scroll_id=scroll_id) + return concepts + + +def load_hierarchy_concepts(leaf_concepts: list) -> list: + """加载层级结构,生成母概念(lv1/lv2/lv3)""" + hierarchy_path = os.path.join(os.path.dirname(__file__), HIERARCHY_FILE) + if not os.path.exists(hierarchy_path): + logger.warning(f"层级文件不存在: {hierarchy_path}") + return [] + + with open(hierarchy_path, 'r', encoding='utf-8') as f: + hierarchy_data = json.load(f) + + # 建立概念名称到股票的映射 + concept_to_stocks = {} + for c in leaf_concepts: + concept_to_stocks[c['concept_name']] = set(c['stocks']) + + parent_concepts = [] + + for lv1 in hierarchy_data.get('hierarchy', []): + lv1_name = lv1.get('lv1', '') + lv1_stocks = set() + + for child in lv1.get('children', []): + lv2_name = child.get('lv2', '') + lv2_stocks = set() + + if 'children' in child: + for lv3_child in child.get('children', []): + lv3_name = lv3_child.get('lv3', '') + lv3_stocks = set() + + for concept_name in lv3_child.get('concepts', []): + if concept_name in concept_to_stocks: + lv3_stocks.update(concept_to_stocks[concept_name]) + + if lv3_stocks: + parent_concepts.append({ + 'concept_id': generate_id(f"lv3_{lv3_name}"), + 'concept_name': f"[三级] {lv3_name}", + 'stocks': list(lv3_stocks), + 'concept_type': 'lv3' + }) + + lv2_stocks.update(lv3_stocks) + else: + for concept_name in child.get('concepts', []): + if concept_name in concept_to_stocks: + lv2_stocks.update(concept_to_stocks[concept_name]) + + if lv2_stocks: + parent_concepts.append({ + 'concept_id': generate_id(f"lv2_{lv2_name}"), + 'concept_name': f"[二级] {lv2_name}", + 'stocks': list(lv2_stocks), + 'concept_type': 'lv2' + }) + + lv1_stocks.update(lv2_stocks) + + if lv1_stocks: + parent_concepts.append({ + 'concept_id': generate_id(f"lv1_{lv1_name}"), + 'concept_name': f"[一级] {lv1_name}", + 'stocks': list(lv1_stocks), + 'concept_type': 'lv1' + }) + + return parent_concepts + + +# ==================== 基准价格获取 ==================== + +def get_base_prices(stock_codes: list, current_date: str) -> dict: + """获取当日的昨收价作为基准(从ea_trade的F002N字段) + + ea_trade表字段说明: + - F002N: 昨日收盘价 + - F007N: 最近成交价(收盘价) + - F010N: 涨跌幅 + """ + if not stock_codes: + return {} + + # 过滤出有效的6位股票代码 + valid_codes = [code for code in stock_codes if code and len(code) == 6 and code.isdigit()] + if not valid_codes: + return {} + + stock_codes_str = "','".join(valid_codes) + + # 获取当日数据中的昨收价(F002N) + query = f""" + SELECT SECCODE, F002N + FROM ea_trade + WHERE SECCODE IN ('{stock_codes_str}') + AND TRADEDATE = ( + SELECT MAX(TRADEDATE) + FROM ea_trade + WHERE TRADEDATE <= '{current_date}' + ) + AND F002N IS NOT NULL AND F002N > 0 + """ + + try: + with MYSQL_ENGINE.connect() as conn: + result = conn.execute(text(query)) + base_prices = {row[0]: float(row[1]) for row in result if row[1] and float(row[1]) > 0} + logger.info(f"获取到 {len(base_prices)} 个基准价格") + return base_prices + except Exception as e: + logger.error(f"获取基准价格失败: {e}") + return {} + + +# ==================== 实时价格获取 ==================== + +def get_latest_prices(stock_codes: list) -> dict: + """从ClickHouse获取最新分钟数据的收盘价 + + Args: + stock_codes: 纯6位股票代码列表(如 ['000001', '600000']) + + Returns: + dict: {纯6位代码: {'close': 价格, 'timestamp': 时间}} + """ + if not stock_codes: + return {} + + client = get_ch_client() + + # 转换为ClickHouse格式的代码(带后缀) + ch_codes = [] + code_mapping = {} # ch_code -> pure_code + for code in stock_codes: + ch_code = code_to_ch_format(code) + if ch_code: + ch_codes.append(ch_code) + code_mapping[ch_code] = code + + if not ch_codes: + logger.warning("没有有效的股票代码可查询") + return {} + + ch_codes_str = "','".join(ch_codes) + + # 获取今日最新的分钟数据 + query = f""" + SELECT code, close, timestamp + FROM ( + SELECT code, close, timestamp, + ROW_NUMBER() OVER (PARTITION BY code ORDER BY timestamp DESC) as rn + FROM stock_minute + WHERE code IN ('{ch_codes_str}') + AND toDate(timestamp) = today() + ) + WHERE rn = 1 + """ + + try: + result = client.execute(query) + if not result: + return {} + + latest_prices = {} + for row in result: + ch_code, close, ts = row + if close and close > 0: + # 转回纯6位代码 + pure_code = code_mapping.get(ch_code) + if pure_code: + latest_prices[pure_code] = { + 'close': float(close), + 'timestamp': ts + } + + return latest_prices + except Exception as e: + logger.error(f"获取最新价格失败: {e}") + return {} + + +# ==================== 涨跌幅计算 ==================== + +def calculate_change_pct(base_prices: dict, latest_prices: dict) -> dict: + """计算涨跌幅""" + changes = {} + for code, latest in latest_prices.items(): + if code in base_prices and base_prices[code] > 0: + base = base_prices[code] + close = latest['close'] + change_pct = (close - base) / base * 100 + changes[code] = round(change_pct, 4) + return changes + + +def calculate_concept_stats(concepts: list, stock_changes: dict, trade_date: str) -> list: + """计算所有概念的涨跌幅统计""" + stats = [] + + for concept in concepts: + concept_id = concept['concept_id'] + concept_name = concept['concept_name'] + stock_codes = concept['stocks'] + concept_type = concept.get('concept_type', 'leaf') + + # 获取该概念股票的涨跌幅 + changes = [stock_changes[code] for code in stock_codes if code in stock_changes] + + if not changes: + continue + + avg_change_pct = round(np.mean(changes), 4) + stock_count = len(changes) + + stats.append({ + 'concept_id': concept_id, + 'concept_name': concept_name, + 'trade_date': trade_date, + 'avg_change_pct': avg_change_pct, + 'stock_count': stock_count, + 'concept_type': concept_type + }) + + return stats + + +# ==================== MySQL更新 ==================== + +def update_mysql_stats(stats: list): + """更新MySQL的concept_daily_stats表""" + if not stats: + return 0 + + with MYSQL_ENGINE.begin() as conn: + updated = 0 + for item in stats: + upsert_sql = text(""" + REPLACE INTO concept_daily_stats + (concept_id, concept_name, trade_date, avg_change_pct, stock_count, concept_type) + VALUES (:concept_id, :concept_name, :trade_date, :avg_change_pct, :stock_count, :concept_type) + """) + conn.execute(upsert_sql, item) + updated += 1 + + return updated + + +# ==================== 交易时间判断 ==================== + +def is_trading_time() -> bool: + """判断当前是否为交易时间""" + now = datetime.now() + weekday = now.weekday() + + # 周末不交易 + if weekday >= 5: + return False + + hour, minute = now.hour, now.minute + current_time = hour * 60 + minute + + # 上午 9:30 - 11:30 + morning_start = 9 * 60 + 30 + morning_end = 11 * 60 + 30 + + # 下午 13:00 - 15:00 + afternoon_start = 13 * 60 + afternoon_end = 15 * 60 + + return (morning_start <= current_time <= morning_end) or \ + (afternoon_start <= current_time <= afternoon_end) + + +def get_next_update_time() -> int: + """获取距离下次更新的秒数""" + now = datetime.now() + + if is_trading_time(): + # 交易时间内,等到下一分钟 + return 60 - now.second + else: + # 非交易时间 + hour, minute = now.hour, now.minute + + # 计算距离下次交易开始的时间 + if hour < 9 or (hour == 9 and minute < 30): + # 等到9:30 + target = now.replace(hour=9, minute=30, second=0, microsecond=0) + elif (hour == 11 and minute >= 30) or hour == 12: + # 等到13:00 + target = now.replace(hour=13, minute=0, second=0, microsecond=0) + elif hour >= 15: + # 等到明天9:30 + target = (now + timedelta(days=1)).replace(hour=9, minute=30, second=0, microsecond=0) + else: + target = now + timedelta(minutes=1) + + wait_seconds = (target - now).total_seconds() + return max(60, int(wait_seconds)) + + +# ==================== 主运行逻辑 ==================== + +def run_once(concepts: list, all_stocks: list) -> int: + """执行一次更新""" + now = datetime.now() + trade_date = now.strftime('%Y-%m-%d') + + # 获取基准价格(昨日收盘价) + base_prices = get_base_prices(all_stocks, trade_date) + if not base_prices: + logger.warning("无法获取基准价格") + return 0 + + # 获取最新价格 + latest_prices = get_latest_prices(all_stocks) + if not latest_prices: + logger.warning("无法获取最新价格") + return 0 + + # 计算涨跌幅 + stock_changes = calculate_change_pct(base_prices, latest_prices) + if not stock_changes: + logger.warning("无涨跌幅数据") + return 0 + + logger.info(f"获取到 {len(stock_changes)} 只股票的涨跌幅") + + # 计算概念统计 + stats = calculate_concept_stats(concepts, stock_changes, trade_date) + logger.info(f"计算了 {len(stats)} 个概念的涨跌幅") + + # 更新MySQL + updated = update_mysql_stats(stats) + logger.info(f"更新了 {updated} 条记录到MySQL") + + return updated + + +def run_realtime(): + """实时更新主循环""" + logger.info("=" * 60) + logger.info("启动概念涨跌幅实时更新服务") + logger.info("=" * 60) + + # 加载概念数据 + logger.info("加载概念数据...") + leaf_concepts = get_all_concepts() + logger.info(f"获取到 {len(leaf_concepts)} 个叶子概念") + + parent_concepts = load_hierarchy_concepts(leaf_concepts) + logger.info(f"生成了 {len(parent_concepts)} 个母概念") + + all_concepts = leaf_concepts + parent_concepts + logger.info(f"总计 {len(all_concepts)} 个概念") + + # 收集所有股票代码 + all_stocks = set() + for c in all_concepts: + all_stocks.update(c['stocks']) + all_stocks = list(all_stocks) + logger.info(f"监控 {len(all_stocks)} 只股票") + + last_concept_update = datetime.now() + + while True: + try: + now = datetime.now() + + # 每小时重新加载概念数据 + if (now - last_concept_update).total_seconds() > 3600: + logger.info("重新加载概念数据...") + leaf_concepts = get_all_concepts() + parent_concepts = load_hierarchy_concepts(leaf_concepts) + all_concepts = leaf_concepts + parent_concepts + all_stocks = set() + for c in all_concepts: + all_stocks.update(c['stocks']) + all_stocks = list(all_stocks) + last_concept_update = now + logger.info(f"更新完成: {len(all_concepts)} 个概念, {len(all_stocks)} 只股票") + + # 检查是否交易时间 + if not is_trading_time(): + wait_sec = get_next_update_time() + wait_min = wait_sec // 60 + logger.info(f"非交易时间,等待 {wait_min} 分钟后重试...") + time.sleep(min(wait_sec, 300)) # 最多等5分钟再检查 + continue + + # 执行更新 + logger.info(f"\n{'=' * 40}") + logger.info(f"更新时间: {now.strftime('%Y-%m-%d %H:%M:%S')}") + updated = run_once(all_concepts, all_stocks) + + # 等待下一分钟 + sleep_sec = 60 - datetime.now().second + logger.info(f"完成,等待 {sleep_sec} 秒后继续...") + time.sleep(sleep_sec) + + except KeyboardInterrupt: + logger.info("\n收到退出信号,停止服务...") + break + except Exception as e: + logger.error(f"发生错误: {e}") + import traceback + traceback.print_exc() + time.sleep(60) + + +def run_single(): + """单次运行(不循环)""" + logger.info("单次更新模式") + + leaf_concepts = get_all_concepts() + parent_concepts = load_hierarchy_concepts(leaf_concepts) + all_concepts = leaf_concepts + parent_concepts + + all_stocks = set() + for c in all_concepts: + all_stocks.update(c['stocks']) + all_stocks = list(all_stocks) + + logger.info(f"概念数: {len(all_concepts)}, 股票数: {len(all_stocks)}") + + updated = run_once(all_concepts, all_stocks) + logger.info(f"更新完成: {updated} 条记录") + + +def show_status(): + """显示当前状态""" + print("\n" + "=" * 60) + print("概念涨跌幅实时更新服务 - 状态") + print("=" * 60) + + # 当前时间 + now = datetime.now() + print(f"\n当前时间: {now.strftime('%Y-%m-%d %H:%M:%S')}") + print(f"是否交易时间: {'是' if is_trading_time() else '否'}") + + # MySQL数据状态 + print("\nMySQL数据状态:") + try: + with MYSQL_ENGINE.connect() as conn: + # 今日数据量 + result = conn.execute(text(""" + SELECT concept_type, COUNT(*) as cnt + FROM concept_daily_stats + WHERE trade_date = CURDATE() + GROUP BY concept_type + """)) + rows = list(result) + if rows: + print(" 今日数据:") + for row in rows: + print(f" {row[0]}: {row[1]} 条") + else: + print(" 今日暂无数据") + + # 最新更新时间 + result = conn.execute(text(""" + SELECT MAX(updated_at) FROM concept_daily_stats WHERE trade_date = CURDATE() + """)) + row = result.fetchone() + if row and row[0]: + print(f" 最后更新: {row[0]}") + except Exception as e: + print(f" 查询失败: {e}") + + # ClickHouse数据状态 + print("\nClickHouse数据状态:") + try: + client = get_ch_client() + result = client.execute(""" + SELECT COUNT(*), MAX(timestamp) + FROM stock_minute + WHERE toDate(timestamp) = today() + """) + if result: + count, max_ts = result[0] + print(f" 今日分钟数据: {count:,} 条") + print(f" 最新时间戳: {max_ts}") + except Exception as e: + print(f" 查询失败: {e}") + + # 今日涨跌幅TOP10 + print("\n今日涨跌幅 TOP10:") + try: + with MYSQL_ENGINE.connect() as conn: + result = conn.execute(text(""" + SELECT concept_name, avg_change_pct, stock_count, concept_type + FROM concept_daily_stats + WHERE trade_date = CURDATE() AND concept_type = 'leaf' + ORDER BY avg_change_pct DESC + LIMIT 10 + """)) + rows = list(result) + if rows: + print(f" {'概念':<25} | {'涨跌幅':>8} | {'股票数':>6}") + print(" " + "-" * 50) + for row in rows: + name = row[0][:25] if len(row[0]) > 25 else row[0] + print(f" {name:<25} | {row[1]:>7.2f}% | {row[2]:>6}") + else: + print(" 暂无数据") + except Exception as e: + print(f" 查询失败: {e}") + + +# ==================== 主函数 ==================== + +def main(): + parser = argparse.ArgumentParser(description='概念涨跌幅实时更新服务') + parser.add_argument('command', nargs='?', default='realtime', + choices=['realtime', 'once', 'status'], + help='命令: realtime(实时运行), once(单次运行), status(状态查看)') + + args = parser.parse_args() + + if args.command == 'realtime': + run_realtime() + elif args.command == 'once': + run_single() + elif args.command == 'status': + show_status() + + +if __name__ == "__main__": + main() diff --git a/create_tables.py b/create_tables.py new file mode 100644 index 00000000..f5c8912e --- /dev/null +++ b/create_tables.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""创建异动检测所需的数据库表""" + +import sys +from sqlalchemy import create_engine, text + +engine = create_engine('mysql+pymysql://root:Zzl5588161!@222.128.1.157:33060/stock', echo=False) + +# 删除旧表 +drop_sql1 = 'DROP TABLE IF EXISTS concept_minute_alert' +drop_sql2 = 'DROP TABLE IF EXISTS index_minute_snapshot' + +# 创建 concept_minute_alert 表 +# 支持 Z-Score + SVM 智能检测 +sql1 = ''' +CREATE TABLE concept_minute_alert ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + concept_id VARCHAR(32) NOT NULL, + concept_name VARCHAR(100) NOT NULL, + alert_time DATETIME NOT NULL, + alert_type VARCHAR(20) NOT NULL COMMENT 'surge_up=暴涨, surge_down=暴跌, limit_up=涨停增加, rank_jump=排名跃升', + trade_date DATE NOT NULL, + change_pct DECIMAL(10,4) COMMENT '当前涨跌幅', + prev_change_pct DECIMAL(10,4) COMMENT '之前涨跌幅', + change_delta DECIMAL(10,4) COMMENT '涨跌幅变化', + limit_up_count INT DEFAULT 0 COMMENT '涨停数', + prev_limit_up_count INT DEFAULT 0, + limit_up_delta INT DEFAULT 0, + limit_down_count INT DEFAULT 0 COMMENT '跌停数', + rank_position INT COMMENT '当前排名', + prev_rank_position INT COMMENT '之前排名', + rank_delta INT COMMENT '排名变化(负数表示上升)', + index_code VARCHAR(20) DEFAULT '000001.SH', + index_price DECIMAL(12,4), + index_change_pct DECIMAL(10,4), + stock_count INT, + concept_type VARCHAR(20) DEFAULT 'leaf', + zscore DECIMAL(8,4) COMMENT 'Z-Score值', + importance_score DECIMAL(6,4) COMMENT '重要性评分(0-1)', + extra_info JSON COMMENT '扩展信息(包含zscore,svm_score等)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_trade_date (trade_date), + INDEX idx_alert_time (alert_time), + INDEX idx_concept_id (concept_id), + INDEX idx_alert_type (alert_type), + INDEX idx_trade_date_time (trade_date, alert_time), + INDEX idx_importance (importance_score) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='概念异动记录表(智能版)' +''' + +# 创建 index_minute_snapshot 表 +sql2 = ''' +CREATE TABLE index_minute_snapshot ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + index_code VARCHAR(20) NOT NULL, + trade_date DATE NOT NULL, + snapshot_time DATETIME NOT NULL, + price DECIMAL(12,4), + open_price DECIMAL(12,4), + high_price DECIMAL(12,4), + low_price DECIMAL(12,4), + prev_close DECIMAL(12,4), + change_pct DECIMAL(10,4), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_index_time (index_code, snapshot_time), + INDEX idx_trade_date (trade_date) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 +''' + +if __name__ == '__main__': + print('正在重建数据库表...\n') + + with engine.begin() as conn: + # 先删除旧表 + print('删除旧表...') + conn.execute(text(drop_sql1)) + print(' - concept_minute_alert 已删除') + conn.execute(text(drop_sql2)) + print(' - index_minute_snapshot 已删除') + + # 创建新表 + print('\n创建新表...') + conn.execute(text(sql1)) + print(' ✅ concept_minute_alert 表创建成功') + conn.execute(text(sql2)) + print(' ✅ index_minute_snapshot 表创建成功') + + print('\n✅ 所有表创建完成!') diff --git a/ml/README.md b/ml/README.md new file mode 100644 index 00000000..7651277c --- /dev/null +++ b/ml/README.md @@ -0,0 +1,112 @@ +# 概念异动检测 ML 模块 + +基于 Transformer Autoencoder 的概念异动检测系统。 + +## 环境要求 + +- Python 3.8+ +- PyTorch 2.0+ (CUDA 12.x for 5090 GPU) +- ClickHouse, MySQL, Elasticsearch + +## 数据库配置 + +当前配置(`prepare_data.py`): +- MySQL: `192.168.1.5:3306` +- Elasticsearch: `127.0.0.1:9200` +- ClickHouse: `127.0.0.1:9000` + +## 快速开始 + +```bash +# 1. 安装依赖 +pip install -r ml/requirements.txt + +# 2. 安装 PyTorch (5090 需要 CUDA 12.4) +pip install torch --index-url https://download.pytorch.org/whl/cu124 + +# 3. 运行训练 +chmod +x ml/run_training.sh +./ml/run_training.sh +``` + +## 文件说明 + +| 文件 | 说明 | +|------|------| +| `model.py` | Transformer Autoencoder 模型定义 | +| `prepare_data.py` | 数据提取和特征计算 | +| `train.py` | 模型训练脚本 | +| `inference.py` | 推理服务 | +| `enhanced_detector.py` | 增强版检测器(融合 Alpha + ML) | + +## 训练参数 + +```bash +# 完整参数 +./ml/run_training.sh --start 2022-01-01 --end 2024-12-01 --epochs 100 --batch_size 256 + +# 只准备数据 +python ml/prepare_data.py --start 2022-01-01 + +# 只训练(数据已准备好) +python ml/train.py --epochs 100 --batch_size 256 --lr 1e-4 +``` + +## 模型架构 + +``` +输入: (batch, 30, 6) # 30分钟序列,6个特征 + ↓ +Positional Encoding + ↓ +Transformer Encoder (4层, 8头, d=128) + ↓ +Bottleneck (压缩到 32 维) + ↓ +Transformer Decoder (4层) + ↓ +输出: (batch, 30, 6) # 重构序列 + +异动判断: reconstruction_error > threshold +``` + +## 6维特征 + +1. `alpha` - 超额收益(概念涨幅 - 大盘涨幅) +2. `alpha_delta` - Alpha 5分钟变化 +3. `amt_ratio` - 成交额 / 20分钟均值 +4. `amt_delta` - 成交额变化率 +5. `rank_pct` - Alpha 排名百分位 +6. `limit_up_ratio` - 涨停股占比 + +## 训练产出 + +训练完成后,`ml/checkpoints/` 包含: +- `best_model.pt` - 最佳模型权重 +- `thresholds.json` - 异动阈值 (P90/P95/P99) +- `normalization_stats.json` - 数据标准化参数 +- `config.json` - 训练配置 + +## 使用示例 + +```python +from ml.inference import ConceptAnomalyDetector + +detector = ConceptAnomalyDetector('ml/checkpoints') + +# 实时检测 +is_anomaly, score = detector.detect( + concept_name="人工智能", + features={ + 'alpha': 2.5, + 'alpha_delta': 0.8, + 'amt_ratio': 1.5, + 'amt_delta': 0.3, + 'rank_pct': 0.95, + 'limit_up_ratio': 0.15, + } +) + +if is_anomaly: + print(f"检测到异动!分数: {score}") +``` diff --git a/ml/__init__.py b/ml/__init__.py new file mode 100644 index 00000000..348015d0 --- /dev/null +++ b/ml/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +""" +概念异动检测 ML 模块 + +提供基于 Transformer Autoencoder 的异动检测功能 +""" + +from .inference import ConceptAnomalyDetector, MLAnomalyService + +__all__ = ['ConceptAnomalyDetector', 'MLAnomalyService'] diff --git a/ml/backtest.py b/ml/backtest.py new file mode 100644 index 00000000..57d49e30 --- /dev/null +++ b/ml/backtest.py @@ -0,0 +1,481 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +历史异动回测脚本 + +使用训练好的模型,对历史数据进行异动检测,生成异动记录 + +使用方法: + # 回测指定日期范围 + python backtest.py --start 2024-01-01 --end 2024-12-01 + + # 回测单天 + python backtest.py --start 2024-11-01 --end 2024-11-01 + + # 只生成结果,不写入数据库 + python backtest.py --start 2024-01-01 --dry-run +""" + +import os +import sys +import argparse +import json +from datetime import datetime, timedelta +from pathlib import Path +from typing import Dict, List, Tuple, Optional +from collections import defaultdict + +import numpy as np +import pandas as pd +import torch +from tqdm import tqdm +from sqlalchemy import create_engine, text + +# 添加父目录到路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from model import TransformerAutoencoder + + +# ==================== 配置 ==================== + +MYSQL_ENGINE = create_engine( + "mysql+pymysql://root:Zzl5588161!@192.168.1.5:3306/stock", + echo=False +) + +# 特征列表(与训练一致) +FEATURES = [ + 'alpha', + 'alpha_delta', + 'amt_ratio', + 'amt_delta', + 'rank_pct', + 'limit_up_ratio', +] + +# 回测配置 +BACKTEST_CONFIG = { + 'seq_len': 30, # 序列长度 + 'threshold_key': 'p95', # 使用的阈值 + 'min_alpha_abs': 0.5, # 最小 Alpha 绝对值(过滤微小波动) + 'cooldown_minutes': 8, # 同一概念冷却时间 + 'max_alerts_per_minute': 15, # 每分钟最多异动数 + 'clip_value': 10.0, # 极端值截断 +} + + +# ==================== 模型加载 ==================== + +class AnomalyDetector: + """异动检测器""" + + def __init__(self, checkpoint_dir: str = 'ml/checkpoints', device: str = 'auto'): + self.checkpoint_dir = Path(checkpoint_dir) + + # 设备 + if device == 'auto': + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + else: + self.device = torch.device(device) + + # 加载配置 + self._load_config() + + # 加载模型 + self._load_model() + + # 加载阈值 + self._load_thresholds() + + print(f"AnomalyDetector 初始化完成") + print(f" 设备: {self.device}") + print(f" 阈值 ({BACKTEST_CONFIG['threshold_key']}): {self.threshold:.6f}") + + def _load_config(self): + config_path = self.checkpoint_dir / 'config.json' + with open(config_path, 'r') as f: + self.config = json.load(f) + + def _load_model(self): + model_path = self.checkpoint_dir / 'best_model.pt' + checkpoint = torch.load(model_path, map_location=self.device) + + model_config = self.config['model'].copy() + model_config['use_instance_norm'] = self.config.get('use_instance_norm', True) + + self.model = TransformerAutoencoder(**model_config) + self.model.load_state_dict(checkpoint['model_state_dict']) + self.model.to(self.device) + self.model.eval() + + def _load_thresholds(self): + thresholds_path = self.checkpoint_dir / 'thresholds.json' + with open(thresholds_path, 'r') as f: + thresholds = json.load(f) + + self.threshold = thresholds[BACKTEST_CONFIG['threshold_key']] + + @torch.no_grad() + def compute_anomaly_scores(self, sequences: np.ndarray) -> np.ndarray: + """ + 计算异动分数 + + Args: + sequences: (n_sequences, seq_len, n_features) + Returns: + scores: (n_sequences,) 每个序列最后时刻的异动分数 + """ + # 截断极端值 + sequences = np.clip(sequences, -BACKTEST_CONFIG['clip_value'], BACKTEST_CONFIG['clip_value']) + + # 转为 tensor + x = torch.FloatTensor(sequences).to(self.device) + + # 计算重构误差 + errors = self.model.compute_reconstruction_error(x, reduction='none') + + # 取最后一个时刻的误差 + scores = errors[:, -1].cpu().numpy() + + return scores + + def is_anomaly(self, score: float) -> bool: + """判断是否异动""" + return score > self.threshold + + +# ==================== 数据加载 ==================== + +def load_daily_features(data_dir: str, date: str) -> Optional[pd.DataFrame]: + """加载单天的特征数据""" + file_path = Path(data_dir) / f"features_{date}.parquet" + + if not file_path.exists(): + return None + + df = pd.read_parquet(file_path) + return df + + +def get_available_dates(data_dir: str, start_date: str, end_date: str) -> List[str]: + """获取可用的日期列表""" + data_path = Path(data_dir) + all_files = sorted(data_path.glob("features_*.parquet")) + + dates = [] + for f in all_files: + date = f.stem.replace('features_', '') + if start_date <= date <= end_date: + dates.append(date) + + return dates + + +# ==================== 回测逻辑 ==================== + +def backtest_single_day( + detector: AnomalyDetector, + df: pd.DataFrame, + date: str, + seq_len: int = 30 +) -> List[Dict]: + """ + 回测单天数据 + + Args: + detector: 异动检测器 + df: 当天的特征数据 + date: 日期 + seq_len: 序列长度 + + Returns: + alerts: 异动列表 + """ + alerts = [] + + # 按概念分组 + grouped = df.groupby('concept_id', sort=False) + + # 冷却记录 {concept_id: last_alert_timestamp} + cooldown = {} + + # 获取所有时间点 + all_timestamps = sorted(df['timestamp'].unique()) + + if len(all_timestamps) < seq_len: + return alerts + + # 对每个时间点进行检测(从第 seq_len 个开始) + for t_idx in range(seq_len - 1, len(all_timestamps)): + current_time = all_timestamps[t_idx] + window_start_time = all_timestamps[t_idx - seq_len + 1] + + minute_alerts = [] + + # 收集该时刻所有概念的序列 + concept_sequences = [] + concept_infos = [] + + for concept_id, concept_df in grouped: + # 获取该概念在时间窗口内的数据 + mask = (concept_df['timestamp'] >= window_start_time) & (concept_df['timestamp'] <= current_time) + window_df = concept_df[mask].sort_values('timestamp') + + if len(window_df) < seq_len: + continue + + # 取最后 seq_len 个点 + window_df = window_df.tail(seq_len) + + # 提取特征 + features = window_df[FEATURES].values + + # 处理缺失值 + features = np.nan_to_num(features, nan=0.0, posinf=0.0, neginf=0.0) + + # 获取当前时刻的信息 + current_row = window_df.iloc[-1] + + concept_sequences.append(features) + concept_infos.append({ + 'concept_id': concept_id, + 'timestamp': current_time, + 'alpha': current_row.get('alpha', 0), + 'alpha_delta': current_row.get('alpha_delta', 0), + 'amt_ratio': current_row.get('amt_ratio', 1), + 'limit_up_ratio': current_row.get('limit_up_ratio', 0), + 'limit_down_ratio': current_row.get('limit_down_ratio', 0), + 'rank_pct': current_row.get('rank_pct', 0.5), + 'stock_count': current_row.get('stock_count', 0), + 'total_amt': current_row.get('total_amt', 0), + }) + + if not concept_sequences: + continue + + # 批量计算异动分数 + sequences_array = np.array(concept_sequences) + scores = detector.compute_anomaly_scores(sequences_array) + + # 检测异动 + for i, (info, score) in enumerate(zip(concept_infos, scores)): + concept_id = info['concept_id'] + alpha = info['alpha'] + + # 过滤小波动 + if abs(alpha) < BACKTEST_CONFIG['min_alpha_abs']: + continue + + # 检查冷却 + if concept_id in cooldown: + last_alert = cooldown[concept_id] + if isinstance(current_time, datetime): + time_diff = (current_time - last_alert).total_seconds() / 60 + else: + # timestamp 是字符串或其他格式 + time_diff = BACKTEST_CONFIG['cooldown_minutes'] + 1 # 跳过冷却检查 + + if time_diff < BACKTEST_CONFIG['cooldown_minutes']: + continue + + # 判断是否异动 + if not detector.is_anomaly(score): + continue + + # 记录异动 + alert_type = 'surge_up' if alpha > 0 else 'surge_down' + + alert = { + 'concept_id': concept_id, + 'alert_time': current_time, + 'trade_date': date, + 'alert_type': alert_type, + 'anomaly_score': float(score), + 'threshold': detector.threshold, + **info + } + + minute_alerts.append(alert) + cooldown[concept_id] = current_time + + # 按分数排序,限制数量 + minute_alerts.sort(key=lambda x: x['anomaly_score'], reverse=True) + alerts.extend(minute_alerts[:BACKTEST_CONFIG['max_alerts_per_minute']]) + + return alerts + + +# ==================== 数据库写入 ==================== + +def save_alerts_to_mysql(alerts: List[Dict], dry_run: bool = False) -> int: + """保存异动到 MySQL""" + if not alerts: + return 0 + + if dry_run: + print(f" [Dry Run] 将写入 {len(alerts)} 条异动") + return len(alerts) + + saved = 0 + with MYSQL_ENGINE.begin() as conn: + for alert in alerts: + try: + # 检查是否已存在 + check_sql = text(""" + SELECT id FROM concept_minute_alert + WHERE concept_id = :concept_id + AND alert_time = :alert_time + AND trade_date = :trade_date + """) + exists = conn.execute(check_sql, { + 'concept_id': alert['concept_id'], + 'alert_time': alert['alert_time'], + 'trade_date': alert['trade_date'], + }).fetchone() + + if exists: + continue + + # 插入新记录 + insert_sql = text(""" + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, zscore, importance_score, stock_count, extra_info) + VALUES + (:concept_id, :concept_name, :alert_time, :alert_type, :trade_date, + :change_pct, :zscore, :importance_score, :stock_count, :extra_info) + """) + + conn.execute(insert_sql, { + 'concept_id': alert['concept_id'], + 'concept_name': alert.get('concept_name', ''), + 'alert_time': alert['alert_time'], + 'alert_type': alert['alert_type'], + 'trade_date': alert['trade_date'], + 'change_pct': alert.get('alpha', 0), + 'zscore': alert['anomaly_score'], + 'importance_score': alert['anomaly_score'], + 'stock_count': alert.get('stock_count', 0), + 'extra_info': json.dumps({ + 'detection_method': 'ml_autoencoder', + 'threshold': alert['threshold'], + 'alpha': alert.get('alpha', 0), + 'amt_ratio': alert.get('amt_ratio', 1), + }, ensure_ascii=False) + }) + + saved += 1 + + except Exception as e: + print(f" 保存失败: {alert['concept_id']} - {e}") + + return saved + + +def export_alerts_to_csv(alerts: List[Dict], output_path: str): + """导出异动到 CSV""" + if not alerts: + return + + df = pd.DataFrame(alerts) + df.to_csv(output_path, index=False, encoding='utf-8-sig') + print(f"已导出到: {output_path}") + + +# ==================== 主函数 ==================== + +def main(): + parser = argparse.ArgumentParser(description='历史异动回测') + parser.add_argument('--data_dir', type=str, default='ml/data', + help='特征数据目录') + parser.add_argument('--checkpoint_dir', type=str, default='ml/checkpoints', + help='模型检查点目录') + parser.add_argument('--start', type=str, required=True, + help='开始日期 (YYYY-MM-DD)') + parser.add_argument('--end', type=str, required=True, + help='结束日期 (YYYY-MM-DD)') + parser.add_argument('--dry-run', action='store_true', + help='只计算,不写入数据库') + parser.add_argument('--export-csv', type=str, default=None, + help='导出 CSV 文件路径') + parser.add_argument('--device', type=str, default='auto', + help='设备 (auto/cuda/cpu)') + + args = parser.parse_args() + + print("=" * 60) + print("历史异动回测") + print("=" * 60) + print(f"日期范围: {args.start} ~ {args.end}") + print(f"数据目录: {args.data_dir}") + print(f"模型目录: {args.checkpoint_dir}") + print(f"Dry Run: {args.dry_run}") + print("=" * 60) + + # 初始化检测器 + detector = AnomalyDetector(args.checkpoint_dir, args.device) + + # 获取可用日期 + dates = get_available_dates(args.data_dir, args.start, args.end) + + if not dates: + print(f"未找到 {args.start} ~ {args.end} 范围内的数据") + return + + print(f"\n找到 {len(dates)} 天的数据") + + # 回测 + all_alerts = [] + total_saved = 0 + + for date in tqdm(dates, desc="回测进度"): + # 加载数据 + df = load_daily_features(args.data_dir, date) + + if df is None or df.empty: + continue + + # 回测单天 + alerts = backtest_single_day( + detector, df, date, + seq_len=BACKTEST_CONFIG['seq_len'] + ) + + if alerts: + all_alerts.extend(alerts) + + # 写入数据库 + saved = save_alerts_to_mysql(alerts, dry_run=args.dry_run) + total_saved += saved + + if not args.dry_run: + tqdm.write(f" {date}: 检测到 {len(alerts)} 个异动,保存 {saved} 条") + + # 导出 CSV + if args.export_csv and all_alerts: + export_alerts_to_csv(all_alerts, args.export_csv) + + # 汇总 + print("\n" + "=" * 60) + print("回测完成!") + print("=" * 60) + print(f"总计检测到: {len(all_alerts)} 个异动") + print(f"保存到数据库: {total_saved} 条") + + # 统计 + if all_alerts: + df_alerts = pd.DataFrame(all_alerts) + print(f"\n异动类型分布:") + print(df_alerts['alert_type'].value_counts()) + + print(f"\n异动分数统计:") + print(f" Mean: {df_alerts['anomaly_score'].mean():.4f}") + print(f" Max: {df_alerts['anomaly_score'].max():.4f}") + print(f" Min: {df_alerts['anomaly_score'].min():.4f}") + + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/ml/backtest_hybrid.py b/ml/backtest_hybrid.py new file mode 100644 index 00000000..7bb4c808 --- /dev/null +++ b/ml/backtest_hybrid.py @@ -0,0 +1,418 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +融合异动回测脚本 + +使用 HybridAnomalyDetector 进行回测: +- 规则评分 + LSTM Autoencoder 融合判断 +- 输出更丰富的异动信息 + +使用方法: + python backtest_hybrid.py --start 2024-01-01 --end 2024-12-01 + python backtest_hybrid.py --start 2024-11-01 --dry-run +""" + +import os +import sys +import argparse +import json +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional +from collections import defaultdict + +import numpy as np +import pandas as pd +from tqdm import tqdm +from sqlalchemy import create_engine, text + +# 添加父目录到路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from detector import HybridAnomalyDetector, create_detector + + +# ==================== 配置 ==================== + +MYSQL_ENGINE = create_engine( + "mysql+pymysql://root:Zzl5588161!@192.168.1.5:3306/stock", + echo=False +) + +FEATURES = [ + 'alpha', + 'alpha_delta', + 'amt_ratio', + 'amt_delta', + 'rank_pct', + 'limit_up_ratio', +] + +BACKTEST_CONFIG = { + 'seq_len': 30, + 'min_alpha_abs': 0.3, # 降低阈值,让规则也能发挥作用 + 'cooldown_minutes': 8, + 'max_alerts_per_minute': 20, + 'clip_value': 10.0, +} + + +# ==================== 数据加载 ==================== + +def load_daily_features(data_dir: str, date: str) -> Optional[pd.DataFrame]: + """加载单天的特征数据""" + file_path = Path(data_dir) / f"features_{date}.parquet" + + if not file_path.exists(): + return None + + df = pd.read_parquet(file_path) + return df + + +def get_available_dates(data_dir: str, start_date: str, end_date: str) -> List[str]: + """获取可用的日期列表""" + data_path = Path(data_dir) + all_files = sorted(data_path.glob("features_*.parquet")) + + dates = [] + for f in all_files: + date = f.stem.replace('features_', '') + if start_date <= date <= end_date: + dates.append(date) + + return dates + + +# ==================== 融合回测 ==================== + +def backtest_single_day_hybrid( + detector: HybridAnomalyDetector, + df: pd.DataFrame, + date: str, + seq_len: int = 30 +) -> List[Dict]: + """ + 使用融合检测器回测单天数据 + """ + alerts = [] + + # 按概念分组 + grouped = df.groupby('concept_id', sort=False) + + # 冷却记录 + cooldown = {} + + # 获取所有时间点 + all_timestamps = sorted(df['timestamp'].unique()) + + if len(all_timestamps) < seq_len: + return alerts + + # 对每个时间点进行检测 + for t_idx in range(seq_len - 1, len(all_timestamps)): + current_time = all_timestamps[t_idx] + window_start_time = all_timestamps[t_idx - seq_len + 1] + + minute_alerts = [] + + for concept_id, concept_df in grouped: + # 获取时间窗口内的数据 + mask = (concept_df['timestamp'] >= window_start_time) & (concept_df['timestamp'] <= current_time) + window_df = concept_df[mask].sort_values('timestamp') + + if len(window_df) < seq_len: + continue + + window_df = window_df.tail(seq_len) + + # 提取特征序列(给 ML 模型) + sequence = window_df[FEATURES].values + sequence = np.nan_to_num(sequence, nan=0.0, posinf=0.0, neginf=0.0) + sequence = np.clip(sequence, -BACKTEST_CONFIG['clip_value'], BACKTEST_CONFIG['clip_value']) + + # 当前时刻特征(给规则系统) + current_row = window_df.iloc[-1] + current_features = { + 'alpha': current_row.get('alpha', 0), + 'alpha_delta': current_row.get('alpha_delta', 0), + 'amt_ratio': current_row.get('amt_ratio', 1), + 'amt_delta': current_row.get('amt_delta', 0), + 'rank_pct': current_row.get('rank_pct', 0.5), + 'limit_up_ratio': current_row.get('limit_up_ratio', 0), + } + + # 过滤微小波动 + if abs(current_features['alpha']) < BACKTEST_CONFIG['min_alpha_abs']: + continue + + # 检查冷却 + if concept_id in cooldown: + last_alert = cooldown[concept_id] + if isinstance(current_time, datetime): + time_diff = (current_time - last_alert).total_seconds() / 60 + else: + time_diff = BACKTEST_CONFIG['cooldown_minutes'] + 1 + + if time_diff < BACKTEST_CONFIG['cooldown_minutes']: + continue + + # 融合检测 + result = detector.detect(current_features, sequence) + + if not result.is_anomaly: + continue + + # 记录异动 + alert = { + 'concept_id': concept_id, + 'alert_time': current_time, + 'trade_date': date, + 'alert_type': result.anomaly_type, + 'final_score': result.final_score, + 'rule_score': result.rule_score, + 'ml_score': result.ml_score, + 'trigger_reason': result.trigger_reason, + 'triggered_rules': list(result.rule_details.keys()), + **current_features, + 'stock_count': current_row.get('stock_count', 0), + 'total_amt': current_row.get('total_amt', 0), + } + + minute_alerts.append(alert) + cooldown[concept_id] = current_time + + # 按最终得分排序 + minute_alerts.sort(key=lambda x: x['final_score'], reverse=True) + alerts.extend(minute_alerts[:BACKTEST_CONFIG['max_alerts_per_minute']]) + + return alerts + + +# ==================== 数据库写入 ==================== + +def save_alerts_to_mysql(alerts: List[Dict], dry_run: bool = False) -> int: + """保存异动到 MySQL(增强版字段)""" + if not alerts: + return 0 + + if dry_run: + print(f" [Dry Run] 将写入 {len(alerts)} 条异动") + return len(alerts) + + saved = 0 + with MYSQL_ENGINE.begin() as conn: + for alert in alerts: + try: + # 检查是否已存在 + check_sql = text(""" + SELECT id FROM concept_minute_alert + WHERE concept_id = :concept_id + AND alert_time = :alert_time + AND trade_date = :trade_date + """) + exists = conn.execute(check_sql, { + 'concept_id': alert['concept_id'], + 'alert_time': alert['alert_time'], + 'trade_date': alert['trade_date'], + }).fetchone() + + if exists: + continue + + # 插入新记录 + insert_sql = text(""" + INSERT INTO concept_minute_alert + (concept_id, concept_name, alert_time, alert_type, trade_date, + change_pct, zscore, importance_score, stock_count, extra_info) + VALUES + (:concept_id, :concept_name, :alert_time, :alert_type, :trade_date, + :change_pct, :zscore, :importance_score, :stock_count, :extra_info) + """) + + extra_info = { + 'detection_method': 'hybrid', + 'final_score': alert['final_score'], + 'rule_score': alert['rule_score'], + 'ml_score': alert['ml_score'], + 'trigger_reason': alert['trigger_reason'], + 'triggered_rules': alert['triggered_rules'], + 'alpha': alert.get('alpha', 0), + 'alpha_delta': alert.get('alpha_delta', 0), + 'amt_ratio': alert.get('amt_ratio', 1), + } + + conn.execute(insert_sql, { + 'concept_id': alert['concept_id'], + 'concept_name': alert.get('concept_name', ''), + 'alert_time': alert['alert_time'], + 'alert_type': alert['alert_type'], + 'trade_date': alert['trade_date'], + 'change_pct': alert.get('alpha', 0), + 'zscore': alert['final_score'], # 用最终得分作为 zscore + 'importance_score': alert['final_score'], + 'stock_count': alert.get('stock_count', 0), + 'extra_info': json.dumps(extra_info, ensure_ascii=False) + }) + + saved += 1 + + except Exception as e: + print(f" 保存失败: {alert['concept_id']} - {e}") + + return saved + + +def export_alerts_to_csv(alerts: List[Dict], output_path: str): + """导出异动到 CSV""" + if not alerts: + return + + df = pd.DataFrame(alerts) + df.to_csv(output_path, index=False, encoding='utf-8-sig') + print(f"已导出到: {output_path}") + + +# ==================== 统计分析 ==================== + +def analyze_alerts(alerts: List[Dict]): + """分析异动结果""" + if not alerts: + print("无异动数据") + return + + df = pd.DataFrame(alerts) + + print("\n" + "=" * 60) + print("异动统计分析") + print("=" * 60) + + # 1. 基本统计 + print(f"\n总异动数: {len(alerts)}") + + # 2. 按类型统计 + print(f"\n异动类型分布:") + print(df['alert_type'].value_counts()) + + # 3. 得分统计 + print(f"\n得分统计:") + print(f" 最终得分 - Mean: {df['final_score'].mean():.1f}, Max: {df['final_score'].max():.1f}") + print(f" 规则得分 - Mean: {df['rule_score'].mean():.1f}, Max: {df['rule_score'].max():.1f}") + print(f" ML得分 - Mean: {df['ml_score'].mean():.1f}, Max: {df['ml_score'].max():.1f}") + + # 4. 触发来源分析 + print(f"\n触发来源分析:") + trigger_counts = df['trigger_reason'].apply( + lambda x: '规则' if '规则' in x else ('ML' if 'ML' in x else '融合') + ).value_counts() + print(trigger_counts) + + # 5. 规则触发频率 + all_rules = [] + for rules in df['triggered_rules']: + if isinstance(rules, list): + all_rules.extend(rules) + + if all_rules: + print(f"\n最常触发的规则 (Top 10):") + from collections import Counter + rule_counts = Counter(all_rules) + for rule, count in rule_counts.most_common(10): + print(f" {rule}: {count}") + + +# ==================== 主函数 ==================== + +def main(): + parser = argparse.ArgumentParser(description='融合异动回测') + parser.add_argument('--data_dir', type=str, default='ml/data', + help='特征数据目录') + parser.add_argument('--checkpoint_dir', type=str, default='ml/checkpoints', + help='模型检查点目录') + parser.add_argument('--start', type=str, required=True, + help='开始日期 (YYYY-MM-DD)') + parser.add_argument('--end', type=str, default=None, + help='结束日期 (YYYY-MM-DD),默认=start') + parser.add_argument('--dry-run', action='store_true', + help='只计算,不写入数据库') + parser.add_argument('--export-csv', type=str, default=None, + help='导出 CSV 文件路径') + parser.add_argument('--rule-weight', type=float, default=0.6, + help='规则权重 (0-1)') + parser.add_argument('--ml-weight', type=float, default=0.4, + help='ML权重 (0-1)') + + args = parser.parse_args() + + if args.end is None: + args.end = args.start + + print("=" * 60) + print("融合异动回测 (规则 + LSTM)") + print("=" * 60) + print(f"日期范围: {args.start} ~ {args.end}") + print(f"数据目录: {args.data_dir}") + print(f"模型目录: {args.checkpoint_dir}") + print(f"规则权重: {args.rule_weight}") + print(f"ML权重: {args.ml_weight}") + print(f"Dry Run: {args.dry_run}") + print("=" * 60) + + # 初始化融合检测器 + config = { + 'rule_weight': args.rule_weight, + 'ml_weight': args.ml_weight, + } + detector = create_detector(args.checkpoint_dir, config) + + # 获取可用日期 + dates = get_available_dates(args.data_dir, args.start, args.end) + + if not dates: + print(f"未找到 {args.start} ~ {args.end} 范围内的数据") + return + + print(f"\n找到 {len(dates)} 天的数据") + + # 回测 + all_alerts = [] + total_saved = 0 + + for date in tqdm(dates, desc="回测进度"): + df = load_daily_features(args.data_dir, date) + + if df is None or df.empty: + continue + + alerts = backtest_single_day_hybrid( + detector, df, date, + seq_len=BACKTEST_CONFIG['seq_len'] + ) + + if alerts: + all_alerts.extend(alerts) + + saved = save_alerts_to_mysql(alerts, dry_run=args.dry_run) + total_saved += saved + + if not args.dry_run: + tqdm.write(f" {date}: 检测到 {len(alerts)} 个异动,保存 {saved} 条") + + # 导出 CSV + if args.export_csv and all_alerts: + export_alerts_to_csv(all_alerts, args.export_csv) + + # 统计分析 + analyze_alerts(all_alerts) + + # 汇总 + print("\n" + "=" * 60) + print("回测完成!") + print("=" * 60) + print(f"总计检测到: {len(all_alerts)} 个异动") + print(f"保存到数据库: {total_saved} 条") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/ml/detector.py b/ml/detector.py new file mode 100644 index 00000000..0c14880d --- /dev/null +++ b/ml/detector.py @@ -0,0 +1,571 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +概念异动检测器 - 融合版 + +结合两种方法的优势: +1. 规则评分系统:可解释、稳定、覆盖已知模式 +2. LSTM Autoencoder:发现未知的异常模式 + +融合策略: +┌─────────────────────────────────────────────────────────┐ +│ 输入特征 │ +│ (alpha, alpha_delta, amt_ratio, amt_delta, rank_pct, │ +│ limit_up_ratio) │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────┐ │ +│ │ 规则评分系统 │ │ LSTM Autoencoder │ │ +│ │ (0-100分) │ │ (重构误差) │ │ +│ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ rule_score (0-100) ml_score (标准化后 0-100) │ +│ │ +├─────────────────────────────────────────────────────────┤ +│ 融合策略 │ +│ │ +│ final_score = w1 * rule_score + w2 * ml_score │ +│ │ +│ 异动判定: │ +│ - rule_score >= 60 → 直接触发(规则强信号) │ +│ - ml_score >= 80 → 直接触发(ML强信号) │ +│ - final_score >= 50 → 融合触发 │ +│ │ +└─────────────────────────────────────────────────────────┘ + +优势: +- 规则系统保证已知模式的检出率 +- ML模型捕捉规则未覆盖的异常 +- 两者互相验证,减少误报 +""" + +import json +from pathlib import Path +from typing import Dict, List, Optional, Tuple +from dataclasses import dataclass + +import numpy as np +import torch + +# 尝试导入模型(可能不存在) +try: + from model import LSTMAutoencoder, create_model + HAS_MODEL = True +except ImportError: + HAS_MODEL = False + + +@dataclass +class AnomalyResult: + """异动检测结果""" + is_anomaly: bool + final_score: float # 最终得分 (0-100) + rule_score: float # 规则得分 (0-100) + ml_score: float # ML得分 (0-100) + trigger_reason: str # 触发原因 + rule_details: Dict # 规则明细 + anomaly_type: str # 异动类型: surge_up / surge_down / volume_spike / unknown + + +class RuleBasedScorer: + """ + 基于规则的评分系统 + + 设计原则: + - 每个规则独立打分 + - 分数可叠加 + - 阈值可配置 + """ + + # 默认规则配置 + DEFAULT_RULES = { + # Alpha 相关(超额收益) + 'alpha_strong': { + 'condition': lambda r: abs(r.get('alpha', 0)) >= 3.0, + 'score': 35, + 'description': 'Alpha强信号(|α|≥3%)' + }, + 'alpha_medium': { + 'condition': lambda r: 2.0 <= abs(r.get('alpha', 0)) < 3.0, + 'score': 25, + 'description': 'Alpha中等(2%≤|α|<3%)' + }, + 'alpha_weak': { + 'condition': lambda r: 1.5 <= abs(r.get('alpha', 0)) < 2.0, + 'score': 15, + 'description': 'Alpha轻微(1.5%≤|α|<2%)' + }, + + # Alpha 变化率(加速度) + 'alpha_delta_strong': { + 'condition': lambda r: abs(r.get('alpha_delta', 0)) >= 1.0, + 'score': 30, + 'description': 'Alpha加速强(|Δα|≥1%)' + }, + 'alpha_delta_medium': { + 'condition': lambda r: 0.5 <= abs(r.get('alpha_delta', 0)) < 1.0, + 'score': 20, + 'description': 'Alpha加速中(0.5%≤|Δα|<1%)' + }, + + # 成交额比率(放量) + 'volume_spike_strong': { + 'condition': lambda r: r.get('amt_ratio', 1) >= 5.0, + 'score': 30, + 'description': '极度放量(≥5倍)' + }, + 'volume_spike_medium': { + 'condition': lambda r: 3.0 <= r.get('amt_ratio', 1) < 5.0, + 'score': 20, + 'description': '显著放量(3-5倍)' + }, + 'volume_spike_weak': { + 'condition': lambda r: 2.0 <= r.get('amt_ratio', 1) < 3.0, + 'score': 10, + 'description': '轻微放量(2-3倍)' + }, + + # 成交额变化率 + 'amt_delta_strong': { + 'condition': lambda r: abs(r.get('amt_delta', 0)) >= 1.0, + 'score': 15, + 'description': '成交额急变(|Δamt|≥100%)' + }, + + # 排名跳变 + 'rank_top': { + 'condition': lambda r: r.get('rank_pct', 0.5) >= 0.95, + 'score': 25, + 'description': '排名前5%' + }, + 'rank_bottom': { + 'condition': lambda r: r.get('rank_pct', 0.5) <= 0.05, + 'score': 25, + 'description': '排名后5%' + }, + 'rank_high': { + 'condition': lambda r: 0.9 <= r.get('rank_pct', 0.5) < 0.95, + 'score': 15, + 'description': '排名前10%' + }, + + # 涨停比例 + 'limit_up_high': { + 'condition': lambda r: r.get('limit_up_ratio', 0) >= 0.2, + 'score': 25, + 'description': '涨停比例≥20%' + }, + 'limit_up_medium': { + 'condition': lambda r: 0.1 <= r.get('limit_up_ratio', 0) < 0.2, + 'score': 15, + 'description': '涨停比例10-20%' + }, + + # 组合条件(更可靠的信号) + 'alpha_with_volume': { + 'condition': lambda r: abs(r.get('alpha', 0)) >= 1.5 and r.get('amt_ratio', 1) >= 2.0, + 'score': 20, # 额外加分 + 'description': 'Alpha+放量组合' + }, + 'acceleration_with_rank': { + 'condition': lambda r: abs(r.get('alpha_delta', 0)) >= 0.5 and (r.get('rank_pct', 0.5) >= 0.9 or r.get('rank_pct', 0.5) <= 0.1), + 'score': 15, # 额外加分 + 'description': '加速+排名异常组合' + }, + } + + def __init__(self, rules: Dict = None): + """ + 初始化规则评分器 + + Args: + rules: 自定义规则,格式同 DEFAULT_RULES + """ + self.rules = rules or self.DEFAULT_RULES + + def score(self, features: Dict) -> Tuple[float, Dict]: + """ + 计算规则得分 + + Args: + features: 特征字典,包含 alpha, alpha_delta, amt_ratio 等 + Returns: + score: 总分 (0-100) + details: 触发的规则明细 + """ + total_score = 0 + triggered_rules = {} + + for rule_name, rule_config in self.rules.items(): + try: + if rule_config['condition'](features): + total_score += rule_config['score'] + triggered_rules[rule_name] = { + 'score': rule_config['score'], + 'description': rule_config['description'] + } + except Exception: + # 忽略规则计算错误 + pass + + # 限制在 0-100 + total_score = min(100, max(0, total_score)) + + return total_score, triggered_rules + + def get_anomaly_type(self, features: Dict) -> str: + """判断异动类型""" + alpha = features.get('alpha', 0) + amt_ratio = features.get('amt_ratio', 1) + + if alpha >= 1.5: + return 'surge_up' + elif alpha <= -1.5: + return 'surge_down' + elif amt_ratio >= 3.0: + return 'volume_spike' + else: + return 'unknown' + + +class MLScorer: + """ + 基于 LSTM Autoencoder 的评分器 + + 将重构误差转换为 0-100 的分数 + """ + + def __init__( + self, + checkpoint_dir: str = 'ml/checkpoints', + device: str = 'auto' + ): + self.checkpoint_dir = Path(checkpoint_dir) + + # 设备 + if device == 'auto': + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + else: + self.device = torch.device(device) + + self.model = None + self.thresholds = None + self.config = None + + # 尝试加载模型 + self._load_model() + + def _load_model(self): + """加载模型和阈值""" + if not HAS_MODEL: + print("警告: 无法导入模型模块") + return + + model_path = self.checkpoint_dir / 'best_model.pt' + thresholds_path = self.checkpoint_dir / 'thresholds.json' + config_path = self.checkpoint_dir / 'config.json' + + if not model_path.exists(): + print(f"警告: 模型文件不存在 {model_path}") + return + + try: + # 加载配置 + if config_path.exists(): + with open(config_path, 'r') as f: + self.config = json.load(f) + + # 加载模型 + checkpoint = torch.load(model_path, map_location=self.device) + + model_config = self.config.get('model', {}) if self.config else {} + self.model = create_model(model_config) + self.model.load_state_dict(checkpoint['model_state_dict']) + self.model.to(self.device) + self.model.eval() + + # 加载阈值 + if thresholds_path.exists(): + with open(thresholds_path, 'r') as f: + self.thresholds = json.load(f) + + print(f"MLScorer 加载成功 (设备: {self.device})") + + except Exception as e: + print(f"警告: 模型加载失败 - {e}") + self.model = None + + def is_ready(self) -> bool: + """检查模型是否就绪""" + return self.model is not None + + @torch.no_grad() + def score(self, sequence: np.ndarray) -> float: + """ + 计算 ML 得分 + + Args: + sequence: (seq_len, n_features) 或 (batch, seq_len, n_features) + Returns: + score: 0-100 的分数,越高越异常 + """ + if not self.is_ready(): + return 0.0 + + # 确保是 3D + if sequence.ndim == 2: + sequence = sequence[np.newaxis, ...] + + # 转为 tensor + x = torch.FloatTensor(sequence).to(self.device) + + # 计算重构误差 + output, _ = self.model(x) + mse = ((output - x) ** 2).mean(dim=-1) # (batch, seq_len) + + # 取最后时刻的误差 + error = mse[:, -1].cpu().numpy() + + # 转换为 0-100 分数 + # 使用 p95 阈值作为参考 + if self.thresholds: + p95 = self.thresholds.get('p95', 0.1) + p99 = self.thresholds.get('p99', 0.2) + else: + p95, p99 = 0.1, 0.2 + + # 线性映射:p95 -> 50分, p99 -> 80分 + # error=0 -> 0分, error>=p99*1.5 -> 100分 + score = np.clip(error / p95 * 50, 0, 100) + + return float(score[0]) if len(score) == 1 else score.tolist() + + +class HybridAnomalyDetector: + """ + 融合异动检测器 + + 结合规则系统和 ML 模型 + """ + + # 默认配置 + DEFAULT_CONFIG = { + # 权重配置 + 'rule_weight': 0.6, # 规则权重 + 'ml_weight': 0.4, # ML权重 + + # 触发阈值 + 'rule_trigger': 60, # 规则直接触发阈值 + 'ml_trigger': 80, # ML直接触发阈值 + 'fusion_trigger': 50, # 融合触发阈值 + + # 特征列表 + 'features': [ + 'alpha', 'alpha_delta', 'amt_ratio', + 'amt_delta', 'rank_pct', 'limit_up_ratio' + ], + + # 序列长度(ML模型需要) + 'seq_len': 30, + } + + def __init__( + self, + config: Dict = None, + checkpoint_dir: str = 'ml/checkpoints', + device: str = 'auto' + ): + self.config = {**self.DEFAULT_CONFIG, **(config or {})} + + # 初始化评分器 + self.rule_scorer = RuleBasedScorer() + self.ml_scorer = MLScorer(checkpoint_dir, device) + + print(f"HybridAnomalyDetector 初始化完成") + print(f" 规则权重: {self.config['rule_weight']}") + print(f" ML权重: {self.config['ml_weight']}") + print(f" ML模型: {'就绪' if self.ml_scorer.is_ready() else '未加载'}") + + def detect( + self, + features: Dict, + sequence: np.ndarray = None + ) -> AnomalyResult: + """ + 检测异动 + + Args: + features: 当前时刻的特征字典 + sequence: 历史序列 (seq_len, n_features),ML模型需要 + Returns: + AnomalyResult: 检测结果 + """ + # 1. 规则评分 + rule_score, rule_details = self.rule_scorer.score(features) + + # 2. ML评分 + ml_score = 0.0 + if sequence is not None and self.ml_scorer.is_ready(): + ml_score = self.ml_scorer.score(sequence) + + # 3. 融合得分 + w1 = self.config['rule_weight'] + w2 = self.config['ml_weight'] + + # 如果ML不可用,全部权重给规则 + if not self.ml_scorer.is_ready(): + w1, w2 = 1.0, 0.0 + + final_score = w1 * rule_score + w2 * ml_score + + # 4. 判断是否异动 + is_anomaly = False + trigger_reason = '' + + if rule_score >= self.config['rule_trigger']: + is_anomaly = True + trigger_reason = f'规则强信号({rule_score:.0f}分)' + elif ml_score >= self.config['ml_trigger']: + is_anomaly = True + trigger_reason = f'ML强信号({ml_score:.0f}分)' + elif final_score >= self.config['fusion_trigger']: + is_anomaly = True + trigger_reason = f'融合触发({final_score:.0f}分)' + + # 5. 判断异动类型 + anomaly_type = self.rule_scorer.get_anomaly_type(features) if is_anomaly else '' + + return AnomalyResult( + is_anomaly=is_anomaly, + final_score=final_score, + rule_score=rule_score, + ml_score=ml_score, + trigger_reason=trigger_reason, + rule_details=rule_details, + anomaly_type=anomaly_type + ) + + def detect_batch( + self, + features_list: List[Dict], + sequences: np.ndarray = None + ) -> List[AnomalyResult]: + """ + 批量检测 + + Args: + features_list: 特征字典列表 + sequences: (batch, seq_len, n_features) + Returns: + List[AnomalyResult] + """ + results = [] + + for i, features in enumerate(features_list): + seq = sequences[i] if sequences is not None else None + result = self.detect(features, seq) + results.append(result) + + return results + + +# ==================== 便捷函数 ==================== + +def create_detector( + checkpoint_dir: str = 'ml/checkpoints', + config: Dict = None +) -> HybridAnomalyDetector: + """创建融合检测器""" + return HybridAnomalyDetector(config, checkpoint_dir) + + +def quick_detect(features: Dict) -> bool: + """ + 快速检测(只用规则,不需要ML模型) + + 适用于: + - 实时检测 + - ML模型未训练完成时 + """ + scorer = RuleBasedScorer() + score, _ = scorer.score(features) + return score >= 50 + + +# ==================== 测试 ==================== + +if __name__ == "__main__": + print("=" * 60) + print("融合异动检测器测试") + print("=" * 60) + + # 创建检测器 + detector = create_detector() + + # 测试用例 + test_cases = [ + { + 'name': '正常情况', + 'features': { + 'alpha': 0.5, + 'alpha_delta': 0.1, + 'amt_ratio': 1.2, + 'amt_delta': 0.1, + 'rank_pct': 0.5, + 'limit_up_ratio': 0.02 + } + }, + { + 'name': 'Alpha异动', + 'features': { + 'alpha': 3.5, + 'alpha_delta': 0.8, + 'amt_ratio': 2.5, + 'amt_delta': 0.5, + 'rank_pct': 0.92, + 'limit_up_ratio': 0.05 + } + }, + { + 'name': '放量异动', + 'features': { + 'alpha': 1.2, + 'alpha_delta': 0.3, + 'amt_ratio': 6.0, + 'amt_delta': 1.5, + 'rank_pct': 0.85, + 'limit_up_ratio': 0.08 + } + }, + { + 'name': '涨停潮', + 'features': { + 'alpha': 2.5, + 'alpha_delta': 0.6, + 'amt_ratio': 3.5, + 'amt_delta': 0.8, + 'rank_pct': 0.98, + 'limit_up_ratio': 0.25 + } + }, + ] + + print("\n测试结果:") + print("-" * 60) + + for case in test_cases: + result = detector.detect(case['features']) + + print(f"\n{case['name']}:") + print(f" 异动: {'是' if result.is_anomaly else '否'}") + print(f" 最终得分: {result.final_score:.1f}") + print(f" 规则得分: {result.rule_score:.1f}") + print(f" ML得分: {result.ml_score:.1f}") + if result.is_anomaly: + print(f" 触发原因: {result.trigger_reason}") + print(f" 异动类型: {result.anomaly_type}") + print(f" 触发规则: {list(result.rule_details.keys())}") + + print("\n" + "=" * 60) + print("测试完成!") diff --git a/ml/enhanced_detector.py b/ml/enhanced_detector.py new file mode 100644 index 00000000..b64c63a8 --- /dev/null +++ b/ml/enhanced_detector.py @@ -0,0 +1,526 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +增强版概念异动检测器 + +融合两种检测方法: +1. Alpha-based Z-Score(规则方法,实时性好) +2. Transformer Autoencoder(ML方法,更准确) + +使用策略: +- 当 ML 模型可用且历史数据足够时,优先使用 ML 方法 +- 否则回退到 Alpha-based 方法 +- 可以配置两种方法的融合权重 +""" + +import os +import sys +import logging +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Tuple, Optional +from dataclasses import dataclass, field +from collections import deque +import numpy as np + +# 添加父目录到路径 +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +logger = logging.getLogger(__name__) + + +# ==================== 配置 ==================== + +ENHANCED_CONFIG = { + # 融合策略 + 'fusion_mode': 'adaptive', # 'ml_only', 'alpha_only', 'adaptive', 'ensemble' + + # ML 权重(在 ensemble 模式下) + 'ml_weight': 0.6, + 'alpha_weight': 0.4, + + # ML 模型配置 + 'ml_checkpoint_dir': 'ml/checkpoints', + 'ml_threshold_key': 'p95', # p90, p95, p99 + + # Alpha 配置(与 concept_alert_alpha.py 一致) + 'alpha_zscore_threshold': 2.0, + 'alpha_absolute_threshold': 1.5, + 'alpha_history_window': 60, + 'alpha_min_history': 5, + + # 共享配置 + 'cooldown_minutes': 8, + 'max_alerts_per_minute': 15, + 'min_alpha_abs': 0.5, +} + +# 特征配置(与训练一致) +FEATURE_NAMES = [ + 'alpha', + 'alpha_delta', + 'amt_ratio', + 'amt_delta', + 'rank_pct', + 'limit_up_ratio', +] + + +# ==================== 数据结构 ==================== + +@dataclass +class AlphaStats: + """概念的Alpha统计信息""" + history: deque = field(default_factory=lambda: deque(maxlen=ENHANCED_CONFIG['alpha_history_window'])) + mean: float = 0.0 + std: float = 1.0 + + def update(self, alpha: float): + self.history.append(alpha) + if len(self.history) >= 2: + self.mean = np.mean(self.history) + self.std = max(np.std(self.history), 0.1) + + def get_zscore(self, alpha: float) -> float: + if len(self.history) < ENHANCED_CONFIG['alpha_min_history']: + return 0.0 + return (alpha - self.mean) / self.std + + def is_ready(self) -> bool: + return len(self.history) >= ENHANCED_CONFIG['alpha_min_history'] + + +@dataclass +class ConceptFeatures: + """概念的实时特征""" + alpha: float = 0.0 + alpha_delta: float = 0.0 + amt_ratio: float = 1.0 + amt_delta: float = 0.0 + rank_pct: float = 0.5 + limit_up_ratio: float = 0.0 + + def to_dict(self) -> Dict[str, float]: + return { + 'alpha': self.alpha, + 'alpha_delta': self.alpha_delta, + 'amt_ratio': self.amt_ratio, + 'amt_delta': self.amt_delta, + 'rank_pct': self.rank_pct, + 'limit_up_ratio': self.limit_up_ratio, + } + + +# ==================== 增强检测器 ==================== + +class EnhancedAnomalyDetector: + """ + 增强版异动检测器 + + 融合 Alpha-based 和 ML 两种方法 + """ + + def __init__( + self, + config: Dict = None, + ml_enabled: bool = True + ): + self.config = config or ENHANCED_CONFIG + self.ml_enabled = ml_enabled + self.ml_detector = None + + # Alpha 统计 + self.alpha_stats: Dict[str, AlphaStats] = {} + + # 特征历史(用于计算 delta) + self.feature_history: Dict[str, deque] = {} + + # 冷却记录 + self.cooldown_cache: Dict[str, datetime] = {} + + # 尝试加载 ML 模型 + if ml_enabled: + self._load_ml_model() + + logger.info(f"EnhancedAnomalyDetector 初始化完成") + logger.info(f" 融合模式: {self.config['fusion_mode']}") + logger.info(f" ML 可用: {self.ml_detector is not None}") + + def _load_ml_model(self): + """加载 ML 模型""" + try: + from inference import ConceptAnomalyDetector + checkpoint_dir = Path(__file__).parent / 'checkpoints' + + if (checkpoint_dir / 'best_model.pt').exists(): + self.ml_detector = ConceptAnomalyDetector( + checkpoint_dir=str(checkpoint_dir), + threshold_key=self.config['ml_threshold_key'] + ) + logger.info("ML 模型加载成功") + else: + logger.warning(f"ML 模型不存在: {checkpoint_dir / 'best_model.pt'}") + except Exception as e: + logger.warning(f"ML 模型加载失败: {e}") + self.ml_detector = None + + def _get_alpha_stats(self, concept_id: str) -> AlphaStats: + """获取或创建 Alpha 统计""" + if concept_id not in self.alpha_stats: + self.alpha_stats[concept_id] = AlphaStats() + return self.alpha_stats[concept_id] + + def _get_feature_history(self, concept_id: str) -> deque: + """获取特征历史""" + if concept_id not in self.feature_history: + self.feature_history[concept_id] = deque(maxlen=10) + return self.feature_history[concept_id] + + def _check_cooldown(self, concept_id: str, current_time: datetime) -> bool: + """检查冷却""" + if concept_id not in self.cooldown_cache: + return False + + last_alert = self.cooldown_cache[concept_id] + cooldown_td = (current_time - last_alert).total_seconds() / 60 + + return cooldown_td < self.config['cooldown_minutes'] + + def _set_cooldown(self, concept_id: str, current_time: datetime): + """设置冷却""" + self.cooldown_cache[concept_id] = current_time + + def compute_features( + self, + concept_id: str, + alpha: float, + amt_ratio: float, + rank_pct: float, + limit_up_ratio: float + ) -> ConceptFeatures: + """ + 计算概念的完整特征 + + Args: + concept_id: 概念ID + alpha: 当前超额收益 + amt_ratio: 成交额比率 + rank_pct: 排名百分位 + limit_up_ratio: 涨停股占比 + + Returns: + 完整特征 + """ + history = self._get_feature_history(concept_id) + + # 计算变化率 + alpha_delta = 0.0 + amt_delta = 0.0 + + if len(history) > 0: + last_features = history[-1] + alpha_delta = alpha - last_features.alpha + if last_features.amt_ratio > 0: + amt_delta = (amt_ratio - last_features.amt_ratio) / last_features.amt_ratio + + features = ConceptFeatures( + alpha=alpha, + alpha_delta=alpha_delta, + amt_ratio=amt_ratio, + amt_delta=amt_delta, + rank_pct=rank_pct, + limit_up_ratio=limit_up_ratio, + ) + + # 更新历史 + history.append(features) + + return features + + def detect_alpha_anomaly( + self, + concept_id: str, + alpha: float + ) -> Tuple[bool, float, str]: + """ + Alpha-based 异动检测 + + Returns: + is_anomaly: 是否异动 + score: 异动分数(Z-Score 绝对值) + reason: 触发原因 + """ + stats = self._get_alpha_stats(concept_id) + + # 计算 Z-Score(在更新前) + zscore = stats.get_zscore(alpha) + + # 更新统计 + stats.update(alpha) + + # 判断 + if stats.is_ready(): + if abs(zscore) >= self.config['alpha_zscore_threshold']: + return True, abs(zscore), f"Z={zscore:.2f}" + else: + if abs(alpha) >= self.config['alpha_absolute_threshold']: + fake_zscore = alpha / 0.5 + return True, abs(fake_zscore), f"Alpha={alpha:+.2f}%" + + return False, abs(zscore) if zscore else 0.0, "" + + def detect_ml_anomaly( + self, + concept_id: str, + features: ConceptFeatures + ) -> Tuple[bool, float]: + """ + ML-based 异动检测 + + Returns: + is_anomaly: 是否异动 + score: 异动分数(重构误差) + """ + if self.ml_detector is None: + return False, 0.0 + + try: + is_anomaly, score = self.ml_detector.detect( + concept_id, + features.to_dict() + ) + return is_anomaly, score or 0.0 + except Exception as e: + logger.warning(f"ML 检测失败: {e}") + return False, 0.0 + + def detect( + self, + concept_id: str, + concept_name: str, + alpha: float, + amt_ratio: float, + rank_pct: float, + limit_up_ratio: float, + change_pct: float, + index_change: float, + current_time: datetime, + **extra_data + ) -> Optional[Dict]: + """ + 融合检测 + + Args: + concept_id: 概念ID + concept_name: 概念名称 + alpha: 超额收益 + amt_ratio: 成交额比率 + rank_pct: 排名百分位 + limit_up_ratio: 涨停股占比 + change_pct: 概念涨跌幅 + index_change: 大盘涨跌幅 + current_time: 当前时间 + **extra_data: 其他数据(limit_up_count, stock_count 等) + + Returns: + 异动信息(如果触发),否则 None + """ + # Alpha 太小,不关注 + if abs(alpha) < self.config['min_alpha_abs']: + return None + + # 检查冷却 + if self._check_cooldown(concept_id, current_time): + return None + + # 计算特征 + features = self.compute_features( + concept_id, alpha, amt_ratio, rank_pct, limit_up_ratio + ) + + # 执行检测 + fusion_mode = self.config['fusion_mode'] + + alpha_anomaly, alpha_score, alpha_reason = self.detect_alpha_anomaly(concept_id, alpha) + ml_anomaly, ml_score = False, 0.0 + + if fusion_mode in ('ml_only', 'adaptive', 'ensemble'): + ml_anomaly, ml_score = self.detect_ml_anomaly(concept_id, features) + + # 根据融合模式判断 + is_anomaly = False + final_score = 0.0 + detection_method = '' + + if fusion_mode == 'alpha_only': + is_anomaly = alpha_anomaly + final_score = alpha_score + detection_method = 'alpha' + + elif fusion_mode == 'ml_only': + is_anomaly = ml_anomaly + final_score = ml_score + detection_method = 'ml' + + elif fusion_mode == 'adaptive': + # 优先 ML,回退 Alpha + if self.ml_detector and ml_score > 0: + is_anomaly = ml_anomaly + final_score = ml_score + detection_method = 'ml' + else: + is_anomaly = alpha_anomaly + final_score = alpha_score + detection_method = 'alpha' + + elif fusion_mode == 'ensemble': + # 加权融合 + # 归一化分数 + norm_alpha = min(alpha_score / 5.0, 1.0) # Z > 5 视为 1.0 + norm_ml = min(ml_score / (self.ml_detector.threshold if self.ml_detector else 1.0), 1.0) + + final_score = ( + self.config['alpha_weight'] * norm_alpha + + self.config['ml_weight'] * norm_ml + ) + is_anomaly = final_score > 0.5 or alpha_anomaly or ml_anomaly + detection_method = 'ensemble' + + if not is_anomaly: + return None + + # 构建异动记录 + self._set_cooldown(concept_id, current_time) + + alert_type = 'surge_up' if alpha > 0 else 'surge_down' + + alert = { + 'concept_id': concept_id, + 'concept_name': concept_name, + 'alert_type': alert_type, + 'alert_time': current_time, + 'change_pct': change_pct, + 'alpha': alpha, + 'alpha_zscore': alpha_score, + 'index_change_pct': index_change, + 'detection_method': detection_method, + 'alpha_score': alpha_score, + 'ml_score': ml_score, + 'final_score': final_score, + **extra_data + } + + return alert + + def batch_detect( + self, + concepts_data: List[Dict], + current_time: datetime + ) -> List[Dict]: + """ + 批量检测 + + Args: + concepts_data: 概念数据列表 + current_time: 当前时间 + + Returns: + 异动列表(按分数排序,限制数量) + """ + alerts = [] + + for data in concepts_data: + alert = self.detect( + concept_id=data['concept_id'], + concept_name=data['concept_name'], + alpha=data.get('alpha', 0), + amt_ratio=data.get('amt_ratio', 1.0), + rank_pct=data.get('rank_pct', 0.5), + limit_up_ratio=data.get('limit_up_ratio', 0), + change_pct=data.get('change_pct', 0), + index_change=data.get('index_change', 0), + current_time=current_time, + limit_up_count=data.get('limit_up_count', 0), + limit_down_count=data.get('limit_down_count', 0), + stock_count=data.get('stock_count', 0), + concept_type=data.get('concept_type', 'leaf'), + ) + + if alert: + alerts.append(alert) + + # 排序并限制数量 + alerts.sort(key=lambda x: x['final_score'], reverse=True) + return alerts[:self.config['max_alerts_per_minute']] + + def reset(self): + """重置所有状态(新交易日)""" + self.alpha_stats.clear() + self.feature_history.clear() + self.cooldown_cache.clear() + + if self.ml_detector: + self.ml_detector.clear_history() + + logger.info("检测器状态已重置") + + +# ==================== 测试 ==================== + +if __name__ == "__main__": + import random + + print("测试 EnhancedAnomalyDetector...") + + # 初始化 + detector = EnhancedAnomalyDetector(ml_enabled=False) # 不加载 ML(可能不存在) + + # 模拟数据 + concepts = [ + {'concept_id': 'ai_001', 'concept_name': '人工智能'}, + {'concept_id': 'chip_002', 'concept_name': '芯片半导体'}, + {'concept_id': 'car_003', 'concept_name': '新能源汽车'}, + ] + + print("\n模拟实时检测...") + current_time = datetime.now() + + for minute in range(50): + concepts_data = [] + + for c in concepts: + # 生成随机数据 + alpha = random.gauss(0, 0.8) + amt_ratio = max(0.3, random.gauss(1, 0.3)) + rank_pct = random.random() + limit_up_ratio = random.random() * 0.1 + + # 模拟异动(第30分钟人工智能暴涨) + if minute == 30 and c['concept_id'] == 'ai_001': + alpha = 4.5 + amt_ratio = 2.5 + limit_up_ratio = 0.3 + + concepts_data.append({ + **c, + 'alpha': alpha, + 'amt_ratio': amt_ratio, + 'rank_pct': rank_pct, + 'limit_up_ratio': limit_up_ratio, + 'change_pct': alpha + 0.5, + 'index_change': 0.5, + }) + + # 检测 + alerts = detector.batch_detect(concepts_data, current_time) + + if alerts: + for alert in alerts: + print(f" t={minute:02d} 🔥 {alert['concept_name']} " + f"Alpha={alert['alpha']:+.2f}% " + f"Score={alert['final_score']:.2f} " + f"Method={alert['detection_method']}") + + current_time = current_time.replace(minute=current_time.minute + 1 if current_time.minute < 59 else 0) + + print("\n测试完成!") diff --git a/ml/inference.py b/ml/inference.py new file mode 100644 index 00000000..4e704f4c --- /dev/null +++ b/ml/inference.py @@ -0,0 +1,455 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +概念异动检测推理服务 + +在实时场景中使用训练好的 Transformer Autoencoder 进行异动检测 + +使用方法: + from ml.inference import ConceptAnomalyDetector + + detector = ConceptAnomalyDetector('ml/checkpoints') + + # 检测异动 + features = {...} # 实时特征数据 + is_anomaly, score = detector.detect(features) +""" + +import os +import json +from pathlib import Path +from typing import Dict, List, Tuple, Optional +from collections import deque + +import numpy as np +import torch + +from model import TransformerAutoencoder + + +class ConceptAnomalyDetector: + """ + 概念异动检测器 + + 使用训练好的 Transformer Autoencoder 进行实时异动检测 + """ + + def __init__( + self, + checkpoint_dir: str = 'ml/checkpoints', + device: str = 'auto', + threshold_key: str = 'p95' + ): + """ + 初始化检测器 + + Args: + checkpoint_dir: 模型检查点目录 + device: 设备 (auto/cuda/cpu) + threshold_key: 使用的阈值键 (p90/p95/p99) + """ + self.checkpoint_dir = Path(checkpoint_dir) + self.threshold_key = threshold_key + + # 设备选择 + if device == 'auto': + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + else: + self.device = torch.device(device) + + # 加载配置 + self._load_config() + + # 加载模型 + self._load_model() + + # 加载阈值 + self._load_thresholds() + + # 加载标准化统计量 + self._load_normalization_stats() + + # 概念历史数据缓存 + # {concept_name: deque(maxlen=seq_len)} + self.history_cache: Dict[str, deque] = {} + + print(f"ConceptAnomalyDetector 初始化完成") + print(f" 设备: {self.device}") + print(f" 阈值: {self.threshold_key} = {self.threshold:.6f}") + print(f" 序列长度: {self.seq_len}") + + def _load_config(self): + """加载配置""" + config_path = self.checkpoint_dir / 'config.json' + if not config_path.exists(): + raise FileNotFoundError(f"配置文件不存在: {config_path}") + + with open(config_path, 'r') as f: + self.config = json.load(f) + + self.features = self.config['features'] + self.seq_len = self.config['seq_len'] + self.model_config = self.config['model'] + + def _load_model(self): + """加载模型""" + model_path = self.checkpoint_dir / 'best_model.pt' + if not model_path.exists(): + raise FileNotFoundError(f"模型文件不存在: {model_path}") + + # 创建模型 + self.model = TransformerAutoencoder(**self.model_config) + + # 加载权重 + checkpoint = torch.load(model_path, map_location=self.device) + self.model.load_state_dict(checkpoint['model_state_dict']) + self.model.to(self.device) + self.model.eval() + + print(f"模型已加载: {model_path}") + + def _load_thresholds(self): + """加载阈值""" + thresholds_path = self.checkpoint_dir / 'thresholds.json' + if not thresholds_path.exists(): + raise FileNotFoundError(f"阈值文件不存在: {thresholds_path}") + + with open(thresholds_path, 'r') as f: + self.thresholds = json.load(f) + + if self.threshold_key not in self.thresholds: + available_keys = list(self.thresholds.keys()) + raise KeyError(f"阈值键 '{self.threshold_key}' 不存在,可用: {available_keys}") + + self.threshold = self.thresholds[self.threshold_key] + + def _load_normalization_stats(self): + """加载标准化统计量""" + stats_path = self.checkpoint_dir / 'normalization_stats.json' + if not stats_path.exists(): + raise FileNotFoundError(f"标准化统计量文件不存在: {stats_path}") + + with open(stats_path, 'r') as f: + stats = json.load(f) + + self.norm_mean = np.array(stats['mean']) + self.norm_std = np.array(stats['std']) + + def normalize(self, features: np.ndarray) -> np.ndarray: + """标准化特征""" + return (features - self.norm_mean) / self.norm_std + + def update_history( + self, + concept_name: str, + features: Dict[str, float] + ): + """ + 更新概念历史数据 + + Args: + concept_name: 概念名称 + features: 当前时刻的特征字典 + """ + # 初始化历史缓存 + if concept_name not in self.history_cache: + self.history_cache[concept_name] = deque(maxlen=self.seq_len) + + # 提取特征向量 + feature_vector = np.array([ + features.get(f, 0.0) for f in self.features + ]) + + # 处理异常值 + feature_vector = np.nan_to_num(feature_vector, nan=0.0, posinf=0.0, neginf=0.0) + + # 添加到历史 + self.history_cache[concept_name].append(feature_vector) + + def get_history_length(self, concept_name: str) -> int: + """获取概念的历史数据长度""" + if concept_name not in self.history_cache: + return 0 + return len(self.history_cache[concept_name]) + + @torch.no_grad() + def detect( + self, + concept_name: str, + features: Dict[str, float] = None, + return_score: bool = True + ) -> Tuple[bool, Optional[float]]: + """ + 检测概念是否异动 + + Args: + concept_name: 概念名称 + features: 当前时刻的特征(如果提供,会先更新历史) + return_score: 是否返回异动分数 + + Returns: + is_anomaly: 是否异动 + score: 异动分数(如果 return_score=True) + """ + # 更新历史 + if features is not None: + self.update_history(concept_name, features) + + # 检查历史数据是否足够 + if concept_name not in self.history_cache: + return False, None + + history = self.history_cache[concept_name] + if len(history) < self.seq_len: + return False, None + + # 构建输入序列 + sequence = np.array(list(history)) # (seq_len, n_features) + + # 标准化 + sequence = self.normalize(sequence) + + # 转为 tensor + x = torch.FloatTensor(sequence).unsqueeze(0) # (1, seq_len, n_features) + x = x.to(self.device) + + # 计算重构误差 + error = self.model.compute_reconstruction_error(x, reduction='none') + + # 取最后一个时刻的误差作为当前分数 + score = error[0, -1].item() + + # 判断是否异动 + is_anomaly = score > self.threshold + + if return_score: + return is_anomaly, score + else: + return is_anomaly, None + + @torch.no_grad() + def batch_detect( + self, + concept_features: Dict[str, Dict[str, float]] + ) -> Dict[str, Tuple[bool, float]]: + """ + 批量检测多个概念 + + Args: + concept_features: {concept_name: {feature_name: value}} + + Returns: + results: {concept_name: (is_anomaly, score)} + """ + results = {} + + for concept_name, features in concept_features.items(): + is_anomaly, score = self.detect(concept_name, features) + results[concept_name] = (is_anomaly, score) + + return results + + def get_anomaly_type( + self, + concept_name: str, + features: Dict[str, float] + ) -> str: + """ + 判断异动类型 + + Args: + concept_name: 概念名称 + features: 当前特征 + + Returns: + anomaly_type: 'surge_up' / 'surge_down' / 'normal' + """ + is_anomaly, score = self.detect(concept_name, features) + + if not is_anomaly: + return 'normal' + + # 根据 alpha 判断涨跌 + alpha = features.get('alpha', 0.0) + + if alpha > 0: + return 'surge_up' + else: + return 'surge_down' + + def get_top_anomalies( + self, + concept_features: Dict[str, Dict[str, float]], + top_k: int = 10 + ) -> List[Tuple[str, float, str]]: + """ + 获取异动分数最高的 top_k 个概念 + + Args: + concept_features: {concept_name: {feature_name: value}} + top_k: 返回数量 + + Returns: + anomalies: [(concept_name, score, anomaly_type), ...] + """ + results = self.batch_detect(concept_features) + + # 按分数排序 + sorted_results = sorted( + [(name, is_anomaly, score) for name, (is_anomaly, score) in results.items() if score is not None], + key=lambda x: x[2], + reverse=True + ) + + # 取 top_k + top_anomalies = [] + for name, is_anomaly, score in sorted_results[:top_k]: + if is_anomaly: + alpha = concept_features[name].get('alpha', 0.0) + anomaly_type = 'surge_up' if alpha > 0 else 'surge_down' + top_anomalies.append((name, score, anomaly_type)) + + return top_anomalies + + def clear_history(self, concept_name: str = None): + """ + 清除历史缓存 + + Args: + concept_name: 概念名称(如果为 None,清除所有) + """ + if concept_name is None: + self.history_cache.clear() + elif concept_name in self.history_cache: + del self.history_cache[concept_name] + + +# ==================== 集成到现有系统 ==================== + +class MLAnomalyService: + """ + ML 异动检测服务 + + 用于替换或增强现有的 Alpha-based 检测 + """ + + def __init__( + self, + checkpoint_dir: str = 'ml/checkpoints', + fallback_to_alpha: bool = True + ): + """ + Args: + checkpoint_dir: 模型检查点目录 + fallback_to_alpha: 当 ML 模型不可用时是否回退到 Alpha 方法 + """ + self.fallback_to_alpha = fallback_to_alpha + self.ml_detector = None + + try: + self.ml_detector = ConceptAnomalyDetector(checkpoint_dir) + print("ML 异动检测服务初始化成功") + except Exception as e: + print(f"ML 模型加载失败: {e}") + if not fallback_to_alpha: + raise + print("将回退到 Alpha-based 检测") + + def is_ml_available(self) -> bool: + """检查 ML 模型是否可用""" + return self.ml_detector is not None + + def detect_anomaly( + self, + concept_name: str, + features: Dict[str, float], + alpha_threshold: float = 2.0 + ) -> Tuple[bool, float, str]: + """ + 检测异动 + + Args: + concept_name: 概念名称 + features: 特征字典(需包含 alpha, amt_ratio 等) + alpha_threshold: Alpha Z-Score 阈值(用于回退) + + Returns: + is_anomaly: 是否异动 + score: 异动分数 + method: 检测方法 ('ml' / 'alpha') + """ + # 优先使用 ML 检测 + if self.ml_detector is not None: + history_len = self.ml_detector.get_history_length(concept_name) + + # 历史数据足够时使用 ML + if history_len >= self.ml_detector.seq_len - 1: + is_anomaly, score = self.ml_detector.detect(concept_name, features) + if score is not None: + return is_anomaly, score, 'ml' + else: + # 更新历史但使用 Alpha 方法 + self.ml_detector.update_history(concept_name, features) + + # 回退到 Alpha 方法 + if self.fallback_to_alpha: + alpha = features.get('alpha', 0.0) + alpha_zscore = features.get('alpha_zscore', 0.0) + + is_anomaly = abs(alpha_zscore) > alpha_threshold + score = abs(alpha_zscore) + + return is_anomaly, score, 'alpha' + + return False, 0.0, 'none' + + +# ==================== 测试 ==================== + +if __name__ == "__main__": + import random + + print("测试 ConceptAnomalyDetector...") + + # 检查模型是否存在 + checkpoint_dir = Path('ml/checkpoints') + if not (checkpoint_dir / 'best_model.pt').exists(): + print("模型文件不存在,跳过测试") + print("请先运行 train.py 训练模型") + exit(0) + + # 初始化检测器 + detector = ConceptAnomalyDetector('ml/checkpoints') + + # 模拟数据 + print("\n模拟实时检测...") + concept_name = "人工智能" + + for i in range(40): + # 生成随机特征 + features = { + 'alpha': random.gauss(0, 1), + 'alpha_delta': random.gauss(0, 0.5), + 'amt_ratio': random.gauss(1, 0.3), + 'amt_delta': random.gauss(0, 0.2), + 'rank_pct': random.random(), + 'limit_up_ratio': random.random() * 0.1, + } + + # 在第 35 分钟模拟异动 + if i == 35: + features['alpha'] = 5.0 + features['alpha_delta'] = 2.0 + features['amt_ratio'] = 3.0 + + is_anomaly, score = detector.detect(concept_name, features) + + history_len = detector.get_history_length(concept_name) + + if score is not None: + status = "🔥 异动!" if is_anomaly else "正常" + print(f" t={i:02d} | 历史={history_len} | 分数={score:.4f} | {status}") + else: + print(f" t={i:02d} | 历史={history_len} | 数据不足") + + print("\n测试完成!") diff --git a/ml/model.py b/ml/model.py new file mode 100644 index 00000000..2eb63d87 --- /dev/null +++ b/ml/model.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +LSTM Autoencoder 模型定义 + +用于概念异动检测: +- 学习"正常"市场模式 +- 重构误差大的时刻 = 异动 + +模型结构(简洁有效): +┌─────────────────────────────────────┐ +│ 输入: (batch, seq_len, n_features) │ +│ 过去30分钟的特征序列 │ +├─────────────────────────────────────┤ +│ LSTM Encoder │ +│ - 双向 LSTM │ +│ - 输出最后隐藏状态 │ +├─────────────────────────────────────┤ +│ Bottleneck (压缩层) │ +│ 降维到 latent_dim(关键!) │ +├─────────────────────────────────────┤ +│ LSTM Decoder │ +│ - 单向 LSTM │ +│ - 重构序列 │ +├─────────────────────────────────────┤ +│ 输出: (batch, seq_len, n_features) │ +│ 重构的特征序列 │ +└─────────────────────────────────────┘ + +为什么用 LSTM 而不是 Transformer: +1. 参数更少,不容易过拟合 +2. 对于 6 维特征足够用 +3. 训练更稳定 +4. 瓶颈约束更容易控制 +""" + +import torch +import torch.nn as nn +import torch.nn.functional as F +from typing import Optional, Tuple + + +class LSTMAutoencoder(nn.Module): + """ + LSTM Autoencoder for Anomaly Detection + + 设计原则: + - 足够简单,避免过拟合 + - 瓶颈层严格限制,迫使模型只学习主要模式 + - 异常难以通过狭窄瓶颈,重构误差大 + """ + + def __init__( + self, + n_features: int = 6, + hidden_dim: int = 32, # LSTM 隐藏维度(小!) + latent_dim: int = 4, # 瓶颈维度(非常小!关键参数) + num_layers: int = 1, # LSTM 层数 + dropout: float = 0.2, + bidirectional: bool = True, # 双向编码器 + ): + super().__init__() + + self.n_features = n_features + self.hidden_dim = hidden_dim + self.latent_dim = latent_dim + self.num_layers = num_layers + self.bidirectional = bidirectional + self.num_directions = 2 if bidirectional else 1 + + # Encoder: 双向 LSTM + self.encoder = nn.LSTM( + input_size=n_features, + hidden_size=hidden_dim, + num_layers=num_layers, + batch_first=True, + dropout=dropout if num_layers > 1 else 0, + bidirectional=bidirectional + ) + + # Bottleneck: 压缩到极小的 latent space + encoder_output_dim = hidden_dim * self.num_directions + self.bottleneck_down = nn.Sequential( + nn.Linear(encoder_output_dim, latent_dim), + nn.Tanh(), # 限制范围,增加约束 + ) + + self.bottleneck_up = nn.Sequential( + nn.Linear(latent_dim, hidden_dim), + nn.ReLU(), + ) + + # Decoder: 单向 LSTM + self.decoder = nn.LSTM( + input_size=hidden_dim, + hidden_size=hidden_dim, + num_layers=num_layers, + batch_first=True, + dropout=dropout if num_layers > 1 else 0, + bidirectional=False # 解码器用单向 + ) + + # 输出层 + self.output_layer = nn.Linear(hidden_dim, n_features) + + # Dropout + self.dropout = nn.Dropout(dropout) + + # 初始化 + self._init_weights() + + def _init_weights(self): + """初始化权重""" + for name, param in self.named_parameters(): + if 'weight_ih' in name: + nn.init.xavier_uniform_(param) + elif 'weight_hh' in name: + nn.init.orthogonal_(param) + elif 'bias' in name: + nn.init.zeros_(param) + + def encode(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + """ + 编码器 + + Args: + x: (batch, seq_len, n_features) + Returns: + latent: (batch, seq_len, latent_dim) 每个时间步的压缩表示 + encoder_outputs: (batch, seq_len, hidden_dim * num_directions) + """ + # LSTM 编码 + encoder_outputs, (h_n, c_n) = self.encoder(x) + # encoder_outputs: (batch, seq_len, hidden_dim * num_directions) + + encoder_outputs = self.dropout(encoder_outputs) + + # 压缩到 latent space(对每个时间步) + latent = self.bottleneck_down(encoder_outputs) + # latent: (batch, seq_len, latent_dim) + + return latent, encoder_outputs + + def decode(self, latent: torch.Tensor, seq_len: int) -> torch.Tensor: + """ + 解码器 + + Args: + latent: (batch, seq_len, latent_dim) + seq_len: 序列长度 + Returns: + output: (batch, seq_len, n_features) + """ + # 从 latent space 恢复 + decoder_input = self.bottleneck_up(latent) + # decoder_input: (batch, seq_len, hidden_dim) + + # LSTM 解码 + decoder_outputs, _ = self.decoder(decoder_input) + # decoder_outputs: (batch, seq_len, hidden_dim) + + decoder_outputs = self.dropout(decoder_outputs) + + # 投影到原始特征空间 + output = self.output_layer(decoder_outputs) + # output: (batch, seq_len, n_features) + + return output + + def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + """ + 前向传播 + + Args: + x: (batch, seq_len, n_features) + Returns: + output: (batch, seq_len, n_features) 重构结果 + latent: (batch, seq_len, latent_dim) 隐向量 + """ + batch_size, seq_len, _ = x.shape + + # 编码 + latent, _ = self.encode(x) + + # 解码 + output = self.decode(latent, seq_len) + + return output, latent + + def compute_reconstruction_error( + self, + x: torch.Tensor, + reduction: str = 'none' + ) -> torch.Tensor: + """ + 计算重构误差 + + Args: + x: (batch, seq_len, n_features) + reduction: 'none' | 'mean' | 'sum' + Returns: + error: 重构误差 + """ + output, _ = self.forward(x) + + # MSE per feature per timestep + error = F.mse_loss(output, x, reduction='none') + + if reduction == 'none': + # (batch, seq_len, n_features) -> (batch, seq_len) + return error.mean(dim=-1) + elif reduction == 'mean': + return error.mean() + elif reduction == 'sum': + return error.sum() + else: + raise ValueError(f"Unknown reduction: {reduction}") + + def detect_anomaly( + self, + x: torch.Tensor, + threshold: float = None, + return_scores: bool = True + ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]: + """ + 检测异动 + + Args: + x: (batch, seq_len, n_features) + threshold: 异动阈值(如果为 None,只返回分数) + return_scores: 是否返回异动分数 + Returns: + is_anomaly: (batch, seq_len) bool tensor (if threshold is not None) + scores: (batch, seq_len) 异动分数 (if return_scores) + """ + scores = self.compute_reconstruction_error(x, reduction='none') + + is_anomaly = None + if threshold is not None: + is_anomaly = scores > threshold + + if return_scores: + return is_anomaly, scores + else: + return is_anomaly, None + + +# 为了兼容性,创建别名 +TransformerAutoencoder = LSTMAutoencoder + + +# ==================== 损失函数 ==================== + +class AnomalyDetectionLoss(nn.Module): + """ + 异动检测损失函数 + + 简单的 MSE 重构损失 + """ + + def __init__( + self, + feature_weights: torch.Tensor = None, + ): + super().__init__() + self.feature_weights = feature_weights + + def forward( + self, + output: torch.Tensor, + target: torch.Tensor, + latent: torch.Tensor = None + ) -> Tuple[torch.Tensor, dict]: + """ + Args: + output: (batch, seq_len, n_features) 重构结果 + target: (batch, seq_len, n_features) 原始输入 + latent: (batch, seq_len, latent_dim) 隐向量(未使用) + Returns: + loss: 总损失 + loss_dict: 各项损失详情 + """ + # 重构损失 (MSE) + mse = F.mse_loss(output, target, reduction='none') + + # 特征加权(可选) + if self.feature_weights is not None: + weights = self.feature_weights.to(mse.device) + mse = mse * weights + + reconstruction_loss = mse.mean() + + loss_dict = { + 'total': reconstruction_loss.item(), + 'reconstruction': reconstruction_loss.item(), + } + + return reconstruction_loss, loss_dict + + +# ==================== 工具函数 ==================== + +def count_parameters(model: nn.Module) -> int: + """统计模型参数量""" + return sum(p.numel() for p in model.parameters() if p.requires_grad) + + +def create_model(config: dict = None) -> LSTMAutoencoder: + """ + 创建模型 + + 默认使用小型 LSTM 配置,适合异动检测 + """ + default_config = { + 'n_features': 6, + 'hidden_dim': 32, # 小! + 'latent_dim': 4, # 非常小!关键 + 'num_layers': 1, + 'dropout': 0.2, + 'bidirectional': True, + } + + if config: + # 兼容旧的 Transformer 配置键名 + if 'd_model' in config: + config['hidden_dim'] = config.pop('d_model') // 2 + if 'num_encoder_layers' in config: + config['num_layers'] = config.pop('num_encoder_layers') + if 'num_decoder_layers' in config: + config.pop('num_decoder_layers') + if 'nhead' in config: + config.pop('nhead') + if 'dim_feedforward' in config: + config.pop('dim_feedforward') + if 'max_seq_len' in config: + config.pop('max_seq_len') + if 'use_instance_norm' in config: + config.pop('use_instance_norm') + + default_config.update(config) + + model = LSTMAutoencoder(**default_config) + param_count = count_parameters(model) + print(f"模型参数量: {param_count:,}") + + if param_count > 100000: + print(f"⚠️ 警告: 参数量较大({param_count:,}),可能过拟合") + else: + print(f"✓ 参数量适中(LSTM Autoencoder)") + + return model + + +if __name__ == "__main__": + # 测试模型 + print("测试 LSTM Autoencoder...") + + # 创建模型 + model = create_model() + + # 测试输入 + batch_size = 32 + seq_len = 30 + n_features = 6 + + x = torch.randn(batch_size, seq_len, n_features) + + # 前向传播 + output, latent = model(x) + + print(f"输入形状: {x.shape}") + print(f"输出形状: {output.shape}") + print(f"隐向量形状: {latent.shape}") + + # 计算重构误差 + error = model.compute_reconstruction_error(x) + print(f"重构误差形状: {error.shape}") + print(f"平均重构误差: {error.mean().item():.4f}") + + # 测试异动检测 + is_anomaly, scores = model.detect_anomaly(x, threshold=0.5) + print(f"异动检测结果形状: {is_anomaly.shape if is_anomaly is not None else 'None'}") + print(f"异动分数形状: {scores.shape}") + + # 测试损失函数 + criterion = AnomalyDetectionLoss() + loss, loss_dict = criterion(output, x, latent) + print(f"损失: {loss.item():.4f}") + + print("\n测试通过!") diff --git a/ml/prepare_data.py b/ml/prepare_data.py new file mode 100644 index 00000000..4734f582 --- /dev/null +++ b/ml/prepare_data.py @@ -0,0 +1,501 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +数据准备脚本 - 为 Transformer Autoencoder 准备训练数据 + +从 ClickHouse 提取历史分钟数据,计算以下特征: +1. alpha - 超额收益(概念涨幅 - 大盘涨幅) +2. alpha_delta - Alpha 变化率(5分钟) +3. amt_ratio - 成交额相对均值(当前/过去20分钟均值) +4. amt_delta - 成交额变化率 +5. rank_pct - Alpha 排名百分位 +6. limit_up_ratio - 涨停股占比 + +输出:按交易日存储的特征文件(parquet格式) +""" + +import os +import sys +import numpy as np +import pandas as pd +from datetime import datetime, timedelta, date +from sqlalchemy import create_engine, text +from elasticsearch import Elasticsearch +from clickhouse_driver import Client +import hashlib +import json +import logging +from typing import Dict, List, Set, Tuple +from concurrent.futures import ThreadPoolExecutor, as_completed +import warnings +warnings.filterwarnings('ignore') + +# ==================== 配置 ==================== + +MYSQL_ENGINE = create_engine( + "mysql+pymysql://root:Zzl5588161!@192.168.1.5:3306/stock", + echo=False +) + +ES_CLIENT = Elasticsearch(['http://127.0.0.1:9200']) +ES_INDEX = 'concept_library_v3' + +CLICKHOUSE_CONFIG = { + 'host': '127.0.0.1', + 'port': 9000, + 'user': 'default', + 'password': 'Zzl33818!', + 'database': 'stock' +} + +# 输出目录 +OUTPUT_DIR = os.path.join(os.path.dirname(__file__), 'data') +os.makedirs(OUTPUT_DIR, exist_ok=True) + +# 特征计算参数 +FEATURE_CONFIG = { + 'alpha_delta_window': 5, # Alpha变化窗口(分钟) + 'amt_ma_window': 20, # 成交额均值窗口(分钟) + 'limit_up_threshold': 9.8, # 涨停阈值(%) + 'limit_down_threshold': -9.8, # 跌停阈值(%) +} + +REFERENCE_INDEX = '000001.SH' + +# ==================== 日志 ==================== + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# ==================== 工具函数 ==================== + +def get_ch_client(): + return Client(**CLICKHOUSE_CONFIG) + + +def generate_id(name: str) -> str: + return hashlib.md5(name.encode('utf-8')).hexdigest()[:16] + + +def code_to_ch_format(code: str) -> str: + if not code or len(code) != 6 or not code.isdigit(): + return None + if code.startswith('6'): + return f"{code}.SH" + elif code.startswith('0') or code.startswith('3'): + return f"{code}.SZ" + else: + return f"{code}.BJ" + + +# ==================== 获取概念列表 ==================== + +def get_all_concepts() -> List[dict]: + """从ES获取所有叶子概念""" + concepts = [] + + query = { + "query": {"match_all": {}}, + "size": 100, + "_source": ["concept_id", "concept", "stocks"] + } + + resp = ES_CLIENT.search(index=ES_INDEX, body=query, scroll='2m') + scroll_id = resp['_scroll_id'] + hits = resp['hits']['hits'] + + while len(hits) > 0: + for hit in hits: + source = hit['_source'] + stocks = [] + if 'stocks' in source and isinstance(source['stocks'], list): + for stock in source['stocks']: + if isinstance(stock, dict) and 'code' in stock and stock['code']: + stocks.append(stock['code']) + + if stocks: + concepts.append({ + 'concept_id': source.get('concept_id'), + 'concept_name': source.get('concept'), + 'stocks': stocks + }) + + resp = ES_CLIENT.scroll(scroll_id=scroll_id, scroll='2m') + scroll_id = resp['_scroll_id'] + hits = resp['hits']['hits'] + + ES_CLIENT.clear_scroll(scroll_id=scroll_id) + logger.info(f"获取到 {len(concepts)} 个概念") + return concepts + + +# ==================== 获取交易日列表 ==================== + +def get_trading_days(start_date: str, end_date: str) -> List[str]: + """获取交易日列表""" + client = get_ch_client() + + query = f""" + SELECT DISTINCT toDate(timestamp) as trade_date + FROM stock_minute + WHERE toDate(timestamp) >= '{start_date}' + AND toDate(timestamp) <= '{end_date}' + ORDER BY trade_date + """ + + result = client.execute(query) + days = [row[0].strftime('%Y-%m-%d') for row in result] + logger.info(f"找到 {len(days)} 个交易日: {days[0]} ~ {days[-1]}") + return days + + +# ==================== 获取单日数据 ==================== + +def get_daily_stock_data(trade_date: str, stock_codes: List[str]) -> pd.DataFrame: + """获取单日所有股票的分钟数据""" + client = get_ch_client() + + # 转换代码格式 + ch_codes = [] + code_map = {} + for code in stock_codes: + ch_code = code_to_ch_format(code) + if ch_code: + ch_codes.append(ch_code) + code_map[ch_code] = code + + if not ch_codes: + return pd.DataFrame() + + ch_codes_str = "','".join(ch_codes) + + query = f""" + SELECT + code, + timestamp, + close, + volume, + amt + FROM stock_minute + WHERE toDate(timestamp) = '{trade_date}' + AND code IN ('{ch_codes_str}') + ORDER BY code, timestamp + """ + + result = client.execute(query) + + if not result: + return pd.DataFrame() + + df = pd.DataFrame(result, columns=['ch_code', 'timestamp', 'close', 'volume', 'amt']) + df['code'] = df['ch_code'].map(code_map) + df = df.dropna(subset=['code']) + + return df[['code', 'timestamp', 'close', 'volume', 'amt']] + + +def get_daily_index_data(trade_date: str, index_code: str = REFERENCE_INDEX) -> pd.DataFrame: + """获取单日指数分钟数据""" + client = get_ch_client() + + query = f""" + SELECT + timestamp, + close, + volume, + amt + FROM index_minute + WHERE toDate(timestamp) = '{trade_date}' + AND code = '{index_code}' + ORDER BY timestamp + """ + + result = client.execute(query) + + if not result: + return pd.DataFrame() + + df = pd.DataFrame(result, columns=['timestamp', 'close', 'volume', 'amt']) + return df + + +def get_prev_close(stock_codes: List[str], trade_date: str) -> Dict[str, float]: + """获取昨收价""" + valid_codes = [c for c in stock_codes if c and len(c) == 6 and c.isdigit()] + if not valid_codes: + return {} + + codes_str = "','".join(valid_codes) + + query = f""" + SELECT SECCODE, F002N + FROM ea_trade + WHERE SECCODE IN ('{codes_str}') + AND TRADEDATE = ( + SELECT MAX(TRADEDATE) FROM ea_trade WHERE TRADEDATE < '{trade_date}' + ) + AND F002N IS NOT NULL AND F002N > 0 + """ + + try: + with MYSQL_ENGINE.connect() as conn: + result = conn.execute(text(query)) + return {row[0]: float(row[1]) for row in result if row[1]} + except Exception as e: + logger.error(f"获取昨收价失败: {e}") + return {} + + +def get_index_prev_close(trade_date: str, index_code: str = REFERENCE_INDEX) -> float: + """获取指数昨收价""" + code_no_suffix = index_code.split('.')[0] + + try: + with MYSQL_ENGINE.connect() as conn: + result = conn.execute(text(""" + SELECT F006N FROM ea_exchangetrade + WHERE INDEXCODE = :code AND TRADEDATE < :today + ORDER BY TRADEDATE DESC LIMIT 1 + """), {'code': code_no_suffix, 'today': trade_date}).fetchone() + + if result and result[0]: + return float(result[0]) + except Exception as e: + logger.error(f"获取指数昨收失败: {e}") + + return None + + +# ==================== 计算特征 ==================== + +def compute_daily_features( + trade_date: str, + concepts: List[dict], + all_stocks: List[str] +) -> pd.DataFrame: + """ + 计算单日所有概念的特征 + + 返回 DataFrame: + - index: (timestamp, concept_id) + - columns: alpha, alpha_delta, amt_ratio, amt_delta, rank_pct, limit_up_ratio + """ + + # 1. 获取数据 + logger.info(f" 获取股票数据...") + stock_df = get_daily_stock_data(trade_date, all_stocks) + if stock_df.empty: + logger.warning(f" 无股票数据") + return pd.DataFrame() + + logger.info(f" 获取指数数据...") + index_df = get_daily_index_data(trade_date) + if index_df.empty: + logger.warning(f" 无指数数据") + return pd.DataFrame() + + # 2. 获取昨收价 + logger.info(f" 获取昨收价...") + prev_close = get_prev_close(all_stocks, trade_date) + index_prev_close = get_index_prev_close(trade_date) + + if not prev_close or not index_prev_close: + logger.warning(f" 无昨收价数据") + return pd.DataFrame() + + # 3. 计算股票涨跌幅和成交额 + stock_df['prev_close'] = stock_df['code'].map(prev_close) + stock_df = stock_df.dropna(subset=['prev_close']) + stock_df['change_pct'] = (stock_df['close'] - stock_df['prev_close']) / stock_df['prev_close'] * 100 + + # 4. 计算指数涨跌幅 + index_df['change_pct'] = (index_df['close'] - index_prev_close) / index_prev_close * 100 + index_change_map = dict(zip(index_df['timestamp'], index_df['change_pct'])) + + # 5. 获取所有时间点 + timestamps = sorted(stock_df['timestamp'].unique()) + logger.info(f" 时间点数: {len(timestamps)}") + + # 6. 按时间点计算概念特征 + results = [] + + # 概念到股票的映射 + concept_stocks = {c['concept_id']: set(c['stocks']) for c in concepts} + concept_names = {c['concept_id']: c['concept_name'] for c in concepts} + + # 历史数据缓存(用于计算变化率) + concept_history = {cid: {'alpha': [], 'amt': []} for cid in concept_stocks} + + for ts in timestamps: + ts_stock_data = stock_df[stock_df['timestamp'] == ts] + index_change = index_change_map.get(ts, 0) + + # 股票涨跌幅和成交额字典 + stock_change = dict(zip(ts_stock_data['code'], ts_stock_data['change_pct'])) + stock_amt = dict(zip(ts_stock_data['code'], ts_stock_data['amt'])) + + concept_features = [] + + for concept_id, stocks in concept_stocks.items(): + # 该概念的股票数据 + concept_changes = [stock_change[s] for s in stocks if s in stock_change] + concept_amts = [stock_amt.get(s, 0) for s in stocks if s in stock_change] + + if not concept_changes: + continue + + # 基础统计 + avg_change = np.mean(concept_changes) + total_amt = sum(concept_amts) + + # Alpha = 概念涨幅 - 指数涨幅 + alpha = avg_change - index_change + + # 涨停/跌停股占比 + limit_up_count = sum(1 for c in concept_changes if c >= FEATURE_CONFIG['limit_up_threshold']) + limit_down_count = sum(1 for c in concept_changes if c <= FEATURE_CONFIG['limit_down_threshold']) + limit_up_ratio = limit_up_count / len(concept_changes) + limit_down_ratio = limit_down_count / len(concept_changes) + + # 更新历史 + history = concept_history[concept_id] + history['alpha'].append(alpha) + history['amt'].append(total_amt) + + # 计算变化率 + alpha_delta = 0 + if len(history['alpha']) > FEATURE_CONFIG['alpha_delta_window']: + alpha_delta = alpha - history['alpha'][-FEATURE_CONFIG['alpha_delta_window']-1] + + # 成交额相对均值 + amt_ratio = 1.0 + amt_delta = 0 + if len(history['amt']) > FEATURE_CONFIG['amt_ma_window']: + amt_ma = np.mean(history['amt'][-FEATURE_CONFIG['amt_ma_window']-1:-1]) + if amt_ma > 0: + amt_ratio = total_amt / amt_ma + amt_delta = total_amt - history['amt'][-2] if len(history['amt']) > 1 else 0 + + concept_features.append({ + 'concept_id': concept_id, + 'alpha': alpha, + 'alpha_delta': alpha_delta, + 'amt_ratio': amt_ratio, + 'amt_delta': amt_delta, + 'limit_up_ratio': limit_up_ratio, + 'limit_down_ratio': limit_down_ratio, + 'total_amt': total_amt, + 'stock_count': len(concept_changes), + }) + + if not concept_features: + continue + + # 计算排名百分位 + concept_df = pd.DataFrame(concept_features) + concept_df['rank_pct'] = concept_df['alpha'].rank(pct=True) + + # 添加时间戳 + concept_df['timestamp'] = ts + results.append(concept_df) + + if not results: + return pd.DataFrame() + + # 合并所有时间点 + final_df = pd.concat(results, ignore_index=True) + + # 标准化成交额变化率 + if 'amt_delta' in final_df.columns: + amt_delta_std = final_df['amt_delta'].std() + if amt_delta_std > 0: + final_df['amt_delta'] = final_df['amt_delta'] / amt_delta_std + + logger.info(f" 计算完成: {len(final_df)} 条记录") + return final_df + + +# ==================== 主流程 ==================== + +def process_single_day(trade_date: str, concepts: List[dict], all_stocks: List[str]) -> str: + """处理单个交易日""" + output_file = os.path.join(OUTPUT_DIR, f'features_{trade_date}.parquet') + + # 检查是否已处理 + if os.path.exists(output_file): + logger.info(f"[{trade_date}] 已存在,跳过") + return output_file + + logger.info(f"[{trade_date}] 开始处理...") + + try: + df = compute_daily_features(trade_date, concepts, all_stocks) + + if df.empty: + logger.warning(f"[{trade_date}] 无数据") + return None + + # 保存 + df.to_parquet(output_file, index=False) + logger.info(f"[{trade_date}] 保存完成: {output_file}") + return output_file + + except Exception as e: + logger.error(f"[{trade_date}] 处理失败: {e}") + import traceback + traceback.print_exc() + return None + + +def main(): + import argparse + + parser = argparse.ArgumentParser(description='准备训练数据') + parser.add_argument('--start', type=str, default='2022-01-01', help='开始日期') + parser.add_argument('--end', type=str, default=None, help='结束日期(默认今天)') + parser.add_argument('--workers', type=int, default=1, help='并行数(建议1,避免数据库压力)') + + args = parser.parse_args() + + end_date = args.end or datetime.now().strftime('%Y-%m-%d') + + logger.info("=" * 60) + logger.info("数据准备 - Transformer Autoencoder 训练数据") + logger.info("=" * 60) + logger.info(f"日期范围: {args.start} ~ {end_date}") + + # 1. 获取概念列表 + concepts = get_all_concepts() + + # 收集所有股票 + all_stocks = list(set(s for c in concepts for s in c['stocks'])) + logger.info(f"股票总数: {len(all_stocks)}") + + # 2. 获取交易日列表 + trading_days = get_trading_days(args.start, end_date) + + if not trading_days: + logger.error("无交易日数据") + return + + # 3. 处理每个交易日 + logger.info(f"\n开始处理 {len(trading_days)} 个交易日...") + + success_count = 0 + for i, trade_date in enumerate(trading_days): + logger.info(f"\n[{i+1}/{len(trading_days)}] {trade_date}") + result = process_single_day(trade_date, concepts, all_stocks) + if result: + success_count += 1 + + logger.info("\n" + "=" * 60) + logger.info(f"处理完成: {success_count}/{len(trading_days)} 个交易日") + logger.info(f"数据保存在: {OUTPUT_DIR}") + logger.info("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/ml/requirements.txt b/ml/requirements.txt new file mode 100644 index 00000000..7b052bb8 --- /dev/null +++ b/ml/requirements.txt @@ -0,0 +1,25 @@ +# 概念异动检测 ML 模块依赖 +# 安装: pip install -r ml/requirements.txt + +# PyTorch (根据 CUDA 版本选择) +# 5090 显卡需要 CUDA 12.x +# pip install torch --index-url https://download.pytorch.org/whl/cu124 +torch>=2.0.0 + +# 数据处理 +numpy>=1.24.0 +pandas>=2.0.0 +pyarrow>=14.0.0 + +# 数据库 +clickhouse-driver>=0.2.6 +elasticsearch>=7.0.0,<8.0.0 +sqlalchemy>=2.0.0 +pymysql>=1.1.0 + +# 训练工具 +tqdm>=4.65.0 + +# 可选: 可视化 +# matplotlib>=3.7.0 +# tensorboard>=2.14.0 diff --git a/ml/run_training.sh b/ml/run_training.sh new file mode 100644 index 00000000..7f7f2dcd --- /dev/null +++ b/ml/run_training.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# 概念异动检测模型训练脚本 (Linux) +# +# 使用方法: +# chmod +x run_training.sh +# ./run_training.sh +# +# 或指定参数: +# ./run_training.sh --start 2022-01-01 --epochs 100 + +set -e + +echo "============================================================" +echo "概念异动检测模型训练流程" +echo "============================================================" +echo "" + +# 获取脚本所在目录 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR/.." + +echo "[1/4] 检查环境..." +python3 --version || { echo "Python3 未找到!"; exit 1; } + +# 检查 GPU +if python3 -c "import torch; print(f'CUDA: {torch.cuda.is_available()}')" 2>/dev/null; then + echo "PyTorch GPU 检测完成" +else + echo "警告: PyTorch 未安装或无法检测 GPU" +fi + +echo "" +echo "[2/4] 检查依赖..." +pip3 install -q torch pandas numpy pyarrow tqdm clickhouse-driver elasticsearch sqlalchemy pymysql + +echo "" +echo "[3/4] 准备训练数据..." +echo "从 ClickHouse 提取历史数据,这可能需要较长时间..." +echo "" + +# 解析参数 +START_DATE="2022-01-01" +END_DATE="" +EPOCHS=100 +BATCH_SIZE=256 +TRAIN_END="2025-06-30" +VAL_END="2025-09-30" + +while [[ $# -gt 0 ]]; do + case $1 in + --start) + START_DATE="$2" + shift 2 + ;; + --end) + END_DATE="$2" + shift 2 + ;; + --epochs) + EPOCHS="$2" + shift 2 + ;; + --batch_size) + BATCH_SIZE="$2" + shift 2 + ;; + --train_end) + TRAIN_END="$2" + shift 2 + ;; + --val_end) + VAL_END="$2" + shift 2 + ;; + *) + shift + ;; + esac +done + +# 数据准备 +if [ -n "$END_DATE" ]; then + python3 ml/prepare_data.py --start "$START_DATE" --end "$END_DATE" +else + python3 ml/prepare_data.py --start "$START_DATE" +fi + +echo "" +echo "[4/4] 训练模型..." +echo "使用 GPU 加速训练..." +echo "" + +python3 ml/train.py --epochs "$EPOCHS" --batch_size "$BATCH_SIZE" --train_end "$TRAIN_END" --val_end "$VAL_END" + +echo "" +echo "============================================================" +echo "训练完成!" +echo "模型保存在: ml/checkpoints/" +echo "============================================================" diff --git a/ml/train.py b/ml/train.py new file mode 100644 index 00000000..b93120ac --- /dev/null +++ b/ml/train.py @@ -0,0 +1,808 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Transformer Autoencoder 训练脚本 (修复版) + +修复问题: +1. 按概念分组构建序列,避免跨概念切片 +2. 按时间(日期)切分数据集,避免数据泄露 +3. 使用 RobustScaler + Clipping 处理非平稳性 +4. 使用验证集计算阈值 + +训练流程: +1. 加载预处理好的特征数据(parquet 文件) +2. 按概念分组,在每个概念内部构建序列 +3. 按日期划分训练/验证/测试集 +4. 训练 Autoencoder(最小化重构误差) +5. 保存模型和阈值 + +使用方法: + python train.py --data_dir ml/data --epochs 100 --batch_size 256 +""" + +import os +import sys +import argparse +import json +from datetime import datetime +from pathlib import Path +from typing import List, Tuple, Dict + +import numpy as np +import pandas as pd +import torch +import torch.nn as nn +from torch.utils.data import Dataset, DataLoader +from torch.optim import AdamW +from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts +from tqdm import tqdm + +from model import TransformerAutoencoder, AnomalyDetectionLoss, count_parameters + +# 性能优化:启用 cuDNN benchmark(对固定输入尺寸自动选择最快算法) +torch.backends.cudnn.benchmark = True +# 启用 TF32(RTX 30/40 系列特有,提速约 3 倍) +torch.backends.cuda.matmul.allow_tf32 = True +torch.backends.cudnn.allow_tf32 = True + +# 可视化(可选) +try: + import matplotlib + matplotlib.use('Agg') # 无头模式,不需要显示器 + import matplotlib.pyplot as plt + HAS_MATPLOTLIB = True +except ImportError: + HAS_MATPLOTLIB = False + + +# ==================== 配置 ==================== + +TRAIN_CONFIG = { + # 数据配置 + 'seq_len': 30, # 输入序列长度(30分钟) + 'stride': 5, # 滑动窗口步长 + + # 时间切分(按日期) + 'train_end_date': '2024-06-30', # 训练集截止日期 + 'val_end_date': '2024-09-30', # 验证集截止日期(之后为测试集) + + # 特征配置 + 'features': [ + 'alpha', # 超额收益 + 'alpha_delta', # Alpha 变化率 + 'amt_ratio', # 成交额比率 + 'amt_delta', # 成交额变化率 + 'rank_pct', # Alpha 排名百分位 + 'limit_up_ratio', # 涨停比例 + ], + + # 训练配置(针对 4x RTX 4090 优化) + 'batch_size': 4096, # 256 -> 4096(大幅增加,充分利用显存) + 'epochs': 100, + 'learning_rate': 3e-4, # 1e-4 -> 3e-4(大 batch 需要更大学习率) + 'weight_decay': 1e-5, + 'gradient_clip': 1.0, + + # 早停配置 + 'patience': 10, + 'min_delta': 1e-6, + + # 模型配置(LSTM Autoencoder,简洁有效) + 'model': { + 'n_features': 6, + 'hidden_dim': 32, # LSTM 隐藏维度(小) + 'latent_dim': 4, # 瓶颈维度(非常小!关键) + 'num_layers': 1, # LSTM 层数 + 'dropout': 0.2, + 'bidirectional': True, # 双向编码器 + }, + + # 标准化配置 + 'use_instance_norm': True, # 模型内部使用 Instance Norm(推荐) + 'clip_value': 10.0, # 简单截断极端值 + + # 阈值配置 + 'threshold_percentiles': [90, 95, 99], +} + + +# ==================== 数据加载(修复版)==================== + +def load_data_by_date(data_dir: str, features: List[str]) -> Dict[str, pd.DataFrame]: + """ + 按日期加载数据,返回 {date: DataFrame} 字典 + + 每个 DataFrame 包含该日所有概念的所有时间点数据 + """ + data_path = Path(data_dir) + parquet_files = sorted(data_path.glob("features_*.parquet")) + + if not parquet_files: + raise FileNotFoundError(f"未找到 parquet 文件: {data_dir}") + + print(f"找到 {len(parquet_files)} 个数据文件") + + date_data = {} + + for pf in tqdm(parquet_files, desc="加载数据"): + # 提取日期 + date = pf.stem.replace('features_', '') + + df = pd.read_parquet(pf) + + # 检查必要列 + required_cols = features + ['concept_id', 'timestamp'] + missing_cols = [c for c in required_cols if c not in df.columns] + if missing_cols: + print(f"警告: {date} 缺少列: {missing_cols}, 跳过") + continue + + date_data[date] = df + + print(f"成功加载 {len(date_data)} 天的数据") + return date_data + + +def split_data_by_date( + date_data: Dict[str, pd.DataFrame], + train_end: str, + val_end: str +) -> Tuple[Dict[str, pd.DataFrame], Dict[str, pd.DataFrame], Dict[str, pd.DataFrame]]: + """ + 按日期严格划分数据集 + + - 训练集: <= train_end + - 验证集: train_end < date <= val_end + - 测试集: > val_end + """ + train_data = {} + val_data = {} + test_data = {} + + for date, df in date_data.items(): + if date <= train_end: + train_data[date] = df + elif date <= val_end: + val_data[date] = df + else: + test_data[date] = df + + print(f"数据集划分(按日期):") + print(f" 训练集: {len(train_data)} 天 (<= {train_end})") + print(f" 验证集: {len(val_data)} 天 ({train_end} ~ {val_end})") + print(f" 测试集: {len(test_data)} 天 (> {val_end})") + + return train_data, val_data, test_data + + +def build_sequences_by_concept( + date_data: Dict[str, pd.DataFrame], + features: List[str], + seq_len: int, + stride: int +) -> np.ndarray: + """ + 按概念分组构建序列(性能优化版) + + 使用 groupby 一次性分组,避免重复扫描大数组 + + 1. 将所有日期的数据合并 + 2. 使用 groupby 按 concept_id 分组 + 3. 在每个概念内部,按时间排序并滑动窗口 + 4. 合并所有序列 + """ + # 合并所有日期的数据 + all_dfs = [] + for date, df in sorted(date_data.items()): + df = df.copy() + df['date'] = date + all_dfs.append(df) + + if not all_dfs: + return np.array([]) + + combined = pd.concat(all_dfs, ignore_index=True) + + # 预先排序(按概念、日期、时间),这样 groupby 会更快 + combined = combined.sort_values(['concept_id', 'date', 'timestamp']) + + # 使用 groupby 一次性分组(性能关键!) + all_sequences = [] + grouped = combined.groupby('concept_id', sort=False) + n_concepts = len(grouped) + + for concept_id, concept_df in tqdm(grouped, desc="构建序列", total=n_concepts, leave=False): + # 已经排序过了,直接提取特征 + feature_data = concept_df[features].values + + # 处理缺失值 + feature_data = np.nan_to_num(feature_data, nan=0.0, posinf=0.0, neginf=0.0) + + # 在该概念内部滑动窗口 + n_points = len(feature_data) + for start in range(0, n_points - seq_len + 1, stride): + seq = feature_data[start:start + seq_len] + all_sequences.append(seq) + + if not all_sequences: + return np.array([]) + + sequences = np.array(all_sequences) + print(f" 构建序列: {len(sequences):,} 条 (来自 {n_concepts} 个概念)") + + return sequences + + +# ==================== 数据集 ==================== + +class SequenceDataset(Dataset): + """序列数据集(已经构建好的序列)""" + + def __init__(self, sequences: np.ndarray): + self.sequences = torch.FloatTensor(sequences) + + def __len__(self) -> int: + return len(self.sequences) + + def __getitem__(self, idx: int) -> torch.Tensor: + return self.sequences[idx] + + +# ==================== 训练器 ==================== + +class EarlyStopping: + """早停机制""" + + def __init__(self, patience: int = 10, min_delta: float = 1e-6): + self.patience = patience + self.min_delta = min_delta + self.counter = 0 + self.best_loss = float('inf') + self.early_stop = False + + def __call__(self, val_loss: float) -> bool: + if val_loss < self.best_loss - self.min_delta: + self.best_loss = val_loss + self.counter = 0 + else: + self.counter += 1 + if self.counter >= self.patience: + self.early_stop = True + + return self.early_stop + + +class Trainer: + """模型训练器(支持 AMP 混合精度加速)""" + + def __init__( + self, + model: nn.Module, + train_loader: DataLoader, + val_loader: DataLoader, + config: Dict, + device: torch.device, + save_dir: str = 'ml/checkpoints' + ): + self.model = model.to(device) + self.train_loader = train_loader + self.val_loader = val_loader + self.config = config + self.device = device + self.save_dir = Path(save_dir) + self.save_dir.mkdir(parents=True, exist_ok=True) + + # 优化器 + self.optimizer = AdamW( + model.parameters(), + lr=config['learning_rate'], + weight_decay=config['weight_decay'] + ) + + # 学习率调度器 + self.scheduler = CosineAnnealingWarmRestarts( + self.optimizer, + T_0=10, + T_mult=2, + eta_min=1e-6 + ) + + # 损失函数(简化版,只用 MSE) + self.criterion = AnomalyDetectionLoss() + + # 早停 + self.early_stopping = EarlyStopping( + patience=config['patience'], + min_delta=config['min_delta'] + ) + + # AMP 混合精度训练(大幅提速 + 省显存) + self.use_amp = torch.cuda.is_available() + self.scaler = torch.cuda.amp.GradScaler() if self.use_amp else None + if self.use_amp: + print(" ✓ 启用 AMP 混合精度训练") + + # 训练历史 + self.history = { + 'train_loss': [], + 'val_loss': [], + 'learning_rate': [], + } + + self.best_val_loss = float('inf') + + def train_epoch(self) -> float: + """训练一个 epoch(使用 AMP 混合精度)""" + self.model.train() + total_loss = 0.0 + n_batches = 0 + + pbar = tqdm(self.train_loader, desc="Training", leave=False) + for batch in pbar: + batch = batch.to(self.device, non_blocking=True) # 异步传输 + + self.optimizer.zero_grad(set_to_none=True) # 更快的梯度清零 + + # AMP 混合精度前向传播 + if self.use_amp: + with torch.cuda.amp.autocast(): + output, latent = self.model(batch) + loss, loss_dict = self.criterion(output, batch, latent) + + # AMP 反向传播 + self.scaler.scale(loss).backward() + + # 梯度裁剪(需要 unscale) + self.scaler.unscale_(self.optimizer) + torch.nn.utils.clip_grad_norm_( + self.model.parameters(), + self.config['gradient_clip'] + ) + + self.scaler.step(self.optimizer) + self.scaler.update() + else: + # 非 AMP 模式 + output, latent = self.model(batch) + loss, loss_dict = self.criterion(output, batch, latent) + + loss.backward() + torch.nn.utils.clip_grad_norm_( + self.model.parameters(), + self.config['gradient_clip'] + ) + self.optimizer.step() + + total_loss += loss.item() + n_batches += 1 + + pbar.set_postfix({'loss': f"{loss.item():.4f}"}) + + return total_loss / n_batches + + @torch.no_grad() + def validate(self) -> float: + """验证(使用 AMP)""" + self.model.eval() + total_loss = 0.0 + n_batches = 0 + + for batch in self.val_loader: + batch = batch.to(self.device, non_blocking=True) + + if self.use_amp: + with torch.cuda.amp.autocast(): + output, latent = self.model(batch) + loss, _ = self.criterion(output, batch, latent) + else: + output, latent = self.model(batch) + loss, _ = self.criterion(output, batch, latent) + + total_loss += loss.item() + n_batches += 1 + + return total_loss / n_batches + + def save_checkpoint(self, epoch: int, val_loss: float, is_best: bool = False): + """保存检查点""" + # 处理 DataParallel 包装 + model_to_save = self.model.module if hasattr(self.model, 'module') else self.model + + checkpoint = { + 'epoch': epoch, + 'model_state_dict': model_to_save.state_dict(), + 'optimizer_state_dict': self.optimizer.state_dict(), + 'scheduler_state_dict': self.scheduler.state_dict(), + 'val_loss': val_loss, + 'config': self.config, + } + + # 保存最新检查点 + torch.save(checkpoint, self.save_dir / 'last_checkpoint.pt') + + # 保存最佳模型 + if is_best: + torch.save(checkpoint, self.save_dir / 'best_model.pt') + print(f" ✓ 保存最佳模型 (val_loss: {val_loss:.6f})") + + def train(self, epochs: int): + """完整训练流程""" + print(f"\n开始训练 ({epochs} epochs)...") + print(f"设备: {self.device}") + print(f"模型参数量: {count_parameters(self.model):,}") + + for epoch in range(1, epochs + 1): + print(f"\nEpoch {epoch}/{epochs}") + + # 训练 + train_loss = self.train_epoch() + + # 验证 + val_loss = self.validate() + + # 更新学习率 + self.scheduler.step() + current_lr = self.optimizer.param_groups[0]['lr'] + + # 记录历史 + self.history['train_loss'].append(train_loss) + self.history['val_loss'].append(val_loss) + self.history['learning_rate'].append(current_lr) + + # 打印进度 + print(f" Train Loss: {train_loss:.6f}") + print(f" Val Loss: {val_loss:.6f}") + print(f" LR: {current_lr:.2e}") + + # 保存检查点 + is_best = val_loss < self.best_val_loss + if is_best: + self.best_val_loss = val_loss + self.save_checkpoint(epoch, val_loss, is_best) + + # 早停检查 + if self.early_stopping(val_loss): + print(f"\n早停触发!验证损失已 {self.early_stopping.patience} 个 epoch 未改善") + break + + print(f"\n训练完成!最佳验证损失: {self.best_val_loss:.6f}") + + # 保存训练历史 + self.save_history() + + return self.history + + def save_history(self): + """保存训练历史""" + history_path = self.save_dir / 'training_history.json' + with open(history_path, 'w') as f: + json.dump(self.history, f, indent=2) + print(f"训练历史已保存: {history_path}") + + # 绘制训练曲线 + self.plot_training_curves() + + def plot_training_curves(self): + """绘制训练曲线""" + if not HAS_MATPLOTLIB: + print("matplotlib 未安装,跳过绘图") + return + + fig, axes = plt.subplots(1, 2, figsize=(14, 5)) + + epochs = range(1, len(self.history['train_loss']) + 1) + + # 1. Loss 曲线 + ax1 = axes[0] + ax1.plot(epochs, self.history['train_loss'], 'b-', label='Train Loss', linewidth=2) + ax1.plot(epochs, self.history['val_loss'], 'r-', label='Val Loss', linewidth=2) + ax1.set_xlabel('Epoch', fontsize=12) + ax1.set_ylabel('Loss', fontsize=12) + ax1.set_title('Training & Validation Loss', fontsize=14) + ax1.legend(fontsize=11) + ax1.grid(True, alpha=0.3) + + # 标记最佳点 + best_epoch = np.argmin(self.history['val_loss']) + 1 + best_val_loss = min(self.history['val_loss']) + ax1.axvline(x=best_epoch, color='g', linestyle='--', alpha=0.7, label=f'Best Epoch: {best_epoch}') + ax1.scatter([best_epoch], [best_val_loss], color='g', s=100, zorder=5) + ax1.annotate(f'Best: {best_val_loss:.6f}', xy=(best_epoch, best_val_loss), + xytext=(best_epoch + 2, best_val_loss + 0.0005), + fontsize=10, color='green') + + # 2. 学习率曲线 + ax2 = axes[1] + ax2.plot(epochs, self.history['learning_rate'], 'g-', linewidth=2) + ax2.set_xlabel('Epoch', fontsize=12) + ax2.set_ylabel('Learning Rate', fontsize=12) + ax2.set_title('Learning Rate Schedule', fontsize=14) + ax2.set_yscale('log') + ax2.grid(True, alpha=0.3) + + plt.tight_layout() + + # 保存图片 + plot_path = self.save_dir / 'training_curves.png' + plt.savefig(plot_path, dpi=150, bbox_inches='tight') + plt.close() + print(f"训练曲线已保存: {plot_path}") + + +# ==================== 阈值计算(使用验证集)==================== + +@torch.no_grad() +def compute_thresholds( + model: nn.Module, + data_loader: DataLoader, + device: torch.device, + percentiles: List[float] = [90, 95, 99] +) -> Dict[str, float]: + """ + 在验证集上计算重构误差的百分位数阈值 + + 注:使用验证集而非测试集,避免数据泄露 + """ + model.eval() + all_errors = [] + + print("计算异动阈值(使用验证集)...") + for batch in tqdm(data_loader, desc="Computing thresholds"): + batch = batch.to(device) + errors = model.compute_reconstruction_error(batch, reduction='none') + + # 取每个序列的最后一个时刻误差(预测当前时刻) + seq_errors = errors[:, -1] # (batch,) + all_errors.append(seq_errors.cpu().numpy()) + + all_errors = np.concatenate(all_errors) + + thresholds = {} + for p in percentiles: + threshold = np.percentile(all_errors, p) + thresholds[f'p{p}'] = float(threshold) + print(f" P{p}: {threshold:.6f}") + + # 额外统计 + thresholds['mean'] = float(np.mean(all_errors)) + thresholds['std'] = float(np.std(all_errors)) + thresholds['median'] = float(np.median(all_errors)) + + print(f" Mean: {thresholds['mean']:.6f}") + print(f" Median: {thresholds['median']:.6f}") + print(f" Std: {thresholds['std']:.6f}") + + return thresholds + + +# ==================== 主函数 ==================== + +def main(): + parser = argparse.ArgumentParser(description='训练概念异动检测模型') + parser.add_argument('--data_dir', type=str, default='ml/data', + help='数据目录路径') + parser.add_argument('--epochs', type=int, default=100, + help='训练轮数') + parser.add_argument('--batch_size', type=int, default=4096, + help='批次大小(4x RTX 4090 推荐 4096~8192)') + parser.add_argument('--lr', type=float, default=3e-4, + help='学习率(大 batch 推荐 3e-4)') + parser.add_argument('--device', type=str, default='auto', + help='设备 (auto/cuda/cpu)') + parser.add_argument('--save_dir', type=str, default='ml/checkpoints', + help='模型保存目录') + parser.add_argument('--train_end', type=str, default='2024-06-30', + help='训练集截止日期') + parser.add_argument('--val_end', type=str, default='2024-09-30', + help='验证集截止日期') + + args = parser.parse_args() + + # 更新配置 + config = TRAIN_CONFIG.copy() + config['batch_size'] = args.batch_size + config['epochs'] = args.epochs + config['learning_rate'] = args.lr + config['train_end_date'] = args.train_end + config['val_end_date'] = args.val_end + + # 设备选择 + if args.device == 'auto': + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + else: + device = torch.device(args.device) + + print("=" * 60) + print("概念异动检测模型训练(修复版)") + print("=" * 60) + print(f"配置:") + print(f" 数据目录: {args.data_dir}") + print(f" 设备: {device}") + print(f" 批次大小: {config['batch_size']}") + print(f" 学习率: {config['learning_rate']}") + print(f" 训练轮数: {config['epochs']}") + print(f" 训练集截止: {config['train_end_date']}") + print(f" 验证集截止: {config['val_end_date']}") + print("=" * 60) + + # 1. 按日期加载数据 + print("\n[1/6] 加载数据...") + date_data = load_data_by_date(args.data_dir, config['features']) + + # 2. 按日期划分 + print("\n[2/6] 按日期划分数据集...") + train_data, val_data, test_data = split_data_by_date( + date_data, + config['train_end_date'], + config['val_end_date'] + ) + + # 3. 按概念构建序列 + print("\n[3/6] 按概念构建序列...") + print("训练集:") + train_sequences = build_sequences_by_concept( + train_data, config['features'], config['seq_len'], config['stride'] + ) + print("验证集:") + val_sequences = build_sequences_by_concept( + val_data, config['features'], config['seq_len'], config['stride'] + ) + print("测试集:") + test_sequences = build_sequences_by_concept( + test_data, config['features'], config['seq_len'], config['stride'] + ) + + if len(train_sequences) == 0: + print("错误: 训练集为空!请检查数据和日期范围") + return + + # 4. 数据预处理(简单截断极端值,标准化在模型内部通过 Instance Norm 完成) + print("\n[4/6] 数据预处理...") + print(" 注意: 使用 Instance Norm,每个序列在模型内部单独标准化") + print(" 这样可以处理不同概念波动率差异(银行 vs 半导体)") + + clip_value = config['clip_value'] + print(f" 截断极端值: ±{clip_value}") + + # 简单截断极端值(防止异常数据影响训练) + train_sequences = np.clip(train_sequences, -clip_value, clip_value) + if len(val_sequences) > 0: + val_sequences = np.clip(val_sequences, -clip_value, clip_value) + if len(test_sequences) > 0: + test_sequences = np.clip(test_sequences, -clip_value, clip_value) + + # 保存配置 + save_dir = Path(args.save_dir) + save_dir.mkdir(parents=True, exist_ok=True) + + preprocess_params = { + 'features': config['features'], + 'normalization': 'instance_norm', # 在模型内部完成 + 'clip_value': clip_value, + 'note': '标准化在模型内部通过 InstanceNorm1d 完成,无需外部 Scaler' + } + + with open(save_dir / 'normalization_stats.json', 'w') as f: + json.dump(preprocess_params, f, indent=2) + print(f" 预处理参数已保存") + + # 5. 创建数据集和加载器 + print("\n[5/6] 创建数据加载器...") + train_dataset = SequenceDataset(train_sequences) + val_dataset = SequenceDataset(val_sequences) if len(val_sequences) > 0 else None + test_dataset = SequenceDataset(test_sequences) if len(test_sequences) > 0 else None + + print(f" 训练序列: {len(train_dataset):,}") + print(f" 验证序列: {len(val_dataset) if val_dataset else 0:,}") + print(f" 测试序列: {len(test_dataset) if test_dataset else 0:,}") + + # 多卡时增加 num_workers(Linux 上可以用更多) + n_gpus = torch.cuda.device_count() if torch.cuda.is_available() else 1 + num_workers = min(32, 8 * n_gpus) if sys.platform != 'win32' else 0 + print(f" DataLoader workers: {num_workers}") + print(f" Batch size: {config['batch_size']}") + + # 大 batch + 多 worker + prefetch 提速 + train_loader = DataLoader( + train_dataset, + batch_size=config['batch_size'], + shuffle=True, + num_workers=num_workers, + pin_memory=True, + prefetch_factor=4 if num_workers > 0 else None, # 预取更多 batch + persistent_workers=True if num_workers > 0 else False, # 保持 worker 存活 + drop_last=True # 丢弃不完整的最后一批,避免 batch 大小不一致 + ) + + val_loader = DataLoader( + val_dataset, + batch_size=config['batch_size'] * 2, # 验证时可以用更大 batch(无梯度) + shuffle=False, + num_workers=num_workers, + pin_memory=True, + prefetch_factor=4 if num_workers > 0 else None, + persistent_workers=True if num_workers > 0 else False, + ) if val_dataset else None + + test_loader = DataLoader( + test_dataset, + batch_size=config['batch_size'] * 2, + shuffle=False, + num_workers=num_workers, + pin_memory=True, + prefetch_factor=4 if num_workers > 0 else None, + persistent_workers=True if num_workers > 0 else False, + ) if test_dataset else None + + # 6. 训练 + print("\n[6/6] 训练模型...") + model_config = config['model'].copy() + model = TransformerAutoencoder(**model_config) + + # 多卡并行 + if torch.cuda.device_count() > 1: + print(f" 使用 {torch.cuda.device_count()} 张 GPU 并行训练") + model = nn.DataParallel(model) + + if val_loader is None: + print("警告: 验证集为空,将使用训练集的一部分作为验证") + # 简单处理:用训练集的后 10% 作为验证 + split_idx = int(len(train_dataset) * 0.9) + train_subset = torch.utils.data.Subset(train_dataset, range(split_idx)) + val_subset = torch.utils.data.Subset(train_dataset, range(split_idx, len(train_dataset))) + + train_loader = DataLoader(train_subset, batch_size=config['batch_size'], shuffle=True, num_workers=num_workers, pin_memory=True) + val_loader = DataLoader(val_subset, batch_size=config['batch_size'], shuffle=False, num_workers=num_workers, pin_memory=True) + + trainer = Trainer( + model=model, + train_loader=train_loader, + val_loader=val_loader, + config=config, + device=device, + save_dir=args.save_dir + ) + + history = trainer.train(config['epochs']) + + # 7. 计算阈值(使用验证集) + print("\n[额外] 计算异动阈值...") + + # 加载最佳模型 + best_checkpoint = torch.load( + save_dir / 'best_model.pt', + map_location=device + ) + model.load_state_dict(best_checkpoint['model_state_dict']) + model.to(device) + + # 使用验证集计算阈值(避免数据泄露) + thresholds = compute_thresholds( + model, + val_loader, + device, + config['threshold_percentiles'] + ) + + # 保存阈值 + with open(save_dir / 'thresholds.json', 'w') as f: + json.dump(thresholds, f, indent=2) + print(f"阈值已保存") + + # 保存完整配置 + with open(save_dir / 'config.json', 'w') as f: + json.dump(config, f, indent=2) + + print("\n" + "=" * 60) + print("训练完成!") + print("=" * 60) + print(f"模型保存位置: {args.save_dir}") + print(f" - best_model.pt: 最佳模型权重") + print(f" - thresholds.json: 异动阈值") + print(f" - normalization_stats.json: 标准化参数") + print(f" - config.json: 训练配置") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/public/htmls/卫星能源太阳翼.html b/public/htmls/卫星能源太阳翼.html new file mode 100644 index 00000000..be93fe89 --- /dev/null +++ b/public/htmls/卫星能源太阳翼.html @@ -0,0 +1,1017 @@ +北京价值前沿科技有限公司 AI投研agent:“价小前投研” + +本报告为AI合成数据,投资需谨慎。 + + + + + + + + 卫星能源太阳翼概念深度报告 + + + + + + + + +
+

卫星能源太阳翼概念深度报告

+

北京价值前沿科技有限公司 AI投研agent:“价小前投研”

+

本报告为AI合成数据,投资需谨慎。

+
+ +
+ + +
+

0. 概念事件:太空能源的时代序章

+
+

“卫星能源太阳翼”概念的兴起,是全球太空探索和商业航天浪潮下,对卫星核心能源供给系统需求的集中体现。其背景是太空算力与卫星互联网的爆发式增长,以及相关前沿技术的突破与政策的强力支持。

+

催化事件与时间轴:

+
    +
  • 2023-2024年:我国多次将搭载钙钛矿组件的卫星送入太空进行初步在轨验证,显示出对下一代光伏技术的积极探索。国际空间站也于2023年通过太空行走安装新型太阳翼,以提升发电效率。
  • +
  • 2024年5月:国内民营企业光阴科技首次将钙钛矿电池发射测试,标志着商业航天在新型能源技术应用上的初步落地。
  • +
  • 2025年7月30日:我国成功发射卫星互联网低轨06组卫星,加速了卫星互联网的组网进程,直接催生了产业链相关公司的市场关注和股价异动,例如卫星化学当日涨幅达7.21%
  • +
  • 2025年8月:市场预期“卫星互联网牌照即将发放”,以及工信部发布《卫星互联网频率轨道资源管理办法(征求意见稿)》,标志着行业发展进入实质建设期。天银机电、三维通信等因此大幅上涨
  • +
  • 225年9月5日盘后:三部委发布《低轨卫星互联网产业高质量发展三年行动计划(2025-2027)》,将“商业航天”提升至国家战略高度,并首次将上海沪工等企业列入“卫星AIT总装公共服务平台”示范承建单位,引发相关公司股价强劲表现。
  • +
  • 2025年9月8日:工信部向中国联通颁发卫星移动通信牌照,加速了卫星通信C端应用的落地。
  • +
  • 2025年9月16日:“卫星互联网技术试验卫星”成功发射,航天机电作为核心配套商,其4.7亿元新单开始收入确认,导致股价涨停。
  • +
  • 2025年9月30日:政策强调“2026年起新发射卫星须配国产高功率密度电源模块”,新雷能作为宇航电源模块供应商,被市场解读为受益于强制国产化政策,股价实现20%涨停
  • +
  • 2025年10月:“千帆星座”第六批卫星成功发射,组网节奏超预期。星网宇达因其低轨卫星地面终端业务受益,三季报扭亏为盈,股价涨停。
  • +
  • 华为Mate80发布在即:市场传闻华为Mate80将实现低轨卫星直连,带动电科芯片等卫星通信芯片供应商的关注度。
  • +
+
+
+ + +
+

1. 核心观点摘要

+
+

“卫星能源太阳翼”概念正处于政策强力推动下的高速发展初期,由太空算力、卫星互联网建设以及国家战略需求共同驱动。尽管下一代钙钛矿电池技术仍面临商业化验证挑战,但柔性化、轻量化趋势已确立,并在砷化镓电池降本和国产化替代中酝酿着巨大的市场机遇。

+
+
+ + +
+

太空能源市场增长展望

+
+
+ + +
+

2. 核心逻辑与市场认知分析

+
+

核心驱动力:

+
    +
  • 太空算力与卫星互联网的能源刚需: 中信证券指出“算力上天背景下太阳翼及能源系统太阳翼将成为最大增量”。未来“太空数据中心”的部署,例如马斯克计划的每年100GW数据中心,将产生对太阳能电池高达4.8万亿元的需求。中国“GW”星座和G60星链等项目的加速推进,亦将大幅提升对高效、可靠卫星能源系统的需求。这是该概念最核心的长期驱动力,因为卫星的一切设备都需要电力,太阳翼是不可或缺的细分王者。
  • +
  • 政策扶持与国家战略: 上海市将“柔性太阳翼”列为重点发展关键配套;三部委《低轨卫星互联网产业高质量发展三年行动计划》明确了产业发展目标;工信部颁发卫星通信牌照并制定频率轨道资源管理办法;“商业航天”被提升为国家经济战略的核心层面。这些政策共同构建了支持卫星能源太阳翼发展的有利环境,尤其强调国产化和自主可控。
  • +
  • 技术创新与降本增效需求: 传统砷化镓电池虽然高效,但成本高昂(>70美元/瓦特)。钙钛矿电池作为下一代技术,兼具高效率、低成本、耐辐射等优点,且太空环境消除了其在地球上的主要降解因素,具备颠覆性潜力。同时,柔性太阳翼因其“展开面积大、功率重量比高、可实现十几颗甚至几十颗卫星层层堆叠发射”的优势,成为低轨商业卫星的主流方案,推动了对PI薄膜、碳纤维等轻量化材料的需求。
  • +
+

市场热度与情绪:

+
    +
  • 当前市场对“卫星能源太阳翼”概念的关注度极高,情绪总体呈乐观积极态势。
  • +
  • 新闻热度高涨: 多方信息强调其“巨大市场空间”(千亿至万亿级别),并将其描述为“最大增量”、“能源供给核心”。
  • +
  • 研报密集发布: 有预测未来五年内市场将保持23.6%的年复合增长率,到2029年市场规模达187亿美元。研报同时关注低轨卫星、太空数据中心等带来的增长点。
  • +
  • 股价表现强劲: 多个关联个股在相关消息催化下出现涨停或大幅上涨,例如航天机电、卫星化学、天银机电、新雷能、上海沪工、星网宇达等,显示出资金对该概念的积极追捧。市场普遍认为,随着卫星互联网建设加速,产业链企业将迎来订单增长。
  • +
+

预期差分析:

+
    +
  • 钙钛矿电池的商业化时间表: 新闻和研报中对钙钛矿寄予厚望,认为其“有望成为重要的太空能源解决方案”、“颠覆低成本市场”。然而,路演信息则更为谨慎,明确指出钙钛矿电池“稳定性差(高温>150℃性能骤降)”、“在轨测试显示性能衰减严重”,并强调需5年以上的长期验证。这表明市场对钙钛矿的大规模商业化落地时间可能过于乐观,其技术风险和验证周期被低估。
  • +
  • 砷化镓电池的短期主导地位与降本空间: 尽管钙钛矿备受关注,路演明确砷化镓电池是当前“主流技术,占传统卫星99%以上”,且国内技术国际领先。体制内单位已将其成本从3年前的超10万元/㎡降至6-7万元/㎡,聚光型砷化镓目标成本更可降至1-2万元/m²。市场可能过度关注钙钛矿的未来潜力,而忽视了砷化镓在短期内的确定性主导地位和持续降本能力。
  • +
  • 市场份额描述的差异性: 乾照光电在新闻中被描述为“国内唯一实现批量供货”且“市场占有率70%”,但在路演中则提到其“国内市占率约50%”,并列出了凯迅光电、中山德华等竞争对手。这种差异可能源于统计口径或信息来源的差异,但暗示了市场对单一公司“绝对垄断”地位的认知可能存在一定偏差。
  • +
+
+
+ + +
+

主要太阳能电池技术对比

+
+
+ + +
+

综合数据洞察:关键技术与市场前瞻

+
+ +
+
+

新闻速览:市场、技术与企业聚焦

+ +
+
+
    +
  • 市场前景: 太空算力能源高度依赖光伏技术,太阳翼将是最大增量。5W太空数据中心需近12平方公里太阳翼,价值2400亿元(钙钛矿)。马斯克未来计划每年100GW数据中心,2040年太空太阳能需求达4.8万亿元。上海政策重点支持柔性太阳翼等关键配套。
  • +
  • 关键技术: +
      +
    • 钙钛矿: 高效率、低成本、耐辐射,太空环境消除其降解因素。23-24年多次搭载卫星测试。Starlink3代或采用,正招聘钙钛矿工程师。上海港湾钙钛矿电池可覆盖航天器全生命周期。
    • +
    • 柔性太阳翼: 低轨商业卫星和深空探测主流方案,展开面积大、功率重量比高,利于堆叠发射。天问二号、雄安一号已配置。
    • +
    • PI薄膜: 柔性/刚性太阳翼确定性材料,瑞华泰耐原子氧PI薄膜研发进展良好,沃格光电CPI膜材已在轨应用。
    • +
    • 翼阵合一: ASTS卫星融合通信天线与太阳片,是相控阵天线和太阳翼一体化。
    • +
    • 砷化镓: 传统技术,效率高、抗辐照,但成本高昂(>70美元/瓦特)。锗晶片是首选关键材料。
    • +
    +
  • +
  • 相关企业: +
      +
    • 总装/机构: 中国卫星、隆盛科技、航天电子、航天环宇。
    • +
    • 材料/电池片: 瑞华泰、沃格光电、光威复材(碳纤维)、乾照光电(砷化镓龙头,占有率70%,预计2026年贡献营收16.8亿)、三安光电(砷化镓芯片)、南大光电(MO源)、正帆科技(磷烷)、上海港湾(钙钛矿)、云南锗业(锗晶片,2025年末产能125万片/年)。
    • +
    • 散热/检测: 顺灏股份、多浦乐。
    • +
    +
  • +
+
+
+ +
+
+

路演聚焦:成本、技术与市场格局

+ +
+
+
    +
  • 价值与成本: 太阳翼占卫星总经费8%-10%(传统卫星10%-20%)。成本拆解:电池电路(40%)、结构(20%)、驱动(40%)。钙钛矿轻量化可降发射成本。
  • +
  • 技术路线: +
      +
    • 硅基: 早期技术,效率低,适合低功率、低成本卫星(如马斯克星链)。国内需加速追赶。
    • +
    • 砷化镓: 当前主流(传统卫星99%以上),效率高(30-32%),抗辐照,寿命10-15年。成本高昂(6-7万元/㎡),但体制内单位已通过技术提升降本。聚光型砷化镓目标成本降至1-2万元/m²。短期内仍主导。
    • +
    • 钙钛矿: 潜力大,薄膜化、轻量化、成本极低(4-5万元/㎡)。但稳定性差(高温>150℃性能骤降),在轨测试衰减严重,需5年以上验证。适配柔性太阳翼,有自修复能力。若解决稳定性,中长期可能颠覆市场。
    • +
    • 柔性与刚性: 柔性太阳翼可折叠,体积小,适合高频发射小卫星。国际空间站已升级采用新型多层材料堆叠太阳翼。
    • +
    +
  • +
  • 市场格局: +
      +
    • 国内供应商: 电池片:体制内811所、18所;民营乾照光电(市占约50%)、凯迅、中山德华(均砷化镓);钙钛矿:光阴科技(首例在轨测试)。
    • +
    • 太阳翼结构: 体制内805所、501部;民营银河航天、开拓重工。
    • +
    • 国际对标: 星链V1.5部分采用钙钛矿(单翼106㎡,1/3钙钛矿),V2.0或全部替换为钙钛矿。
    • +
    +
  • +
  • 投资与风险: 短期布局砷化镓龙头,中长期关注钙钛矿突破民企。风险:钙钛矿稳定性未解决,砷化镓降价空间有限,商业卫星竞争加剧。
  • +
+
+
+ +
+
+

研报精要:应用、规模与挑战

+ +
+
+
    +
  • 技术概述: 卫星能源太阳翼是创新能源解决方案,结合辐射屏蔽与高效能量转换,确保极端环境稳定供电。特点:高效率(30%-40%)、轻量化、强抗辐射、柔性可折叠、寿命长(>15年)。
  • +
  • 应用场景: 太空探索(国际空间站、通信导航卫星、深空探测器)、地球能源供应(微波/激光传输)、军事应用(侦察通信)、民用(偏远地区供电)、特殊环境(极地、沙漠)。
  • +
  • 市场规模: 全球卫星产业蓬勃,政策支持空间太阳能电站,商业航天崛起。预计未来五年CAGR 23.6%,2029年市场规模达187亿美元。中国“GW”星座、G60星链加速推进,低轨卫星对高效太阳翼需求显著。
  • +
  • 挑战: 制造成本高、技术门槛高(能量传输、环境适应)、维护困难。辐射屏蔽与能量转换效率有待提升,轻量化设计受材料限制。
  • +
  • 发展建议: 加大研发(辐射屏蔽、效率),分阶段市场渗透(优先军事特种),规模化生产降本,建立行业标准。
  • +
  • 潜在相关: 我国发射太空计算卫星星座,对能源需求更高。卫星小型化趋势推动轻量化、高效率太阳翼。商业航天爆发带动配套产业。
  • +
+
+
+
+
+ + +
+

3. 关键催化剂与未来发展路径

+
+

近期催化剂(未来3-6个月):

+
    +
  • 政策细则与执行落地: 三部委《低轨卫星互联网产业高质量发展三年行动计划(2025-2027)》的具体执行细则发布,以及工信部《卫星互联网频率轨道资源管理办法》的正式实施,将带来明确的订单预期和产业发展指引。
  • +
  • 低轨卫星密集发射与组网提速: 如“千帆星座”、“GW星网”等项目按计划甚至超预期完成新的卫星批次发射和成功组网。每次发射成功都将是重要的市场情绪催化剂,并直接拉动太阳翼及相关部件的需求。
  • +
  • 钙钛矿电池在轨验证进展: 如果有关键的在轨测试报告发布,显示钙钛矿在稳定性、抗辐射能力和效率衰减方面取得突破性进展,将极大地提升市场对其商业化前景的信心。
  • +
  • 下游应用商业化进展: 华为Mate80等支持卫星直连的消费级终端发布,将推动卫星通信服务走向大众市场,间接刺激上游卫星制造环节的活跃度。
  • +
  • 国产化替代订单兑现: 例如“2026年起新发射卫星须配国产高功率密度电源模块”这类政策带来的具体订单,将直接利好国内相关供应商。
  • +
+

长期发展路径:

+
    +
  • 技术成熟与成本优化: +
      +
    • 钙钛矿电池: 彻底解决其在太空环境下的长期稳定性问题,使其转换效率进一步提升至30%以上,并实现规模化生产,将成本降至砷化镓的1/10甚至更低
    • +
    • 柔性太阳翼: 进一步提升功率重量比,实现更高集成度,同时优化展开机构的可靠性和精度。
    • +
    • 聚光型砷化镓: 通过技术创新大幅降低砷化镓用量,将成本降至5,000元/m²以下
    • +
    +
  • +
  • 产业链垂直整合与生态构建: 从上游特种材料到中游电池片、组件、结构件,再到下游总装、测试、电源管理及应用,形成高效协同、具备全球竞争力的产业链生态。
  • +
  • 应用场景的广度与深度拓展: 从当前主要服务于通信、遥感卫星,扩展到太空数据中心的能源供给,实现“天数天算”。未来向深空探测器载人航天器、甚至空间太阳能电站等更宏大的目标迈进。
  • +
  • 行业标准化与国际合作: 建立统一的太空能源系统设计、制造、测试标准,推动国际间在太空资源开发和利用方面的合作。
  • +
+
+
+ + +
+

4. 产业链与核心公司深度剖析

+
+

产业链图谱:

+
    +
  • 上游(材料及基础器件): +
      +
    • 锗晶片: 云南锗业(生产卫星太阳能电池的首选关键材料)
    • +
    • PI薄膜/CPI膜: 瑞华泰(高性能PI薄膜)、沃格光电(CPI膜材已在轨应用)
    • +
    • 碳纤维: 光威复材(卫星结构和太阳能电池板用高性能碳纤维主力供应商)
    • +
    • MO源: 南大光电(砷化镓太阳能电池关键原材料)
    • +
    • 磷烷: 正帆科技(砷化镓太阳能电池化学气相沉积工艺所需原材料)
    • +
    +
  • +
  • 中游(电池片、组件、结构与机构): +
      +
    • 砷化镓电池片: 乾照光电(砷化镓外延片出货量国内第一)、三安光电(砷化镓电池芯片已应用于商用卫星)
    • +
    • 钙钛矿电池片/组件: 上海港湾(钙钛矿电池覆盖航天器全生命周期用电)、协鑫集成、海优新材等
    • +
    • 钙钛矿电池设备: 迈为股份、宇晶股份等
    • +
    • 太阳翼结构件/机构: 隆盛科技(卫星太阳翼铰链、压紧释放机构)、航天电子、航天环宇
    • +
    • 散热: 顺灏股份
    • +
    +
  • +
  • 下游(总装、检测、电源系统与应用): +
      +
    • 总装/集成: 中国卫星、上海沪工(卫星AIT总装公共服务平台)
    • +
    • 检测: 多浦乐
    • +
    • 电源模块/芯片: 新雷能(电源模块)、电科芯片(宇航级射频/星载电源管理芯片)
    • +
    • 应用/服务: 天银机电(恒星/太阳敏感器)、三维通信(卫星宽带运营)、星网宇达(低轨卫星地面终端)
    • +
    +
  • +
+

核心玩家对比:

+
+ +
+
+

乾照光电 (300102.SZ)

+
    +
  • 优势与进展: 国内砷化镓太阳能电池(外延片)的绝对龙头,新闻称其出货量稳居国内第一,市场占有率高达70%(路演称50%),是“国内唯一实现批量供货”的供应商。预计“太阳翼单星8平米,每平米的电池片售价20万元/平米,单星价值量160万元”。预计2026年可贡献16.8亿营收,8.4亿利润。产品已应用于G60、千帆星座等国内头部星网。
  • +
  • 逻辑纯度: 高,直接受益于低轨卫星对砷化镓电池的刚性需求。
  • +
  • 潜在风险: 路演中提及市占率有差异,且面临钙钛矿电池的长期技术替代风险。
  • +
+
+
+

上海港湾 (605598.SH)

+
    +
  • 优势与进展: 新闻提及其钙钛矿电池“完全能够覆盖卫星等航天器的全生命周期用电需求”,且子公司拥有“柔性太阳帆板展开锁定机构”及“空间航天器用钙钛矿电池片粘贴方法及系统”的核心专利。
  • +
  • 逻辑纯度: 高,作为钙钛矿电池在航天领域的探索者,直接受益于新型能源技术突破。
  • +
  • 潜在风险: 钙钛矿电池的长期稳定性在轨验证仍需5年以上,商业化落地存在不确定性。
  • +
+
+
+

隆盛科技 (300680.SZ)

+
    +
  • 优势与进展: 控股子公司产品涵盖“卫星太阳翼铰链、压紧释放机构、驱动组件”等核心结构件,并参与银河航天的柔性太阳翼项目机械结构部分。
  • +
  • 逻辑纯度: 较高,直接受益于柔性太阳翼的广泛应用。
  • +
  • 潜在风险: 作为结构件供应商,其业绩弹性依赖于下游整星制造的规模。
  • +
+
+
+

瑞华泰 (688323.SH) / 沃格光电 (603773.SH)

+
    +
  • 优势与进展: 瑞华泰高性能PI薄膜已应用于空间飞行器太阳翼基板,并研发耐原子氧PI薄膜。沃格光电的CPI膜材和防护镀膜产品已在轨应用。PI薄膜是柔性太阳翼的关键基材,需求确定。
  • +
  • 逻辑纯度: 中等偏高,为柔性太阳翼提供核心材料。
  • +
  • 潜在风险: 竞争可能加剧,毛利率受制于行业竞争。
  • +
+
+
+

云南锗业 (002428.SZ)

+
    +
  • 优势与进展: “锗晶片是生产卫星太阳能电池的首选关键材料”,公司有明确的产能扩张计划,2025年末锗晶片产能125万片/年,项目建成后250万片/年
  • +
  • 逻辑纯度: 高,卡位砷化镓电池最上游核心材料,具有稀缺性。
  • +
  • 潜在风险: 砷化镓电池若被钙钛矿大规模替代,对其长期需求会有影响。
  • +
+
+
+
+

验证与证伪:

+
    +
  • 钙钛矿的乐观预期被证伪(部分): 新闻和研报中渲染的钙钛矿低成本、高效率前景,在路演中被指出了关键的技术瓶颈——稳定性差、在轨衰减严重、需要5年以上验证期。这表明市场对钙钛矿的商业化路径存在显著的“预期差”,投资者需警惕其短期内无法大规模应用的风险。
  • +
  • 乾照光电的“王者”地位有待进一步验证: 新闻称其市占率70%,而路演则称50%,并提及竞争对手。这可能反映出市场信息源的差异,需要投资者审慎评估其真实的竞争格局和市场地位,防止过度神话单一公司。
  • +
  • 星链应用钙钛矿的深层信息缺失: 路演提到“星链V1.5版本部分采用钙钛矿”,V2.0可能“全部替换为钙钛矿”。这与国内钙钛矿仍处于早期验证的谨慎态度形成对比,可能意味着国外在钙钛矿技术或应用场景选择上已取得突破,但具体细节未披露,无法全面交叉验证。
  • +
+
+
+ + +
+

5. 潜在风险与挑战

+
+

技术风险:

+
    +
  • 钙钛矿电池的稳定性与可靠性: 这是当前最大的技术瓶颈。路演明确指出其在太空极端环境(±120℃温差、高真空、辐射)下的“稳定性差”和“性能衰减严重”,需要5年以上的长期在轨验证。
  • +
  • 辐射屏蔽技术仍需提升: 太空中的高能辐射对电池寿命和性能构成威胁。
  • +
  • 能量转换效率的瓶颈: 尽管目前效率已高,但仍有改善空间,持续的研发投入必不可少。
  • +
+

商业化风险:

+
    +
  • 高制造成本: 砷化镓电池成本高昂。钙钛矿初期商业化面临制造工艺、封装及可靠性验证的高成本,市场接受度受价格因素制约。
  • +
  • 应用场景受限: 钙钛矿在未完全解决稳定性问题前,可能仅适用于“低寿命、低成本卫星或高空浮空器”。
  • +
  • 维护问题: 太空设备维护和更换困难,增加了运营风险。
  • +
+

政策与竞争风险:

+
    +
  • 政策变动不确定性: 未来政策方向、补贴力度或技术标准的变化,都可能对行业发展产生影响。
  • +
  • 行业竞争加剧: 技术竞争白热化和价格战,可能影响企业盈利能力。
  • +
  • 技术替代风险: 硅基或钙钛矿技术突破可能冲击砷化镓等现有主流技术供应商的市场份额。
  • +
+

信息交叉验证风险:

+
    +
  • 钙钛矿商业化时间表的显著矛盾: 新闻和研报乐观,路演谨慎,投资者需审慎评估。
  • +
  • 市场份额数据差异: 乾照光电市占率在不同信息源中存在20%的差异,需更深入求证。
  • +
  • 国际与国内技术进展的对比: 星链已部分采用钙钛矿,国内仍在早期测试,可能存在技术代差或应用策略差异。
  • +
+
+
+ + +
+

6. 综合结论与投资启示

+
+

“卫星能源太阳翼”概念当前正处于主题炒作与早期基本面驱动并存的阶段。宏大的市场空间(数万亿元)、密集的政策支持以及颠覆性技术(钙钛矿、柔性化)的潜力,构成了强大的主题吸引力,促使市场情绪高度乐观。同时,随着低轨卫星组网加速和国产化政策落地,部分具备核心技术的产业链公司已开始有订单兑现,显示出基本面驱动的初步迹象。

+

最具投资价值的细分环节或方向:

+
    +
  • 砷化镓电池产业链核心供应商: 在未来3-5年内,砷化镓电池仍是高功率、高可靠性卫星的主流选择。具备稳定供应能力、技术领先且已实现规模化供货的龙头企业,如乾照光电(砷化镓电池片)和云南锗业(锗晶片),具有较强的业绩确定性和较高的市场份额。
  • +
  • 柔性太阳翼结构件与关键材料: 柔性化是低轨商业卫星和深空探测的明确趋势。提供高性能PI薄膜(瑞华泰、沃格光电)精密展开机构、结构件(隆盛科技)的企业,直接受益于柔性太阳翼的普及。
  • +
  • 卫星总装、核心电源模块及宇航级芯片: 对卫星AIT总装能力以及符合“国产化”要求的电源模块和宇航级芯片的需求将显著爆发。上海沪工(卫星AIT总装),新雷能(电源模块)和电科芯片(宇航级射频/电源管理芯片),卡位关键环节,受益于政策强制国产化。
  • +
+

接下来需要重点跟踪和验证的关键指标:

+
    +
  • 钙钛矿电池在轨验证结果: 密切关注国内外钙钛矿电池在太空环境下的长期稳定性、抗辐照能力以及功率衰减率的官方报告。
  • +
  • 低轨卫星的实际发射数量与组网进度: 跟踪中国“GW”星座、G60星链、千帆星座等项目的年度发射量、在轨卫星数量以及组网完成度
  • +
  • 核心供应商的订单签订与收入确认: 重点跟踪相关公司航天相关业务的订单金额、营收占比、毛利率变化以及产能利用率
  • +
  • 成本下降速度与功率重量比提升: 关注砷化镓电池、钙钛矿电池的单瓦成本或单平米成本的实际下降趋势,以及柔性太阳翼的功率重量比提升情况。
  • +
+
+
+ + +
+

“卫星能源太阳翼”概念相关股票

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
股票代码股票名称关联原因其他标签
600118中国卫星公司开展了基于工业机器人的太阳翼自动对接系统等先进总装装备和系统研制太阳翼结构, 总装, 调研
300680隆盛科技控股子公司产品包括卫星太阳翼铰链、压紧释放机构、驱动组件;公司参与银河航天的柔性太阳翼项目机械结构部分太阳翼结构, 结构件, 互动
600879航天电子太阳翼机构在高分三号卫星实现了成功应用太阳翼结构, 结构件, 年报
688523航天环宇公司产品有用于承载太阳能电池片的复合材料太阳翼、柔性太阳翼展开机构太阳翼结构, 结构件, 招股书
688323瑞华泰公司产品(高性能PI薄膜)已应用于空间飞行器太阳翼基板太阳翼结构, PI薄膜, 互动
603773沃格光电公司柔性太阳翼产品所用CPI膜材和防护镀膜产品对客户的交付,并已实现在轨应用太阳翼结构, PI薄膜, 互动
300699光威复材公司是卫星结构和太阳能电池板(太阳翼)用高性能碳纤维的主力供应商太阳翼结构, 碳纤维, 互动
002565顺灏股份公司连同北京一家头部研发机构做出产品,可实现整个能源链路的热平衡太阳翼结构, 散热, 调研
301528多浦乐公司曾向北京卫星制造厂销售无损检测系统用于卫星支杆太阳能帆板的支撑管检测太阳翼结构, 检测, 互动
300102乾照光电公司砷化镓太阳能电池产品(外延片)出货量稳居国内市场第一砷化镓电池, 外延片, 互动
600703三安光电公司生产的砷化镓太阳能电池芯片已应用于商用卫星电源等领域砷化镓电池, 芯片, 互动
300346南大光电MO源逐渐进入砷化镓太阳能电池;公司为全球MO源主要生产厂商,国内市占率约40%砷化镓电池, 材料, 公告
688596正帆科技磷烷是砷化镓太阳能电池领域的化学气相沉积工艺所需的重要原材料;磷烷属于公司自研自产产品砷化镓电池, 电子特气, 年报
605598上海港湾公司钙钛矿电池完全能够覆盖卫星等航天器的全生命周期用电需求钙钛矿电池, 航天产品, 调研
002506协鑫集成作为钙钛矿电池组件供应商钙钛矿电池, 组件, 产业库
688680海优新材作为钙钛矿电池组件供应商钙钛矿电池, 组件, 产业库
002329皇氏集团作为钙钛矿电池组件供应商钙钛矿电池, 组件, 产业库
300393中来股份作为钙钛矿电池组件供应商钙钛矿电池, 组件, 产业库
603212赛伍技术作为钙钛矿电池组件供应商钙钛矿电池, 组件, 产业库
601012隆基绿能作为钙钛矿电池组件供应商钙钛矿电池, 组件, 产业库
688599天合光能作为钙钛矿电池组件供应商钙钛矿电池, 组件, 产业库
002459晶澳科技作为钙钛矿电池组件供应商钙钛矿电池, 组件, 产业库
688223晶科能源作为钙钛矿电池组件供应商钙钛矿电池, 组件, 产业库
600477杭萧钢构作为钙钛矿电池组件供应商钙钛矿电池, 组件, 产业库
600438通威股份作为钙钛矿电池组件供应商钙钛矿电池, 组件, 产业库
002514宝馨科技作为钙钛矿电池组件供应商钙钛矿电池, 组件, 产业库
688611杭州柯林作为钙钛矿电池组件供应商钙钛矿电池, 组件, 产业库
300750宁德时代作为钙钛矿电池组件供应商钙钛矿电池, 组件, 产业库
688472阿特斯作为钙钛矿电池组件供应商钙钛矿电池, 组件, 产业库
300751迈为股份作为钙钛矿电池设备供应商钙钛矿电池, 设备, 产业库
002943宇晶股份作为钙钛矿电池设备供应商钙钛矿电池, 设备, 产业库
300724捷佳伟创作为钙钛矿电池设备供应商钙钛矿电池, 设备, 产业库
000821京山轻机作为钙钛矿电池设备供应商钙钛矿电池, 设备, 产业库
301325曼恩斯特作为钙钛矿电池设备供应商钙钛矿电池, 设备, 产业库
688170德龙激光作为钙钛矿电池设备供应商钙钛矿电池, 设备, 产业库
002008大族激光作为钙钛矿电池设备供应商钙钛矿电池, 设备, 产业库
300776帝尔激光作为钙钛矿电池设备供应商钙钛矿电池, 设备, 产业库
603396金辰股份作为钙钛矿电池设备供应商钙钛矿电池, 设备, 产业库
688378奥来德作为钙钛矿电池设备供应商钙钛矿电池, 设备, 产业库
688025杰普特作为钙钛矿电池设备供应商钙钛矿电池, 设备, 产业库
002428云南锗业锗晶片是生产卫星太阳能电池的首选关键材料;公司空间太阳能电池用锗晶片建设项目:2025年末锗晶片产能125万片/年,项目建成后250万片/年锗电池, 锗晶片, 调研
+
+

涨幅分析补充:

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
股票代码股票名称涨幅交易日期涨幅原因
600151航天机电9.97%2025-09-16核心结论
9月16日“卫星互联网技术试验卫星”成功发射,航天科技集团批产订单进入兑现期,航天机电作为卫星平台热控+太阳翼核心配套商,4.7亿元新单开始收入确认,机构资金抢筹导致涨停。

驱动概念
卫星互联网+光伏反内卷+一带一路

个股异动解析
1. 消息面
(1)卫星互联网:9月16日09:06我国成功发射技术试验星,民用航天中长期规划要求2025-2035年完成130颗业务星组网;航天机电全资子公司上海航天设备制造总厂为本次试验星唯一热控分系统+太阳翼中标单位,合同额4.7亿元,2025Q4起交付。
(2)光伏反内卷:9月15-16日《求是》连发两文整治低价竞争,硅料价格已回升,组件排产超预期;航天机电拥有1.2GW光伏组件产能,2025H1收入占比28%,板块贝塔修复带动估值回升。
(3)一带一路:海南10亿元离岸“航天主题债”9月12日香港发行,资金用于航天科研及基础设施;航天机电光伏业务海外收入占比45%,历史参与巴基斯坦、中东多个EPC项目,直接受益“外贸优品中华行”政策。

2. 基本面
暂无新增财报数据,但2025半年报显示航天零部件收入5.3亿元同比+42%,毛利率28%,新签航天订单7.1亿元已超2024全年,奠定后续业绩高增基础。

总结
发射成功催化订单落地+光伏政策带动估值修复+海外融资支持一带一路需求,三线共振触发涨停。
002648卫星化学7.21%2025-07-30# 卫星化学(002648)涨幅原因分析

卫星化学在2025年7月30日上涨7.21%,这一显著涨幅主要与以下因素密切相关:

## 一、直接催化因素:卫星互联网低轨卫星发射成功

1. **重大利好消息**:7月30日,我国在海南商业航天发射场使用长征八号甲运载火箭成功将卫星互联网低轨06组卫星发射升空,卫星顺利进入预定轨道。这一事件成为当日卫星相关板块上涨的直接催化剂。

2. **技术创新突破**:此次发射中,长征八号甲火箭实现了全流程自动化、起飞主动漂移控制、过冷液氧加注等多项技术创新,表明我国卫星互联网技术能力持续提升,为产业链发展提供了技术保障。

## 二、行业背景与政策环境

1. **卫星互联网建设加速**:根据7月29日专家会议预告信息,\"千帆星座预计明年将完成几百颗低轨卫星的发射\",且\"下半年卫星发射速度有望加快\"。这表明卫星互联网建设已进入加速期。

2. **产业链价值凸显**:媒体报道指出,卫星互联网领域在批量卫星中的价值量达到70%,将率先迎来快速增长。特别是相控阵天线和主线占卫星成本的约30%-40%,T/R组件占10%-20%,这些核心部件供应商将直接受益。

3. **政策支持明显**:7月31日长江军工研报指出\"国内商业航天审批转入绿通,各公司计划/路线/产品节点或有提前\",表明政策环境对卫星产业链的支持力度加大。

## 三、市场预期与机构观点

1. **机构积极推荐**:长江军工在7月31日发布研报\"持续推荐商业【火箭&卫星】链,#下半年发射&审批全面提速\",明确看好卫星产业链前景,并推荐了多家卫星相关企业。

2. **发射密集度提升**:研报提到\"XW开启疯狂7/8月,#20天5连密集发射,一期组网建设再提速\",表明卫星发射频率明显提高,产业链企业订单预期增强。

3. **国家队与民营资本共同推动**:报道指出\"国家队托底两大星座明年50+次组网\",以及\"Q4国家队&民营多型可回收火箭先后首飞\",表明卫星互联网建设得到多方力量共同推动。

## 四、公司业务关联性推测

虽然提供的信息中没有直接说明卫星化学的具体业务,但从公司名称与市场反应可以推测:

1. **名称关联性强**:公司名称\"卫星化学\"直接与卫星产业相关,市场可能将其视为卫星产业链的重要参与者。

2. **潜在业务方向**:结合化学行业背景,卫星化学可能在卫星材料、特种化学品、燃料或相关化工产品方面有业务布局,这些均是卫星制造和发射过程中不可或缺的组成部分。

3. **技术协同可能性**:新闻中提到\"铜铟镓硒技术有潜力将卫星电池成本降低至原来的10%到30%,这些企业已有该技术的研发经验和先发优势\",卫星化学作为化工企业,可能在该领域有技术积累。

## 五、市场情绪与资金流向

1. **板块效应明显**:当日卫星相关概念股普遍受到资金追捧,形成板块效应,卫星化学作为名称直接相关的标的,自然受到资金青睐。

2. **预期改善**:随着卫星互联网建设加速,市场对卫星产业链企业未来业绩预期明显改善,推动估值提升。

3. **技术面因素**:股价上涨可能也受到技术面因素影响,如突破关键阻力位、成交量放大等,进一步吸引跟风盘入场。

## 结论

卫星化学7月30日上涨7.21%的核心原因是我国卫星互联网低轨卫星发射成功的直接催化,叠加卫星互联网建设加速的行业背景和政策支持,市场预期卫星产业链将迎来快速增长期。作为名称直接相关的标的,卫星化学被市场视为卫星产业链的重要参与者,从而获得资金青睐。虽然具体业务关联性需要进一步验证,但行业景气度提升和板块效应是推动股价上涨的主要因素。

投资者应关注公司后续是否发布与卫星业务相关的公告或说明,以验证市场预期的合理性。同时,卫星互联网建设是一个长期过程,投资者应理性看待短期股价波动,关注公司基本面和长期发展前景。
300342天银机电9.17%2025-08-25# 天银机电(300342)涨幅9.17%原因分析

## 核心驱动因素:卫星互联网牌照预期发酵

天银机电在2025年8月25日上涨9.17%的最直接催化剂是**卫星互联网牌照即将发放**的市场预期。根据当日新闻信息,A股卫星互联网指数直线拉升,长江通信、中国卫星涨停,天银机电涨逾10%,媒体报道称相关部门近期将会发放卫星互联网牌照。这一消息直接点燃了整个卫星导航板块的投资热情。

## 公司业务与卫星互联网的关联性

天银机电通过多家子公司在卫星通信领域进行了深度布局,形成了一个完整的卫星产业链参与体系:

1. **天银星际(控股72.5%)**:研发生产恒星敏感器、太阳敏感器等卫星核心部件,已应用于\"千帆星座\"等低轨互联网卫星项目,为各类卫星(包括通信卫星)提供高精度姿态确定与导航服务。

2. **上海讯析(全资)**:研发生产超宽带高速信号处理平台、低轨互联网卫星信号处理设备,直接参与低轨互联网卫星信号处理设备的研发与生产。

3. **华清瑞达(全资)**:从事雷达射频仿真及电磁环境仿真系统研发,已就6G\"通感一体化\"的仿真测试与某头部通信企业达成合作。

4. **工大雷信(参股40%)**:研制新体制雷达、探地雷达及雷达核心部件,与华清瑞达形成技术协同。

## 政策支持与行业前景

1. **国家战略定位提升**:根据投资者论坛信息,在2025年政府工作报告中,\"商业航天\"被置于国家经济战略的核心层面,被视为驱动未来经济增长的关键力量之一。

2. **行业发展预期**:市场普遍预期中国商业航天将在未来几年迎来爆发式的黄金发展期,卫星互联网作为商业航天的重要组成部分,将率先受益。

3. **牌照发放意义**:卫星互联网牌照的发放意味着行业发展进入新阶段,将带动整个产业链的需求增长,天银机电作为卫星关键部件供应商有望直接受益。

## 市场情绪与技术面因素

1. **板块联动效应**:当日卫星导航板块整体走强,长江通信、中国卫星等个股均大幅上涨,板块效应带动了个股表现。

2. **投资者情绪积极**:从投资者论坛讨论可以看出,市场普遍看好天银机电在卫星互联网领域的布局,认为公司将是卫星互联网牌照发放的直接受益者之一。

3. **技术面因素**:有投资者指出,该股在6月10日、7月3日已有两次试盘行为,此次突破可能是第三次,\"事不过三\"的心理预期增强了投资者持股信心。

## 总结

天银机电此次上涨9.17%是多重因素共同作用的结果:核心驱动力是卫星互联网牌照即将发放的消息刺激;公司在卫星产业链的完整布局使其成为直接受益标的;国家将商业航天提升至战略层面的政策支持为长期发展提供保障;加上板块联动和积极的市场情绪共同推动了股价的强势表现。投资者普遍认为卫星互联网牌照的发放将加速行业发展,而天银机电凭借其在卫星关键部件领域的技术积累和项目经验,有望在行业爆发期获得显著的增长机会。
300593新雷能20.0%2025-09-30核心结论
9月30日20cm涨停是“卫星互联网电源强制国产化”政策0→1增量订单预期与股权激励高增速目标共振的结果,资金提前锁仓2026-2027年航天电源模块红利。

驱动概念
卫星互联网电源+国产替代+数据中心HVDC

个股异动解析
1. 消息面
(1)卫星互联网电源:2026年起新发射卫星须配国产高功率密度电源模块——新雷能已列装长六/长七/鹊桥,军工资质+宇航目录双齐全,市占率3%→20%空间打开。
(2)国产替代:三部门《卫星互联网能源系统技术规范(暂行)》首次单列“卫星电源系统”,国产模块成强制标准,公司DC-DC模块国内独供,暂无竞品。
(3)数据中心HVDC:英伟达Rubin架构2027年全面导入800V HVDC,公司48V-12V/12V-1V DC-DC价值量=AC-DC,已送样海外CSP,股权激励目标隐含AI电源订单落地。

2. 基本面
暂无2024年报外新数据;公告2025-2027收入复合增速≥30%,2027年营收目标220亿元(2024年26亿元×8.5倍),对应军工+卫星+AI三大电源赛道同时放量。

总结
政策强制国产化创造纯增量市场→股权激励给出高增速路径→公司具备军工资质与宇航级产品先发优势→资金抢跑2026-2027年卫星互联网电源订单。
600877电科芯片10.01%2025-08-11核心结论: 华为Mate80卫星直连手机发布在即叠加中国星网万星组网加速,电科芯片作为唯一量产宇航级射频+星载电源管理芯片的核心供应商,订单预期爆发,资金抢筹涨停。
驱动概念: 商业航天+卫星互联网+华为链

个股异动解析:
1. 消息面:
(1)商业航天:8月11日我国可重复使用火箭十公里级垂直起降试验成功,标志商业航天进入批量化发射阶段。
- 公司与商业航天的相关事实:公司宇航级射频前端芯片、星载电源管理芯片已批量配套长征系列商业火箭及低轨卫星,2024年航天收入4.7亿元,占比18%,同比+92%。
(2)卫星互联网:中国星网计划2025-2035年发射1.3万颗低轨卫星,8月发射日程密集。
- 公司与卫星互联网的相关事实:参与中国电科商业航天专项二期,为低轨宽带星座提供射频与功率芯片整体解决方案。
(3)华为链:市场传华为Mate80将于9月发布,首次实现低轨卫星直连。
- 公司与华为链的相关事实:Mate80卫星通信基带芯片核心供应商,已获华为订单前置锁定。

2. 基本面:
- 2024年航天业务收入4.7亿元,同比+92%,毛利率55%,显著高于公司平均。
- 北斗短报文芯片已规模应用于军工、应急,2025年预计贡献收入2亿元。
- 8英寸宇航级芯片产线满产,产能利用率98%,订单排至2026年.\n驱动概念: 商业航天+卫星互联网+华为链 \n
相关信息解析: \n1. 行业动态: \n(1)商业航天:我国可重复使用火箭试验成功,标志商业航天进入批量化发射阶段。\n- 公司相关业务:公司宇航级射频前端芯片、星载电源管理芯片已批量配套长征系列商业火箭及低轨卫星,2024年航天收入4.7亿元,占比18%,同比+92%。 \n(2)卫星互联网:中国星网计划发射大量低轨卫星。\n- 公司相关业务:参与商业航天专项,为低轨宽带星座提供射频与功率芯片整体解决方案。\n(3)华为链:市场关注华为Mate80可能支持低轨卫星直连功能。\n- 公司与华为链的相关事实:是华为Mate80卫星通信基带芯片的供应商之一,并已获得华为相关订单。\n
2. 公司基本面: \n- 航天业务收入增长显著:2024年航天业务收入4.7亿元,同比+92%,毛利率55%。 \n- 北斗短报文芯片业务:已规模应用于军工、应急,预计2025年该业务有望贡献收入约2亿元.\n- 产能情况:8英寸宇航级芯片产线产能利用率98%,订单已排至2026年。
603131上海沪工9.99%2025-09-08核心结论:三部委9月5日盘后发布的《低轨卫星互联网产业高质量发展三年行动计划(2025-2027)》首次将上海沪工列入“卫星AIT总装公共服务平台”示范承建单位,并明确2025Q4前须完成产线通线验收,资金确认公司0→1订单拐点,9月8日午后集中抢筹封板。

驱动概念:卫星AIT总装+深空经济+低轨卫星互联网

个股异动解析
1. 消息面
(1)卫星AIT总装:三部委文件首次把“深空经济”写入官方规划,2027年目标在轨卫星≥3000颗、产业规模破1.2万亿元;上海沪工母公司上海沪工焊接集团被列为全国仅2家的“商业卫星AIT总装公共服务平台”承建单位之一,奉贤基地须在2025Q4前完成首批示范星座产线验收,资本开支已提前到位。
(2)深空经济:9月4日合肥首届深空经济大会提出2040年全球深空经济规模达万亿美元,并规划小行星动能撞击验证任务;公司因具备卫星总装资质被资金视为“深空经济”核心标的。
(3)低轨卫星互联网:工信部9月8日向中国联通颁发卫星移动通信牌照,提出2030年卫星通信用户超千万,运营商C端加速落地;板块午后集体拉升,上海沪工与中国卫星、三维通信同步涨停,形成政策共振。

2. 基本面
暂无最新财报数据,但公司传统焊接设备业务为卫星整星AIT提供高精度金属结构件与总装产线集成能力,政策驱动下2025H2起有望新增卫星外协订单,带来0→1收入增量。

总结
1. 政策首次官方确认“深空经济”赛道并给出量化目标
2. 公司独家拿到卫星AIT总装“门票”,2025Q4前必须投产
3. 运营商牌照发放催化板块情绪,资金午后集中建仓
002115三维通信10.06%2025-08-28核心结论: 工信部《卫星互联网频率轨道资源管理办法》落地叠加海卫通运营数据超预期,三维通信被资金重新定价为“卫星互联网运营+终端”双重受益标的而涨停。
驱动概念: 卫星互联网+6G天地一体网络+船舶卫星宽带运营

个股异动解析:
1. 消息面:
(1)卫星互联网:8月27日工信部发布《卫星互联网频率轨道资源管理办法(征求意见稿)》,首次明确低轨星座申报、协调、落地流程,标志6G天地一体网络进入实质建设期;公司子公司海卫通为国内船舶卫星宽带市占率第一,已服务3000艘船舶、4万船员,2025H1收入同比+62%,毛利率升至46%,并参与低轨星座地面终端标准制定,预计2026年批量供货。
(2)6G天地一体网络:政策提出“手机直连卫星”商用试验,推动空天地融合;公司具备“卫星+4G+自组网”多模态通信能力,契合6G网络需求。
(3)船舶卫星宽带运营服务:海卫通全球五大洲覆盖,运营规模居前,政策催化下运营及终端市场打开,公司率先受益。

2. 基本面:
- 2025H1海卫通收入同比+62%,毛利率46%,业绩拐点显现。
- 参与低轨星座地面终端标准制定,2026年有望批量出货。
- 船舶卫星宽带市占率国内第一,用户规模与网络覆盖具备先发优势。

总结: 政策落地打开卫星互联网万亿市场,海卫通运营数据超预期验证业绩拐点,三维通信兼具运营与终端双重卡位,资金快速抢筹致涨停。
002829星网宇达10.01%2025-10-20核心结论:
“千帆星座”第六批卫星发射成功引爆卫星互联网情绪,公司三季报扭亏且低轨卫星地面终端收入占比首次过半,事件与业绩共振封板。

驱动概念:
商业航天+千帆星座+卫星导航

个股异动解析:
1. 消息面
(1)商业航天:10-19力箭一号“一箭3星”成功,10-17千帆星座“一箭18星”发射,星座在轨卫星达108颗,组网节奏超预期。
- 公司主营低轨卫星地面终端(相控阵平板天线、动中通),为千帆星座首批合格供应商,Q3批量交付。
- 10-20早盘板块指数涨6%,公司率先涨停带动跟风。

(2)卫星导航:工信部“毫秒用算”专项行动提出<10ms星地时延,空天算力需求提升。
- 公司导航产品配套无人机、无人船,已列装边海防及应急测绘项目。

(3)无人机:英国授权军方“当场击落”威胁无人机,地缘冲突升级。
- 公司无人系统配套导航与数据链,前三季度无人机收入同比+39%。

2. 基本面
- 2025Q3归母净利润5,428万元,同比+816%,环比扭亏;毛利率由18%升至27%,主因高毛利卫星终端放量。
- 前三季度低轨卫星地面终端收入1.34亿元,占总营收50.4%,首次突破半数,订单排产至2026Q2。
- 资产负债率42%,账面现金4.7亿元,可转债剩余规模2.1亿元,赎回压力小。

总结:
千帆星座发射催化+三季报业绩拐点+低轨卫星地面终端订单兑现,形成“情绪+基本面”双升,推动10-20涨停。
+
+
+
+ + + + \ No newline at end of file diff --git a/public/htmls/厦门“十五五规划”.html b/public/htmls/厦门“十五五规划”.html new file mode 100644 index 00000000..dc3888cc --- /dev/null +++ b/public/htmls/厦门“十五五规划”.html @@ -0,0 +1,588 @@ + + + + + + 厦门“十五五规划”概念深度分析报告 - 价小前投研 + + + + + + + + + + + + + + +
+ + +
+

+ 厦门“十五五规划”概念深度分析报告 +

+

+ 北京价值前沿科技有限公司 AI投研agent:“价小前投研” 进行投研呈现 +

+

+ 本报告为AI合成数据,投资需谨慎。 +

+
+ + +
+

0. 概念事件:核心催化与时间轴

+
+
+

核心事件

+

+ 《中共厦门市委关于制定厦门市国民经济和社会发展第十五个五年规划的建议》的正式发布。该文件是厦门未来五年(2026-2030年)国民经济和社会发展的纲领性文件,旨在明确厦门在国家“十五五”规划背景下的战略定位、发展目标和重点任务。 +

+
+
+

催化事件及时间轴

+
    +
  • 2025年下半年(大概率在10月): 中央全会将召开讨论国家“十五五”规划建议,定下总基调,特别是以“新质生产力”为统领,聚焦科技创新、低碳、智能。
  • +
  • 2025年12月8日: 《中共厦门市委关于制定厦门市国民经济和社会发展第十五个五年规划的建议》正式发布,直接引爆市场关注。
  • +
  • 国台办回应: 发言人陈斌华表示,“十五五”规划将为两岸优势互补、融合发展注入强劲动能,与厦门规划高度契合。
  • +
  • 路演信息披露: 厦门国贸(2025-11-21)和厦门银行(2025-04-29)均提及公司“十五五”战略规划编制及国际化、两岸金融布局。
  • +
  • 2026年“两会”期间: 国家“十五五”规划全文将正式公布并实施,进一步细化国家层面对厦门等重点区域的战略部署。
  • +
+
+
+

规划内容重点 (新闻摘要)

+
    +
  • 深化国资国企改革: 做强做优做大国有企业,鼓励全球产业链供应链布局。
  • +
  • 探索自由港特征经济特区: 对接国际高标准经贸规则,深化制度型开放。
  • +
  • 加强厦台融合: 打造两岸共同市场与共同产业,优化涉台营商环境,推进厦金率先融合,建设“厦门-台湾-全球”通道。
  • +
  • 强化数字经济与人工智能: 深入推进数字厦门建设,全面实施“人工智能+”行动,打造国家人工智能应用中试基地。
  • +
+
+
+
+ + +
+

1. 核心观点摘要

+
+

+ “厦门‘十五五’规划”概念处于政策驱动下的初期主题炒作与长期价值投资并存阶段。其核心驱动力在于国家赋予厦门作为经济特区、对台前沿和“一带一路”节点城市的特殊战略地位,叠加地方政府在深化改革、产业升级和制度型开放方面的积极部署,特别是人工智能、数字经济、高端制造以及两岸深度融合领域,有望带来显著的政策红利和产业机遇,但需警惕短期情绪炒作风险。 +

+
+
+ + +
+

2. 概念的核心逻辑与市场认知分析

+ + + +
+
+

国家战略赋能下的政策高地

+

厦门作为经济特区、自由贸易试验区、对台交流合作前沿以及“一带一路”倡议的重要节点城市,承载特殊战略任务。国家“十五五”规划将以“新质生产力”为统领,强调科技创新、产业升级、制度型开放和区域协调发展,厦门将获得更多政策倾斜。研报指出支持厦门建设“一带一路”重要节点城市和两岸融合发展示范区。

+
+
+

“新质生产力”引领的产业升级

+

厦门规划积极响应国家号召,强化数字经济与人工智能发展,实施“人工智能+”行动,打造国家人工智能应用中试基地。在先进制造业(新能源汽车、半导体与集成电路、生物医药)、海洋经济等战略性新兴产业领域发力。与全国“十五五”规划中“人工智能、机器人、半导体”最强共振方向高度契合。

+
+
+

制度型开放与两岸深度融合

+

探索建设具有自由港特征的经济特区,对接国际高标准经贸规则,完善以投资贸易自由化便利化为核心的制度体系。在两岸融合方面,规划提出加快打造两岸共同市场、共同产业,推进厦金率先融合,建设“厦门-台湾-全球”通道,为厦门带来独特的区域发展优势。

+
+
+

国资国企改革激发内生动力

+

深化国资国企改革,做强做优做大国有企业和国有资本,优化国有经济布局和结构调整,增强国有企业核心功能,提升核心竞争力。这为厦门本地国有上市公司提供了资产整合、业务拓展的契机。

+
+
+ +
+

市场热度与情绪:乐观与期待并存

+
    +
  • 新闻热度: 规划发布当日,媒体密集报道,国台办发言人专门回应,显示官方高度重视。
  • +
  • 研报密集度: 多份研报对其在区域发展、产业升级、金融开放、两岸融合等方面的战略定位给予高度评价,表明机构投资者对其长期价值的认可。
  • +
  • 关联个股反应: 大量厦门本地国资控股或注册地在厦门的上市公司被市场关联,涉及交通、金融、智算、高端制造等多个领域。部分股票(如凤竹纺织)在规划发布当日涨停,反映市场对政策红利的快速响应。漳州发展和海峡创新等福建本地股在类似区域政策催化下也曾出现大幅上涨。
  • +
  • 情绪分歧: 尽管整体乐观,但部分个股的上涨被指出是“区域性板块联动和概念性炒作”,缺乏基本面直接支撑,并伴有“融资净偿还”的警示,说明部分资金对炒作的持续性持有谨慎态度。
  • +
+
+ +
+

预期差分析

+
    +
  • 宏大叙事与落地细节的预期差: “自由港特征的经济特区”、“两岸共同市场”等愿景宏大,具体实施细则和可量化的落地项目仍需时间,市场可能对政策的执行效率和短期业绩贡献存在过高预期。
  • +
  • “新质生产力”的理解偏差: 全国“十五五”规划强调“科技为产业服务、脱虚向实”,若市场仍停留在纯概念炒作层面,可能与政策导向产生偏差。
  • +
  • 国资国企改革的力度与速度: 市场寄予厚望,但实际整合的复杂性和推进速度往往超出预期,可能存在“雷声大雨点小”的风险。
  • +
  • 房地产定位的转变: 全国“十五五”规划将房地产定位为“民生”,可能与市场对地方政府传统上依赖土地财政的预期产生落差。
  • +
  • 概念股的“纯度”与“炒作成分”: 部分关联个股(如凤竹纺织、漳州发展、海峡创新)的上涨是受区域板块联动、市场情绪、甚至名称联想和未经证实传闻的推动,而非公司基本面或直接利好,概念纯度不高,追高风险较高。
  • +
+
+
+ + +
+

3. 关键催化剂与未来发展路径

+
+
+

近期催化剂 (未来3-6个月)

+
    +
  • 国家“十五五”规划建议正式发布: 预计2025年下半年(大概率在10月)中央全会讨论通过,引发新一轮市场关注。
  • +
  • 厦门市配套政策及行动计划出台: 关于自由港、人工智能+、厦台融合等具体实施方案、配套资金安排、重点项目清单等细则。
  • +
  • 国资国企改革实质性进展: 具体资产整合、重组方案或核心业务注入的公告,如厦门港务的承诺。
  • +
  • 重大项目落地或阶段性成果: 国家人工智能应用中试基地、两岸共同产业园区、厦金基础设施互联互通等重大工程进展。
  • +
  • 重要会议或论坛的举办: 围绕数字经济、人工智能、两岸融合主题的高级别会议,发布重要合作成果。
  • +
+
+
+

长期发展路径

+
    +
  • 2026年全国“两会”规划全文公布: 为厦门提供更明确、长期的战略指引。
  • +
  • 自由港特征经济特区全面建成: 完善与国际高标准经贸规则接轨的制度体系,形成国际一流营商环境。
  • +
  • 两岸融合发展示范区与共同市场成熟: 实现厦金率先融合,形成具有国际影响力的“两岸共同市场”和“厦门-台湾-全球”通道。
  • +
  • “数字厦门”与“人工智能+”深度渗透: 城市大脑建设深化,AI全面赋能千行百业,形成数字经济核心产业和人工智能应用场景。
  • +
  • 新质生产力产业集群壮大: 在新能源汽车、半导体、生物医药、海洋新兴产业等领域形成自主可控能力和国际影响力的产业集群。
  • +
  • 海洋强国战略的先行区: 持续推动海洋新兴产业发展,在海洋科技创新等方面发挥引领作用。
  • +
+
+
+
+ + +
+

4. 产业链与核心公司深度剖析

+ + + +
+
+

上游(技术与核心部件/服务)

+
    +
  • 智算与半导体: AI芯片设计与制造(星宸科技、麦克奥迪)、光通信与光传感器(优迅股份、光莆股份、华懋科技)、数据中心电源(科华数据、中创环保)、光刻材料(恒坤新材)、数据中心托管(垒知集团)、智能硬件及算力方案(弘信电子)。
  • +
  • 新材料: 薄膜电容器(法拉电子)、砷化镓太阳能电池(乾照光电、三安光电)。
  • +
+
+
+

中游(核心产业与平台运营)

+
    +
  • 国资运营与供应链: 大宗商品经营及物流(厦门象屿、厦门国贸)、供应链运营(建发股份)、工程机械(厦工股份)、医用手套(中红医疗)、铜母线(电工合金)。
  • +
  • 交通物流: 港口运营(厦门港务)、机场管理(厦门空港)、交通信息化(路桥信息)、客车制造(金龙汽车)。
  • +
  • 金融服务: 商业银行(厦门银行)、期货(瑞达期货)、融资租赁/大宗商品金融服务(厦门信达)。
  • +
  • 数字经济与AI应用: 城市大脑、大数据、网络空间安全解决方案(国投智能)、民生领域数字化解决方案(易联众)。
  • +
  • 两岸融合平台: 涉台园区管理、跨境电商集散中心(通过交通、金融、供应链企业间接体现)。
  • +
+
+
+

下游(应用场景与终端市场)

+
    +
  • 城市建设与民生: 工程施工(建发合诚)、家居(美凯龙)、民生服务(易联众)、建筑产业互联网(规划中)。
  • +
  • 新兴产业应用: 商业航天(乾照光电、三安光电、思泰克)、新能源汽车(金龙汽车)。
  • +
  • 全球贸易: 国际产业链供应链布局(厦门国贸、建发股份)。
  • +
+
+
+ +
+
+

国资国企改革与全球供应链领导者

+
    +
  • 建发股份 (600153): 实控人厦门国资,主营供应链运营。资源整合能力强,国际化经验丰富。受益于“一带一路”和RCEP。庞大的供应链网络和资金实力是其优势。
  • +
  • 厦门国贸 (600755): 实控人厦门国资,主营供应链管理。已成立“国际化发展部”,在东南亚、中亚、中东、南美、欧洲设50+家境外机构,与170国、9万余家企业合作。深度耕耘“一带一路”和RCEP区域。路演信息验证了规划中“鼓励国有企业积极开展全球产业链供应链布局”的逻辑。
  • +
  • 厦门象屿 (600057): 实控人厦门国资,主营大宗商品经营及物流服务。中国仓储物流企业百强榜首,具备铁路、公路、水路多式联运能力,受益于统一大市场和“促出海”战略。
  • +
+
+
+

两岸融合与区域交通枢纽

+
    +
  • 厦门港务 (000905): 实控人福建国资委,福建省港口整合平台,定位国际航运中心。受益于“完善厦台海、空直航运体系”和“海洋强国”战略,是该方向的领导者。
  • +
  • 厦门空港 (600897): 实控人厦门国资,厦门机场为海西地区机场龙头。受益于“完善厦台海、空直航运体系”、“打造‘厦门-台湾-全球’通道”以及“支持金门共用厦门翔安国际机场”等政策。
  • +
+
+
+

数字经济与人工智能核心受益者

+
    +
  • 星宸科技 (301536): 地处厦门,自研AI芯片累计出货量已超1亿颗。受益于“全面实施‘人工智能+’行动”和“强化算力、算法、数据等高效供给”,是技术领导者。
  • +
  • 科华数据 (002335): 地处厦门,智算中心领域布局电源、液冷、算力等市场。受益于“强化算力、算法、数据等高效供给”和“数字厦门”建设,是算力基础设施的关键提供商。
  • +
  • 麦克奥迪 (300341): 地处厦门,产品主要用于半导体前道缺陷检查、后道封装。受益于半导体产业升级和科技自立,其在半导体关键环节的地位值得关注。
  • +
  • 安妮股份 (002235): 地处厦门,新控股股东专注于“行业智算+垂类大模型”一体化解决方案。未来发展方向与“AI+产业”高度契合。
  • +
+
+
+

两岸特色金融服务

+
    +
  • 厦门银行 (601187): 厦门市第一大上市城商行,深耕两岸金融特色业务,台企客户数增长19%,台胞存贷款分别增长45%/23.3%。受益于“两岸区域性金融中心建设”以及两岸金融创新政策支持。是该领域最纯粹、最直接的受益者。
  • +
+
+
+
+ + +
+

5. 潜在风险与挑战

+
+
+

政策落地不及预期风险

+

“自由港特征的经济特区”、“两岸共同市场”等战略实施复杂,涉及制度创新、跨区域协调和长期投入。从规划建议到具体政策出台、项目落地存在时间差,且实际执行力度和效果可能不及市场预期。

+
+
+

地缘政治风险

+

厦台融合深度发展与两岸关系走向高度绑定。若地缘政治冲突加剧,可能影响台商投资信心,阻碍厦台经贸往来和人员流动,进而对规划中的两岸共同市场、共同产业建设造成冲击(研报曾提示)。

+
+
+

全球经济下行与外部环境变化

+

厦门作为外向型城市,对国际贸易依赖高。研报提示需关注欧美经济回落背景下的出口压力。全球贸易保护主义抬头等也可能对厦门的制度型开放和全球产业链布局带来不确定性。

+
+
+

产业转型升级挑战

+

发展数字经济、人工智能、高端制造等“新质生产力”需要持续技术投入和人才支撑。厦门面临关键核心技术攻关、产业链自主可控以及与国内外头部企业竞争的压力。传统制造业转型也面临多重挑战。

+
+
+

国资国企改革阻力

+

深化国资国企改革涉及利益调整、体制机制创新,可能面临内部阻力,导致资产整合、战略重组和核心功能提升的进程缓慢,难以在短期内充分释放改革红利。

+
+
+

信息交叉验证风险

+

概念炒作与基本面脱钩风险较高,部分关联个股上涨由情绪和传闻推动,而非公司基本面。部分公司“主营业务暂无”的模糊性可能导致投资者过度追逐概念,面临较大不确定性。

+
+
+
+ + +
+

6. 综合结论与投资启示

+ +
+

最终看法

+

+ “厦门‘十五五’规划”概念目前处于典型的政策驱动下的主题炒作阶段,但其背后蕴含着明确的国家战略支撑和长期的基本面改善潜力。 规划的发布激活了市场对厦门区域发展和国企改革的宏大想象,尤其是在“新质生产力”、两岸融合和自由港建设方面。然而,短期内股价的波动更多受到市场情绪、板块联动和资金流向的影响,部分关联个股的上涨与自身基本面脱钩的风险较高。长期来看,若规划得以有效执行,将为厦门带来结构性投资机遇。 +

+
+ +
+
+

最具投资价值的细分环节或方向

+
+
+

1. 数字经济与人工智能核心技术及基础设施

+

+ 原因: 国家和厦门规划共同强调的“新质生产力”核心,技术壁垒高、成长空间广阔。规划提及“强化算力、算法、数据等高效供给,全力打造国家人工智能应用中试基地”。研报强调“人工智能、机器人、半导体”是五年规划最强共振方向。 +
重点关注: 星宸科技 (301536) (自研AI芯片出货超1亿颗), 科华数据 (002335) (智算中心电源、液冷、算力基础设施), 麦克奥迪 (300341) (半导体前道缺陷检查)。 +

+
+
+

2. 两岸融合与制度型开放下的特色金融及交通物流

+

+ 原因: 厦门在对台战略中的特殊地位,是两岸融合发展的桥头堡。规划提及“打造两岸共同市场”、“完善厦台海、空直航运输体系”。 +
重点关注: 厦门银行 (601187) (深耕两岸金融特色,台企客户、台胞存贷款增长显著), 厦门港务 (000905) (福建省港口整合平台,国际航运中心)。 +

+
+
+

3. 深化国资国企改革下的供应链及新兴产业龙头

+

+ 原因: 国企改革将推动资源优化配置,增强国有企业核心竞争力。规划提及“深化国资国企改革,鼓励国有企业积极开展全球产业链供应链布局”。 +
重点关注: 厦门国贸 (600755)建发股份 (600153) (两者皆为供应链巨头,国际化布局深远)。 +

+
+
+
+ +
+

接下来需要重点跟踪和验证的关键指标

+
    +
  • 政策落地具体进度: 密切关注厦门市和福建省层面关于自由港、两岸共同市场、人工智能应用中试基地等具体实施方案、配套政策和资金安排的发布时间表和详细内容。
  • +
  • 重大项目投资与建设: 跟踪规划中提及的重大基础设施(如厦金通电、通气、通桥、供水第二通道)、两岸共同产业园区、数字经济核心项目等的实际投资额、开工和建设进度。
  • +
  • 核心技术突破与产业化数据: 关注厦门本地半导体、AI芯片、生物医药等新兴产业的技术突破、专利申请数量,以及关键产品的出货量、市场占有率和产业化应用案例。
  • +
  • 两岸经贸往来数据: 监测厦台货物进出口额、跨境电商交易额、台商在厦投资额及增减情况,以及厦金航线客货运量等,以评估两岸共同市场建设的实际成效。
  • +
  • 关联上市公司业绩增长点: 重点关注上述推荐企业财报中在新业务、新市场(尤其是与“十五五”规划相关领域)的收入贡献、毛利率变化,以及国企改革背景下的资产整合、盈利能力提升等具体指标。
  • +
+
+
+
+ + +
+

7. 概念关联股票列表与涨幅分析

+
“十五五”海风规划草案把年均15GW装机“锚定”为底线,资金在长三角资源储备最厚、绿电装机占比已超60%的上海电力上一次性兑现政策弹性。

驱动概念
海风+十五五规划+长三角资源

个股异动解析
1. 消息面
(1)海风:十五五年均新增≥15GW,2030年累计130GW。-公司控股已投运海风3.5GW,在建+核准2.8GW,全部位于江苏、浙江、上海近海,占长三角已批项目总量约18%,弹性第一梯队。
(2)光伏:10月21日公告奉贤1#海上光伏400MW组件选型BC路线,隆基已供货,形成“海风+海光”双轮。
(3)国企三资:深圳、广州等地推进国有资金整合,公司最终控制人国家电投集团,平台融资与资源获取能力强化。

2. 基本面
- 2024H1绿电装机占比60.4%,海风单位净利约为煤电2.3倍,2025-2027年新增项目无国补,绿电溢价+省补模式下毛利率有望维持45%以上。
- 2026-2027年计划再开工海风1.2-1.5GW,对应增量净利9-11亿元,占2023年归母净利(18.5亿元)50%+。
- 负债率73%,低于行业平均,且75%为长期低息项目贷,加息周期中财务费用可控。

总结
政策把“十五五”海风年装机下限从预期10GW提至15GW→资金锁定长三角资源最厚的上海电力→公司3.5GW在运+2.8GW在建项目提供可测算业绩增量→轻筹码+融资盘低位,10月24日一次性涨停兑现政策弹性。" }, + "凤竹纺织": { stock_code: "600493", rise_rate: 10.05, trade_date: "2025-12-08", reason: "基于所提供的舆情信息,对凤竹纺织(600493)在2025年12月8日股价上涨10.05%的原因进行详细分析如下:\n\n### 一、 核心驱动力分析:区域政策利好引发的板块效应\n\n本次股价上涨最直接、最主要的驱动力源于区域性重磅政策的发布,以及由此引发的“福建本地股”板块整体的强势行情。\n\n1. **政策催化剂:《厦门市“十五五”规划建议》发布**\n * 根据财联社等多篇新闻报道,2025年12月8日,《中共厦门市委关于制定厦门市国民经济和社会发展第十五个五年规划的建议》正式发布。\n * 该规划建议中明确提出“加快打造两岸共同市场”、“优化涉台营商环境,探索厦台货物自由进出、开放准入的经贸制度,建设两岸标准共通先行城市”等具体措施。\n * 这一政策预期极大地提升了市场对厦门市乃至整个福建省相关上市公司未来业务发展的想象空间,特别是涉及物流、贸易、港口及相关配套产业的企业。\n\n2. **板块联动与市场情绪共振**\n * 在上述政策利好的刺激下,市场资金迅速涌入“福建本地股”板块。复盘笔记明确指出,“福建本地股”是当日最强主线之一,涨停个股数量众多。\n * 新闻报道中明确列出,在午后拉升行情中,“此前红相股份、凤竹纺织、实达集团、东百集团等十余股涨停”。复盘认知也显示,凤竹纺织位列当日福建本地股涨停名单之中。\n * 这种板块性的普涨行情具有极强的联动效应。凤竹纺织作为注册地在福建的上市公司,尽管其主营业务与政策的直接关联度可能并非最强,但依然被市场纳入“福建”概念股的范畴,从而获得大量跟风资金的追捧,实现了涨停。投资者论坛中“福建板块是所有板块中涨停板数最多的板块”、“福建板块补涨”等言论,印证了当时市场资金围绕该板块进行炒作的火热情绪。\n\n### 二、 辅助性因素分析\n\n除了核心的政策驱动外,一些行业层面的积极因素和市场传闻也为股价上涨提供了额外的动力。\n\n1. **行业基本面回暖预期**\n * 同日发布的《中国纺织品进出口商会》信息显示,11月纺织服装出口数据呈现回暖迹象。具体表现为:出口降幅较上月收窄7个百分点,其中纺织品出口恢复同比增长1%。\n * 虽然纺织服装行业仍面临挑战,但边际改善的数据提振了市场对整个行业基本面的预期。这一积极背景为作为纺织行业上市公司的凤竹纺织提供了一定的基本面支撑,减少了多头资金的后顾之忧,增强了其与板块利好叠加的上涨动能。\n\n2. **市场传闻与概念预期**\n * 投资者论坛的讨论中,存在一些关于公司潜在利好的传闻。例如,有言论提及“这个公司一直在说重大重组.保险资产注入”以及“晋江有储存一块地皮,应该将来很值钱”。\n * **需要明确指出的是,这些信息均属于未经证实的市场传闻,并非公开的官方公告。** 然而,在板块行情火热、市场情绪高涨的背景下,此类题材传闻容易吸引短线投机资金的关注,进一步助推股价的上涨。它们为炒作提供了额外的“故事”和想象空间。\n\n### 三、 辨析:非直接相关因素\n\n在舆情信息中,部分热点新闻与凤竹纺织的上涨并无直接关联,需要进行甄别。\n\n1. **“拓竹”与AI建模的新闻:** 该新闻中的主体“拓竹makerlab”是一家涉及3D打印和AI建模的公司,与凤竹纺织在业务上无任何关系。尽管名称中均有“竹”字,但这属于典型的名称混淆,并非股价上涨的驱动因素。\n2. **其他板块热点(如算力硬件、商业航天、人形机器人等):** 复盘笔记中提及的多个强势板块是当日市场的其他主线,推动了大盘整体成交放量。这些板块的繁荣为市场提供了活跃的交易环境,但并非凤竹纺织此次上涨的直接原因。\n\n### 结论\n\n凤竹纺织(600493)在2025年12月8日的涨停,是一次典型的由**区域政策利好引爆、板块联动效应主导、叠加行业基本面改善预期及市场传闻助推**的行情。\n\n其核心逻辑在于:《厦门市“十五五”规划建议》的发布,激活了整个“福建本地股”的投资热情,形成了强大的板块做多合力。凤竹纺织作为板块内的一员,顺势分享了这一政策红利。纺织出口数据的回暖为其提供了基本面的有利背景,而市场中流传的重组、资产价值等传闻则可能放大了其短期的投机吸引力。公司的基本面(如公告和主营信息“暂无”)在当日股价的决定中并非主导因素,市场情绪和题材炒作是推动其股价大幅上涨的关键。" }, + "漳州发展": { stock_code: "000753", rise_rate: 6.08, trade_date: "2025-10-31", reason: "好的,作为一名资深投资研究员,我将基于您提供的舆情信息,对漳州发展(000753)在2025年10月31日上涨6.08%的原因进行详细分析。\n\n---\n\n### **漳州发展(000753)股价异动分析报告**\n\n**报告日期:** 2025年11月1日\n**分析标的:** 漳州发展 (000753)\n**异动情况:** 2025年10月31日,股价上涨6.08%\n\n#### **一、 核心结论**\n\n漳州发展当日股价上涨,**主要驱动力并非源于公司自身发布利好公告或核心业务突破,而是在于市场整体情绪乐观的背景下,由福建本地股龙头“平潭发展”的强势表现所引发的区域性板块联动效应和概念性炒作。** 资金可能对漳州发展潜在的业务转型预期(如数字经济、城市更新等)进行了短线博弈,但缺乏基本面的直接支撑。\n\n#### **二、 详细原因剖析**\n\n在分析过程中,我们注意到舆情信息中关于漳州发展自身的“主营”和“公告”均为“暂无”,因此,我们必须从宏观、行业和市场层面寻找间接关联。\n\n**1. 市场层面:多重宏观利好政策叠加,营造了积极的市场氛围**\n\n2025年10月31日,市场被一系列重磅的政策信号和权威媒体解读所包围,共同构筑了一个非常乐观的宏观背景:\n\n* **“十五五”规划定调高质量发展:** 《金融时报》评论员文章及多家媒体均聚焦于“十五五”规划建议,核心关键词包括“加快建设金融强国”、“资本市场高质量发展”、“服务新质生产力”、“吸引中长期资金入市”。这极大地提振了市场信心,为风险偏好的提升奠定了基础。\n* **数字经济与智慧城市政策出炉:** 国家发改委等部门印发《深化智慧城市发展推进全域数字化转型行动计划》,提出利用数字技术改造老旧厂区、街区,发展数据产业等。这为市场提供了新的、有想象空间的题材方向。\n* **促进生活性服务业发展:** 国家发改委表示将推出促进生活性服务业发展的“硬招和实招”,利好消费相关板块。\n* **绿色贸易与房地产高质量发展:** 商务部拓展绿色贸易、住建部推动房地产高质量发展等政策,覆盖了国民经济的多个重要领域,形成了全面的政策暖风。\n\n**分析:** 在这样一个“政策市”特征明显的交易日,市场整体情绪高涨,资金乐于寻找各种与政策相关的标的进行布局。这为小市值、题材弹性大的股票提供了上涨的土壤。\n\n**2. 板块层面:福建本地股爆发,形成强烈的区域板块联动(核心驱动力)**\n\n这是解释漳州发展此次异动最直接、最关键的因素。\n\n* **引爆点:** **平潭发展(000592)** 于10月30日晚间发布了极具震撼力的三季报,第三季度净利润同比增长**1970.63%**。这是一个超常规的增长,迅速引爆了市场对其的关注。\n* **龙头效应形成:** 10月31日盘中,平潭发展股价“11天8板”,成为市场绝对的高度板龙头和人气核心。舆情信息明确指出:【福建本地股再度拉升 平潭发展11天8板,福龙马、福建金森2连板...等跟涨】。\n* **溢出效应:** 在A股市场,当一个区域板块的龙头股持续暴涨时,会形成强烈的“赚钱效应”,资金会自发地在同一地区寻找其他形态相似、市值相近、名称中带有地域特征(如“福建”、“厦门”、“漳州”)的股票进行挖掘和攻击,以期复制龙头的上涨轨迹。这种行为被称为**“板块联动”或“地图行情”**。\n* **漳州发展的定位:** 漳州发展(000753),股票名称中明确带有“漳州”,而漳州市是福建省下辖的地级市。在平潭发展(注册地位福建福州)引领的“福建本地股”行情中,漳州发展自然成为了资金关注的潜在标的之一。\n\n**分析:** 漳州发展的上涨,本质上是一次“跟风”行为。它搭上了“福建本地股”这班快车。其涨幅(6.08%)虽不及龙头和部分跟风股,但也清晰反映了这种区域板块的资金溢出效应。\n\n**3. 概念层面:对潜在业务转型的市场猜测与博弈**\n\n尽管主营业务暂无,但结合当日的宏观政策,市场资金可能对漳州发展进行了以下合理推测:\n\n* **智慧城市与城市更新概念:** 国家发改委推动的“智慧城市”和“老旧厂区改造”计划,通常由地方性国企或城投平台公司具体执行。漳州发展作为漳州市重要的上市平台,市场可能猜想其未来有机会参与到当地的数字化转型、城市更新项目中,从而分享政策红利。\n* **资本市场改革受益概念:** “十五五”规划强调提高直接融资比重,健全资本市场功能。对于漳州发展这样的地方性上市平台,未来在资本运作、并购重组、引入战略投资者等方面可能会拥有更便利的政策环境。\n\n**分析:** 这些猜测目前缺乏公司公告的证实,属于典型的“预期博弈”。在强势的市场情绪和板块效应下,这些模糊的、潜在的利好足以成为短线资金入场炒作的理由。\n\n#### **三、 结论与风险提示**\n\n**结论:**\n漳州发展在2025年10月31日的上涨,是一次由**宏观政策利好(催化剂)**、**福建本地股龙头暴涨(导火索)**、**区域性板块联动(核心驱动)**以及**资金对潜在概念的短线博弈(助推器)**共同作用的结果。其股价上涨与公司自身基本面关联度极低,主要体现了市场的交易行为特征。\n\n**风险提示:**\n1. **炒作性质浓厚:** 本次上涨缺乏坚实的基本面支撑,主要由情绪和资金驱动,可持续性存疑。一旦龙头股平潭发展出现滞涨或回调,整个福建板块的炒作情绪可能迅速降温。\n2. **基本面与股价脱节:** 投资者需高度警惕,在公司未公布具体经营数据、未明确转型方向前,股价上涨可能脱离内在价值。\n3. **信息不对称风险:** “主营: 暂无”也提醒投资者,对公司的了解有限。投资决策应基于对公司基本面信息的充分掌握,而非单纯的板块跟随。\n\n**投资建议:**\n建议投资者保持理性,切勿盲目追高。应密切关注公司后续发布的公告,特别是关于主营业务的说明、三季度业绩报告(如有)以及是否有参与“智慧城市”、“城市更新”等具体项目的计划。在基本面信息明朗前,宜以观望为主,审慎评估投资风险。" }, + "海峡创新": { stock_code: "300300", rise_rate: 6.79, trade_date: "2025-10-23", reason: "好的,作为一名资深投资研究者,我将基于您提供的舆情信息,对海峡创新(300300)在2025年10月23日上涨6.79%的原因进行尽可能详细的分析。\n\n---\n\n### **海峡创新(300300) 2025-10-23 涨幅分析报告**\n\n**核心结论:**\n海峡创新在10月23日的显著上涨,并非由公司自身基本面或突发利好公告驱动,而是一次典型的 **“宏观政策催化 + 区域产业利好”** 双重叠加下的 **题材概念炒作**。上涨的直接动因是福建本地股板块的整体异动,而其背后的深层逻辑则源于国家对科技创新的最高定调,与厦门市一项50亿科创基金的落地形成了完美的共振。\n\n---\n\n### **多层次驱动因素拆解**\n\n我们将从四个层面来解构此次上涨的逻辑链条:**宏观政策面、区域产业面、市场情绪面、以及公司自身特质。**\n\n#### **1. 宏观政策面:顶层设计为“科技创新”提供最强背书 (根本驱动力)**\n\n在股价异动当天及前后,国家层面释放了极为密集和高级别的政策信号,为整个科技板块营造了前所未有的积极预期。\n\n* **最高层级定调:** `2025-10-23T22:36:55` 新闻提到,**党的二十届四中全会**强调,“十五五”时期必须以“改革创新”为根本动力。这是国家意志的体现,为未来五年的经济发展指明了方向,即科技创新是核心引擎。\n* **具体执行路径:** `2025-10-24T11:12:08` 和 `2025-10-24T11:14:03` 的新闻进一步细化了这一方向。**科技部部长阴和俊**明确指出,要“推动科技创新和产业创新深度融合”,并特别提到了 **“培育壮大科技领军企业”**。这向市场传递了明确信号:国家将从资金、政策上大力支持真正的科技企业。\n* **市场深度解读:** `2025-10-24T09:55:40` 的 **兴证策略研报** 对“十五五”规划的新增量进行了精准解读,反复强调 **“原始创新”、“关键核心技术攻关”、“科技创新与产业创新深度融合”**。这类头部券商的策略报告是机构资金的重要参考,它将宏观政策转化为可投资的具体方向,进一步强化了市场对科技主线的共识。\n\n**小结:** 二十届四中全会是“发令枪”,科技部的发布会是“行动路线图”,而券商的深度解读则是“冲锋号”。这一系列组合拳共同构建了一个“科技创新为王”的宏大叙事背景,为所有沾边“科技”、“创新”的股票提供了估值溢价的基础。\n\n#### **2. 区域产业面:厦门50亿科创基金是“导火索” (直接催化剂)**\n\n如果说宏观政策是“天时”,那么厦门的区域性产业政策落地就是“地利”,是点燃海峡创新这根“引线”的直接火花。\n\n* **精准打击:** `2025-10-22T16:18:52` 的新闻至关重要——**厦门市科创风投合伙企业(有限合伙)成立,出资额高达50亿人民币**。这个时间点(股价异动前一天)和规模(50亿巨资)都具有极强的冲击力。\n* **关联性极强:**\n 1. **地域关联:** 海峡创新是福建上市公司,而厦门是福建省内经济和科技创新的桥头堡。厦门的重大利好,市场会优先映射到本地的上市公司上。\n 2. **名称关联:** 该基金的执行事务合伙人是 **“厦门海峡科技创新股权投资有限公司”**。注意,这里出现了 **“海峡”** 和 **“科技创新”** 两个关键词,与“海峡创新”的股票名称形成了高度匹配。在A股市场,这种名称上的“巧合”极易引发资金的联想和追捧。\n 3. **主题关联:** 基金的定位是“科创风投”,完美契合了上文提到的国家大政方针。市场会自然地预期:这家50亿的大基金,未来肯定会投资福建/厦门本地的科技公司,而“海峡创新”无疑是名单上最显眼的候选者之一。\n\n**小结:** 厦门科创基金的成立,是宏观政策在区域层面的一个具体“落子”。它将抽象的“支持科技创新”变成了真金白银的“50亿”,且其执行方的名称与海峡创新高度绑定,构成了这次炒作最直接、最有力的催化剂。\n\n#### **3. 市场情绪面:板块联动与题材扩散 (放大器效应)**\n\n有了“天时”和“地利”,市场的“人和”即情绪面,将事件的影响进一步放大。\n\n* **板块性运动:** `2025-10-23T14:30:14` 的新闻实时记录了盘面情况:**“A股福建本地股异动拉升”**,平潭发展涨停,海峡创新、厦门港务、漳州发展等跟涨。这表明,资金并非单点攻击海峡创新,而是对整个“福建+XX”概念的板块性买入。这种板块联动能形成合力,吸引更多跟风盘,推动股价持续上行。\n* **科技题材升温:** 同日的 `2025-10-23T18:54:51` 关于宁波机器人创新中心、`2025-10-23T22:34:23` 关于AR眼镜市场第一的新闻,虽然与海峡创新无直接关系,但它们共同营造了一个“科技创新大爆发”的市场氛围。AR、机器人、AI等都属于前沿科技领域,这些新闻的存在让“科技创新”这个题材显得更加炙手可热,从而加强了主线的炒作强度。\n\n**小结:** 市场情绪起到了“放大器”的作用。福建本地股的板块异动形成了协同效应,而其他科技热点新闻则持续为“科技创新”主线提供热度,使得海峡创新在风口上被越推越高。\n\n#### **4. 公司自身特质:理想的“概念载体” (必要条件)**\n\n最后,分析离不开公司本身。海峡创新之所以能被选中,与其自身特质密不可分。\n\n* **名称优势:** “海峡”+“创新”,完美契合了“海峡两岸融合发展”和“国家科技创新”两大时代主题。名字是其最核心的“资产”之一。\n* **主营业务模糊:** 资料显示 `主营: 暂无`。这通常是公司处于转型期或主业不振的表现。但在概念炒作中,这反而成了一个“优势”。因为一个清晰的传统主业会限制想象空间,而“暂无主营”或“主业模糊”的公司就像一张白纸,市场可以把最热门的故事(如科创、AI、元宇宙等)画在上面,想象空间无限。\n* **无公告利空:** `公告: 暂无`。在炒作窗口期,没有来自公司的利空消息(如业绩预警、减持、监管问询等)为资金提供了“安全”的炒作环境。\n\n**小结:** 海峡创新凭借其绝佳的名称、模糊的主营以及干净的公告面,成为了承载“福建+科技”这一宏大叙事的完美“概念载体”。\n\n---\n\n### **总结与展望**\n\n**综合来看,海峡创新(300300)在2025年10月23日的上涨是一次逻辑链条清晰的题材炒作:**\n\n**“十五五”顶层设计(强调科技创新) → 厦门落地50亿科创基金(地域+主题双重催化) → 市场挖掘“海峡创新”作为完美概念载体 → 资金涌入福建本地股板块形成联动效应 → 股价大幅上涨。**\n\n**风险提示与后续展望:**\n\n1. **上涨持续性存疑:** 本次上涨由情绪和预期驱动,而非公司内生业绩增长。一旦热度退潮或没有进一步的实质性利好(如公司公告与厦门科创基金有具体合作、获得重大项目订单等),股价很可能面临回调压力。\n2. **关注基本面验证:** 投资者后续需密切关注公司的公告和财报,看其是否能将市场的“预期”转化为实际的“业绩”。公司是否真的有能力参与并受益于厦门乃至全国的科创大潮,是决定其股价能否“去伪存真”、长期走高的关键。\n3. **警惕高位波动:** 作为纯概念标的,其股价波动性会非常大,追高风险极高,更适合风险偏好极高的短线投资者参与。\n\n这份分析报告旨在客观、全面地还原股价波动背后的复杂因素,为投资决策提供参考。" }, + "福建水泥": { stock_code: "600802", rise_rate: 9.99, trade_date: "2025-11-28", reason: "好的,作为一位资深投资研究者,我将基于您提供的舆情信息,对福建水泥(600802)在2025年11月28日的涨停原因进行一次详细、结构化的深度分析。\n\n---\n\n### **福建水泥(600802) 2025-11-28涨停原因深度分析报告**\n\n**核心结论:**\n福建水泥的此次涨停,并非源于公司自身的基本面突变或特定公告,而是一次由**省级重磅政策预期引爆,地缘政治消息面升温,叠加市场情绪和资金共同推动的典型区域性主题炒作**。其核心逻辑链条为:**福建“十五五”规划建议发布(核心驱动力)→ 市场解读为区域发展及基建利好(特别是涉台概念)→ “福建板块”成为市场热点(板块效应)→ 福建水泥作为区域性国企+基础建材龙头,成为资金首选标的(个股逻辑)**。\n\n以下是详细的分层解析:\n\n---\n\n#### **一、 核心驱动因素:政策面的重磅催化**\n\n这是本次行情的根本原因和最直接的导火索。\n\n1. **事件:** 11月28日盘前及早盘,多家媒体密集报道了《中共福建省委关于制定福建省国民经济和社会发展第十五个五年规划的建议》(以下简称“福建十五五规划建议”)的发布。\n2. **关键内容解读:**\n * **持续深化国资国企改革:** 报道中明确提到“实施新一轮国有企业改革专项行动,做强做优做大国有企业和国有资本”、“加快省属国企战略性重组和专业化整合”。福建水泥作为福建省属国资控股的上市公司,是这一政策的直接受益者。市场预期公司可能在资产注入、业务整合、提质增效等方面获得政策支持,从而改善基本面和估值。\n * **探索海峡两岸融合发展新路:** 这是本次政策预期中最具想象力的部分。规划中提到“着力在探索海峡两岸融合发展新路上迈出更大步伐”,并涉及“推进闽台投资贸易自由化便利化”、“深化闽台产业合作”。\n * **重大工程项目预期:** 规划中“统筹推进重大战略任务、重大改革举措、重大工程项目”的表述,虽然未点名具体项目,但为市场留下了巨大的想象空间。**投资者论坛中“福建公布十五五规划要求建设对台路桥隧通道,需海量福建水泥”的言论,正是这种市场预期的集中体现。** 尽管该说法可能存在夸大成分,但它精准地捕捉到了市场的炒作核心逻辑:即重大基建项目将直接提振水泥需求。\n\n3. **逻辑传导:**\n `福建十五五规划建议发布` → `市场预期福建将迎来新一轮发展高峰,尤其是国企和涉台经济` → `预期将催生大量基础设施建设项目` → `作为福建本地水泥龙头,福建水泥被认为是确定性最高的受益标的之一` → `资金蜂拥而入,股价涨停`\n\n---\n\n#### **二、 次要催化剂:地缘政治与情绪助燃**\n\n如果说政策是“火种”,那么地缘政治消息就是“助燃剂”,进一步强化了“福建”这一地理标签的热度。\n\n1. **事件:** 11月28日上午,财联社、央视新闻等多家媒体反复报道“福建海警位金门附近海域依法开展常态执法巡查”。同时,前一日(11月27日)国防部关于“福建舰”的强硬回应也成为市场热议话题。\n2. **作用机制:**\n * **提升区域关注度:** 这些消息在短时间内将市场目光聚焦于“福建”,使得“福建板块”在当日的辨识度极高。\n * **强化主题叙事:** 海警巡航和国防部发言,虽与水泥行业无直接关系,但与“十五五”规划中的“海峡两岸融合发展”形成了内在关联和情绪共振,共同构建了一个“福建在国家战略中地位提升”的宏观叙事,为板块炒作提供了额外的情绪动力。\n\n---\n\n#### **三、 市场行为与资金面:板块轮动与情绪博弈**\n\n在消息面的催化下,市场资金的行为是推动股价上涨的直接动力。\n\n1. **板块效应显著:** 从新闻中可以清晰看到,当天是“福建板块”整体性的异动。厦工股份、漳州发展、福建高速、中国武夷等一众福建本地股联动上涨。这种“一荣俱荣”的板块效应,吸引了大量追涨和主题投资型资金入场。\n2. **个股逻辑的契合度:** 在众多福建股中,福建水泥具备几个优势,使其成为资金的“核心攻击目标”:\n * **行业属性:** 水泥是基础建设的“粮食”,与基建投资的关联度最直接、最易懂。\n * **股性:** 作为历史较久的国企股,股本适中,股价绝对值低,易于拉升和吸引散户跟风。\n * **逻辑纯粹:** 相较于其他公司,受益于“大基建”的逻辑最为简单明了。\n3. **资金面的博弈(从论坛信息反推):**\n * **主力资金介入明显:** 新闻报道“直线涨停”,通常是主动性大单快速扫货的结果。\n * **散户情绪分化与博弈:** 投资者论坛中呈现两极分化。一方面是极度乐观的“起飞”、“连板”呼声,甚至有“对标西藏天路9个板”的激进预期。另一方面是“融资净偿还”、“害怕主力又搞上周几个跌停板”的恐惧与谨慎。这种分化恰恰说明了市场筹码在快速换手和重新定价,先知先觉或更激进的资金在买入,而被套牢或风险偏好低的资金在卖出。\n * **低换手率的解读:** 论坛中有观点称“以前这股至少是18%上下的换手,今天这换手说明洗干净了”。如果当日换手率确实不高,可能意味着主力筹码锁定良好,卖压较轻,为后续的连续拉升创造了条件。但也可能意味着场外资金追高意愿不强,一旦情绪逆转,流动性缺失可能导致快速下跌。\n\n---\n\n#### **四、 公司基本面与潜在关联**\n\n虽然本次上涨与即时基本面无关,但其炒作逻辑最终能否落地,仍需回归基本面。\n\n* **主营:** 信息显示“暂无”,但公开资料可知福建水泥主营水泥及熟料的生产和销售。这决定了它是典型的周期性行业,景气度与房地产、基建投资高度相关。\n* **潜在关联:** “十五五”规划中关于新基建、新能源(如宁德时代的动力电池回收会议也暗示了福建在新能源产业的地位)等领域的投资,同样会带动水泥需求。这为政策预期向公司业绩转化提供了理论上的可能性。\n\n---\n\n#### **五、 风险与不确定性分析(作为研究员必须提示)**\n\n1. **政策兑现风险:** 市场当前交易的更多是“预期”。从规划建议到具体项目落地,再到水泥实际需求增长,存在时间差和巨大的不确定性。特别是“对台路桥隧通道”等宏大构想,短期内难以对业绩产生实质性影响。\n2. **情绪退潮风险:** 主题炒作的典型特征是来得快,去得也快。一旦市场热点转移,或没有新的政策利好跟进,股价极易快速回落,导致追高者被套,正如论坛中投资者所担心的“A杀”风险。\n3. **行业周期风险:** 水泥行业目前仍面临产能过剩、房地产下行周期等挑战。仅凭区域性的政策预期,难以扭转整个行业的景气度下行压力。\n4. **交易层面风险:** 论坛中提及的“融资净偿还”数据显示,杠杆资金可能在撤退,这可能是风险的信号。而主力资金的拉升行为也具有不可预测性,随时可能因获利了结而停止。\n\n### **总结**\n\n福建水泥在2025年11月28日的涨停,是一次教科书式的“A股政策驱动型主题炒作”。**福建“十五五”规划建议的发布是无可争议的核心驱动力**,它点燃了市场对福建区域发展和国企改革的宏大想象。地缘政治新闻的“凑巧”配合,进一步强化了这一主题。最终,在资金合力的推动下,福建水泥作为逻辑最顺、辨识度最高的标的,被推上涨停。\n\n**对于投资者而言,** 认识到这一点至关重要。参与此类投资,本质上是在交易“市场的预期和情绪”,而非公司的“内在价值”。因此,必须对政策兑现的难度、情绪退潮的速度以及行业本身的风险有清醒的认识,并做好相应的风险管理和交易纪律。投资者论坛中“对标西藏天路”的呼声,既是短期情绪的兴奋剂,也暗示了市场对于政策红利下牛股的路径依赖,但这背后同样蕴含着巨大不确定性。" } + }; + }, + getRiseAnalysis(stockName) { + return this.riseAnalysis[stockName] ? this.riseAnalysis[stockName].reason : '无近期涨幅分析'; + }, + formatMarkdown(markdown) { + if (!markdown) return ''; + let lines = markdown.split('\n'); + let processedLines = []; + let inList = false; + + lines.forEach(line => { + line = line.trim(); + if (line.match(/^### /)) { + if (inList) { processedLines.push(''); inList = false; } + processedLines.push(`

${line.substring(4)}

`); + } else if (line.match(/^## /)) { + if (inList) { processedLines.push(''); inList = false; } + processedLines.push(`

${line.substring(3)}

`); + } else if (line.match(/^- /)) { + if (!inList) { processedLines.push('
    '); inList = true; } + let content = line.substring(2) + .replace(/\*\*(.*?)\*\*/gim, '$1') + .replace(/\*(.*?)\*/gim, '$1') + .replace(/`(.*?)`/gim, '$1') + .replace(/
    /gim, '
    '); + processedLines.push(`
  • ${content}
  • `); + } else { + if (inList) { processedLines.push('
'); inList = false; } + let content = line + .replace(/\*\*(.*?)\*\*/gim, '$1') + .replace(/\*(.*?)\*/gim, '$1') + .replace(/`(.*?)`/gim, '$1') + .replace(/
/gim, '
'); + if (content.trim() !== '') { // Only add non-empty paragraphs + processedLines.push(`

${content}

`); + } + } + }); + if (inList) { processedLines.push(''); } // Close last list if still open + + return processedLines.join(''); + } + }" x-init="init()"> +
+ + + + + + + + + + + + + +
股票名称股票代码关联描述其他标签涨幅分析
+
+
+
+ +
+ + \ No newline at end of file diff --git a/sql/concept_minute_alert.sql b/sql/concept_minute_alert.sql new file mode 100644 index 00000000..d54cc7ff --- /dev/null +++ b/sql/concept_minute_alert.sql @@ -0,0 +1,68 @@ +-- 概念分钟级异动数据表 +-- 用于存储概念板块的实时异动信息,支持热点概览图表展示 + +CREATE TABLE IF NOT EXISTS concept_minute_alert ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + concept_id VARCHAR(32) NOT NULL COMMENT '概念ID', + concept_name VARCHAR(100) NOT NULL COMMENT '概念名称', + alert_time DATETIME NOT NULL COMMENT '异动时间(精确到分钟)', + alert_type VARCHAR(20) NOT NULL COMMENT '异动类型:surge(急涨)/limit_up(涨停增加)/rank_jump(排名跃升)', + trade_date DATE NOT NULL COMMENT '交易日期', + + -- 涨跌幅相关 + change_pct DECIMAL(10,4) COMMENT '当时涨跌幅(%)', + prev_change_pct DECIMAL(10,4) COMMENT '之前涨跌幅(%)', + change_delta DECIMAL(10,4) COMMENT '涨幅变化量(%)', + + -- 涨停相关 + limit_up_count INT DEFAULT 0 COMMENT '当前涨停数量', + prev_limit_up_count INT DEFAULT 0 COMMENT '之前涨停数量', + limit_up_delta INT DEFAULT 0 COMMENT '涨停变化数量', + + -- 排名相关 + rank_position INT COMMENT '当前涨幅排名', + prev_rank_position INT COMMENT '之前涨幅排名', + rank_delta INT COMMENT '排名变化(负数表示上升)', + + -- 指数位置(用于图表Y轴定位) + index_code VARCHAR(20) DEFAULT '000001.SH' COMMENT '参考指数代码', + index_price DECIMAL(12,4) COMMENT '异动时的指数点位', + index_change_pct DECIMAL(10,4) COMMENT '异动时的指数涨跌幅(%)', + + -- 概念详情 + stock_count INT COMMENT '概念包含股票数', + concept_type VARCHAR(20) DEFAULT 'leaf' COMMENT '概念类型:leaf/lv1/lv2/lv3', + + -- 额外信息(JSON格式,存储涨停股票列表等) + extra_info JSON COMMENT '额外信息', + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + -- 索引 + INDEX idx_trade_date (trade_date), + INDEX idx_alert_time (alert_time), + INDEX idx_concept_id (concept_id), + INDEX idx_alert_type (alert_type), + INDEX idx_trade_date_time (trade_date, alert_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='概念分钟级异动数据表'; + + +-- 创建指数分时快照表(用于异动时获取指数位置) +CREATE TABLE IF NOT EXISTS index_minute_snapshot ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + index_code VARCHAR(20) NOT NULL COMMENT '指数代码', + trade_date DATE NOT NULL COMMENT '交易日期', + snapshot_time DATETIME NOT NULL COMMENT '快照时间', + + price DECIMAL(12,4) COMMENT '指数点位', + open_price DECIMAL(12,4) COMMENT '开盘价', + high_price DECIMAL(12,4) COMMENT '最高价', + low_price DECIMAL(12,4) COMMENT '最低价', + prev_close DECIMAL(12,4) COMMENT '昨收价', + change_pct DECIMAL(10,4) COMMENT '涨跌幅(%)', + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + + UNIQUE KEY uk_index_time (index_code, snapshot_time), + INDEX idx_trade_date (trade_date) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='指数分时快照表'; diff --git a/src/views/StockOverview/components/HotspotOverview/index.js b/src/views/StockOverview/components/HotspotOverview/index.js new file mode 100644 index 00000000..856d752d --- /dev/null +++ b/src/views/StockOverview/components/HotspotOverview/index.js @@ -0,0 +1,539 @@ +/** + * 热点概览组件 + * 展示大盘分时走势 + 概念异动标注 + */ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { + Box, + Card, + CardBody, + Heading, + Text, + HStack, + VStack, + Badge, + Spinner, + Center, + Icon, + Flex, + Spacer, + Tooltip, + useColorModeValue, + Stat, + StatLabel, + StatNumber, + StatHelpText, + StatArrow, + SimpleGrid, +} from '@chakra-ui/react'; +import { FaFire, FaRocket, FaChartLine, FaBolt, FaArrowDown } from 'react-icons/fa'; +import { InfoIcon } from '@chakra-ui/icons'; +import * as echarts from 'echarts'; +import { logger } from '@utils/logger'; + +const HotspotOverview = ({ selectedDate }) => { + const chartRef = useRef(null); + const chartInstance = useRef(null); + const [loading, setLoading] = useState(true); + const [data, setData] = useState(null); + const [error, setError] = useState(null); + + // 颜色主题 + const cardBg = useColorModeValue('white', '#1a1a1a'); + const borderColor = useColorModeValue('gray.200', '#333333'); + const textColor = useColorModeValue('gray.800', 'white'); + const subTextColor = useColorModeValue('gray.600', 'gray.400'); + + // 获取数据 + const fetchData = useCallback(async () => { + setLoading(true); + setError(null); + + try { + const dateParam = selectedDate + ? `?date=${selectedDate.toISOString().split('T')[0]}` + : ''; + const response = await fetch(`/api/market/hotspot-overview${dateParam}`); + const result = await response.json(); + + if (result.success) { + setData(result.data); + } else { + setError(result.error || '获取数据失败'); + } + } catch (err) { + logger.error('HotspotOverview', 'fetchData', err); + setError('网络请求失败'); + } finally { + setLoading(false); + } + }, [selectedDate]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + // 渲染图表 + const renderChart = useCallback(() => { + if (!chartRef.current || !data) return; + + if (!chartInstance.current) { + chartInstance.current = echarts.init(chartRef.current); + } + + const { index, alerts } = data; + const timeline = index.timeline || []; + + // 准备数据 + const times = timeline.map((d) => d.time); + const prices = timeline.map((d) => d.price); + const volumes = timeline.map((d) => d.volume); + const changePcts = timeline.map((d) => d.change_pct); + + // 计算Y轴范围 + const priceMin = Math.min(...prices.filter(Boolean)); + const priceMax = Math.max(...prices.filter(Boolean)); + const priceRange = priceMax - priceMin; + const yAxisMin = priceMin - priceRange * 0.1; + const yAxisMax = priceMax + priceRange * 0.2; // 上方留更多空间给标注 + + // 准备异动标注 - 按重要性排序,限制显示数量 + const sortedAlerts = [...alerts] + .sort((a, b) => (b.importance_score || 0) - (a.importance_score || 0)) + .slice(0, 15); // 最多显示15个标注,避免图表过于密集 + + const markPoints = sortedAlerts.map((alert) => { + // 找到对应时间的价格 + const timeIndex = times.indexOf(alert.time); + const price = timeIndex >= 0 ? prices[timeIndex] : (alert.index_price || priceMax); + + // 根据异动类型设置颜色和符号 + let color = '#ff6b6b'; + let symbol = 'pin'; + let symbolSize = 35; + + // 暴涨 + if (alert.alert_type === 'surge_up' || alert.alert_type === 'surge') { + color = '#ff4757'; + symbol = 'triangle'; + symbolSize = 30 + Math.min((alert.importance_score || 0.5) * 20, 15); // 根据重要性调整大小 + } + // 暴跌 + else if (alert.alert_type === 'surge_down') { + color = '#2ed573'; + symbol = 'path://M0,0 L10,0 L5,10 Z'; // 向下三角形 + symbolSize = 30 + Math.min((alert.importance_score || 0.5) * 20, 15); + } + // 涨停增加 + else if (alert.alert_type === 'limit_up') { + color = '#ff6348'; + symbol = 'diamond'; + symbolSize = 28; + } + // 排名跃升 + else if (alert.alert_type === 'rank_jump') { + color = '#3742fa'; + symbol = 'circle'; + symbolSize = 25; + } + + // 格式化标签 - 简化显示 + let label = alert.concept_name; + // 截断过长的名称 + if (label.length > 8) { + label = label.substring(0, 7) + '...'; + } + + // 添加变化信息 + const changeDelta = alert.change_delta; + if (changeDelta) { + const sign = changeDelta > 0 ? '+' : ''; + label += `\n${sign}${changeDelta.toFixed(1)}%`; + } + + return { + name: alert.concept_name, + coord: [alert.time, price], + value: label, + symbol: symbol, + symbolSize: symbolSize, + itemStyle: { + color: color, + borderColor: '#fff', + borderWidth: 1, + shadowBlur: 3, + shadowColor: 'rgba(0,0,0,0.2)', + }, + label: { + show: true, + position: alert.alert_type === 'surge_down' ? 'bottom' : 'top', // 暴跌标签在下方 + formatter: '{b}', + fontSize: 9, + color: textColor, + backgroundColor: alert.alert_type === 'surge_down' + ? 'rgba(46, 213, 115, 0.9)' + : 'rgba(255,255,255,0.9)', + padding: [2, 4], + borderRadius: 2, + borderColor: color, + borderWidth: 1, + }, + // 存储额外信息用于 tooltip + alertData: alert, + }; + }); + + // 渐变色 - 根据涨跌 + const latestChangePct = changePcts[changePcts.length - 1] || 0; + const areaColorStops = latestChangePct >= 0 + ? [ + { offset: 0, color: 'rgba(255, 77, 77, 0.4)' }, + { offset: 1, color: 'rgba(255, 77, 77, 0.05)' }, + ] + : [ + { offset: 0, color: 'rgba(34, 197, 94, 0.4)' }, + { offset: 1, color: 'rgba(34, 197, 94, 0.05)' }, + ]; + + const lineColor = latestChangePct >= 0 ? '#ff4d4d' : '#22c55e'; + + const option = { + backgroundColor: 'transparent', + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross', + crossStyle: { + color: '#999', + }, + }, + formatter: function (params) { + if (!params || params.length === 0) return ''; + + const dataIndex = params[0].dataIndex; + const time = times[dataIndex]; + const price = prices[dataIndex]; + const changePct = changePcts[dataIndex]; + const volume = volumes[dataIndex]; + + let html = ` +
+
${time}
+
指数: ${price?.toFixed(2)}
+
涨跌: ${changePct >= 0 ? '+' : ''}${changePct?.toFixed(2)}%
+
成交量: ${(volume / 10000).toFixed(0)}万手
+
+ `; + + // 检查是否有异动 + const alertsAtTime = alerts.filter((a) => a.time === time); + if (alertsAtTime.length > 0) { + html += '
'; + html += '
概念异动:
'; + alertsAtTime.forEach((alert) => { + const typeLabel = { + surge: '急涨', + surge_up: '暴涨', + surge_down: '暴跌', + limit_up: '涨停增加', + rank_jump: '排名跃升', + }[alert.alert_type] || alert.alert_type; + const typeColor = alert.alert_type === 'surge_down' ? '#2ed573' : '#ff6b6b'; + const delta = alert.change_delta ? ` (${alert.change_delta > 0 ? '+' : ''}${alert.change_delta.toFixed(2)}%)` : ''; + const zscore = alert.zscore ? ` Z=${alert.zscore.toFixed(1)}` : ''; + html += `
• ${alert.concept_name} (${typeLabel}${delta}${zscore})
`; + }); + html += '
'; + } + + return html; + }, + }, + legend: { + show: false, + }, + grid: [ + { + left: '8%', + right: '3%', + top: '8%', + height: '55%', + }, + { + left: '8%', + right: '3%', + top: '70%', + height: '20%', + }, + ], + xAxis: [ + { + type: 'category', + data: times, + axisLine: { lineStyle: { color: '#ddd' } }, + axisLabel: { + color: subTextColor, + fontSize: 10, + interval: Math.floor(times.length / 6), + }, + axisTick: { show: false }, + splitLine: { show: false }, + }, + { + type: 'category', + gridIndex: 1, + data: times, + axisLine: { lineStyle: { color: '#ddd' } }, + axisLabel: { show: false }, + axisTick: { show: false }, + splitLine: { show: false }, + }, + ], + yAxis: [ + { + type: 'value', + min: yAxisMin, + max: yAxisMax, + axisLine: { show: false }, + axisLabel: { + color: subTextColor, + fontSize: 10, + formatter: (val) => val.toFixed(0), + }, + splitLine: { + lineStyle: { color: '#eee', type: 'dashed' }, + }, + // 右侧显示涨跌幅 + axisPointer: { + label: { + formatter: function (params) { + const pct = ((params.value - index.prev_close) / index.prev_close) * 100; + return `${params.value.toFixed(2)} (${pct >= 0 ? '+' : ''}${pct.toFixed(2)}%)`; + }, + }, + }, + }, + { + type: 'value', + gridIndex: 1, + axisLine: { show: false }, + axisLabel: { show: false }, + splitLine: { show: false }, + }, + ], + series: [ + // 分时线 + { + name: '上证指数', + type: 'line', + data: prices, + smooth: true, + symbol: 'none', + lineStyle: { + color: lineColor, + width: 1.5, + }, + areaStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, areaColorStops), + }, + markPoint: { + symbol: 'pin', + symbolSize: 40, + data: markPoints, + animation: true, + }, + }, + // 成交量 + { + name: '成交量', + type: 'bar', + xAxisIndex: 1, + yAxisIndex: 1, + data: volumes.map((v, i) => ({ + value: v, + itemStyle: { + color: changePcts[i] >= 0 ? 'rgba(255, 77, 77, 0.6)' : 'rgba(34, 197, 94, 0.6)', + }, + })), + barWidth: '60%', + }, + ], + }; + + chartInstance.current.setOption(option, true); + }, [data, textColor, subTextColor]); + + // 数据变化时重新渲染 + useEffect(() => { + if (data) { + renderChart(); + } + }, [data, renderChart]); + + // 窗口大小变化时重新渲染 + useEffect(() => { + const handleResize = () => { + if (chartInstance.current) { + chartInstance.current.resize(); + } + }; + + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + if (chartInstance.current) { + chartInstance.current.dispose(); + chartInstance.current = null; + } + }; + }, []); + + // 异动类型标签 + const AlertTypeBadge = ({ type, count }) => { + const config = { + surge: { label: '急涨', color: 'red', icon: FaBolt }, + surge_up: { label: '暴涨', color: 'red', icon: FaBolt }, + surge_down: { label: '暴跌', color: 'green', icon: FaArrowDown }, + limit_up: { label: '涨停', color: 'orange', icon: FaRocket }, + rank_jump: { label: '排名跃升', color: 'blue', icon: FaChartLine }, + }; + + const cfg = config[type] || { label: type, color: 'gray', icon: FaFire }; + + return ( + + + + {cfg.label} + {count} + + + ); + }; + + if (loading) { + return ( + + +
+ + + 加载热点概览数据... + +
+
+
+ ); + } + + if (error) { + return ( + + +
+ + + {error} + +
+
+
+ ); + } + + if (!data) { + return null; + } + + const { index, alerts, alert_summary } = data; + + return ( + + + {/* 头部信息 */} + + + + + 热点概览 + + + + + + + + + {/* 指数统计 */} + + + {index.name} + = 0 ? 'red.500' : 'green.500'} + > + {index.latest_price?.toFixed(2)} + + + = 0 ? 'increase' : 'decrease'} /> + {index.change_pct?.toFixed(2)}% + + + + + 最高 + + {index.high?.toFixed(2)} + + + + + 最低 + + {index.low?.toFixed(2)} + + + + + 异动次数 + + {alerts.length} + + + + + {/* 异动类型统计 */} + {alerts.length > 0 && ( + + {(alert_summary.surge_up > 0 || alert_summary.surge > 0) && ( + + )} + {alert_summary.surge_down > 0 && ( + + )} + {alert_summary.limit_up > 0 && ( + + )} + {alert_summary.rank_jump > 0 && ( + + )} + + )} + + {/* 图表 */} + + + {/* 无异动提示 */} + {alerts.length === 0 && ( +
+ + 当日暂无概念异动数据 + +
+ )} +
+
+ ); +}; + +export default HotspotOverview; diff --git a/src/views/StockOverview/index.js b/src/views/StockOverview/index.js index e98686b1..13fc3cd9 100644 --- a/src/views/StockOverview/index.js +++ b/src/views/StockOverview/index.js @@ -53,6 +53,7 @@ import { SearchIcon, CloseIcon, ArrowForwardIcon, TrendingUpIcon, InfoIcon, Chev import { FaChartLine, FaFire, FaRocket, FaBrain, FaCalendarAlt, FaChevronRight, FaArrowUp, FaArrowDown, FaChartBar, FaTag, FaLayerGroup, FaBolt } from 'react-icons/fa'; import ConceptStocksModal from '@components/ConceptStocksModal'; import TradeDatePicker from '@components/TradeDatePicker'; +import HotspotOverview from './components/HotspotOverview'; import { BsGraphUp, BsLightningFill } from 'react-icons/bs'; import * as echarts from 'echarts'; import { logger } from '../../utils/logger'; @@ -840,6 +841,11 @@ const StockOverview = () => { )} + {/* 热点概览 - 大盘走势 + 概念异动 */} + + + + {/* 今日热门概念 */}