diff --git a/concept_alert_20251208.log b/concept_alert_20251208.log deleted file mode 100644 index dffe4ef2..00000000 --- a/concept_alert_20251208.log +++ /dev/null @@ -1,2823 +0,0 @@ -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 deleted file mode 100644 index 0e8c8f54..00000000 --- a/concept_alert_alpha.py +++ /dev/null @@ -1,1078 +0,0 @@ -#!/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 deleted file mode 100644 index 656199bf..00000000 --- a/concept_alert_alpha_20251208.log +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index dbe50ea0..00000000 --- a/concept_alert_ml.py +++ /dev/null @@ -1,1625 +0,0 @@ -#!/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 deleted file mode 100644 index 6e931a91..00000000 --- a/concept_alert_realtime.py +++ /dev/null @@ -1,1366 +0,0 @@ -#!/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_quota_realtime.py b/concept_quota_realtime.py deleted file mode 100644 index 103b586e..00000000 --- a/concept_quota_realtime.py +++ /dev/null @@ -1,681 +0,0 @@ -#!/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 deleted file mode 100644 index f5c8912e..00000000 --- a/create_tables.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/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/__pycache__/realtime_detector.cpython-310.pyc b/ml/__pycache__/realtime_detector.cpython-310.pyc new file mode 100644 index 00000000..b926f280 Binary files /dev/null and b/ml/__pycache__/realtime_detector.cpython-310.pyc differ diff --git a/ml/backtest_fast.py b/ml/backtest_fast.py new file mode 100644 index 00000000..e1c06254 --- /dev/null +++ b/ml/backtest_fast.py @@ -0,0 +1,859 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +快速融合异动回测脚本 + +优化策略: +1. 预先构建所有序列(向量化),避免循环内重复切片 +2. 批量 ML 推理(一次推理所有候选) +3. 使用 NumPy 向量化操作替代 Python 循环 + +性能对比: +- 原版:5分钟/天 +- 优化版:预计 10-30秒/天 +""" + +import os +import sys +import argparse +import json +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional, Tuple +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__)))) + + +# ==================== 配置 ==================== + +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'] + +CONFIG = { + 'seq_len': 15, # 序列长度(支持跨日后可从 9:30 检测) + 'min_alpha_abs': 0.3, # 最小 alpha 过滤 + 'cooldown_minutes': 8, + 'max_alerts_per_minute': 20, + 'clip_value': 10.0, + # === 融合权重:均衡 === + 'rule_weight': 0.5, + 'ml_weight': 0.5, + # === 触发阈值 === + 'rule_trigger': 65, # 60 -> 65,略提高规则门槛 + 'ml_trigger': 70, # 75 -> 70,略降低 ML 门槛 + 'fusion_trigger': 45, +} + + +# ==================== 规则评分(向量化版)==================== + +def get_size_adjusted_thresholds(stock_count: np.ndarray) -> dict: + """ + 根据概念股票数量计算动态阈值 + + 设计思路: + - 小概念(<10 只):波动大是正常的,需要更高阈值 + - 中概念(10-50 只):标准阈值 + - 大概念(>50 只):能有明显波动说明是真异动,降低阈值 + + 返回各指标的调整系数(乘以基准阈值) + """ + n = len(stock_count) + + # 基于股票数量的调整系数 + # 小概念:系数 > 1(提高阈值,更难触发) + # 大概念:系数 < 1(降低阈值,更容易触发) + size_factor = np.ones(n) + + # 微型概念(<5 只):阈值 × 1.8 + tiny = stock_count < 5 + size_factor[tiny] = 1.8 + + # 小概念(5-10 只):阈值 × 1.4 + small = (stock_count >= 5) & (stock_count < 10) + size_factor[small] = 1.4 + + # 中小概念(10-20 只):阈值 × 1.2 + medium_small = (stock_count >= 10) & (stock_count < 20) + size_factor[medium_small] = 1.2 + + # 中概念(20-50 只):标准阈值 × 1.0 + medium = (stock_count >= 20) & (stock_count < 50) + size_factor[medium] = 1.0 + + # 大概念(50-100 只):阈值 × 0.85 + large = (stock_count >= 50) & (stock_count < 100) + size_factor[large] = 0.85 + + # 超大概念(>100 只):阈值 × 0.7 + xlarge = stock_count >= 100 + size_factor[xlarge] = 0.7 + + return size_factor + + +def score_rules_batch(df: pd.DataFrame) -> Tuple[np.ndarray, List[List[str]]]: + """ + 批量计算规则得分(向量化)- 考虑概念规模版 + + 设计原则: + - 规则作为辅助信号,不应单独主导决策 + - 根据概念股票数量动态调整阈值 + - 大概念异动更有价值,小概念需要更大波动才算异动 + + Args: + df: DataFrame,包含所有特征列(必须包含 stock_count) + Returns: + scores: (n,) 规则得分数组 + triggered_rules: 每行触发的规则列表 + """ + n = len(df) + scores = np.zeros(n) + triggered = [[] for _ in range(n)] + + alpha = df['alpha'].values + alpha_delta = df['alpha_delta'].values + amt_ratio = df['amt_ratio'].values + amt_delta = df['amt_delta'].values + rank_pct = df['rank_pct'].values + limit_up_ratio = df['limit_up_ratio'].values + stock_count = df['stock_count'].values if 'stock_count' in df.columns else np.full(n, 20) + + alpha_abs = np.abs(alpha) + alpha_delta_abs = np.abs(alpha_delta) + + # 获取基于规模的调整系数 + size_factor = get_size_adjusted_thresholds(stock_count) + + # ========== Alpha 规则(动态阈值)========== + # 基准阈值:极强 5%,强 4%,中等 3% + # 实际阈值 = 基准 × size_factor + + # 极强信号 + alpha_extreme_thresh = 5.0 * size_factor + mask = alpha_abs >= alpha_extreme_thresh + scores[mask] += 20 + for i in np.where(mask)[0]: triggered[i].append('alpha_extreme') + + # 强信号 + alpha_strong_thresh = 4.0 * size_factor + mask = (alpha_abs >= alpha_strong_thresh) & (alpha_abs < alpha_extreme_thresh) + scores[mask] += 15 + for i in np.where(mask)[0]: triggered[i].append('alpha_strong') + + # 中等信号 + alpha_medium_thresh = 3.0 * size_factor + mask = (alpha_abs >= alpha_medium_thresh) & (alpha_abs < alpha_strong_thresh) + scores[mask] += 10 + for i in np.where(mask)[0]: triggered[i].append('alpha_medium') + + # ========== Alpha 加速度规则(动态阈值)========== + delta_strong_thresh = 2.0 * size_factor + mask = alpha_delta_abs >= delta_strong_thresh + scores[mask] += 15 + for i in np.where(mask)[0]: triggered[i].append('alpha_delta_strong') + + delta_medium_thresh = 1.5 * size_factor + mask = (alpha_delta_abs >= delta_medium_thresh) & (alpha_delta_abs < delta_strong_thresh) + scores[mask] += 10 + for i in np.where(mask)[0]: triggered[i].append('alpha_delta_medium') + + # ========== 成交额规则(不受规模影响,放量就是放量)========== + mask = amt_ratio >= 10.0 + scores[mask] += 20 + for i in np.where(mask)[0]: triggered[i].append('volume_extreme') + + mask = (amt_ratio >= 6.0) & (amt_ratio < 10.0) + scores[mask] += 12 + for i in np.where(mask)[0]: triggered[i].append('volume_strong') + + # ========== 排名规则 ========== + mask = rank_pct >= 0.98 + scores[mask] += 15 + for i in np.where(mask)[0]: triggered[i].append('rank_top') + + mask = rank_pct <= 0.02 + scores[mask] += 15 + for i in np.where(mask)[0]: triggered[i].append('rank_bottom') + + # ========== 涨停规则(动态阈值)========== + # 大概念有涨停更有意义 + limit_high_thresh = 0.30 * size_factor + mask = limit_up_ratio >= limit_high_thresh + scores[mask] += 20 + for i in np.where(mask)[0]: triggered[i].append('limit_up_high') + + limit_medium_thresh = 0.20 * size_factor + mask = (limit_up_ratio >= limit_medium_thresh) & (limit_up_ratio < limit_high_thresh) + scores[mask] += 12 + for i in np.where(mask)[0]: triggered[i].append('limit_up_medium') + + # ========== 概念规模加分(大概念异动更有价值)========== + # 大概念(50+)额外加分 + large_concept = stock_count >= 50 + has_signal = scores > 0 # 至少触发了某个规则 + mask = large_concept & has_signal + scores[mask] += 10 + for i in np.where(mask)[0]: triggered[i].append('large_concept_bonus') + + # 超大概念(100+)再加分 + xlarge_concept = stock_count >= 100 + mask = xlarge_concept & has_signal + scores[mask] += 10 + for i in np.where(mask)[0]: triggered[i].append('xlarge_concept_bonus') + + # ========== 组合规则(动态阈值)========== + combo_alpha_thresh = 3.0 * size_factor + + # Alpha + 放量 + 排名(三重验证) + mask = (alpha_abs >= combo_alpha_thresh) & (amt_ratio >= 5.0) & ((rank_pct >= 0.95) | (rank_pct <= 0.05)) + scores[mask] += 20 + for i in np.where(mask)[0]: triggered[i].append('triple_signal') + + # Alpha + 涨停(强组合) + mask = (alpha_abs >= combo_alpha_thresh) & (limit_up_ratio >= 0.15 * size_factor) + scores[mask] += 15 + for i in np.where(mask)[0]: triggered[i].append('alpha_with_limit') + + # ========== 小概念惩罚(过滤噪音)========== + # 微型概念(<5 只)如果只有单一信号,减分 + tiny_concept = stock_count < 5 + single_rule = np.array([len(t) <= 1 for t in triggered]) + mask = tiny_concept & single_rule & (scores > 0) + scores[mask] *= 0.5 # 减半 + for i in np.where(mask)[0]: triggered[i].append('tiny_concept_penalty') + + scores = np.clip(scores, 0, 100) + return scores, triggered + + +# ==================== ML 评分器 ==================== + +class FastMLScorer: + """快速 ML 评分器""" + + 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') + elif device == 'cuda' and not torch.cuda.is_available(): + print("警告: CUDA 不可用,使用 CPU") + self.device = torch.device('cpu') + else: + self.device = torch.device(device) + + self.model = None + self.thresholds = None + self._load_model() + + def _load_model(self): + 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: + from model import LSTMAutoencoder + + config = {} + if config_path.exists(): + with open(config_path) as f: + config = json.load(f).get('model', {}) + + # 处理旧配置键名 + if 'd_model' in config: + config['hidden_dim'] = config.pop('d_model') // 2 + for key in ['num_encoder_layers', 'num_decoder_layers', 'nhead', 'dim_feedforward', 'max_seq_len', 'use_instance_norm']: + config.pop(key, None) + if 'num_layers' not in config: + config['num_layers'] = 1 + + checkpoint = torch.load(model_path, map_location='cpu') + self.model = LSTMAutoencoder(**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) as f: + self.thresholds = json.load(f) + + print(f"ML模型加载成功 (设备: {self.device})") + except Exception as e: + print(f"ML模型加载失败: {e}") + self.model = None + + def is_ready(self): + return self.model is not None + + @torch.no_grad() + def score_batch(self, sequences: np.ndarray) -> np.ndarray: + """ + 批量计算 ML 得分 + + Args: + sequences: (batch, seq_len, n_features) + Returns: + scores: (batch,) 0-100 分数 + """ + if not self.is_ready() or len(sequences) == 0: + return np.zeros(len(sequences)) + + x = torch.FloatTensor(sequences).to(self.device) + output, _ = self.model(x) + mse = ((output - x) ** 2).mean(dim=-1) + errors = mse[:, -1].cpu().numpy() + + p95 = self.thresholds.get('p95', 0.1) if self.thresholds else 0.1 + scores = np.clip(errors / p95 * 50, 0, 100) + return scores + + +# ==================== 快速回测 ==================== + +def build_sequences_fast( + df: pd.DataFrame, + seq_len: int = 30, + prev_df: pd.DataFrame = None +) -> Tuple[np.ndarray, pd.DataFrame]: + """ + 快速构建所有有效序列 + + 支持跨日序列:用前一天收盘数据 + 当天开盘数据拼接,实现 9:30 就能检测 + + Args: + df: 当天数据 + seq_len: 序列长度 + prev_df: 前一天数据(可选,用于构建开盘时的序列) + + 返回: + sequences: (n_valid, seq_len, n_features) 所有有效序列 + info_df: 对应的元信息 DataFrame + """ + # 确保按概念和时间排序 + df = df.sort_values(['concept_id', 'timestamp']).reset_index(drop=True) + + # 如果有前一天数据,按概念构建尾部缓存(取每个概念最后 seq_len-1 条) + prev_cache = {} + if prev_df is not None and len(prev_df) > 0: + prev_df = prev_df.sort_values(['concept_id', 'timestamp']) + for concept_id, gdf in prev_df.groupby('concept_id'): + tail_data = gdf.tail(seq_len - 1) + if len(tail_data) > 0: + feat_matrix = tail_data[FEATURES].values + feat_matrix = np.nan_to_num(feat_matrix, nan=0.0, posinf=0.0, neginf=0.0) + feat_matrix = np.clip(feat_matrix, -CONFIG['clip_value'], CONFIG['clip_value']) + prev_cache[concept_id] = feat_matrix + + # 按概念分组 + groups = df.groupby('concept_id') + + sequences = [] + infos = [] + + for concept_id, gdf in groups: + gdf = gdf.reset_index(drop=True) + + # 获取特征矩阵 + feat_matrix = gdf[FEATURES].values + feat_matrix = np.nan_to_num(feat_matrix, nan=0.0, posinf=0.0, neginf=0.0) + feat_matrix = np.clip(feat_matrix, -CONFIG['clip_value'], CONFIG['clip_value']) + + # 如果有前一天缓存,拼接到当天数据前面 + if concept_id in prev_cache: + prev_data = prev_cache[concept_id] + combined_matrix = np.vstack([prev_data, feat_matrix]) + # 计算偏移量:前一天数据的长度 + offset = len(prev_data) + else: + combined_matrix = feat_matrix + offset = 0 + + # 滑动窗口构建序列 + n_total = len(combined_matrix) + if n_total < seq_len: + continue + + for i in range(n_total - seq_len + 1): + seq = combined_matrix[i:i + seq_len] + + # 计算对应当天数据的索引 + # 序列最后一个点的位置 = i + seq_len - 1 + # 对应当天数据的索引 = (i + seq_len - 1) - offset + today_idx = i + seq_len - 1 - offset + + # 只要序列的最后一个点是当天的数据,就记录 + if today_idx < 0 or today_idx >= len(gdf): + continue + + sequences.append(seq) + + # 记录最后一个时间步的信息(当天的) + row = gdf.iloc[today_idx] + infos.append({ + 'concept_id': concept_id, + 'timestamp': row['timestamp'], + 'alpha': row['alpha'], + 'alpha_delta': row.get('alpha_delta', 0), + 'amt_ratio': row.get('amt_ratio', 1), + 'amt_delta': row.get('amt_delta', 0), + 'rank_pct': row.get('rank_pct', 0.5), + 'limit_up_ratio': row.get('limit_up_ratio', 0), + 'stock_count': row.get('stock_count', 0), + 'total_amt': row.get('total_amt', 0), + }) + + if not sequences: + return np.array([]), pd.DataFrame() + + return np.array(sequences), pd.DataFrame(infos) + + +def backtest_single_day_fast( + ml_scorer: FastMLScorer, + df: pd.DataFrame, + date: str, + config: Dict, + prev_df: pd.DataFrame = None +) -> List[Dict]: + """ + 快速回测单天(向量化版本) + + Args: + ml_scorer: ML 评分器 + df: 当天数据 + date: 日期 + config: 配置 + prev_df: 前一天数据(用于 9:30 开始检测) + """ + seq_len = config.get('seq_len', 30) + + # 1. 构建所有序列(支持跨日) + sequences, info_df = build_sequences_fast(df, seq_len, prev_df) + + if len(sequences) == 0: + return [] + + # 2. 过滤小波动 + alpha_abs = np.abs(info_df['alpha'].values) + valid_mask = alpha_abs >= config['min_alpha_abs'] + + sequences = sequences[valid_mask] + info_df = info_df[valid_mask].reset_index(drop=True) + + if len(sequences) == 0: + return [] + + # 3. 批量规则评分 + rule_scores, triggered_rules = score_rules_batch(info_df) + + # 4. 批量 ML 评分(分批处理避免显存溢出) + batch_size = 2048 + ml_scores = [] + for i in range(0, len(sequences), batch_size): + batch_seq = sequences[i:i+batch_size] + batch_scores = ml_scorer.score_batch(batch_seq) + ml_scores.append(batch_scores) + ml_scores = np.concatenate(ml_scores) if ml_scores else np.zeros(len(sequences)) + + # 5. 融合得分 + w1, w2 = config['rule_weight'], config['ml_weight'] + final_scores = w1 * rule_scores + w2 * ml_scores + + # 6. 判断异动 + is_anomaly = ( + (rule_scores >= config['rule_trigger']) | + (ml_scores >= config['ml_trigger']) | + (final_scores >= config['fusion_trigger']) + ) + + # 7. 应用冷却期(按概念+时间排序后处理) + info_df['rule_score'] = rule_scores + info_df['ml_score'] = ml_scores + info_df['final_score'] = final_scores + info_df['is_anomaly'] = is_anomaly + info_df['triggered_rules'] = triggered_rules + + # 只保留异动 + anomaly_df = info_df[info_df['is_anomaly']].copy() + + if len(anomaly_df) == 0: + return [] + + # 应用冷却期 + anomaly_df = anomaly_df.sort_values(['concept_id', 'timestamp']) + cooldown = {} + keep_mask = [] + + for _, row in anomaly_df.iterrows(): + cid = row['concept_id'] + ts = row['timestamp'] + + if cid in cooldown: + try: + diff = (ts - cooldown[cid]).total_seconds() / 60 + except: + diff = config['cooldown_minutes'] + 1 + + if diff < config['cooldown_minutes']: + keep_mask.append(False) + continue + + cooldown[cid] = ts + keep_mask.append(True) + + anomaly_df = anomaly_df[keep_mask] + + # 8. 按时间分组,每分钟最多 max_alerts_per_minute 个 + alerts = [] + for ts, group in anomaly_df.groupby('timestamp'): + group = group.nlargest(config['max_alerts_per_minute'], 'final_score') + + for _, row in group.iterrows(): + alpha = row['alpha'] + if alpha >= 1.5: + atype = 'surge_up' + elif alpha <= -1.5: + atype = 'surge_down' + elif row['amt_ratio'] >= 3.0: + atype = 'volume_spike' + else: + atype = 'unknown' + + rule_score = row['rule_score'] + ml_score = row['ml_score'] + final_score = row['final_score'] + + if rule_score >= config['rule_trigger']: + trigger = f'规则强信号({rule_score:.0f}分)' + elif ml_score >= config['ml_trigger']: + trigger = f'ML强信号({ml_score:.0f}分)' + else: + trigger = f'融合触发({final_score:.0f}分)' + + alerts.append({ + 'concept_id': row['concept_id'], + 'alert_time': row['timestamp'], + 'trade_date': date, + 'alert_type': atype, + 'final_score': final_score, + 'rule_score': rule_score, + 'ml_score': ml_score, + 'trigger_reason': trigger, + 'triggered_rules': row['triggered_rules'], + 'alpha': alpha, + 'alpha_delta': row['alpha_delta'], + 'amt_ratio': row['amt_ratio'], + 'amt_delta': row['amt_delta'], + 'rank_pct': row['rank_pct'], + 'limit_up_ratio': row['limit_up_ratio'], + 'stock_count': row['stock_count'], + 'total_amt': row['total_amt'], + }) + + return alerts + + +# ==================== 数据加载 ==================== + +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 + return pd.read_parquet(file_path) + + +def get_available_dates(data_dir: str, start: str, end: str) -> List[str]: + data_path = Path(data_dir) + dates = [] + for f in sorted(data_path.glob("features_*.parquet")): + d = f.stem.replace('features_', '') + if start <= d <= end: + dates.append(d) + return dates + + +def get_prev_trading_day(data_dir: str, date: str) -> Optional[str]: + """获取给定日期之前最近的有数据的交易日""" + data_path = Path(data_dir) + all_dates = sorted([f.stem.replace('features_', '') for f in data_path.glob("features_*.parquet")]) + + for i, d in enumerate(all_dates): + if d == date and i > 0: + return all_dates[i - 1] + return None + + +def export_to_csv(alerts: List[Dict], path: str): + if alerts: + pd.DataFrame(alerts).to_csv(path, index=False, encoding='utf-8-sig') + print(f"已导出: {path}") + + +# ==================== 数据库写入 ==================== + +def init_db_table(): + """ + 初始化数据库表(如果不存在则创建) + + 表结构说明: + - concept_id: 概念ID + - alert_time: 异动时间(精确到分钟) + - trade_date: 交易日期 + - alert_type: 异动类型(surge_up/surge_down/volume_spike/unknown) + - final_score: 最终得分(0-100) + - rule_score: 规则得分(0-100) + - ml_score: ML得分(0-100) + - trigger_reason: 触发原因 + - alpha: 超额收益率 + - alpha_delta: alpha变化速度 + - amt_ratio: 成交额放大倍数 + - rank_pct: 排名百分位 + - stock_count: 概念股票数量 + - triggered_rules: 触发的规则列表(JSON) + """ + create_sql = text(""" + CREATE TABLE IF NOT EXISTS concept_anomaly_hybrid ( + id INT AUTO_INCREMENT PRIMARY KEY, + concept_id VARCHAR(64) NOT NULL, + alert_time DATETIME NOT NULL, + trade_date DATE NOT NULL, + alert_type VARCHAR(32) NOT NULL, + final_score FLOAT NOT NULL, + rule_score FLOAT NOT NULL, + ml_score FLOAT NOT NULL, + trigger_reason VARCHAR(64), + alpha FLOAT, + alpha_delta FLOAT, + amt_ratio FLOAT, + amt_delta FLOAT, + rank_pct FLOAT, + limit_up_ratio FLOAT, + stock_count INT, + total_amt FLOAT, + triggered_rules JSON, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_concept_time (concept_id, alert_time, trade_date), + INDEX idx_trade_date (trade_date), + INDEX idx_concept_id (concept_id), + INDEX idx_final_score (final_score), + INDEX idx_alert_type (alert_type) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='概念异动检测结果(融合版)' + """) + + with MYSQL_ENGINE.begin() as conn: + conn.execute(create_sql) + print("数据库表已就绪: concept_anomaly_hybrid") + + +def save_alerts_to_mysql(alerts: List[Dict], dry_run: bool = False) -> int: + """ + 保存异动到 MySQL + + Args: + alerts: 异动列表 + dry_run: 是否只模拟,不实际写入 + + Returns: + 实际保存的记录数 + """ + if not alerts: + return 0 + + if dry_run: + print(f" [Dry Run] 将写入 {len(alerts)} 条异动") + return len(alerts) + + saved = 0 + skipped = 0 + + with MYSQL_ENGINE.begin() as conn: + for alert in alerts: + try: + # 检查是否已存在(使用 INSERT IGNORE 更高效) + insert_sql = text(""" + INSERT IGNORE INTO concept_anomaly_hybrid + (concept_id, alert_time, trade_date, alert_type, + final_score, rule_score, ml_score, trigger_reason, + alpha, alpha_delta, amt_ratio, amt_delta, + rank_pct, limit_up_ratio, stock_count, total_amt, + triggered_rules) + VALUES + (:concept_id, :alert_time, :trade_date, :alert_type, + :final_score, :rule_score, :ml_score, :trigger_reason, + :alpha, :alpha_delta, :amt_ratio, :amt_delta, + :rank_pct, :limit_up_ratio, :stock_count, :total_amt, + :triggered_rules) + """) + + result = conn.execute(insert_sql, { + 'concept_id': alert['concept_id'], + 'alert_time': alert['alert_time'], + 'trade_date': alert['trade_date'], + 'alert_type': alert['alert_type'], + 'final_score': alert['final_score'], + 'rule_score': alert['rule_score'], + 'ml_score': alert['ml_score'], + 'trigger_reason': alert['trigger_reason'], + 'alpha': alert.get('alpha', 0), + 'alpha_delta': alert.get('alpha_delta', 0), + 'amt_ratio': alert.get('amt_ratio', 1), + 'amt_delta': alert.get('amt_delta', 0), + 'rank_pct': alert.get('rank_pct', 0.5), + 'limit_up_ratio': alert.get('limit_up_ratio', 0), + 'stock_count': alert.get('stock_count', 0), + 'total_amt': alert.get('total_amt', 0), + 'triggered_rules': json.dumps(alert.get('triggered_rules', []), ensure_ascii=False), + }) + + if result.rowcount > 0: + saved += 1 + else: + skipped += 1 + + except Exception as e: + print(f" 保存失败: {alert['concept_id']} @ {alert['alert_time']} - {e}") + + if skipped > 0: + print(f" 跳过 {skipped} 条重复记录") + + return saved + + +def clear_alerts_by_date(trade_date: str) -> int: + """清除指定日期的异动记录(用于重新回测)""" + with MYSQL_ENGINE.begin() as conn: + result = conn.execute( + text("DELETE FROM concept_anomaly_hybrid WHERE trade_date = :trade_date"), + {'trade_date': trade_date} + ) + return result.rowcount + + +def analyze_alerts(alerts: List[Dict]): + if not alerts: + print("无异动") + return + + df = pd.DataFrame(alerts) + print(f"\n总异动: {len(alerts)}") + print(f"\n类型分布:\n{df['alert_type'].value_counts()}") + print(f"\n得分统计:") + print(f" 最终: {df['final_score'].mean():.1f} (max: {df['final_score'].max():.1f})") + print(f" 规则: {df['rule_score'].mean():.1f} (max: {df['rule_score'].max():.1f})") + print(f" ML: {df['ml_score'].mean():.1f} (max: {df['ml_score'].max():.1f})") + + trigger_type = df['trigger_reason'].apply( + lambda x: '规则' if '规则' in x else ('ML' if 'ML' in x else '融合') + ) + print(f"\n触发来源:\n{trigger_type.value_counts()}") + + +# ==================== 主函数 ==================== + +def main(): + parser = argparse.ArgumentParser(description='快速融合异动回测') + parser.add_argument('--data_dir', default='ml/data') + parser.add_argument('--checkpoint_dir', default='ml/checkpoints') + parser.add_argument('--start', required=True) + parser.add_argument('--end', default=None) + parser.add_argument('--dry-run', action='store_true', help='模拟运行,不写入数据库') + parser.add_argument('--export-csv', default=None, help='导出 CSV 文件路径') + parser.add_argument('--save-db', action='store_true', help='保存结果到数据库') + parser.add_argument('--clear-first', action='store_true', help='写入前先清除该日期的旧数据') + parser.add_argument('--device', default='auto') + + args = parser.parse_args() + if args.end is None: + args.end = args.start + + print("=" * 60) + print("快速融合异动回测") + print("=" * 60) + print(f"日期: {args.start} ~ {args.end}") + print(f"设备: {args.device}") + print(f"保存数据库: {args.save_db}") + print("=" * 60) + + # 初始化数据库表(如果需要保存) + if args.save_db and not args.dry_run: + init_db_table() + + # 初始化 ML 评分器 + ml_scorer = FastMLScorer(args.checkpoint_dir, args.device) + + # 获取日期 + dates = get_available_dates(args.data_dir, args.start, args.end) + if not dates: + print("无数据") + return + + print(f"找到 {len(dates)} 天数据\n") + + # 回测(支持跨日序列) + all_alerts = [] + total_saved = 0 + prev_df = None # 缓存前一天数据 + + for i, date in enumerate(tqdm(dates, desc="回测")): + df = load_daily_features(args.data_dir, date) + if df is None or df.empty: + prev_df = None # 当天无数据,清空缓存 + continue + + # 第一天需要加载前一天数据(如果存在) + if i == 0 and prev_df is None: + prev_date = get_prev_trading_day(args.data_dir, date) + if prev_date: + prev_df = load_daily_features(args.data_dir, prev_date) + if prev_df is not None: + tqdm.write(f" 加载前一天数据: {prev_date}") + + alerts = backtest_single_day_fast(ml_scorer, df, date, CONFIG, prev_df) + all_alerts.extend(alerts) + + # 保存到数据库 + if args.save_db and alerts: + if args.clear_first and not args.dry_run: + cleared = clear_alerts_by_date(date) + if cleared > 0: + tqdm.write(f" 清除 {date} 旧数据: {cleared} 条") + + saved = save_alerts_to_mysql(alerts, dry_run=args.dry_run) + total_saved += saved + tqdm.write(f" {date}: {len(alerts)} 个异动, 保存 {saved} 条") + elif alerts: + tqdm.write(f" {date}: {len(alerts)} 个异动") + + # 当天数据成为下一天的 prev_df + prev_df = df + + # 导出 CSV + if args.export_csv: + export_to_csv(all_alerts, args.export_csv) + + # 分析 + analyze_alerts(all_alerts) + + print(f"\n总计: {len(all_alerts)} 个异动") + if args.save_db: + print(f"已保存到数据库: {total_saved} 条") + + +if __name__ == "__main__": + main() diff --git a/ml/backtest_hybrid.py b/ml/backtest_hybrid.py index 7bb4c808..6913f204 100644 --- a/ml/backtest_hybrid.py +++ b/ml/backtest_hybrid.py @@ -93,12 +93,12 @@ def backtest_single_day_hybrid( seq_len: int = 30 ) -> List[Dict]: """ - 使用融合检测器回测单天数据 + 使用融合检测器回测单天数据(批量优化版) """ alerts = [] - # 按概念分组 - grouped = df.groupby('concept_id', sort=False) + # 按概念分组,预先构建字典 + grouped_dict = {cid: cdf for cid, cdf in df.groupby('concept_id', sort=False)} # 冷却记录 cooldown = {} @@ -114,27 +114,46 @@ def backtest_single_day_hybrid( current_time = all_timestamps[t_idx] window_start_time = all_timestamps[t_idx - seq_len + 1] - minute_alerts = [] + # 批量收集该时刻所有候选概念 + batch_sequences = [] + batch_features = [] + batch_infos = [] + + for concept_id, concept_df in grouped_dict.items(): + # 检查冷却(提前过滤) + 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 - 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') + window_df = concept_df.loc[mask] if len(window_df) < seq_len: continue - window_df = window_df.tail(seq_len) + window_df = window_df.sort_values('timestamp').tail(seq_len) - # 提取特征序列(给 ML 模型) + # 当前时刻特征 + current_row = window_df.iloc[-1] + alpha = current_row.get('alpha', 0) + + # 过滤微小波动(提前过滤) + if abs(alpha) < BACKTEST_CONFIG['min_alpha_abs']: + continue + + # 提取特征序列 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': alpha, 'alpha_delta': current_row.get('alpha_delta', 0), 'amt_ratio': current_row.get('amt_ratio', 1), 'amt_delta': current_row.get('amt_delta', 0), @@ -142,41 +161,79 @@ def backtest_single_day_hybrid( 'limit_up_ratio': current_row.get('limit_up_ratio', 0), } - # 过滤微小波动 - if abs(current_features['alpha']) < BACKTEST_CONFIG['min_alpha_abs']: + batch_sequences.append(sequence) + batch_features.append(current_features) + batch_infos.append({ + 'concept_id': concept_id, + 'stock_count': current_row.get('stock_count', 0), + 'total_amt': current_row.get('total_amt', 0), + }) + + if not batch_sequences: + continue + + # 批量 ML 推理 + sequences_array = np.array(batch_sequences) + ml_scores = detector.ml_scorer.score(sequences_array) if detector.ml_scorer.is_ready() else [0.0] * len(batch_sequences) + if isinstance(ml_scores, float): + ml_scores = [ml_scores] + + # 批量规则评分 + 融合 + minute_alerts = [] + for i, (features, info) in enumerate(zip(batch_features, batch_infos)): + concept_id = info['concept_id'] + + # 规则评分 + rule_score, rule_details = detector.rule_scorer.score(features) + + # ML 评分 + ml_score = ml_scores[i] if i < len(ml_scores) else 0.0 + + # 融合 + w1 = detector.config['rule_weight'] + w2 = detector.config['ml_weight'] + final_score = w1 * rule_score + w2 * ml_score + + # 判断是否异动 + is_anomaly = False + trigger_reason = '' + + if rule_score >= detector.config['rule_trigger']: + is_anomaly = True + trigger_reason = f'规则强信号({rule_score:.0f}分)' + elif ml_score >= detector.config['ml_trigger']: + is_anomaly = True + trigger_reason = f'ML强信号({ml_score:.0f}分)' + elif final_score >= detector.config['fusion_trigger']: + is_anomaly = True + trigger_reason = f'融合触发({final_score:.0f}分)' + + if not is_anomaly: 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 + # 异动类型 + alpha = features.get('alpha', 0) + if alpha >= 1.5: + anomaly_type = 'surge_up' + elif alpha <= -1.5: + anomaly_type = 'surge_down' + elif features.get('amt_ratio', 1) >= 3.0: + anomaly_type = 'volume_spike' + else: + anomaly_type = 'unknown' - 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), + 'alert_type': anomaly_type, + 'final_score': final_score, + 'rule_score': rule_score, + 'ml_score': ml_score, + 'trigger_reason': trigger_reason, + 'triggered_rules': list(rule_details.keys()), + **features, + **info, } minute_alerts.append(alert) @@ -341,6 +398,8 @@ def main(): help='规则权重 (0-1)') parser.add_argument('--ml-weight', type=float, default=0.4, help='ML权重 (0-1)') + parser.add_argument('--device', type=str, default='cuda', + help='设备 (cuda/cpu),默认 cuda') args = parser.parse_args() @@ -355,15 +414,19 @@ def main(): print(f"模型目录: {args.checkpoint_dir}") print(f"规则权重: {args.rule_weight}") print(f"ML权重: {args.ml_weight}") + print(f"设备: {args.device}") print(f"Dry Run: {args.dry_run}") print("=" * 60) - # 初始化融合检测器 + # 初始化融合检测器(使用 GPU) config = { 'rule_weight': args.rule_weight, 'ml_weight': args.ml_weight, } - detector = create_detector(args.checkpoint_dir, config) + + # 修改 detector.py 中 MLScorer 的设备 + from detector import HybridAnomalyDetector + detector = HybridAnomalyDetector(config, args.checkpoint_dir, device=args.device) # 获取可用日期 dates = get_available_dates(args.data_dir, args.start, args.end) diff --git a/ml/detector.py b/ml/detector.py index 0c14880d..c5184771 100644 --- a/ml/detector.py +++ b/ml/detector.py @@ -243,9 +243,12 @@ class MLScorer: ): self.checkpoint_dir = Path(checkpoint_dir) - # 设备 + # 设备检测 if device == 'auto': self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + elif device == 'cuda' and not torch.cuda.is_available(): + print("警告: CUDA 不可用,使用 CPU") + self.device = torch.device('cpu') else: self.device = torch.device(device) @@ -276,8 +279,8 @@ class MLScorer: with open(config_path, 'r') as f: self.config = json.load(f) - # 加载模型 - checkpoint = torch.load(model_path, map_location=self.device) + # 先用 CPU 加载模型(避免 CUDA 不可用问题),再移动到目标设备 + checkpoint = torch.load(model_path, map_location='cpu') model_config = self.config.get('model', {}) if self.config else {} self.model = create_model(model_config) @@ -294,6 +297,8 @@ class MLScorer: except Exception as e: print(f"警告: 模型加载失败 - {e}") + import traceback + traceback.print_exc() self.model = None def is_ready(self) -> bool: @@ -551,7 +556,8 @@ if __name__ == "__main__": }, ] - print("\n测试结果:") + print("\n" + "-" * 60) + print("测试1: 只用规则(无序列数据)") print("-" * 60) for case in test_cases: @@ -567,5 +573,63 @@ if __name__ == "__main__": print(f" 异动类型: {result.anomaly_type}") print(f" 触发规则: {list(result.rule_details.keys())}") + # 测试2: 带序列数据的融合检测 + print("\n" + "-" * 60) + print("测试2: 融合检测(规则 + ML)") + print("-" * 60) + + # 生成模拟序列数据 + seq_len = 30 + n_features = 6 + + # 正常序列:小幅波动 + normal_sequence = np.random.randn(seq_len, n_features) * 0.3 + normal_sequence[:, 0] = np.linspace(0, 0.5, seq_len) # alpha 缓慢上升 + normal_sequence[:, 2] = np.abs(normal_sequence[:, 2]) + 1 # amt_ratio > 0 + + # 异常序列:最后几个时间步突然变化 + anomaly_sequence = np.random.randn(seq_len, n_features) * 0.3 + anomaly_sequence[-5:, 0] = np.linspace(1, 4, 5) # alpha 突然飙升 + anomaly_sequence[-5:, 1] = np.linspace(0.2, 1.5, 5) # alpha_delta 加速 + anomaly_sequence[-5:, 2] = np.linspace(2, 6, 5) # amt_ratio 放量 + anomaly_sequence[:, 2] = np.abs(anomaly_sequence[:, 2]) + 1 + + # 测试正常序列 + normal_features = { + 'alpha': float(normal_sequence[-1, 0]), + 'alpha_delta': float(normal_sequence[-1, 1]), + 'amt_ratio': float(normal_sequence[-1, 2]), + 'amt_delta': float(normal_sequence[-1, 3]), + 'rank_pct': 0.5, + 'limit_up_ratio': 0.02 + } + + result = detector.detect(normal_features, normal_sequence) + print(f"\n正常序列:") + 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}") + + # 测试异常序列 + anomaly_features = { + 'alpha': float(anomaly_sequence[-1, 0]), + 'alpha_delta': float(anomaly_sequence[-1, 1]), + 'amt_ratio': float(anomaly_sequence[-1, 2]), + 'amt_delta': float(anomaly_sequence[-1, 3]), + 'rank_pct': 0.95, + 'limit_up_ratio': 0.15 + } + + result = detector.detect(anomaly_features, anomaly_sequence) + print(f"\n异常序列:") + 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("\n" + "=" * 60) print("测试完成!") diff --git a/ml/realtime_detector.py b/ml/realtime_detector.py new file mode 100644 index 00000000..0144add3 --- /dev/null +++ b/ml/realtime_detector.py @@ -0,0 +1,1518 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +实时概念异动检测服务(实盘可用) + +盘中每分钟运行一次,检测概念异动并写入数据库 + +数据流程: +1. 启动时从 ES 获取概念列表,从 MySQL 获取昨收价 +2. 自动预热:从 ClickHouse 加载当天已有的历史分钟数据 +3. 每分钟增量获取最新分钟数据 +4. 在内存中实时计算概念特征(无前瞻偏差) +5. 使用规则+ML融合评分检测异动 +6. 异动写入 MySQL + +特征计算说明(无 Looking Forward): +- alpha: 当前时间点的概念超额收益 +- alpha_delta: 使用过去 5 分钟的 alpha 变化 +- amt_ratio: 使用过去 20 分钟的成交额均值 +- rank_pct: 当前时间点所有概念的 alpha 排名 +- limit_up_ratio: 当前时间点的涨停股占比 + +使用方法: + # 实盘模式(推荐)- 自动预热,不依赖 prepare_data.py + python realtime_detector.py + + # 单次检测 + python realtime_detector.py --once + + # 回补历史异动到数据库(需要 prepare_data.py 生成 parquet) + python realtime_detector.py --backfill-only + + # 实盘模式 + 启动时回补历史 + python realtime_detector.py --backfill + +最小数据量要求: +- ML 评分需要 seq_len=15 分钟的序列 +- amt_ratio 需要 amt_ma_window=20 分钟的历史 +- 即:开盘后约 35 分钟才能正常工作 +""" + +import os +import sys +import time +import json +import argparse +import schedule +from datetime import datetime, timedelta +from pathlib import Path +from typing import Dict, List, Optional, Tuple, Set +from collections import defaultdict + +import numpy as np +import pandas as pd +import torch +from sqlalchemy import create_engine, text +from elasticsearch import Elasticsearch +from clickhouse_driver import Client as CHClient + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + + +# ==================== 配置 ==================== + +MYSQL_ENGINE = create_engine( + "mysql+pymysql://root:Zzl5588161!@192.168.1.5:3306/stock", + echo=False, + pool_pre_ping=True, + pool_recycle=3600, +) + +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' +} + +REFERENCE_INDEX = '000001.SH' + +FEATURES = ['alpha', 'alpha_delta', 'amt_ratio', 'amt_delta', 'rank_pct', 'limit_up_ratio'] + +# 特征计算参数 +FEATURE_CONFIG = { + 'alpha_delta_window': 5, + 'amt_ma_window': 20, + 'limit_up_threshold': 9.8, + 'limit_down_threshold': -9.8, +} + +# 检测配置(与 backtest_fast.py 保持一致) +CONFIG = { + 'seq_len': 15, + 'min_alpha_abs': 0.3, + 'cooldown_minutes': 8, + 'max_alerts_per_minute': 20, + 'clip_value': 10.0, + # === 融合权重:与 backtest_fast.py 一致 === + 'rule_weight': 0.5, + 'ml_weight': 0.5, + # === 触发阈值:与 backtest_fast.py 一致 === + 'rule_trigger': 65, + 'ml_trigger': 70, + 'fusion_trigger': 45, +} + +TRADING_PERIODS = [ + ('09:30', '11:30'), + ('13:00', '15:00'), +] + + +# ==================== 工具函数 ==================== + +def get_ch_client(): + return CHClient(**CLICKHOUSE_CONFIG) + + +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 is_trading_time() -> bool: + now = datetime.now() + if now.weekday() >= 5: + return False + current_time = now.strftime('%H:%M') + for start, end in TRADING_PERIODS: + if start <= current_time <= end: + return True + return False + + +def get_current_trade_date() -> str: + now = datetime.now() + if now.hour < 9: + now = now - timedelta(days=1) + return now.strftime('%Y-%m-%d') + + +# ==================== 数据获取 ==================== + +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) + print(f"获取到 {len(concepts)} 个概念") + return concepts + + +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: + print(f"获取昨收价失败: {e}") + return {} + + +def get_index_prev_close(trade_date: str) -> float: + """获取指数昨收价""" + code_no_suffix = REFERENCE_INDEX.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: + print(f"获取指数昨收失败: {e}") + return None + + +def get_stock_minute_data(trade_date: str, stock_codes: List[str], since_time: datetime = None) -> pd.DataFrame: + """ + 从 ClickHouse 获取股票分钟数据 + + Args: + trade_date: 交易日期 + stock_codes: 股票代码列表 + since_time: 只获取该时间之后的数据(增量获取) + """ + 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) + + time_filter = "" + if since_time: + time_filter = f"AND timestamp > '{since_time.strftime('%Y-%m-%d %H:%M:%S')}'" + + query = f""" + SELECT code, timestamp, close, volume, amt + FROM stock_minute + WHERE toDate(timestamp) = '{trade_date}' + AND code IN ('{ch_codes_str}') + {time_filter} + 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_index_minute_data(trade_date: str, since_time: datetime = None) -> pd.DataFrame: + """从 ClickHouse 获取指数分钟数据""" + client = get_ch_client() + + time_filter = "" + if since_time: + time_filter = f"AND timestamp > '{since_time.strftime('%Y-%m-%d %H:%M:%S')}'" + + query = f""" + SELECT timestamp, close, volume, amt + FROM index_minute + WHERE toDate(timestamp) = '{trade_date}' + AND code = '{REFERENCE_INDEX}' + {time_filter} + ORDER BY timestamp + """ + + result = client.execute(query) + if not result: + return pd.DataFrame() + + return pd.DataFrame(result, columns=['timestamp', 'close', 'volume', 'amt']) + + +# ==================== 规则评分 ==================== + +def get_size_adjusted_thresholds(stock_count: np.ndarray) -> np.ndarray: + """根据概念股票数量计算动态阈值""" + n = len(stock_count) + size_factor = np.ones(n) + + size_factor[stock_count < 5] = 1.8 + size_factor[(stock_count >= 5) & (stock_count < 10)] = 1.4 + size_factor[(stock_count >= 10) & (stock_count < 20)] = 1.2 + size_factor[(stock_count >= 20) & (stock_count < 50)] = 1.0 + size_factor[(stock_count >= 50) & (stock_count < 100)] = 0.85 + size_factor[stock_count >= 100] = 0.7 + + return size_factor + + +def score_rules_batch(df: pd.DataFrame) -> Tuple[np.ndarray, List[List[str]]]: + """批量计算规则得分""" + n = len(df) + scores = np.zeros(n) + triggered = [[] for _ in range(n)] + + alpha = df['alpha'].values + alpha_delta = df['alpha_delta'].values + amt_ratio = df['amt_ratio'].values + rank_pct = df['rank_pct'].values + limit_up_ratio = df['limit_up_ratio'].values + stock_count = df['stock_count'].values if 'stock_count' in df.columns else np.full(n, 20) + + alpha_abs = np.abs(alpha) + alpha_delta_abs = np.abs(alpha_delta) + size_factor = get_size_adjusted_thresholds(stock_count) + + # Alpha 规则 + alpha_extreme_thresh = 5.0 * size_factor + mask = alpha_abs >= alpha_extreme_thresh + scores[mask] += 20 + for i in np.where(mask)[0]: triggered[i].append('alpha_extreme') + + alpha_strong_thresh = 4.0 * size_factor + mask = (alpha_abs >= alpha_strong_thresh) & (alpha_abs < alpha_extreme_thresh) + scores[mask] += 15 + for i in np.where(mask)[0]: triggered[i].append('alpha_strong') + + alpha_medium_thresh = 3.0 * size_factor + mask = (alpha_abs >= alpha_medium_thresh) & (alpha_abs < alpha_strong_thresh) + scores[mask] += 10 + for i in np.where(mask)[0]: triggered[i].append('alpha_medium') + + # Alpha 加速度 + delta_strong_thresh = 2.0 * size_factor + mask = alpha_delta_abs >= delta_strong_thresh + scores[mask] += 15 + for i in np.where(mask)[0]: triggered[i].append('alpha_delta_strong') + + delta_medium_thresh = 1.5 * size_factor + mask = (alpha_delta_abs >= delta_medium_thresh) & (alpha_delta_abs < delta_strong_thresh) + scores[mask] += 10 + for i in np.where(mask)[0]: triggered[i].append('alpha_delta_medium') + + # 成交额 + mask = amt_ratio >= 10.0 + scores[mask] += 20 + for i in np.where(mask)[0]: triggered[i].append('volume_extreme') + + mask = (amt_ratio >= 6.0) & (amt_ratio < 10.0) + scores[mask] += 12 + for i in np.where(mask)[0]: triggered[i].append('volume_strong') + + # 排名 + mask = rank_pct >= 0.98 + scores[mask] += 15 + for i in np.where(mask)[0]: triggered[i].append('rank_top') + + mask = rank_pct <= 0.02 + scores[mask] += 15 + for i in np.where(mask)[0]: triggered[i].append('rank_bottom') + + # 涨停 + limit_high_thresh = 0.30 * size_factor + mask = limit_up_ratio >= limit_high_thresh + scores[mask] += 20 + for i in np.where(mask)[0]: triggered[i].append('limit_up_high') + + limit_medium_thresh = 0.20 * size_factor + mask = (limit_up_ratio >= limit_medium_thresh) & (limit_up_ratio < limit_high_thresh) + scores[mask] += 12 + for i in np.where(mask)[0]: triggered[i].append('limit_up_medium') + + # 概念规模加分 + large_concept = stock_count >= 50 + has_signal = scores > 0 + mask = large_concept & has_signal + scores[mask] += 10 + for i in np.where(mask)[0]: triggered[i].append('large_concept_bonus') + + xlarge_concept = stock_count >= 100 + mask = xlarge_concept & has_signal + scores[mask] += 10 + for i in np.where(mask)[0]: triggered[i].append('xlarge_concept_bonus') + + # 组合规则 + combo_alpha_thresh = 3.0 * size_factor + mask = (alpha_abs >= combo_alpha_thresh) & (amt_ratio >= 5.0) & ((rank_pct >= 0.95) | (rank_pct <= 0.05)) + scores[mask] += 20 + for i in np.where(mask)[0]: triggered[i].append('triple_signal') + + mask = (alpha_abs >= combo_alpha_thresh) & (limit_up_ratio >= 0.15 * size_factor) + scores[mask] += 15 + for i in np.where(mask)[0]: triggered[i].append('alpha_with_limit') + + # 小概念惩罚 + tiny_concept = stock_count < 5 + single_rule = np.array([len(t) <= 1 for t in triggered]) + mask = tiny_concept & single_rule & (scores > 0) + scores[mask] *= 0.5 + for i in np.where(mask)[0]: triggered[i].append('tiny_concept_penalty') + + scores = np.clip(scores, 0, 100) + return scores, triggered + + +def rule_score_with_details(features: Dict, stock_count: int = 50) -> Tuple[float, Dict[str, float]]: + """ + 单条记录的规则评分(带详情) + + Args: + features: 特征字典,包含 alpha, alpha_delta, amt_ratio, rank_pct, limit_up_ratio + stock_count: 概念股票数量 + + Returns: + (score, details): 总分和各规则触发详情 + """ + score = 0.0 + details = {} + + alpha = features.get('alpha', 0) + alpha_delta = features.get('alpha_delta', 0) + amt_ratio = features.get('amt_ratio', 1) + rank_pct = features.get('rank_pct', 0.5) + limit_up_ratio = features.get('limit_up_ratio', 0) + + alpha_abs = abs(alpha) + alpha_delta_abs = abs(alpha_delta) + size_factor = get_size_adjusted_thresholds(np.array([stock_count]))[0] + + # Alpha 规则 + alpha_extreme_thresh = 5.0 * size_factor + alpha_strong_thresh = 4.0 * size_factor + alpha_medium_thresh = 3.0 * size_factor + + if alpha_abs >= alpha_extreme_thresh: + score += 20 + details['alpha_extreme'] = 20 + elif alpha_abs >= alpha_strong_thresh: + score += 15 + details['alpha_strong'] = 15 + elif alpha_abs >= alpha_medium_thresh: + score += 10 + details['alpha_medium'] = 10 + + # Alpha 加速度 + delta_strong_thresh = 2.0 * size_factor + delta_medium_thresh = 1.5 * size_factor + + if alpha_delta_abs >= delta_strong_thresh: + score += 15 + details['alpha_delta_strong'] = 15 + elif alpha_delta_abs >= delta_medium_thresh: + score += 10 + details['alpha_delta_medium'] = 10 + + # 成交额 + if amt_ratio >= 10.0: + score += 20 + details['volume_extreme'] = 20 + elif amt_ratio >= 6.0: + score += 12 + details['volume_strong'] = 12 + + # 排名 + if rank_pct >= 0.98: + score += 15 + details['rank_top'] = 15 + elif rank_pct <= 0.02: + score += 15 + details['rank_bottom'] = 15 + + # 涨停 + limit_high_thresh = 0.30 * size_factor + limit_medium_thresh = 0.20 * size_factor + + if limit_up_ratio >= limit_high_thresh: + score += 20 + details['limit_up_high'] = 20 + elif limit_up_ratio >= limit_medium_thresh: + score += 12 + details['limit_up_medium'] = 12 + + # 概念规模加分 + if score > 0: + if stock_count >= 50: + score += 10 + details['large_concept_bonus'] = 10 + if stock_count >= 100: + score += 10 + details['xlarge_concept_bonus'] = 10 + + # 组合规则 + combo_alpha_thresh = 3.0 * size_factor + + if alpha_abs >= combo_alpha_thresh and amt_ratio >= 5.0 and (rank_pct >= 0.95 or rank_pct <= 0.05): + score += 20 + details['triple_signal'] = 20 + + if alpha_abs >= combo_alpha_thresh and limit_up_ratio >= 0.15 * size_factor: + score += 15 + details['alpha_with_limit'] = 15 + + # 小概念惩罚 + if stock_count < 5 and len(details) <= 1 and score > 0: + penalty = score * 0.5 + score *= 0.5 + details['tiny_concept_penalty'] = -penalty + + score = min(max(score, 0), 100) + return score, details + + +# ==================== ML 评分器 ==================== + +class MLScorer: + 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._load_model() + + def _load_model(self): + 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: + from model import LSTMAutoencoder + + config = {} + if config_path.exists(): + with open(config_path) as f: + config = json.load(f).get('model', {}) + + if 'd_model' in config: + config['hidden_dim'] = config.pop('d_model') // 2 + for key in ['num_encoder_layers', 'num_decoder_layers', 'nhead', 'dim_feedforward', 'max_seq_len', 'use_instance_norm']: + config.pop(key, None) + if 'num_layers' not in config: + config['num_layers'] = 1 + + checkpoint = torch.load(model_path, map_location='cpu') + self.model = LSTMAutoencoder(**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) as f: + self.thresholds = json.load(f) + + print(f"ML模型加载成功 (设备: {self.device})") + except Exception as e: + print(f"ML模型加载失败: {e}") + + def is_ready(self): + return self.model is not None + + @torch.no_grad() + def score_batch(self, sequences: np.ndarray, debug: bool = False) -> np.ndarray: + if not self.is_ready() or len(sequences) == 0: + return np.zeros(len(sequences)) + + x = torch.FloatTensor(sequences).to(self.device) + output, _ = self.model(x) + mse = ((output - x) ** 2).mean(dim=-1) + errors = mse[:, -1].cpu().numpy() + + p95 = self.thresholds.get('p95', 0.1) if self.thresholds else 0.1 + scores = np.clip(errors / p95 * 50, 0, 100) + + if debug and len(errors) > 0: + print(f"[ML调试] p95={p95:.4f}, errors: min={errors.min():.4f}, max={errors.max():.4f}, mean={errors.mean():.4f}") + print(f"[ML调试] scores: min={scores.min():.0f}, max={scores.max():.0f}, mean={scores.mean():.0f}, =100占比={100*(scores>=100).mean():.1f}%") + + return scores + + +# ==================== 内存数据管理器 ==================== + +class RealtimeDataManager: + """ + 内存数据管理器 + + - 缓存股票分钟数据和指数数据 + - 增量获取新数据 + - 实时计算概念特征 + """ + + def __init__(self, concepts: List[dict], prev_close: Dict[str, float], index_prev_close: float): + self.concepts = concepts + self.prev_close = prev_close + self.index_prev_close = index_prev_close + + # 概念到股票的映射 + self.concept_stocks = {c['concept_id']: set(c['stocks']) for c in concepts} + self.all_stocks = list(set(s for c in concepts for s in c['stocks'])) + + # 内存缓存:股票分钟数据 + self.stock_data = pd.DataFrame() # code, timestamp, close, volume, amt, change_pct + self.index_data = pd.DataFrame() # timestamp, close, change_pct + + # 最后更新时间 + self.last_update_time = None + + # 概念历史(用于计算变化率) + self.concept_history = defaultdict(lambda: {'alpha': [], 'amt': []}) + + # 概念特征时间序列(用于 ML) + self.concept_features_history = defaultdict(list) # concept_id -> list of feature dicts + + def update(self, trade_date: str) -> int: + """ + 增量更新数据 + + Returns: + 新增的时间点数量 + """ + # 获取增量数据 + new_stock_df = get_stock_minute_data(trade_date, self.all_stocks, self.last_update_time) + new_index_df = get_index_minute_data(trade_date, self.last_update_time) + + if new_stock_df.empty and new_index_df.empty: + return 0 + + # 计算涨跌幅 + if not new_stock_df.empty: + new_stock_df['prev_close'] = new_stock_df['code'].map(self.prev_close) + new_stock_df = new_stock_df.dropna(subset=['prev_close']) + new_stock_df['change_pct'] = (new_stock_df['close'] - new_stock_df['prev_close']) / new_stock_df['prev_close'] * 100 + + # 合并到缓存 + self.stock_data = pd.concat([self.stock_data, new_stock_df], ignore_index=True) + self.stock_data = self.stock_data.drop_duplicates(subset=['code', 'timestamp'], keep='last') + + if not new_index_df.empty: + new_index_df['change_pct'] = (new_index_df['close'] - self.index_prev_close) / self.index_prev_close * 100 + + # 调试:打印指数涨跌幅范围 + if len(self.index_data) == 0: # 第一次 + print(f"[调试] 指数 close 范围: {new_index_df['close'].min():.2f} ~ {new_index_df['close'].max():.2f}") + print(f"[调试] 指数 change_pct 范围: {new_index_df['change_pct'].min():.2f}% ~ {new_index_df['change_pct'].max():.2f}%") + + self.index_data = pd.concat([self.index_data, new_index_df], ignore_index=True) + self.index_data = self.index_data.drop_duplicates(subset=['timestamp'], keep='last') + + # 更新最后时间 + if not new_stock_df.empty: + self.last_update_time = new_stock_df['timestamp'].max() + elif not new_index_df.empty: + self.last_update_time = new_index_df['timestamp'].max() + + # 获取新时间点 + new_timestamps = sorted(new_stock_df['timestamp'].unique()) if not new_stock_df.empty else [] + + # 计算新时间点的概念特征 + for ts in new_timestamps: + self._compute_features_for_timestamp(ts) + + return len(new_timestamps) + + def _compute_features_for_timestamp(self, ts): + """计算单个时间点的概念特征""" + ts_stock_data = self.stock_data[self.stock_data['timestamp'] == ts] + index_row = self.index_data[self.index_data['timestamp'] == ts] + + if ts_stock_data.empty or index_row.empty: + return + + index_change = index_row['change_pct'].values[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'])) + + for concept_id, stocks in self.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 = avg_change - index_change + + # 涨停比例 + limit_up_count = sum(1 for c in concept_changes if c >= FEATURE_CONFIG['limit_up_threshold']) + limit_up_ratio = limit_up_count / len(concept_changes) + + # 更新历史 + history = self.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 + + features = { + 'timestamp': ts, + 'concept_id': concept_id, + 'alpha': alpha, + 'alpha_delta': alpha_delta, + 'amt_ratio': amt_ratio, + 'amt_delta': amt_delta, + 'limit_up_ratio': limit_up_ratio, + 'stock_count': len(concept_changes), + 'total_amt': total_amt, + } + + self.concept_features_history[concept_id].append(features) + + def get_latest_features(self) -> pd.DataFrame: + """获取最新时间点的所有概念特征""" + if not self.concept_features_history: + return pd.DataFrame() + + latest_features = [] + for concept_id, history in self.concept_features_history.items(): + if history: + latest_features.append(history[-1]) + + if not latest_features: + return pd.DataFrame() + + df = pd.DataFrame(latest_features) + + # 计算排名百分位 + if len(df) > 1: + df['rank_pct'] = df['alpha'].rank(pct=True) + else: + df['rank_pct'] = 0.5 + + return df + + def get_sequences_for_concepts(self, seq_len: int) -> Tuple[np.ndarray, pd.DataFrame]: + """获取所有概念的特征序列(用于 ML 评分)""" + sequences = [] + infos = [] + + for concept_id, history in self.concept_features_history.items(): + if len(history) < seq_len: + continue + + # 取最近 seq_len 个时间点 + recent = history[-seq_len:] + + # 构建序列 + seq = np.array([[ + f['alpha'], + f['alpha_delta'], + f['amt_ratio'], + f['amt_delta'], + f.get('rank_pct', 0.5), + f['limit_up_ratio'] + ] for f in recent]) + + seq = np.nan_to_num(seq, nan=0.0, posinf=0.0, neginf=0.0) + seq = np.clip(seq, -CONFIG['clip_value'], CONFIG['clip_value']) + + sequences.append(seq) + infos.append(recent[-1]) # 最新特征 + + if not sequences: + return np.array([]), pd.DataFrame() + + # 补充 rank_pct + info_df = pd.DataFrame(infos) + if 'rank_pct' not in info_df.columns and len(info_df) > 1: + info_df['rank_pct'] = info_df['alpha'].rank(pct=True) + + return np.array(sequences), info_df + + def get_all_timestamps(self) -> List: + """获取所有时间点""" + if self.stock_data.empty: + return [] + return sorted(self.stock_data['timestamp'].unique()) + + def get_concept_features_df(self) -> pd.DataFrame: + """获取概念特征的 DataFrame 形式(用于批量回测)""" + if not self.concept_features_history: + return pd.DataFrame() + + rows = [] + for concept_id, history in self.concept_features_history.items(): + for f in history: + row = { + 'concept_id': concept_id, + 'timestamp': f['timestamp'], + 'alpha': f['alpha'], + 'alpha_delta': f['alpha_delta'], + 'amt_ratio': f['amt_ratio'], + 'amt_delta': f.get('amt_delta', 0), + 'limit_up_ratio': f['limit_up_ratio'], + 'stock_count': f.get('stock_count', 0), + 'total_amt': f.get('total_amt', 0), + } + rows.append(row) + + if not rows: + return pd.DataFrame() + + df = pd.DataFrame(rows) + + # 按时间点计算 rank_pct(每个时间点内部排名) + df['rank_pct'] = df.groupby('timestamp')['alpha'].rank(pct=True) + + return df + + +# ==================== 冷却期管理 ==================== + +class CooldownManager: + def __init__(self, cooldown_minutes: int = 8): + self.cooldown_minutes = cooldown_minutes + self.last_alert_time = {} + + def is_in_cooldown(self, concept_id: str, current_time: datetime) -> bool: + if concept_id not in self.last_alert_time: + return False + last_time = self.last_alert_time[concept_id] + diff = (current_time - last_time).total_seconds() / 60 + return diff < self.cooldown_minutes + + def record_alert(self, concept_id: str, alert_time: datetime): + self.last_alert_time[concept_id] = alert_time + + def cleanup_old(self, current_time: datetime): + cutoff = current_time - timedelta(minutes=self.cooldown_minutes * 2) + self.last_alert_time = {cid: t for cid, t in self.last_alert_time.items() if t > cutoff} + + +# ==================== 异动检测 ==================== + +def detect_anomalies( + ml_scorer: MLScorer, + data_mgr: RealtimeDataManager, + cooldown_mgr: CooldownManager, + trade_date: str, + config: Dict +) -> List[Dict]: + """检测当前时刻的异动""" + + # 获取最新特征 + latest_df = data_mgr.get_latest_features() + if latest_df.empty: + return [] + + # 获取 ML 序列 + sequences, info_df = data_mgr.get_sequences_for_concepts(config['seq_len']) + + if len(sequences) == 0: + return [] + + # 获取当前时间 + current_time = pd.to_datetime(info_df['timestamp'].iloc[0]) + + # 清理过期冷却 + cooldown_mgr.cleanup_old(current_time) + + # 过滤冷却中的概念 + valid_mask = [] + for _, row in info_df.iterrows(): + in_cooldown = cooldown_mgr.is_in_cooldown(row['concept_id'], current_time) + valid_mask.append(not in_cooldown) + + valid_mask = np.array(valid_mask) + sequences = sequences[valid_mask] + info_df = info_df[valid_mask].reset_index(drop=True) + + if len(sequences) == 0: + return [] + + # 过滤小波动 + alpha_mask = np.abs(info_df['alpha'].values) >= config['min_alpha_abs'] + sequences = sequences[alpha_mask] + info_df = info_df[alpha_mask].reset_index(drop=True) + + if len(sequences) == 0: + return [] + + # 规则评分 + rule_scores, triggered_rules = score_rules_batch(info_df) + + # ML 评分 + ml_scores = ml_scorer.score_batch(sequences) + + # 融合得分 + w1, w2 = config['rule_weight'], config['ml_weight'] + final_scores = w1 * rule_scores + w2 * ml_scores + + # 判断异动 + alerts = [] + for i, row in info_df.iterrows(): + rule_score = rule_scores[i] + ml_score = ml_scores[i] + final_score = final_scores[i] + + is_anomaly = ( + rule_score >= config['rule_trigger'] or + ml_score >= config['ml_trigger'] or + final_score >= config['fusion_trigger'] + ) + + if not is_anomaly: + continue + + # 触发原因 + if rule_score >= config['rule_trigger']: + trigger = f'规则强信号({rule_score:.0f}分)' + elif ml_score >= config['ml_trigger']: + trigger = f'ML强信号({ml_score:.0f}分)' + else: + trigger = f'融合触发({final_score:.0f}分)' + + # 异动类型 + alpha = row['alpha'] + if alpha >= 1.5: + alert_type = 'surge_up' + elif alpha <= -1.5: + alert_type = 'surge_down' + elif row['amt_ratio'] >= 3.0: + alert_type = 'volume_spike' + else: + alert_type = 'unknown' + + alert = { + 'concept_id': row['concept_id'], + 'alert_time': row['timestamp'], + 'trade_date': trade_date, + 'alert_type': alert_type, + 'final_score': final_score, + 'rule_score': rule_score, + 'ml_score': ml_score, + 'trigger_reason': trigger, + 'triggered_rules': triggered_rules[i], + 'alpha': row['alpha'], + 'alpha_delta': row['alpha_delta'], + 'amt_ratio': row['amt_ratio'], + 'amt_delta': row.get('amt_delta', 0), + 'rank_pct': row.get('rank_pct', 0.5), + 'limit_up_ratio': row['limit_up_ratio'], + 'stock_count': row['stock_count'], + 'total_amt': row['total_amt'], + } + + alerts.append(alert) + cooldown_mgr.record_alert(row['concept_id'], current_time) + + # 按得分排序 + alerts.sort(key=lambda x: x['final_score'], reverse=True) + return alerts[:config['max_alerts_per_minute']] + + +# ==================== 数据库写入 ==================== + +def save_alerts_to_mysql(alerts: List[Dict]) -> int: + if not alerts: + return 0 + + saved = 0 + with MYSQL_ENGINE.begin() as conn: + for alert in alerts: + try: + insert_sql = text(""" + INSERT IGNORE INTO concept_anomaly_hybrid + (concept_id, alert_time, trade_date, alert_type, + final_score, rule_score, ml_score, trigger_reason, + alpha, alpha_delta, amt_ratio, amt_delta, + rank_pct, limit_up_ratio, stock_count, total_amt, + triggered_rules) + VALUES + (:concept_id, :alert_time, :trade_date, :alert_type, + :final_score, :rule_score, :ml_score, :trigger_reason, + :alpha, :alpha_delta, :amt_ratio, :amt_delta, + :rank_pct, :limit_up_ratio, :stock_count, :total_amt, + :triggered_rules) + """) + + result = conn.execute(insert_sql, { + 'concept_id': alert['concept_id'], + 'alert_time': alert['alert_time'], + 'trade_date': alert['trade_date'], + 'alert_type': alert['alert_type'], + 'final_score': alert['final_score'], + 'rule_score': alert['rule_score'], + 'ml_score': alert['ml_score'], + 'trigger_reason': alert['trigger_reason'], + 'alpha': alert.get('alpha', 0), + 'alpha_delta': alert.get('alpha_delta', 0), + 'amt_ratio': alert.get('amt_ratio', 1), + 'amt_delta': alert.get('amt_delta', 0), + 'rank_pct': alert.get('rank_pct', 0.5), + 'limit_up_ratio': alert.get('limit_up_ratio', 0), + 'stock_count': alert.get('stock_count', 0), + 'total_amt': alert.get('total_amt', 0), + 'triggered_rules': json.dumps(alert.get('triggered_rules', []), ensure_ascii=False), + }) + + if result.rowcount > 0: + saved += 1 + except Exception as e: + print(f"保存失败: {alert['concept_id']} - {e}") + + return saved + + +# ==================== 主服务 ==================== + +class RealtimeDetectorService: + def __init__(self, checkpoint_dir: str = 'ml/checkpoints', device: str = 'auto'): + self.checkpoint_dir = checkpoint_dir + self.device = device + + # 初始化 ML 评分器 + self.ml_scorer = MLScorer(checkpoint_dir, device) + + # 这些在 init_for_trade_date 中初始化 + self.data_mgr = None + self.cooldown_mgr = None + self.trade_date = None + + def init_for_trade_date(self, trade_date: str, preload_history: bool = True): + """ + 为指定交易日初始化 + + Args: + trade_date: 交易日期 + preload_history: 是否预加载当天已有的历史数据(实盘必须为 True) + """ + if self.trade_date == trade_date and self.data_mgr is not None: + return + + print(f"[初始化] 交易日: {trade_date}") + + # 获取概念列表 + print(f"[初始化] 获取概念列表...") + concepts = get_all_concepts() + + # 获取所有股票 + all_stocks = list(set(s for c in concepts for s in c['stocks'])) + print(f"[初始化] 共 {len(all_stocks)} 只股票") + + # 获取昨收价 + print(f"[初始化] 获取昨收价...") + prev_close = get_prev_close(all_stocks, trade_date) + index_prev_close = get_index_prev_close(trade_date) + print(f"[初始化] 获取到 {len(prev_close)} 只股票的昨收价") + print(f"[初始化] 指数昨收价: {index_prev_close}") + + # 创建数据管理器 + self.data_mgr = RealtimeDataManager(concepts, prev_close, index_prev_close) + self.cooldown_mgr = CooldownManager(CONFIG['cooldown_minutes']) + self.trade_date = trade_date + + # 预加载当天已有的历史数据(实盘关键) + if preload_history: + self._preload_today_history(trade_date) + + def _preload_today_history(self, trade_date: str): + """ + 预加载当天已有的历史数据到内存 + + 这是实盘运行的关键: + - 在盘中任意时刻启动服务时,需要先加载当天已有的数据 + - 这样才能正确计算 alpha_delta(需要过去 5 分钟)和 amt_ratio(需要过去 20 分钟) + - 以及构建 ML 所需的序列(需要 seq_len=15 分钟) + + 整个过程不依赖 prepare_data.py,直接从 ClickHouse 读取原始数据计算 + """ + print(f"[预热] 加载当天历史数据...") + + # 直接调用 update,但不设置 last_update_time,会获取当天所有数据 + # data_mgr.last_update_time 初始为 None,会获取全部数据 + n_updates = self.data_mgr.update(trade_date) + + if n_updates > 0: + print(f"[预热] 加载完成,共 {n_updates} 个时间点") + + # 检查是否满足 ML 所需的最小数据量 + min_required = CONFIG['seq_len'] + FEATURE_CONFIG['amt_ma_window'] + if n_updates < min_required: + print(f"[预热] 警告:数据量 {n_updates} < 最小需求 {min_required},部分特征可能不准确") + else: + print(f"[预热] 数据充足,可以正常检测") + else: + print(f"[预热] 当天暂无历史数据(可能是开盘前)") + + def backfill_today(self): + """ + 补齐当天历史数据并检测异动(回补模式) + + 使用与 backtest_fast.py 完全相同的逻辑: + 1. 先用 prepare_data.py 生成当天的 parquet 文件 + 2. 读取 parquet 文件进行回测 + + 注意:这个方法用于回补历史异动记录,不是实盘必须的 + 实盘模式下,init_for_trade_date 会自动预热历史数据 + """ + trade_date = get_current_trade_date() + print(f"[补齐] 交易日: {trade_date}") + + # 1. 生成当天的 parquet 文件 + parquet_path = Path('ml/data') / f'features_{trade_date}.parquet' + + if not parquet_path.exists(): + print(f"[补齐] 生成当天特征数据...") + self._generate_today_parquet(trade_date) + + if not parquet_path.exists(): + print(f"[补齐] 无法生成特征数据,跳过") + return + + # 2. 读取 parquet 文件 + df = pd.read_parquet(parquet_path) + print(f"[补齐] 读取到 {len(df)} 条特征数据") + + if df.empty: + print("[补齐] 无数据") + return + + # 打印特征分布(调试) + print(f"[调试] alpha 分布: min={df['alpha'].min():.2f}, max={df['alpha'].max():.2f}, mean={df['alpha'].mean():.2f}") + print(f"[调试] |alpha| >= 0.3 的数量: {(df['alpha'].abs() >= 0.3).sum()}") + + # 3. 使用 backtest_fast.py 相同的回测逻辑 + alerts = self._backtest_from_parquet(df, trade_date) + + # 4. 保存结果 + if alerts: + saved = save_alerts_to_mysql(alerts) + print(f"[补齐] 完成!共 {len(alerts)} 个异动, 保存 {saved} 条") + + # 统计触发来源 + trigger_stats = {'规则': 0, 'ML': 0, '融合': 0} + for a in alerts: + reason = a['trigger_reason'] + if '规则' in reason: + trigger_stats['规则'] += 1 + elif 'ML' in reason: + trigger_stats['ML'] += 1 + else: + trigger_stats['融合'] += 1 + print(f"[补齐] 触发来源: {trigger_stats}") + else: + print("[补齐] 无异动") + + def _generate_today_parquet(self, trade_date: str): + """ + 生成当天的 parquet 文件(调用 prepare_data.py 的逻辑) + """ + import subprocess + cmd = ['python', 'ml/prepare_data.py', '--start', trade_date, '--end', trade_date] + print(f"[补齐] 执行: {' '.join(cmd)}") + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + if result.returncode != 0: + print(f"[补齐] prepare_data.py 执行失败: {result.stderr}") + except Exception as e: + print(f"[补齐] prepare_data.py 执行异常: {e}") + + def _backtest_from_parquet(self, df: pd.DataFrame, trade_date: str) -> List[Dict]: + """ + 从 parquet 数据回测(与 backtest_fast.py 完全一致的逻辑) + """ + seq_len = CONFIG['seq_len'] + now = datetime.now() + + # 确保按概念和时间排序 + df = df.sort_values(['concept_id', 'timestamp']).reset_index(drop=True) + + # 获取所有时间点 + all_timestamps = sorted(df['timestamp'].unique()) + + # 只处理当前时间之前的 + past_timestamps = [] + for ts in all_timestamps: + try: + ts_dt = pd.to_datetime(ts) + if ts_dt.tzinfo is not None: + ts_dt = ts_dt.tz_localize(None) + if ts_dt < now: + past_timestamps.append(ts) + except Exception: + continue + + print(f"[补齐] 处理 {len(past_timestamps)} 个历史时间点...") + + if len(past_timestamps) < seq_len: + print(f"[补齐] 时间点不足 {seq_len},跳过") + return [] + + # 构建序列(与 backtest_fast.py 的 build_sequences_fast 一致) + sequences = [] + infos = [] + + groups = df.groupby('concept_id') + + for concept_id, gdf in groups: + gdf = gdf.reset_index(drop=True) + feat_matrix = gdf[FEATURES].values + feat_matrix = np.nan_to_num(feat_matrix, nan=0.0, posinf=0.0, neginf=0.0) + feat_matrix = np.clip(feat_matrix, -CONFIG['clip_value'], CONFIG['clip_value']) + + n_total = len(feat_matrix) + if n_total < seq_len: + continue + + for i in range(n_total - seq_len + 1): + seq = feat_matrix[i:i + seq_len] + row = gdf.iloc[i + seq_len - 1] + + # 只保留当前时间之前的 + ts = row['timestamp'] + try: + ts_dt = pd.to_datetime(ts) + if ts_dt.tzinfo is not None: + ts_dt = ts_dt.tz_localize(None) + if ts_dt >= now: + continue + except Exception: + continue + + sequences.append(seq) + infos.append({ + 'concept_id': concept_id, + 'timestamp': row['timestamp'], + 'alpha': row['alpha'], + 'alpha_delta': row.get('alpha_delta', 0), + 'amt_ratio': row.get('amt_ratio', 1), + 'amt_delta': row.get('amt_delta', 0), + 'rank_pct': row.get('rank_pct', 0.5), + 'limit_up_ratio': row.get('limit_up_ratio', 0), + 'stock_count': row.get('stock_count', 0), + 'total_amt': row.get('total_amt', 0), + }) + + if not sequences: + return [] + + sequences = np.array(sequences) + info_df = pd.DataFrame(infos) + + print(f"[补齐] 构建了 {len(sequences)} 个序列") + + # 过滤小波动 + alpha_abs = np.abs(info_df['alpha'].values) + valid_mask = alpha_abs >= CONFIG['min_alpha_abs'] + sequences = sequences[valid_mask] + info_df = info_df[valid_mask].reset_index(drop=True) + + if len(sequences) == 0: + return [] + + print(f"[补齐] 过滤后 {len(sequences)} 个序列") + + # 批量规则评分 + rule_scores, triggered_rules = score_rules_batch(info_df) + + # 批量 ML 评分 + batch_size = 2048 + ml_scores = [] + for i in range(0, len(sequences), batch_size): + batch_seq = sequences[i:i+batch_size] + batch_scores = self.ml_scorer.score_batch(batch_seq) + ml_scores.append(batch_scores) + ml_scores = np.concatenate(ml_scores) if ml_scores else np.zeros(len(sequences)) + + # 融合得分 + w1, w2 = CONFIG['rule_weight'], CONFIG['ml_weight'] + final_scores = w1 * rule_scores + w2 * ml_scores + + # 判断异动 + is_anomaly = ( + (rule_scores >= CONFIG['rule_trigger']) | + (ml_scores >= CONFIG['ml_trigger']) | + (final_scores >= CONFIG['fusion_trigger']) + ) + + # 添加分数到 info_df + info_df['rule_score'] = rule_scores + info_df['ml_score'] = ml_scores + info_df['final_score'] = final_scores + info_df['is_anomaly'] = is_anomaly + info_df['triggered_rules'] = triggered_rules + + # 只保留异动 + anomaly_df = info_df[info_df['is_anomaly']].copy() + + if len(anomaly_df) == 0: + return [] + + print(f"[补齐] 发现 {len(anomaly_df)} 个候选异动") + + # 应用冷却期 + anomaly_df = anomaly_df.sort_values(['concept_id', 'timestamp']) + cooldown = {} + keep_mask = [] + + for _, row in anomaly_df.iterrows(): + cid = row['concept_id'] + ts = row['timestamp'] + + if cid in cooldown: + try: + diff = (pd.to_datetime(ts) - pd.to_datetime(cooldown[cid])).total_seconds() / 60 + except: + diff = CONFIG['cooldown_minutes'] + 1 + + if diff < CONFIG['cooldown_minutes']: + keep_mask.append(False) + continue + + cooldown[cid] = ts + keep_mask.append(True) + + anomaly_df = anomaly_df[keep_mask] + + print(f"[补齐] 冷却后 {len(anomaly_df)} 个异动") + + # 按时间分组,每分钟最多 max_alerts_per_minute 个 + alerts = [] + for ts, group in anomaly_df.groupby('timestamp'): + group = group.nlargest(CONFIG['max_alerts_per_minute'], 'final_score') + + for _, row in group.iterrows(): + alpha = row['alpha'] + if alpha >= 1.5: + atype = 'surge_up' + elif alpha <= -1.5: + atype = 'surge_down' + elif row['amt_ratio'] >= 3.0: + atype = 'volume_spike' + else: + atype = 'unknown' + + rule_score = row['rule_score'] + ml_score = row['ml_score'] + final_score = row['final_score'] + + if rule_score >= CONFIG['rule_trigger']: + trigger = f'规则强信号({rule_score:.0f}分)' + elif ml_score >= CONFIG['ml_trigger']: + trigger = f'ML强信号({ml_score:.0f}分)' + else: + trigger = f'融合触发({final_score:.0f}分)' + + alerts.append({ + 'concept_id': row['concept_id'], + 'alert_time': row['timestamp'], + 'trade_date': trade_date, + 'alert_type': atype, + 'final_score': final_score, + 'rule_score': rule_score, + 'ml_score': ml_score, + 'trigger_reason': trigger, + 'triggered_rules': row['triggered_rules'], + 'alpha': alpha, + 'alpha_delta': row['alpha_delta'], + 'amt_ratio': row['amt_ratio'], + 'amt_delta': row['amt_delta'], + 'rank_pct': row['rank_pct'], + 'limit_up_ratio': row['limit_up_ratio'], + 'stock_count': row['stock_count'], + 'total_amt': row['total_amt'], + }) + + return alerts + + def run_once(self): + """执行一次检测""" + now = datetime.now() + trade_date = get_current_trade_date() + + if not is_trading_time(): + print(f"[{now.strftime('%H:%M:%S')}] 非交易时间,跳过") + return + + # 初始化 + self.init_for_trade_date(trade_date) + + print(f"[{now.strftime('%H:%M:%S')}] 获取新数据...") + + # 增量更新 + n_updates = self.data_mgr.update(trade_date) + print(f" 新增 {n_updates} 个时间点") + + if n_updates == 0: + print(f" 无新数据") + return + + # 检测 + alerts = detect_anomalies( + self.ml_scorer, + self.data_mgr, + self.cooldown_mgr, + trade_date, + CONFIG + ) + + if alerts: + saved = save_alerts_to_mysql(alerts) + print(f" 检测到 {len(alerts)} 个异动, 保存 {saved} 条") + + for alert in alerts[:5]: + print(f" - {alert['concept_id']}: {alert['alert_type']} " + f"(final={alert['final_score']:.0f}, rule={alert['rule_score']:.0f}, ml={alert['ml_score']:.0f})") + else: + print(f" 无异动") + + def run_loop(self, backfill: bool = False): + """ + 持续运行(实盘模式) + + Args: + backfill: 是否回补历史异动到数据库(使用 prepare_data.py 方式) + 默认 False,因为实盘模式下 init_for_trade_date 会自动预热数据 + """ + print("=" * 60) + print("实时概念异动检测服务(实盘模式)") + print("=" * 60) + print(f"模型目录: {self.checkpoint_dir}") + print(f"交易时段: {TRADING_PERIODS}") + print(f"ML 序列长度: {CONFIG['seq_len']} 分钟") + print(f"成交额均值窗口: {FEATURE_CONFIG['amt_ma_window']} 分钟") + print("=" * 60) + + # 立即初始化并预热(即使不在交易时间也预热,方便测试) + trade_date = get_current_trade_date() + print(f"\n[启动] 初始化交易日 {trade_date}...") + self.init_for_trade_date(trade_date, preload_history=True) + + # 可选:回补历史异动记录到数据库 + if backfill and is_trading_time(): + print("\n[启动] 回补历史异动...") + self.backfill_today() + + # 每分钟第 10 秒执行 + schedule.every().minute.at(":10").do(self.run_once) + + print("\n服务已启动,等待下一分钟...") + + while True: + schedule.run_pending() + time.sleep(1) + + +# ==================== 主函数 ==================== + +def main(): + parser = argparse.ArgumentParser(description='实时概念异动检测') + parser.add_argument('--checkpoint_dir', default='ml/checkpoints', help='模型目录') + parser.add_argument('--device', default='auto', help='设备 (auto/cpu/cuda)') + parser.add_argument('--once', action='store_true', help='只运行一次检测') + parser.add_argument('--backfill', action='store_true', help='启动时回补历史异动到数据库') + parser.add_argument('--backfill-only', action='store_true', help='只回补历史(不持续运行)') + + args = parser.parse_args() + + service = RealtimeDetectorService( + checkpoint_dir=args.checkpoint_dir, + device=args.device + ) + + if args.once: + # 单次检测模式 + service.run_once() + elif args.backfill_only: + # 仅回补历史模式(需要 prepare_data.py) + service.backfill_today() + else: + # 实盘持续运行模式(自动预热,不依赖 prepare_data.py) + service.run_loop(backfill=args.backfill) + + +if __name__ == "__main__": + main() diff --git a/src/mocks/handlers/market.js b/src/mocks/handlers/market.js index 19857e45..4e15a72d 100644 --- a/src/mocks/handlers/market.js +++ b/src/mocks/handlers/market.js @@ -346,7 +346,173 @@ export const marketHandlers = [ }); }), - // 11. 市场统计数据(个股中心页面使用) + // 11. 热点概览数据(大盘分时 + 概念异动) + http.get('/api/market/hotspot-overview', async ({ request }) => { + await delay(300); + const url = new URL(request.url); + const date = url.searchParams.get('date'); + + const tradeDate = date || new Date().toISOString().split('T')[0]; + + // 生成分时数据(240个点,9:30-11:30 + 13:00-15:00) + const timeline = []; + const basePrice = 3900 + Math.random() * 100; // 基准价格 3900-4000 + const prevClose = basePrice; + let currentPrice = basePrice; + let cumulativeVolume = 0; + + // 上午时段 9:30-11:30 (120分钟) + for (let i = 0; i < 120; i++) { + const hour = 9 + Math.floor((i + 30) / 60); + const minute = (i + 30) % 60; + const time = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; + + // 模拟价格波动 + const volatility = 0.002; // 0.2%波动 + const drift = (Math.random() - 0.5) * 0.001; // 微小趋势 + currentPrice = currentPrice * (1 + (Math.random() - 0.5) * volatility + drift); + + const volume = Math.floor(Math.random() * 500000 + 100000); // 成交量 + cumulativeVolume += volume; + + timeline.push({ + time, + price: parseFloat(currentPrice.toFixed(2)), + volume: cumulativeVolume, + change_pct: parseFloat(((currentPrice - prevClose) / prevClose * 100).toFixed(2)) + }); + } + + // 下午时段 13:00-15:00 (120分钟) + for (let i = 0; i < 120; i++) { + const hour = 13 + Math.floor(i / 60); + const minute = i % 60; + const time = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; + + // 下午波动略小 + const volatility = 0.0015; + const drift = (Math.random() - 0.5) * 0.0008; + currentPrice = currentPrice * (1 + (Math.random() - 0.5) * volatility + drift); + + const volume = Math.floor(Math.random() * 400000 + 80000); + cumulativeVolume += volume; + + timeline.push({ + time, + price: parseFloat(currentPrice.toFixed(2)), + volume: cumulativeVolume, + change_pct: parseFloat(((currentPrice - prevClose) / prevClose * 100).toFixed(2)) + }); + } + + // 生成概念异动数据 + const conceptNames = [ + '人工智能', 'AI眼镜', '机器人', '核电', '国企', '卫星导航', + '福建自贸区', '两岸融合', 'CRO', '三季报增长', '百货零售', + '人形机器人', '央企', '数据中心', 'CPO', '新能源', '电网设备', + '氢能源', '算力租赁', '厦门国资', '乳业', '低空安防', '创新药', + '商业航天', '控制权变更', '文化传媒', '海峡两岸' + ]; + + const alertTypes = ['surge_up', 'surge_down', 'volume_spike', 'limit_up', 'rank_jump']; + + // 生成 15-25 个异动 + const alertCount = Math.floor(Math.random() * 10) + 15; + const alerts = []; + const usedTimes = new Set(); + + for (let i = 0; i < alertCount; i++) { + // 随机选择一个时间点 + let timeIdx; + let attempts = 0; + do { + timeIdx = Math.floor(Math.random() * timeline.length); + attempts++; + } while (usedTimes.has(timeIdx) && attempts < 50); + + if (attempts >= 50) continue; + + // 同一时间可以有多个异动 + const time = timeline[timeIdx].time; + const conceptName = conceptNames[Math.floor(Math.random() * conceptNames.length)]; + const alertType = alertTypes[Math.floor(Math.random() * alertTypes.length)]; + + // 根据类型生成 alpha + let alpha; + if (alertType === 'surge_up') { + alpha = parseFloat((Math.random() * 3 + 2).toFixed(2)); // +2% ~ +5% + } else if (alertType === 'surge_down') { + alpha = parseFloat((-Math.random() * 3 - 1.5).toFixed(2)); // -1.5% ~ -4.5% + } else { + alpha = parseFloat((Math.random() * 4 - 1).toFixed(2)); // -1% ~ +3% + } + + const finalScore = Math.floor(Math.random() * 40 + 45); // 45-85分 + const ruleScore = Math.floor(Math.random() * 30 + 40); + const mlScore = Math.floor(Math.random() * 30 + 40); + + alerts.push({ + concept_id: `CONCEPT_${1000 + i}`, + concept_name: conceptName, + time, + alert_type: alertType, + alpha, + alpha_delta: parseFloat((Math.random() * 2 - 0.5).toFixed(2)), + amt_ratio: parseFloat((Math.random() * 5 + 1).toFixed(2)), + limit_up_count: alertType === 'limit_up' ? Math.floor(Math.random() * 5 + 1) : 0, + limit_up_ratio: parseFloat((Math.random() * 0.3).toFixed(3)), + final_score: finalScore, + rule_score: ruleScore, + ml_score: mlScore, + trigger_reason: finalScore >= 65 ? '规则强信号' : (mlScore >= 70 ? 'ML强信号' : '融合触发'), + importance_score: parseFloat((finalScore / 100).toFixed(2)), + index_price: timeline[timeIdx].price + }); + } + + // 按时间排序 + alerts.sort((a, b) => a.time.localeCompare(b.time)); + + // 统计异动类型 + const alertSummary = alerts.reduce((acc, alert) => { + acc[alert.alert_type] = (acc[alert.alert_type] || 0) + 1; + return acc; + }, {}); + + // 计算指数统计 + const prices = timeline.map(t => t.price); + const latestPrice = prices[prices.length - 1]; + const highPrice = Math.max(...prices); + const lowPrice = Math.min(...prices); + const changePct = ((latestPrice - prevClose) / prevClose * 100); + + console.log('[Mock Market] 获取热点概览数据:', { + date: tradeDate, + timelinePoints: timeline.length, + alertCount: alerts.length + }); + + return HttpResponse.json({ + success: true, + data: { + index: { + code: '000001.SH', + name: '上证指数', + latest_price: latestPrice, + prev_close: prevClose, + high: highPrice, + low: lowPrice, + change_pct: parseFloat(changePct.toFixed(2)), + timeline + }, + alerts, + alert_summary: alertSummary + }, + trade_date: tradeDate + }); + }), + + // 12. 市场统计数据(个股中心页面使用) http.get('/api/market/statistics', async ({ request }) => { await delay(200); const url = new URL(request.url); diff --git a/src/views/StockOverview/components/HotspotOverview/components/AlertSummary.js b/src/views/StockOverview/components/HotspotOverview/components/AlertSummary.js new file mode 100644 index 00000000..37a871c6 --- /dev/null +++ b/src/views/StockOverview/components/HotspotOverview/components/AlertSummary.js @@ -0,0 +1,147 @@ +/** + * 异动统计摘要组件 + * 展示指数统计和异动类型统计 + */ +import React from 'react'; +import { + Box, + HStack, + VStack, + Text, + Badge, + Icon, + Stat, + StatLabel, + StatNumber, + StatHelpText, + StatArrow, + SimpleGrid, + useColorModeValue, +} from '@chakra-ui/react'; +import { FaBolt, FaArrowDown, FaRocket, FaChartLine, FaFire, FaVolumeUp } from 'react-icons/fa'; + +/** + * 异动类型徽章 + */ +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 }, + volume_spike: { label: '放量', color: 'purple', icon: FaVolumeUp }, + }; + + const cfg = config[type] || { label: type, color: 'gray', icon: FaFire }; + + return ( + + + + {cfg.label} + {count} + + + ); +}; + +/** + * 指数统计卡片 + */ +const IndexStatCard = ({ indexData }) => { + const cardBg = useColorModeValue('white', '#1a1a1a'); + const borderColor = useColorModeValue('gray.200', '#333'); + const subTextColor = useColorModeValue('gray.600', 'gray.400'); + + if (!indexData) return null; + + const changePct = indexData.change_pct || 0; + const isUp = changePct >= 0; + + return ( + + + {indexData.name || '上证指数'} + + {indexData.latest_price?.toFixed(2) || '-'} + + + + {changePct?.toFixed(2)}% + + + + + 最高 + + {indexData.high?.toFixed(2) || '-'} + + + + + 最低 + + {indexData.low?.toFixed(2) || '-'} + + + + + 振幅 + + {indexData.high && indexData.low && indexData.prev_close + ? (((indexData.high - indexData.low) / indexData.prev_close) * 100).toFixed(2) + '%' + : '-'} + + + + ); +}; + +/** + * 异动统计摘要 + * @param {Object} props + * @param {Object} props.indexData - 指数数据 + * @param {Array} props.alerts - 异动数组 + * @param {Object} props.alertSummary - 异动类型统计 + */ +const AlertSummary = ({ indexData, alerts = [], alertSummary = {} }) => { + const cardBg = useColorModeValue('white', '#1a1a1a'); + const borderColor = useColorModeValue('gray.200', '#333'); + + // 如果没有 alertSummary,从 alerts 中统计 + const summary = alertSummary && Object.keys(alertSummary).length > 0 + ? alertSummary + : alerts.reduce((acc, alert) => { + const type = alert.alert_type || 'unknown'; + acc[type] = (acc[type] || 0) + 1; + return acc; + }, {}); + + const totalAlerts = alerts.length; + + return ( + + {/* 指数统计 */} + + + {/* 异动统计 */} + {totalAlerts > 0 && ( + + + 异动 {totalAlerts} 次: + + {(summary.surge_up > 0 || summary.surge > 0) && ( + + )} + {summary.surge_down > 0 && } + {summary.limit_up > 0 && } + {summary.volume_spike > 0 && } + {summary.rank_jump > 0 && } + + )} + + ); +}; + +export default AlertSummary; diff --git a/src/views/StockOverview/components/HotspotOverview/components/ConceptAlertList.js b/src/views/StockOverview/components/HotspotOverview/components/ConceptAlertList.js new file mode 100644 index 00000000..9c846122 --- /dev/null +++ b/src/views/StockOverview/components/HotspotOverview/components/ConceptAlertList.js @@ -0,0 +1,194 @@ +/** + * 概念异动列表组件 + * 展示当日的概念异动记录 + */ +import React from 'react'; +import { + Box, + VStack, + HStack, + Text, + Badge, + Icon, + Tooltip, + useColorModeValue, + Flex, + Divider, +} from '@chakra-ui/react'; +import { FaBolt, FaArrowUp, FaArrowDown, FaChartLine, FaFire, FaVolumeUp } from 'react-icons/fa'; +import { getAlertTypeLabel, formatScore, getScoreColor } from '../utils/chartHelpers'; + +/** + * 单个异动项组件 + */ +const AlertItem = ({ alert, onClick, isSelected }) => { + const bgColor = useColorModeValue('white', '#1a1a1a'); + const hoverBg = useColorModeValue('gray.50', '#2a2a2a'); + const borderColor = useColorModeValue('gray.200', '#333'); + const selectedBg = useColorModeValue('purple.50', '#2a2a3a'); + + const isUp = alert.alert_type !== 'surge_down'; + const typeColor = isUp ? 'red' : 'green'; + + // 获取异动类型图标 + const getTypeIcon = (type) => { + switch (type) { + case 'surge_up': + case 'surge': + return FaArrowUp; + case 'surge_down': + return FaArrowDown; + case 'limit_up': + return FaFire; + case 'volume_spike': + return FaVolumeUp; + case 'rank_jump': + return FaChartLine; + default: + return FaBolt; + } + }; + + return ( + onClick?.(alert)} + > + + {/* 左侧:概念名称和时间 */} + + + + + {alert.concept_name} + + + + {alert.time} + + {getAlertTypeLabel(alert.alert_type)} + + + + + {/* 右侧:分数和关键指标 */} + + {/* 综合得分 */} + {alert.final_score !== undefined && ( + + + {formatScore(alert.final_score)}分 + + + )} + + {/* Alpha 值 */} + {alert.alpha !== undefined && ( + = 0 ? 'red.500' : 'green.500'} fontWeight="medium"> + α {alert.alpha >= 0 ? '+' : ''}{alert.alpha.toFixed(2)}% + + )} + + {/* 涨停数量 */} + {alert.limit_up_count > 0 && ( + + + + 涨停 {alert.limit_up_count} + + + )} + + + + ); +}; + +/** + * 概念异动列表 + * @param {Object} props + * @param {Array} props.alerts - 异动数据数组 + * @param {Function} props.onAlertClick - 点击异动的回调 + * @param {Object} props.selectedAlert - 当前选中的异动 + * @param {number} props.maxHeight - 最大高度 + */ +const ConceptAlertList = ({ alerts = [], onAlertClick, selectedAlert, maxHeight = '400px' }) => { + const textColor = useColorModeValue('gray.800', 'white'); + const subTextColor = useColorModeValue('gray.500', 'gray.400'); + + if (!alerts || alerts.length === 0) { + return ( + + + 当日暂无概念异动 + + + ); + } + + // 按时间分组 + const groupedAlerts = alerts.reduce((acc, alert) => { + const time = alert.time || '未知时间'; + if (!acc[time]) { + acc[time] = []; + } + acc[time].push(alert); + return acc; + }, {}); + + // 按时间排序 + const sortedTimes = Object.keys(groupedAlerts).sort(); + + return ( + + + {sortedTimes.map((time, timeIndex) => ( + + {/* 时间分隔线 */} + {timeIndex > 0 && } + + {/* 时间标签 */} + + + + {time} + + + ({groupedAlerts[time].length}个异动) + + + + {/* 该时间点的异动 */} + + {groupedAlerts[time].map((alert, idx) => ( + + ))} + + + ))} + + + ); +}; + +export default ConceptAlertList; diff --git a/src/views/StockOverview/components/HotspotOverview/components/IndexMinuteChart.js b/src/views/StockOverview/components/HotspotOverview/components/IndexMinuteChart.js new file mode 100644 index 00000000..a7c360a5 --- /dev/null +++ b/src/views/StockOverview/components/HotspotOverview/components/IndexMinuteChart.js @@ -0,0 +1,264 @@ +/** + * 指数分时图组件 + * 展示大盘分时走势,支持概念异动标注 + */ +import React, { useRef, useEffect, useCallback, useMemo } from 'react'; +import { Box, useColorModeValue } from '@chakra-ui/react'; +import * as echarts from 'echarts'; +import { getAlertMarkPoints } from '../utils/chartHelpers'; + +/** + * @param {Object} props + * @param {Object} props.indexData - 指数数据 { timeline, prev_close, name, ... } + * @param {Array} props.alerts - 异动数据数组 + * @param {Function} props.onAlertClick - 点击异动标注的回调 + * @param {string} props.height - 图表高度 + */ +const IndexMinuteChart = ({ indexData, alerts = [], onAlertClick, height = '350px' }) => { + const chartRef = useRef(null); + const chartInstance = useRef(null); + + const textColor = useColorModeValue('gray.800', 'white'); + const subTextColor = useColorModeValue('gray.600', 'gray.400'); + const gridLineColor = useColorModeValue('#eee', '#333'); + + // 计算图表配置 + const chartOption = useMemo(() => { + if (!indexData || !indexData.timeline || indexData.timeline.length === 0) { + return null; + } + + const timeline = indexData.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 validPrices = prices.filter(Boolean); + if (validPrices.length === 0) return null; + + const priceMin = Math.min(...validPrices); + const priceMax = Math.max(...validPrices); + const priceRange = priceMax - priceMin; + const yAxisMin = priceMin - priceRange * 0.1; + const yAxisMax = priceMax + priceRange * 0.25; // 上方留更多空间给标注 + + // 准备异动标注 + const markPoints = getAlertMarkPoints(alerts, times, prices, priceMax); + + // 渐变色 - 根据涨跌 + const latestChangePct = changePcts[changePcts.length - 1] || 0; + const isUp = latestChangePct >= 0; + const lineColor = isUp ? '#ff4d4d' : '#22c55e'; + const areaColorStops = isUp + ? [ + { 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)' }, + ]; + + return { + backgroundColor: 'transparent', + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'cross', + crossStyle: { color: '#999' }, + }, + formatter: (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: '排名跃升', + volume_spike: '放量', + }[alert.alert_type] || alert.alert_type; + const typeColor = alert.alert_type === 'surge_down' ? '#2ed573' : '#ff6b6b'; + const alpha = alert.alpha ? ` (α${alert.alpha > 0 ? '+' : ''}${alert.alpha.toFixed(2)}%)` : ''; + html += `
• ${alert.concept_name} (${typeLabel}${alpha})
`; + }); + html += '
'; + } + + return html; + }, + }, + legend: { show: false }, + grid: [ + { left: '8%', right: '3%', top: '8%', height: '58%' }, + { left: '8%', right: '3%', top: '72%', height: '18%' }, + ], + xAxis: [ + { + type: 'category', + data: times, + axisLine: { lineStyle: { color: gridLineColor } }, + 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: gridLineColor } }, + 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: gridLineColor, type: 'dashed' } }, + axisPointer: { + label: { + formatter: (params) => { + if (!indexData.prev_close) return params.value.toFixed(2); + const pct = ((params.value - indexData.prev_close) / indexData.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: indexData.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%', + }, + ], + }; + }, [indexData, alerts, subTextColor, gridLineColor]); + + // 渲染图表 + const renderChart = useCallback(() => { + if (!chartRef.current || !chartOption) return; + + if (!chartInstance.current) { + chartInstance.current = echarts.init(chartRef.current); + } + + chartInstance.current.setOption(chartOption, true); + + // 点击事件 + if (onAlertClick) { + chartInstance.current.off('click'); + chartInstance.current.on('click', 'series.line.markPoint', (params) => { + if (params.data && params.data.alertData) { + onAlertClick(params.data.alertData); + } + }); + } + }, [chartOption, onAlertClick]); + + // 数据变化时重新渲染 + useEffect(() => { + renderChart(); + }, [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; + } + }; + }, []); + + if (!chartOption) { + return ( + + 暂无数据 + + ); + } + + return ; +}; + +export default IndexMinuteChart; diff --git a/src/views/StockOverview/components/HotspotOverview/components/index.js b/src/views/StockOverview/components/HotspotOverview/components/index.js new file mode 100644 index 00000000..2401b9bd --- /dev/null +++ b/src/views/StockOverview/components/HotspotOverview/components/index.js @@ -0,0 +1,3 @@ +export { default as IndexMinuteChart } from './IndexMinuteChart'; +export { default as ConceptAlertList } from './ConceptAlertList'; +export { default as AlertSummary } from './AlertSummary'; diff --git a/src/views/StockOverview/components/HotspotOverview/hooks/index.js b/src/views/StockOverview/components/HotspotOverview/hooks/index.js new file mode 100644 index 00000000..43a31148 --- /dev/null +++ b/src/views/StockOverview/components/HotspotOverview/hooks/index.js @@ -0,0 +1 @@ +export { useHotspotData } from './useHotspotData'; diff --git a/src/views/StockOverview/components/HotspotOverview/hooks/useHotspotData.js b/src/views/StockOverview/components/HotspotOverview/hooks/useHotspotData.js new file mode 100644 index 00000000..2fbca02a --- /dev/null +++ b/src/views/StockOverview/components/HotspotOverview/hooks/useHotspotData.js @@ -0,0 +1,53 @@ +/** + * 热点概览数据获取 Hook + * 负责获取指数分时数据和概念异动数据 + */ +import { useState, useEffect, useCallback } from 'react'; +import { logger } from '@utils/logger'; + +/** + * @param {Date|null} selectedDate - 选中的交易日期 + * @returns {Object} 数据和状态 + */ +export const useHotspotData = (selectedDate) => { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [data, setData] = useState(null); + + 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('useHotspotData', 'fetchData', err); + setError('网络请求失败'); + } finally { + setLoading(false); + } + }, [selectedDate]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { + loading, + error, + data, + refetch: fetchData, + }; +}; + +export default useHotspotData; diff --git a/src/views/StockOverview/components/HotspotOverview/index.js b/src/views/StockOverview/components/HotspotOverview/index.js index 856d752d..ccef8cbb 100644 --- a/src/views/StockOverview/components/HotspotOverview/index.js +++ b/src/views/StockOverview/components/HotspotOverview/index.js @@ -1,8 +1,15 @@ /** * 热点概览组件 * 展示大盘分时走势 + 概念异动标注 + * + * 模块化结构: + * - hooks/useHotspotData.js - 数据获取 + * - components/IndexMinuteChart.js - 分时图 + * - components/ConceptAlertList.js - 异动列表 + * - components/AlertSummary.js - 统计摘要 + * - utils/chartHelpers.js - 图表辅助函数 */ -import React, { useState, useEffect, useRef, useCallback } from 'react'; +import React, { useState, useCallback } from 'react'; import { Box, Card, @@ -11,7 +18,6 @@ import { Text, HStack, VStack, - Badge, Spinner, Center, Icon, @@ -19,24 +25,29 @@ import { Spacer, Tooltip, useColorModeValue, - Stat, - StatLabel, - StatNumber, - StatHelpText, - StatArrow, - SimpleGrid, + Grid, + GridItem, + Divider, + IconButton, + Collapse, } from '@chakra-ui/react'; -import { FaFire, FaRocket, FaChartLine, FaBolt, FaArrowDown } from 'react-icons/fa'; +import { FaFire, FaList, FaChartArea, FaChevronDown, FaChevronUp } from 'react-icons/fa'; import { InfoIcon } from '@chakra-ui/icons'; -import * as echarts from 'echarts'; -import { logger } from '@utils/logger'; +import { useHotspotData } from './hooks'; +import { IndexMinuteChart, ConceptAlertList, AlertSummary } from './components'; + +/** + * 热点概览主组件 + * @param {Object} props + * @param {Date|null} props.selectedDate - 选中的交易日期 + */ 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 [selectedAlert, setSelectedAlert] = useState(null); + const [showAlertList, setShowAlertList] = useState(true); + + // 获取数据 + const { loading, error, data } = useHotspotData(selectedDate); // 颜色主题 const cardBg = useColorModeValue('white', '#1a1a1a'); @@ -44,373 +55,13 @@ const HotspotOverview = ({ selectedDate }) => { 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 handleAlertClick = useCallback((alert) => { + setSelectedAlert(alert); + // 可以在这里添加滚动到对应位置的逻辑 }, []); - // 异动类型标签 - 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 ( @@ -426,6 +77,7 @@ const HotspotOverview = ({ selectedDate }) => { ); } + // 渲染错误状态 if (error) { return ( @@ -441,6 +93,7 @@ const HotspotOverview = ({ selectedDate }) => { ); } + // 无数据 if (!data) { return null; } @@ -450,7 +103,7 @@ const HotspotOverview = ({ selectedDate }) => { return ( - {/* 头部信息 */} + {/* 头部 */} @@ -459,69 +112,75 @@ const HotspotOverview = ({ selectedDate }) => { - - - + + + : } + size="sm" + variant="ghost" + onClick={() => setShowAlertList(!showAlertList)} + aria-label="切换异动列表" + /> + + + + + - {/* 指数统计 */} - - - {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}) + + + + + + + {/* 无异动提示 */} {alerts.length === 0 && ( diff --git a/src/views/StockOverview/components/HotspotOverview/utils/chartHelpers.js b/src/views/StockOverview/components/HotspotOverview/utils/chartHelpers.js new file mode 100644 index 00000000..96ba2d5b --- /dev/null +++ b/src/views/StockOverview/components/HotspotOverview/utils/chartHelpers.js @@ -0,0 +1,159 @@ +/** + * 图表辅助函数 + * 用于处理异动标注等图表相关逻辑 + */ + +/** + * 获取异动标注的配色和符号 + * @param {string} alertType - 异动类型 + * @param {number} importanceScore - 重要性得分 + * @returns {Object} { color, symbol, symbolSize } + */ +export const getAlertStyle = (alertType, importanceScore = 0.5) => { + let color = '#ff6b6b'; + let symbol = 'pin'; + let symbolSize = 35; + + switch (alertType) { + case 'surge_up': + case 'surge': + color = '#ff4757'; + symbol = 'triangle'; + symbolSize = 30 + Math.min(importanceScore * 20, 15); + break; + case 'surge_down': + color = '#2ed573'; + symbol = 'path://M0,0 L10,0 L5,10 Z'; // 向下三角形 + symbolSize = 30 + Math.min(importanceScore * 20, 15); + break; + case 'limit_up': + color = '#ff6348'; + symbol = 'diamond'; + symbolSize = 28; + break; + case 'rank_jump': + color = '#3742fa'; + symbol = 'circle'; + symbolSize = 25; + break; + case 'volume_spike': + color = '#ffa502'; + symbol = 'rect'; + symbolSize = 25; + break; + default: + break; + } + + return { color, symbol, symbolSize }; +}; + +/** + * 获取异动类型的显示标签 + * @param {string} alertType - 异动类型 + * @returns {string} 显示标签 + */ +export const getAlertTypeLabel = (alertType) => { + const labels = { + surge: '急涨', + surge_up: '暴涨', + surge_down: '暴跌', + limit_up: '涨停增加', + rank_jump: '排名跃升', + volume_spike: '放量', + unknown: '异动', + }; + return labels[alertType] || alertType; +}; + +/** + * 生成图表标注点数据 + * @param {Array} alerts - 异动数据数组 + * @param {Array} times - 时间数组 + * @param {Array} prices - 价格数组 + * @param {number} priceMax - 最高价格(用于无法匹配时间时的默认位置) + * @param {number} maxCount - 最大显示数量 + * @returns {Array} ECharts markPoint data + */ +export const getAlertMarkPoints = (alerts, times, prices, priceMax, maxCount = 15) => { + if (!alerts || alerts.length === 0) return []; + + // 按重要性排序,限制显示数量 + const sortedAlerts = [...alerts] + .sort((a, b) => (b.final_score || b.importance_score || 0) - (a.final_score || a.importance_score || 0)) + .slice(0, maxCount); + + return sortedAlerts.map((alert) => { + // 找到对应时间的价格 + const timeIndex = times.indexOf(alert.time); + const price = timeIndex >= 0 ? prices[timeIndex] : (alert.index_price || priceMax); + + const { color, symbol, symbolSize } = getAlertStyle( + alert.alert_type, + alert.final_score / 100 || alert.importance_score || 0.5 + ); + + // 格式化标签 + let label = alert.concept_name || ''; + if (label.length > 6) { + label = label.substring(0, 5) + '...'; + } + + // 添加涨停数量(如果有) + if (alert.limit_up_count > 0) { + label += `\n涨停: ${alert.limit_up_count}`; + } + + const isDown = alert.alert_type === 'surge_down'; + + return { + name: alert.concept_name, + coord: [alert.time, price], + value: label, + symbol, + symbolSize, + itemStyle: { + color, + borderColor: '#fff', + borderWidth: 1, + shadowBlur: 3, + shadowColor: 'rgba(0,0,0,0.2)', + }, + label: { + show: true, + position: isDown ? 'bottom' : 'top', + formatter: '{b}', + fontSize: 9, + color: '#333', + backgroundColor: isDown ? 'rgba(46, 213, 115, 0.9)' : 'rgba(255,255,255,0.9)', + padding: [2, 4], + borderRadius: 2, + borderColor: color, + borderWidth: 1, + }, + alertData: alert, // 存储原始数据 + }; + }); +}; + +/** + * 格式化分数显示 + * @param {number} score - 分数 + * @returns {string} 格式化后的分数 + */ +export const formatScore = (score) => { + if (score === null || score === undefined) return '-'; + return Math.round(score).toString(); +}; + +/** + * 获取分数对应的颜色 + * @param {number} score - 分数 (0-100) + * @returns {string} 颜色代码 + */ +export const getScoreColor = (score) => { + if (score >= 80) return '#ff4757'; + if (score >= 60) return '#ff6348'; + if (score >= 40) return '#ffa502'; + return '#747d8c'; +}; diff --git a/src/views/StockOverview/components/HotspotOverview/utils/index.js b/src/views/StockOverview/components/HotspotOverview/utils/index.js new file mode 100644 index 00000000..133f3c2d --- /dev/null +++ b/src/views/StockOverview/components/HotspotOverview/utils/index.js @@ -0,0 +1 @@ +export * from './chartHelpers';