68 lines
3.5 KiB
Python
68 lines
3.5 KiB
Python
|
|
# -*- 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}")
|