diff --git a/__pycache__/app_vx.cpython-310.pyc b/__pycache__/app_vx.cpython-310.pyc index 59b31b8a..a772109e 100644 Binary files a/__pycache__/app_vx.cpython-310.pyc and b/__pycache__/app_vx.cpython-310.pyc differ diff --git a/app_vx.py b/app_vx.py index 1e51f6c6..9685449b 100644 --- a/app_vx.py +++ b/app_vx.py @@ -5381,6 +5381,114 @@ def get_comment_replies(comment_id): }), 500 +# 工具函数:解析JSON字段 +def parse_json_field(field_value): + """解析JSON字段""" + if not field_value: + return [] + try: + if isinstance(field_value, str): + if field_value.startswith('['): + return json.loads(field_value) + else: + return field_value.split(',') + else: + return field_value + except: + return [] + + +# 工具函数:获取 future_events 表字段值,支持新旧字段回退 +def get_future_event_field(row, new_field, old_field): + """ + 获取 future_events 表字段值,支持新旧字段回退 + 如果新字段存在且不为空,使用新字段;否则使用旧字段 + """ + new_value = getattr(row, new_field, None) if hasattr(row, new_field) else None + old_value = getattr(row, old_field, None) if hasattr(row, old_field) else None + + # 如果新字段有值(不为空字符串),使用新字段 + if new_value is not None and str(new_value).strip(): + return new_value + return old_value + + +# 工具函数:解析新的 best_matches 数据结构(含研报引用信息) +def parse_best_matches(best_matches_value): + """ + 解析新的 best_matches 数据结构(含研报引用信息) + + 新结构示例: + [ + { + "stock_code": "300451.SZ", + "company_name": "创业慧康", + "original_description": "核心标的,医疗信息化...", + "best_report_title": "报告标题", + "best_report_author": "作者", + "best_report_sentences": "相关内容", + "best_report_match_score": "好", + "best_report_match_ratio": 0.9285714285714286, + "best_report_declare_date": "2023-04-25T00:00:00", + "total_reports": 9, + "high_score_reports": 6 + }, + ... + ] + + 返回统一格式的股票列表,兼容旧格式 + """ + if not best_matches_value: + return [] + + try: + # 解析 JSON + if isinstance(best_matches_value, str): + data = json.loads(best_matches_value) + else: + data = best_matches_value + + if not isinstance(data, list): + return [] + + result = [] + for item in data: + if isinstance(item, dict): + # 新结构:包含研报信息的字典 + stock_info = { + 'code': item.get('stock_code', ''), + 'name': item.get('company_name', ''), + 'description': item.get('original_description', ''), + 'score': item.get('best_report_match_ratio', 0), + # 研报引用信息 + 'report': { + 'title': item.get('best_report_title', ''), + 'author': item.get('best_report_author', ''), + 'sentences': item.get('best_report_sentences', ''), + 'match_score': item.get('best_report_match_score', ''), + 'match_ratio': item.get('best_report_match_ratio', 0), + 'declare_date': item.get('best_report_declare_date', ''), + 'total_reports': item.get('total_reports', 0), + 'high_score_reports': item.get('high_score_reports', 0) + } if item.get('best_report_title') else None + } + result.append(stock_info) + elif isinstance(item, (list, tuple)) and len(item) >= 2: + # 旧结构:[code, name, description, score] + result.append({ + 'code': item[0], + 'name': item[1], + 'description': item[2] if len(item) > 2 else '', + 'score': item[3] if len(item) > 3 else 0, + 'report': None + }) + + return result + except Exception as e: + print(f"parse_best_matches error: {e}") + return [] + + # 工具函数:处理转义字符,保留 Markdown 格式 def unescape_markdown_text(text): """ @@ -5470,6 +5578,7 @@ def api_calendar_events(): offset = (page - 1) * per_page # 构建基础查询 - 使用 future_events 表 + # 添加新字段 second_modified_text, `second_modified_text.1`, best_matches 支持新旧回退 query = """ SELECT data_id, \ calendar_time, \ @@ -5481,7 +5590,10 @@ def api_calendar_events(): fact, \ related_stocks, \ concepts, \ - inferred_tag + inferred_tag, \ + second_modified_text, \ + `second_modified_text.1` as second_modified_text_1, \ + best_matches FROM future_events WHERE 1 = 1 \ """ @@ -5552,90 +5664,114 @@ def api_calendar_events(): events_data = [] for event in events: - # 解析相关股票 + # 使用新字段回退机制获取 former 和 forecast + # second_modified_text -> former + former_value = get_future_event_field(event, 'second_modified_text', 'former') + # second_modified_text.1 -> forecast + forecast_new = getattr(event, 'second_modified_text_1', None) + forecast_value = forecast_new if (forecast_new and str(forecast_new).strip()) else getattr(event, 'forecast', None) + + # 解析相关股票 - 优先使用 best_matches,回退到 related_stocks related_stocks_list = [] related_avg_chg = 0 related_max_chg = 0 related_week_chg = 0 - # 处理相关股票数据 - if event.related_stocks: + # 优先使用 best_matches(新结构,含研报引用) + best_matches = getattr(event, 'best_matches', None) + if best_matches and str(best_matches).strip(): + # 使用新的 parse_best_matches 函数解析 + parsed_stocks = parse_best_matches(best_matches) + else: + # 回退到旧的 related_stocks 处理 + parsed_stocks = [] + if event.related_stocks: + try: + import ast + if isinstance(event.related_stocks, str): + try: + stock_data = json.loads(event.related_stocks) + except: + stock_data = ast.literal_eval(event.related_stocks) + else: + stock_data = event.related_stocks + + if stock_data: + for stock_info in stock_data: + if isinstance(stock_info, list) and len(stock_info) >= 2: + parsed_stocks.append({ + 'code': stock_info[0], + 'name': stock_info[1], + 'description': stock_info[2] if len(stock_info) > 2 else '', + 'score': stock_info[3] if len(stock_info) > 3 else 0, + 'report': None + }) + except Exception as e: + print(f"Error parsing related_stocks for event {event.data_id}: {e}") + + # 处理解析后的股票数据,获取交易信息 + if parsed_stocks: try: - import json - import ast + daily_changes = [] + week_changes = [] - # 使用与detail接口相同的解析逻辑 - if isinstance(event.related_stocks, str): - try: - stock_data = json.loads(event.related_stocks) - except: - stock_data = ast.literal_eval(event.related_stocks) - else: - stock_data = event.related_stocks + for stock_info in parsed_stocks: + stock_code = stock_info.get('code', '') + stock_name = stock_info.get('name', '') + description = stock_info.get('description', '') + score = stock_info.get('score', 0) + report = stock_info.get('report', None) - if stock_data: - daily_changes = [] - week_changes = [] + if stock_code: + # 规范化股票代码,移除后缀 + clean_code = stock_code.replace('.SZ', '').replace('.SH', '').replace('.BJ', '') - # 处理正确的数据格式 [股票代码, 股票名称, 描述, 分数] - for stock_info in stock_data: - if isinstance(stock_info, list) and len(stock_info) >= 2: - stock_code = stock_info[0] # 股票代码 - stock_name = stock_info[1] # 股票名称 - description = stock_info[2] if len(stock_info) > 2 else '' - score = stock_info[3] if len(stock_info) > 3 else 0 - else: - continue + # 使用模糊匹配查询真实的交易数据 + trade_query = """ + SELECT F007N as close_price, F010N as change_pct, TRADEDATE + FROM ea_trade + WHERE SECCODE LIKE :stock_code_pattern + ORDER BY TRADEDATE DESC LIMIT 7 \ + """ + trade_result = db.session.execute(text(trade_query), + {'stock_code_pattern': f'{clean_code}%'}) + trade_data = trade_result.fetchall() - if stock_code: - # 规范化股票代码,移除后缀 - clean_code = stock_code.replace('.SZ', '').replace('.SH', '').replace('.BJ', '') + daily_chg = 0 + week_chg = 0 - # 使用模糊匹配查询真实的交易数据 - trade_query = """ - SELECT F007N as close_price, F010N as change_pct, TRADEDATE - FROM ea_trade - WHERE SECCODE LIKE :stock_code_pattern - ORDER BY TRADEDATE DESC LIMIT 7 \ - """ - trade_result = db.session.execute(text(trade_query), - {'stock_code_pattern': f'{clean_code}%'}) - trade_data = trade_result.fetchall() + if trade_data: + # 日涨跌幅(当日) + daily_chg = float(trade_data[0].change_pct or 0) - daily_chg = 0 - week_chg = 0 + # 周涨跌幅(5个交易日) + if len(trade_data) >= 5: + current_price = float(trade_data[0].close_price or 0) + week_ago_price = float(trade_data[4].close_price or 0) + if week_ago_price > 0: + week_chg = ((current_price - week_ago_price) / week_ago_price) * 100 - if trade_data: - # 日涨跌幅(当日) - daily_chg = float(trade_data[0].change_pct or 0) + # 收集涨跌幅数据 + daily_changes.append(daily_chg) + week_changes.append(week_chg) - # 周涨跌幅(5个交易日) - if len(trade_data) >= 5: - current_price = float(trade_data[0].close_price or 0) - week_ago_price = float(trade_data[4].close_price or 0) - if week_ago_price > 0: - week_chg = ((current_price - week_ago_price) / week_ago_price) * 100 + related_stocks_list.append({ + 'code': stock_code, + 'name': stock_name, + 'description': description, + 'score': score, + 'daily_chg': daily_chg, + 'week_chg': week_chg, + 'report': report # 添加研报引用信息 + }) - # 收集涨跌幅数据 - daily_changes.append(daily_chg) - week_changes.append(week_chg) + # 计算平均收益率 + if daily_changes: + related_avg_chg = round(sum(daily_changes) / len(daily_changes), 4) + related_max_chg = round(max(daily_changes), 4) - related_stocks_list.append({ - 'code': stock_code, - 'name': stock_name, - 'description': description, - 'score': score, - 'daily_chg': daily_chg, - 'week_chg': week_chg - }) - - # 计算平均收益率 - if daily_changes: - related_avg_chg = round(sum(daily_changes) / len(daily_changes), 4) - related_max_chg = round(max(daily_changes), 4) - - if week_changes: - related_week_chg = round(sum(week_changes) / len(week_changes), 4) + if week_changes: + related_week_chg = round(sum(week_changes) / len(week_changes), 4) except Exception as e: print(f"Error processing related stocks for event {event.data_id}: {e}") @@ -5660,8 +5796,9 @@ def api_calendar_events(): highlight_match = 'concepts' # 将转义的换行符转换为真正的换行符,保留 Markdown 格式 - cleaned_former = unescape_markdown_text(event.former) - cleaned_forecast = unescape_markdown_text(event.forecast) + # 使用新字段回退后的值(former_value, forecast_value) + cleaned_former = unescape_markdown_text(former_value) + cleaned_forecast = unescape_markdown_text(forecast_value) cleaned_fact = unescape_markdown_text(event.fact) event_dict = { @@ -5907,6 +6044,7 @@ def api_future_event_detail(item_id): """未来事件详情接口 - 连接 future_events 表 (修正数据解析) - 仅限 Pro/Max 会员""" try: # 从 future_events 表查询事件详情 + # 添加新字段 second_modified_text, `second_modified_text.1`, best_matches 支持新旧回退 query = """ SELECT data_id, \ calendar_time, \ @@ -5917,7 +6055,10 @@ def api_future_event_detail(item_id): forecast, \ fact, \ related_stocks, \ - concepts + concepts, \ + second_modified_text, \ + `second_modified_text.1` as second_modified_text_1, \ + best_matches FROM future_events WHERE data_id = :item_id \ """ @@ -5932,6 +6073,13 @@ def api_future_event_detail(item_id): 'data': None }), 404 + # 使用新字段回退机制获取 former 和 forecast + # second_modified_text -> former + former_value = get_future_event_field(event, 'second_modified_text', 'former') + # second_modified_text.1 -> forecast + forecast_new = getattr(event, 'second_modified_text_1', None) + forecast_value = forecast_new if (forecast_new and str(forecast_new).strip()) else getattr(event, 'forecast', None) + extracted_concepts = extract_concepts_from_concepts_field(event.concepts) # 解析相关股票 @@ -5975,136 +6123,150 @@ def api_future_event_detail(item_id): '环保': '公共产业板块', '综合': '公共产业板块' } - # 处理相关股票 + # 处理相关股票 - 优先使用 best_matches,回退到 related_stocks related_avg_chg = 0 related_max_chg = 0 related_week_chg = 0 - if event.related_stocks: + # 优先使用 best_matches(新结构,含研报引用) + best_matches = getattr(event, 'best_matches', None) + if best_matches and str(best_matches).strip(): + # 使用新的 parse_best_matches 函数解析 + parsed_stocks = parse_best_matches(best_matches) + else: + # 回退到旧的 related_stocks 处理 + parsed_stocks = [] + if event.related_stocks: + try: + import ast + if isinstance(event.related_stocks, str): + try: + stock_data = json.loads(event.related_stocks) + except: + stock_data = ast.literal_eval(event.related_stocks) + else: + stock_data = event.related_stocks + + if stock_data: + for stock_info in stock_data: + if isinstance(stock_info, list) and len(stock_info) >= 2: + parsed_stocks.append({ + 'code': stock_info[0], + 'name': stock_info[1], + 'description': stock_info[2] if len(stock_info) > 2 else '', + 'score': stock_info[3] if len(stock_info) > 3 else 0, + 'report': None + }) + except Exception as e: + print(f"Error parsing related_stocks for event {event.data_id}: {e}") + + # 处理解析后的股票数据 + if parsed_stocks: try: - import json - import ast + daily_changes = [] + week_changes = [] - # **修正:正确解析related_stocks数据结构** - if isinstance(event.related_stocks, str): - try: - # 先尝试JSON解析 - stock_data = json.loads(event.related_stocks) - except: - # 如果JSON解析失败,尝试ast.literal_eval解析 - stock_data = ast.literal_eval(event.related_stocks) - else: - stock_data = event.related_stocks + for stock_info in parsed_stocks: + stock_code = stock_info.get('code', '') + stock_name = stock_info.get('name', '') + description = stock_info.get('description', '') + score = stock_info.get('score', 0) + report = stock_info.get('report', None) - print(f"Parsed stock_data: {stock_data}") # 调试输出 + if stock_code: + # 规范化股票代码,移除后缀 + clean_code = stock_code.replace('.SZ', '').replace('.SH', '').replace('.BJ', '') - if stock_data: - daily_changes = [] - week_changes = [] + print(f"Processing stock: {clean_code} - {stock_name}") # 调试输出 - # **修正:处理正确的数据格式 [股票代码, 股票名称, 描述, 分数]** - for stock_info in stock_data: - if isinstance(stock_info, list) and len(stock_info) >= 2: - stock_code = stock_info[0] # 第一个元素是股票代码 - stock_name = stock_info[1] # 第二个元素是股票名称 - description = stock_info[2] if len(stock_info) > 2 else '' - score = stock_info[3] if len(stock_info) > 3 else 0 - else: - continue # 跳过格式不正确的数据 + # 使用模糊匹配LIKE查询申万一级行业F004V + sector_query = """ + SELECT F004V as sw_primary_sector + FROM ea_sector + WHERE SECCODE LIKE :stock_code_pattern + AND F002V = '申银万国行业分类' LIMIT 1 \ + """ + sector_result = db.session.execute(text(sector_query), + {'stock_code_pattern': f'{clean_code}%'}) + sector_row = sector_result.fetchone() - if stock_code: - # 规范化股票代码,移除后缀 - clean_code = stock_code.replace('.SZ', '').replace('.SH', '').replace('.BJ', '') + # 根据申万一级行业(F004V)映射到主板块 + sw_primary_sector = sector_row.sw_primary_sector if sector_row else None + primary_sector = sector_map.get(sw_primary_sector, '其他') if sw_primary_sector else '其他' - print(f"Processing stock: {clean_code} - {stock_name}") # 调试输出 + print( + f"Stock: {clean_code}, SW Primary: {sw_primary_sector}, Primary Sector: {primary_sector}") - # 使用模糊匹配LIKE查询申万一级行业F004V - sector_query = """ - SELECT F004V as sw_primary_sector - FROM ea_sector - WHERE SECCODE LIKE :stock_code_pattern - AND F002V = '申银万国行业分类' LIMIT 1 \ - """ - sector_result = db.session.execute(text(sector_query), - {'stock_code_pattern': f'{clean_code}%'}) - sector_row = sector_result.fetchone() + # 通过SQL查询获取真实的日涨跌幅和周涨跌幅 + trade_query = """ + SELECT F007N as close_price, F010N as change_pct, TRADEDATE + FROM ea_trade + WHERE SECCODE LIKE :stock_code_pattern + ORDER BY TRADEDATE DESC LIMIT 7 \ + """ + trade_result = db.session.execute(text(trade_query), + {'stock_code_pattern': f'{clean_code}%'}) + trade_data = trade_result.fetchall() - # 根据申万一级行业(F004V)映射到主板块 - sw_primary_sector = sector_row.sw_primary_sector if sector_row else None - primary_sector = sector_map.get(sw_primary_sector, '其他') if sw_primary_sector else '其他' + daily_chg = 0 + week_chg = 0 - print( - f"Stock: {clean_code}, SW Primary: {sw_primary_sector}, Primary Sector: {primary_sector}") + if trade_data: + # 日涨跌幅(当日) + daily_chg = float(trade_data[0].change_pct or 0) - # 通过SQL查询获取真实的日涨跌幅和周涨跌幅 - trade_query = """ - SELECT F007N as close_price, F010N as change_pct, TRADEDATE - FROM ea_trade - WHERE SECCODE LIKE :stock_code_pattern - ORDER BY TRADEDATE DESC LIMIT 7 \ - """ - trade_result = db.session.execute(text(trade_query), - {'stock_code_pattern': f'{clean_code}%'}) - trade_data = trade_result.fetchall() + # 周涨跌幅(5个交易日) + if len(trade_data) >= 5: + current_price = float(trade_data[0].close_price or 0) + week_ago_price = float(trade_data[4].close_price or 0) + if week_ago_price > 0: + week_chg = ((current_price - week_ago_price) / week_ago_price) * 100 - daily_chg = 0 - week_chg = 0 + print( + f"Trade data found: {len(trade_data) if trade_data else 0} records, daily_chg: {daily_chg}") - if trade_data: - # 日涨跌幅(当日) - daily_chg = float(trade_data[0].change_pct or 0) + # 统计各分类数量 + sector_stats['全部股票'] += 1 + sector_stats[primary_sector] += 1 - # 周涨跌幅(5个交易日) - if len(trade_data) >= 5: - current_price = float(trade_data[0].close_price or 0) - week_ago_price = float(trade_data[4].close_price or 0) - if week_ago_price > 0: - week_chg = ((current_price - week_ago_price) / week_ago_price) * 100 + # 收集涨跌幅数据 + daily_changes.append(daily_chg) + week_changes.append(week_chg) - print( - f"Trade data found: {len(trade_data) if trade_data else 0} records, daily_chg: {daily_chg}") + related_stocks_list.append({ + 'code': stock_code, # 原始股票代码 + 'name': stock_name, # 股票名称 + 'description': description, # 关联描述 + 'score': score, # 关联分数 + 'sw_primary_sector': sw_primary_sector, # 申万一级行业(F004V) + 'primary_sector': primary_sector, # 主板块分类 + 'daily_change': daily_chg, # 真实的日涨跌幅 + 'week_change': week_chg, # 真实的周涨跌幅 + 'report': report # 研报引用信息(新字段) + }) - # 统计各分类数量 - sector_stats['全部股票'] += 1 - sector_stats[primary_sector] += 1 + # 计算平均收益率 + if daily_changes: + related_avg_chg = sum(daily_changes) / len(daily_changes) + related_max_chg = max(daily_changes) - # 收集涨跌幅数据 - daily_changes.append(daily_chg) - week_changes.append(week_chg) - - related_stocks_list.append({ - 'code': stock_code, # 原始股票代码 - 'name': stock_name, # 股票名称 - 'description': description, # 关联描述 - 'score': score, # 关联分数 - 'sw_primary_sector': sw_primary_sector, # 申万一级行业(F004V) - 'primary_sector': primary_sector, # 主板块分类 - 'daily_change': daily_chg, # 真实的日涨跌幅 - 'week_change': week_chg # 真实的周涨跌幅 - }) - - # 计算平均收益率 - if daily_changes: - related_avg_chg = sum(daily_changes) / len(daily_changes) - related_max_chg = max(daily_changes) - - if week_changes: - related_week_chg = sum(week_changes) / len(week_changes) + if week_changes: + related_week_chg = sum(week_changes) / len(week_changes) except Exception as e: print(f"Error processing related stocks: {e}") import traceback traceback.print_exc() - # 构建返回数据 + # 构建返回数据,使用新字段回退后的值 detail_data = { 'id': event.data_id, 'title': event.title, 'type': event.type, 'star': event.star, 'calendar_time': event.calendar_time.isoformat() if event.calendar_time else None, - 'former': event.former, - 'forecast': event.forecast, + 'former': former_value, # 使用回退后的值(优先 second_modified_text) + 'forecast': forecast_value, # 使用回退后的值(优先 second_modified_text.1) 'fact': event.fact, 'concepts': event.concepts, 'extracted_concepts': extracted_concepts,