Files
freedakgmail cf7341fc63 perf: 6项性能与正确性优化
1. get_stock_name: 延迟批量保存缓存(60秒去抖),避免每次新缓存都写文件
2. get_stock_name: 优先从DB stock_realtime_price查name,未命中再调腾讯API
3. compute_comprehensive_score: 单股模式也使用_fund_flow_cache(30分钟TTL)
4. _get_kline_from_local_db: 移除conn.autocommit=True,避免污染连接池事务模式
5. deep_analyze: K线天数从180改为120,与实时检测统一
6. detect_all_signals: 已有指标列时跳过重复calc_all_indicators
2026-07-22 08:00:27 +08:00

316 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
综合评分引擎 — 整合所有影响因素到统一评分体系
将技术面(基础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: 主力资金进出(带30分钟缓存,与批量模式一致)----
try:
now = time.time()
cached_ff = _fund_flow_cache.get(stock_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(stock_code, days=5)
_fund_flow_cache[stock_code] = (fund_result, now)
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