tradingSystem/app/core/fund_rules.py

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}")