# -*- coding: utf-8 -*- """基本面立场合成 (2026-09-11 三源合议, 纯逻辑, 零外部依赖)。 输入是选股系统的公司深度评析 (plan_feed._company_review_or_none 的结构: overall 质地档、 valuation 估值、grade_counts 十视角计数、groups 三类、doubt_hard 硬疑点、age_days 报告日龄)。 规则见《技术面接入与三源合议方案》第四节与台账 007: 质地好为看多; 质地中且三类里两好零差、十视角差不超过一个也算看多 (整体方向为好就够); 质地差或有硬疑点为看空; 其余中性; 缺报告或超 120 天为无读数。 十视角好达八个且估值贵时打「共识已定价」标记, 不改立场, 只让首批推迟。 立场取值只有: 看多 / 看空 / 中性 / 无读数。无读数是弃权, 绝不折成看空 (设计原则二)。 """ from __future__ import annotations _GROUPS = ("回报与护城河", "盈余质量与财务安全", "成长与含金量") DEFAULTS = { "stale_days": 120, # 评析报告超这么多天算无读数 (PMS_FUND_STALE_DAYS) "consensus_good_min": 8, # 十视角好达这么多个且估值贵 → 共识已定价 (PMS_FUND_CONSENSUS_GOOD_MIN) } def _out(stance, *, fact=None, why=None, mark=None, priced_in=False, valuation=None, overall=None): return {"stance": stance, "fact": fact, "mark": mark, "priced_in": priced_in, "valuation": valuation, "overall": overall, "no_read_why": why} def synthesize(company_review, *, params=None) -> dict: p = dict(DEFAULTS) if params: p.update({k: params[k] for k in DEFAULTS if params.get(k) is not None}) cr = company_review if isinstance(company_review, dict) else None # ---- 无读数: 缺报告或超期 (无读数是弃权) ---- if not cr or not cr.get("overall"): return _out("无读数", why="没有买方评析") age = cr.get("age_days") if age is not None and int(age) > int(p["stale_days"]): return _out("无读数", why=f"评析报告已 {age} 天, 超过 {p['stale_days']} 天") overall = str(cr.get("overall") or "") gc = cr.get("grade_counts") if isinstance(cr.get("grade_counts"), dict) else {} groups = cr.get("groups") if isinstance(cr.get("groups"), dict) else {} good_views = int(gc.get("好") or 0) bad_views = int(gc.get("差") or 0) doubt = bool(cr.get("doubt_hard")) # 共识已定价: 十视角好够多且估值贵 (不改立场, 只让首批推迟) priced_in = good_views >= int(p["consensus_good_min"]) and "贵" in str(cr.get("valuation") or "") def out(stance, fact): return _out(stance, fact=fact, mark=("共识已定价" if priced_in else None), priced_in=priced_in, valuation=cr.get("valuation"), overall=overall) # ---- 看空: 质地差或硬疑点 ---- if overall == "差" or doubt: return out("看空", "质地差" if overall == "差" else "有硬疑点") # ---- 看多: 质地好 ---- if overall == "好": return out("看多", "质地好") # ---- 看多: 质地中但「整体方向为好就够」(三类两好零差, 十视角差 ≤ 1) ---- if overall == "中": group_good = sum(1 for k in _GROUPS if str(groups.get(k) or "") == "好") group_bad = sum(1 for k in _GROUPS if str(groups.get(k) or "") == "差") if group_good >= 2 and group_bad == 0 and bad_views <= 1: return out("看多", "质地中但三类两好零差、十视角差不过一个") # ---- 其余中性 ---- return out("中性", f"质地{overall}")