akg-factor-bridge/test_company_quality.py

114 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""公司质地接入2026-09-09 接入方案第四节):取数、第五路信号与合成、判决收敛只降不升、卡上文字。离线,不连库。"""
import datetime as dt
import json
import sys
import types
for _n in ("pandas", "pymysql", "psycopg"):
if _n not in sys.modules:
try:
__import__(_n)
except Exception: # noqa: BLE001
_m = types.ModuleType(_n)
if _n == "pandas":
_m.DataFrame = type("DataFrame", (), {})
sys.modules[_n] = _m
import card # noqa: E402
import logic_state as ls # noqa: E402
import sources # noqa: E402
def t(name, cond):
print((" ok " if cond else " FAIL") + name)
assert cond, name
class _DF:
def __init__(self, rows): self._rows = rows
def to_dict(self, _kind): return self._rows
def _review(overall="", doubt=False, ran="2026-09-08", groups=None):
return {"overall": overall, "groups": groups or {"回报与护城河": "", "盈余质量与财务安全": "", "成长与含金量": ""},
"doubt_hard": doubt, "ran_at": ran, "age_days": 1, "period": "2026Q1",
"grade_counts": {"": 5, "": 3, "": 1, "证据不足": 1}, "valuation": "",
"thesis": "质地好但估值贵", "confidence": "", "invalidation": "下一期看现金流", "quality_reasons": ["回报类好"],
"report_url": "http://192.168.16.178:8000/company/601126.SH/review"}
def test_sources():
calls = []
def rp(sql, params=None):
calls.append(params)
return _DF([{"ts_code": "601126.SH", "period": "2026Q1", "review_version": "2026.09-2", "grades": json.dumps({"penman": ""}),
"summary": json.dumps({"overall": "", "groups": {"回报与护城河": ""}, "valuation": "", "doubt_hard": False,
"thesis": "x", "confidence": "", "grade_counts": {"": 5, "": 3, "": 1, "证据不足": 1},
"report_path": "/company/601126.SH/review"}, ensure_ascii=False),
"brain_used": True, "brain_status": "ok", "ran_at": dt.datetime(2026, 9, 8, 10, 0)},
{"ts_code": "688008.SH", "period": "2026Q1", "review_version": "2026.09-2", "grades": {"buffett": ""},
"summary": None, "brain_used": False, "brain_status": None, "ran_at": dt.datetime(2026, 5, 1, 10, 0)}])
out = sources.company_reviews(["SH601126", "SH688008", "SZ000001"], "2026-09-09", read_pg=rp)
t("按点后缀码查", calls[0][0] == ["601126.SH", "688008.SH", "000001.SZ"])
t("摘要解析与报告链接", out["SH601126"]["overall"] == "" and out["SH601126"]["report_url"].endswith("/company/601126.SH/review"))
t("运行日距数据日", out["SH601126"]["age_days"] == 1 and out["SH688008"]["age_days"] == 131)
t("没摘要的老行只有评价计数", out["SH688008"]["overall"] is None and out["SH688008"]["grade_counts"][""] == 1)
t("没报告的票不在字典里", "SZ000001" not in out)
def boom(*a, **k): raise RuntimeError("no table")
t("表读不到整体缺席", sources.company_reviews(["SH601126"], "2026-09-09", read_pg=boom) == {})
def test_fifth_path():
up = ls.from_company(_review(""), "2026-09-09")
t("质地好向好且带截止日", up["signal"] == ls.SIG_UP and up["as_of"] == "2026-09-08" and up["path"] == ls.PATH_COMPANY)
flat = ls.from_company(_review(""), "2026-09-09")
t("质地中平稳", flat["signal"] == ls.SIG_FLAT)
down = ls.from_company(_review("", doubt=True), "2026-09-09")
t("质地差且盈余差:转弱且达到存疑条件", down["signal"] == ls.SIG_DOWN and down["hard"])
soft = ls.from_company(_review("", doubt=False), "2026-09-09")
t("质地差但盈余不差:转弱不硬", soft["signal"] == ls.SIG_DOWN and not soft["hard"])
t("没报告缺失", ls.from_company(None, "2026-09-09")["signal"] == ls.SIG_NONE)
stale = _review(""); stale["age_days"] = 200
t("报告陈旧缺失", ls.from_company(stale, "2026-09-09")["signal"] == ls.SIG_NONE)
t("证据不足缺失", ls.from_company(_review("证据不足"), "2026-09-09")["signal"] == ls.SIG_NONE)
# 合成:质地好单独不能定态(慢信号);配研报论断向好则强化;质地差硬转弱单独就存疑
alone = ls.compose([ls.signal(ls.PATH_CLAIM, ls.SIG_NONE, why=""), up])
t("只剩公司质地这一路慢信号不能单独定态", alone["state"] == ls.STATE_UNKNOWN)
with_claim = ls.compose([ls.signal(ls.PATH_CLAIM, ls.SIG_UP, as_of="2026-09-08", coverage=3, why="利好"), up])
t("研报论断向好加质地好为逻辑强化", with_claim["state"] == ls.STATE_STRONG)
doubt = ls.compose([ls.signal(ls.PATH_CLAIM, ls.SIG_UP, as_of="2026-09-08", coverage=3, why="利好"), down])
t("一路向好一路转弱是证据矛盾交人", doubt["state"] == ls.STATE_UNKNOWN)
doubt2 = ls.compose([ls.signal(ls.PATH_CLAIM, ls.SIG_NONE, why=""), down])
t("质地差达到进入条件则逻辑存疑", doubt2["state"] == ls.STATE_DOUBT)
t("存疑触发表里有第五路", ls.PATH_COMPANY in ls.DOUBT_TRIGGERS and ls.SPEED[ls.PATH_COMPANY] == "")
def test_card_gate_and_views():
ev = {"pointed": True, "theme": "端侧AI芯片", "n_sources": 3, "pct0": 4.0, "covered": True, "upside": 0.2, "risk_name": False,
"accum_state": "明确吸筹·量在价先", "accum_score": 80, "accum_age": 1, "y_signal": "WATCH", "stale_snapshot": False, "logic": []}
base = card.judge(ev)
t("底子是候选", base["verdict"] == "候选")
down = card.judge(ev, company_review=_review("", doubt=True))
t("质地差候选降为关注且依据写明", down["verdict"] == "关注" and "公司质地评为差" in down["basis"] and down["company_gate"])
t("质地中不动", card.judge(ev, company_review=_review(""))["verdict"] == "候选")
t("质地好不升档、证据不足不动", card.judge(ev, company_review=_review(""))["verdict"] == "候选"
and card.judge(ev, company_review=_review("证据不足"))["verdict"] == "候选")
t("开关关掉不降", card.judge(ev, company_review=_review(""), quality_gate=False)["verdict"] == "候选")
watch_ev = {**ev, "accum_state": ""}
t("本来就是关注的不再变", card.judge(watch_ev, company_review=_review(""))["verdict"] == "关注")
v = card.company_review_view(_review(""))
t("整句带质地三类、十视角计数、论点、失效条件、链接", all(x in v for x in ("质地好", "回报好", "好5/中3/差1", "买方论点", "失效条件", "报告 http")))
t("短句与分组标签", card.company_review_short(_review("")).startswith("好·贵") and card.company_review_label(_review("证据不足")) == ""
and card.company_review_label(None) == "" and card.company_review_view(None).startswith("公司深度:暂无"))
def main():
test_sources()
test_fifth_path()
test_card_gate_and_views()
print("ALL OK — 公司质地:取数 / 第五路 / 判决收敛 / 卡上文字")
if __name__ == "__main__":
main()