Files
stock-advisor/engine/scoring.py
T

150 lines
4.3 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""
荐股评分引擎:多因子打分模型(满分100)
- 趋势 25分:均线多头排列 + 站上MA20
- 动量 20分:5日涨幅区间映射
- 技术 15分:RSI健康区间 / 超买超卖
- 量能 10分:量比
- 消息 15分:近7日相关新闻情感均值(RAG 信号)
- 机构 15分:近30日评级上调 + 基金持仓环比增持
评级:>=82 强烈推荐 / >=68 推荐 / >=55 关注 / <55 观望
"""
RATINGS = [
(82, "强烈推荐"),
(68, "推荐"),
(55, "关注"),
(-1e9, "观望"),
]
def _rating(score):
for threshold, name in RATINGS:
if score >= threshold:
return name
return "观望"
def _bracket(v, cuts):
"""v 落在 [val, 分数] 区间的第一个匹配"""
for hi, lo, score in cuts:
if hi is None or v <= hi:
if v >= lo:
return score
return 0
def score_stock(ind, news_score, inst_score):
"""ind: indicators dictnews_score: -1~1(无新闻用0);inst_score: 0~1 归一化机构热度"""
s = {}
# 1. 趋势 25
if ind.get("trend_bull"):
s["trend"] = 25
elif ind.get("trend_partial"):
s["trend"] = 17
else:
s["trend"] = 8
ma20 = ind.get("ma20")
if ma20 and ind.get("close", 0) >= ma20:
s["trend"] = min(25, s["trend"] + 4)
# 2. 动量 205日涨幅)
chg5 = ind.get("chg_5d", 0)
if chg5 > 12:
s["momentum"] = 16 # 过急,扣分防追高
elif chg5 > 6:
s["momentum"] = 20
elif chg5 > 2:
s["momentum"] = 15
elif chg5 > -2:
s["momentum"] = 10
elif chg5 > -6:
s["momentum"] = 6
else:
s["momentum"] = 3
# 3. 技术 15RSI
rsi = ind.get("rsi", 50)
if 50 <= rsi <= 68:
s["technical"] = 15
elif 40 <= rsi < 50:
s["technical"] = 11
elif 68 < rsi <= 80:
s["technical"] = 8 # 接近超买
elif rsi < 35:
s["technical"] = 9 # 超卖修复机会
else:
s["technical"] = 5
# 4. 量能 10(量比)
vr = ind.get("vol_ratio", 1.0)
if vr >= 2.0:
s["volume"] = 10
elif vr >= 1.3:
s["volume"] = 8
elif vr >= 0.8:
s["volume"] = 6
else:
s["volume"] = 4
# 5. 消息 15
s["news"] = round(max(0, min(15, (news_score + 1) / 2 * 15)), 1)
# 6. 机构 15
s["institutional"] = round(inst_score * 15, 1)
total = round(sum(s.values()), 1)
return {
"total": total,
"trend": s["trend"],
"momentum": s["momentum"],
"technical": s["technical"],
"volume": s["volume"],
"news": s["news"],
"institutional": s["institutional"],
"rating": _rating(total),
"score_parts": s,
}
def build_reasons(ind, news_score, inst_up, hold_up):
"""生成规则化推荐理由(供列表直接展示,无需 LLM"""
reasons = []
if ind.get("trend_bull"):
reasons.append("均线呈多头排列,中短期趋势向上")
elif ind.get("close", 0) >= (ind.get("ma20") or 0):
reasons.append("股价站上20日均线,趋势转强")
else:
reasons.append("均线空头排列,趋势偏弱,注意风险")
chg5 = ind.get("chg_5d", 0)
if chg5 >= 6:
reasons.append(f"5日涨幅{chg5:+.1f}%,动量强劲")
elif chg5 < -6:
reasons.append(f"5日跌幅{chg5:+.1f}%,弱势调整")
else:
reasons.append(f"5日涨跌{chg5:+.1f}%,动量平稳")
rsi = ind.get("rsi", 50)
if rsi >= 70:
reasons.append(f"RSI {rsi:.0f} 超买,短线回调风险增大")
elif rsi <= 30:
reasons.append(f"RSI {rsi:.0f} 超卖,存在修复反弹机会")
vr = ind.get("vol_ratio", 1.0)
if vr >= 1.5:
reasons.append(f"量比{vr:.2f},放量明显,资金活跃")
elif vr < 0.7:
reasons.append(f"量比{vr:.2f},缩量整理")
if news_score > 0.25:
reasons.append("近期相关消息面偏正面,情绪回暖")
elif news_score < -0.25:
reasons.append("近期消息面偏空,注意利空扰动")
if inst_up > 0:
reasons.append(f"近30日{inst_up}家机构给出正面评级")
if hold_up > 0:
reasons.append("基金最新季度环比增持")
return reasons[:4]