""" 综合评分引擎 — 整合所有影响因素到统一评分体系 将技术面(基础50%+)与外部因素(加减分项)整合为最终评分。 权重分配: - 技术面评分(compute_deep_analysis 原始分):基础分(0-100) - P0 资金面:±20 - P1 市场情绪:±10 - P2 南向资金:±10 - P3 美股外盘:±10 - P4 大宗商品:±5 - P5 公告/异动:±15 - P6 政策面:±10 - P7 汇率:±3 最终评分 = 技术面基础分 + 外部因素加减分(上限100,下限0) """ import logging import time from datetime import datetime logger = logging.getLogger(__name__) # ═══════════════════════════════════════════════════════ # 市场级因素缓存(开市前预计算,日内复用) # ═══════════════════════════════════════════════════════ _market_cache = { 'data': None, # (market_score, market_factors, market_reasons, summaries) 'timestamp': 0, # 计算时间戳 'ttl': 4 * 3600, # 缓存有效期 4 小时 } # 个股资金面缓存(30分钟 TTL) _fund_flow_cache = {} _FUND_FLOW_TTL = 30 * 60 def precompute_market_factors(): """预计算市场级外部因素并缓存(供定时任务在开市前调用)。 计算 P1 市场情绪、P2 南向、P3 美股、P4 商品、P7 汇率、P6 政策面, 结果存入内存缓存,后续 compute_comprehensive_score_batch 直接复用。 """ market_score = 0 market_factors = {} market_reasons = [] summaries = [] # P1: 市场情绪 try: from services.market_sentiment import calc_market_sentiment sentiment_result = calc_market_sentiment() market_factors['market_sentiment'] = sentiment_result market_score += sentiment_result.get('score', 0) market_reasons.extend(sentiment_result.get('reasons', [])) s = sentiment_result.get('sentiment', '') if s and '无数据' not in s: summaries.append( f'市场情绪:{s}(涨跌停{sentiment_result.get("limit_up_count", 0)}:' f'{sentiment_result.get("limit_down_count", 0)})' ) except Exception as e: logger.warning(f"预计算P1市场情绪失败: {e}") market_factors['market_sentiment'] = {'score': 0, 'summary': '分析失败', 'reasons': []} # P2/P3/P4/P7: 外部因素(南向/美股/商品/汇率) try: from services.external_factors import get_all_external_factors ext_result = get_all_external_factors() market_factors['external'] = ext_result market_score += ext_result.get('total_score', 0) market_reasons.extend(ext_result.get('all_reasons', [])) s = ext_result.get('summary', '') if s: summaries.append(s) except Exception as e: logger.warning(f"预计算P2-P7外部因素失败: {e}") market_factors['external'] = {'total_score': 0, 'summary': '分析失败', 'all_reasons': []} # P6: 政策面(市场级,只算一次) try: from services.news_analyzer import get_policy_news, analyze_policy_impact policy_news = get_policy_news(days=3) policy_result = analyze_policy_impact(policy_news) market_factors['policy'] = policy_result market_score += policy_result.get('score', 0) market_reasons.extend(policy_result.get('reasons', [])) s = policy_result.get('summary', '') if s and '失败' not in s: summaries.append(f'政策面:{s}') except Exception as e: logger.warning(f"预计算P6政策面失败: {e}") market_factors['policy'] = {'score': 0, 'summary': '分析失败', 'reasons': []} _market_cache['data'] = (market_score, market_factors, market_reasons, summaries) _market_cache['timestamp'] = time.time() print(f'[评分引擎] 市场级因素预计算完成 (score={market_score}, {datetime.now():%H:%M:%S})') return market_score, market_factors, market_reasons, summaries def _get_market_factors(): """获取市场级因素(优先读缓存,过期则重新计算)""" now = time.time() if _market_cache['data'] is not None and (now - _market_cache['timestamp']) < _market_cache['ttl']: return _market_cache['data'] # 缓存不存在或过期,重新计算 return precompute_market_factors() def compute_comprehensive_score(stock_code, stock_name, technical_score, df=None): """ 综合评分引擎 — 整合技术面和所有外部因素 参数: stock_code: 股票代码 stock_name: 股票名称 technical_score: float — 技术面基础评分(0-100,来自 compute_deep_analysis) df: K线DataFrame(用于异动检测,可选) 返回: dict: { 'technical_score': float, # 技术面基础分 'external_score': int, # 外部因素总加减分 'final_score': int, # 最终综合评分(0-100) 'verdict': str, # 最终评级 'factors': dict, # 各因素详细数据 'all_reasons': list, # 所有评分原因 'summary': str, # 综合白话总结 } """ factors = {} all_reasons = [] external_score = 0 summaries = [] # ---- P0: 主力资金进出 ---- try: from services.fund_flow_analyzer import analyze_fund_flow fund_result = analyze_fund_flow(stock_code, days=5) factors['fund_flow'] = fund_result external_score += fund_result.get('score', 0) all_reasons.extend(fund_result.get('reasons', [])) s = fund_result.get('summary', '') if s and '暂无' not in s: summaries.append(f'资金面:{s}') except Exception as e: logger.warning(f"P0资金面分析失败: {e}") factors['fund_flow'] = {'score': 0, 'summary': '分析失败', 'reasons': []} # ---- P1: 市场情绪指标 ---- try: from services.market_sentiment import calc_market_sentiment sentiment_result = calc_market_sentiment() factors['market_sentiment'] = sentiment_result external_score += sentiment_result.get('score', 0) all_reasons.extend(sentiment_result.get('reasons', [])) s = sentiment_result.get('sentiment', '') if s and '无数据' not in s: summaries.append(f'市场情绪:{s}(涨跌停{sentiment_result.get("limit_up_count",0)}:{sentiment_result.get("limit_down_count",0)})') except Exception as e: logger.warning(f"P1市场情绪分析失败: {e}") factors['market_sentiment'] = {'score': 0, 'summary': '分析失败', 'reasons': []} # ---- P2/P3/P4/P7: 外部因素(南向/美股/商品/汇率)---- try: from services.external_factors import get_all_external_factors ext_result = get_all_external_factors() factors['external'] = ext_result external_score += ext_result.get('total_score', 0) all_reasons.extend(ext_result.get('all_reasons', [])) s = ext_result.get('summary', '') if s: summaries.append(s) except Exception as e: logger.warning(f"P2-P7外部因素分析失败: {e}") factors['external'] = {'total_score': 0, 'summary': '分析失败', 'all_reasons': []} # ---- P5/P6: 公告/异动/政策 ---- try: from services.news_analyzer import analyze_news_factors news_result = analyze_news_factors(stock_code, stock_name, df) factors['news'] = news_result external_score += news_result.get('total_score', 0) all_reasons.extend(news_result.get('all_reasons', [])) s = news_result.get('summary', '') if s: summaries.append(s) except Exception as e: logger.warning(f"P5/P6消息面分析失败: {e}") factors['news'] = {'total_score': 0, 'summary': '分析失败', 'all_reasons': []} # ---- 最终评分 ---- # 外部因素加减分上限:±40(避免喧宾夺主) external_score = max(-40, min(40, external_score)) final_score = max(0, min(100, int(technical_score + external_score))) # 最终评级 if final_score >= 80: verdict = '强烈看多' elif final_score >= 65: verdict = '看多' elif final_score >= 50: verdict = '中性偏多' elif final_score >= 35: verdict = '中性偏空' else: verdict = '看空' # 综合总结 summary = ' | '.join(summaries) if summaries else '暂无外部因素数据' return { 'technical_score': round(technical_score, 0), 'external_score': external_score, 'final_score': final_score, 'verdict': verdict, 'factors': factors, 'all_reasons': all_reasons, 'summary': summary, } def compute_comprehensive_score_batch(stocks_data): """ 批量计算综合评分 — 市场级因素只计算一次,个股级因素逐只计算。 优化点: - P1 市场情绪、P2 南向、P3 美股、P4 商品、P7 汇率、P6 政策 → 市场级,只算一次 - P0 资金面 → 个股级,逐只从DB读取 - P5 公告/异动 → 批量模式跳过(需AKShare API + LLM,太慢),在深度分析时补充 参数: stocks_data: list[dict],每个元素包含: - stock_code: str 股票代码 - stock_name: str 股票名称(可选) - technical_score: float 技术得分(0-100) 返回: dict: {stock_code: {technical_score, external_score, final_score, verdict, factors, all_reasons, summary}} """ # ---- 市场级因素(从缓存读取,开市前由定时任务预计算)---- market_score, market_factors, market_reasons, summaries = _get_market_factors() # ---- 为每只股票计算个股级因素 ---- results = {} for stock in stocks_data: code = stock.get('stock_code', '') name = stock.get('stock_name', '') tech_score = stock.get('technical_score', 50) stock_external = market_score stock_factors = { 'market_sentiment': market_factors.get('market_sentiment', {}), 'external': market_factors.get('external', {}), 'policy': market_factors.get('policy', {}), } stock_reasons = list(market_reasons) # P0: 资金面(个股级,从DB读取,带30分钟缓存) try: now = time.time() cached_ff = _fund_flow_cache.get(code) if cached_ff and (now - cached_ff[1]) < _FUND_FLOW_TTL: fund_result = cached_ff[0] else: from services.fund_flow_analyzer import analyze_fund_flow fund_result = analyze_fund_flow(code, days=5) _fund_flow_cache[code] = (fund_result, now) stock_factors['fund_flow'] = fund_result stock_external += fund_result.get('score', 0) stock_reasons.extend(fund_result.get('reasons', [])) except Exception as e: logger.warning(f"批量P0资金面分析失败 {code}: {e}") stock_factors['fund_flow'] = {'score': 0, 'summary': '分析失败', 'reasons': []} # P5: 公告/异动 — 批量模式跳过(需AKShare API + LLM,在深度分析时补充) stock_factors['news'] = { 'total_score': 0, 'summary': '批量模式跳过,请使用深度分析查看', 'all_reasons': [], } # 外部得分上限 ±40 stock_external = max(-40, min(40, stock_external)) final_score = max(0, min(100, int(tech_score + stock_external))) # 评级 if final_score >= 80: verdict = '强烈看多' elif final_score >= 65: verdict = '看多' elif final_score >= 50: verdict = '中性偏多' elif final_score >= 35: verdict = '中性偏空' else: verdict = '看空' results[code] = { 'technical_score': round(tech_score, 0), 'external_score': stock_external, 'final_score': final_score, 'verdict': verdict, 'factors': stock_factors, 'all_reasons': stock_reasons, 'summary': ' | '.join(summaries) if summaries else '', } return results