150 lines
4.3 KiB
Python
150 lines
4.3 KiB
Python
# -*- 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 dict;news_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. 动量 20(5日涨幅)
|
|||
|
|
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. 技术 15(RSI)
|
|||
|
|
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]
|