# -*- 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]