Files
stock-advisor/engine/scoring.py
T

151 lines
4.3 KiB
Python
Raw 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.
# -*- 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]