tradingSystem/scripts/test_batch23_units.py

176 lines
9.9 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""公司质地与建议方案接入 (2026-09-09 接入方案, 台账 053)。全部离线, 不连库不调模型。
1. 解析层收公司深度两键 (固定键缺质地归 None), 候选选择透传, 硬数字带它
2. 整句进送研判白名单; 开关关掉就不送; 原值字典不送
3. 逐票逻辑状态解析收公司深度两键; 持仓页 company_view 一行
4. 建议方案矩阵: 六种情形各一例; 研判补档; 开关开着时评估参数按建议
"""
import os
import sys
import traceback
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.core import action_engine as ae # noqa: E402
from app.services import advice_service as adv # noqa: E402
from app.services import judge as jd # noqa: E402
from app.services import logic_state_service as lss # noqa: E402
from app.services import param_store # noqa: E402
from app.services import plan_feed as pf # noqa: E402
RESULTS = []
def case(name):
def deco(fn):
RESULTS.append((name, fn))
return fn
return deco
class _Patch:
def __init__(self): self._saved = []
def __enter__(self): return self
def __call__(self, obj, name, value):
self._saved.append((obj, name, getattr(obj, name))); setattr(obj, name, value)
def __exit__(self, *a):
for obj, name, v in reversed(self._saved):
setattr(obj, name, v)
def _cr(overall="", valuation="", doubt=False):
return {"period": "2026Q1", "overall": overall, "valuation": valuation, "doubt_hard": doubt,
"groups": {"回报与护城河": "", "盈余质量与财务安全": "", "成长与含金量": ""},
"grade_counts": {"": 5, "": 3, "": 1, "证据不足": 1}, "thesis": "质地好但估值贵", "confidence": "",
"invalidation": "下一期看现金流", "antithesis": "覆盖少", "catalyst": "无法判断", "brain_status": "ok",
"ran_at": "2026-09-08", "age_days": 1, "report_url": "http://192.168.16.178:8000/company/601126.SH/review", "extra": 1}
@case("解析层收公司深度两键: 固定键、缺质地归 None、候选透传、硬数字带它")
def test_passthrough():
row = pf._rows([{"code": "601126.SH", "rank": 1, "score": 210, "company_review": _cr(), "company_review_text": "公司深度:质地好…",
"company_gate": ""}], "main")[0]
assert row["company_review"]["overall"] == "" and "extra" not in row["company_review"], row["company_review"]
assert row["company_review"]["grade_counts"][""] == 5 and row["company_review"]["groups"]["回报与护城河"] == ""
assert row["company_review_text"].startswith("公司深度")
empty = pf._rows([{"code": "601126.SH", "rank": 1, "score": 210, "company_review": {"overall": None}}], "main")[0]
assert empty["company_review"] is None and empty["company_review_text"] is None
plan = {"date": "2026-09-09", "main": [row], "observe": []}
item = pf.select_candidates(plan, top_n=10)["items"][0]
assert item["company_review"]["valuation"] == ""
c = {**item, "price": 10.0}
params = {"scale": 1_000_000, "stock_target_default": 0.06, "batch_split": (0.5, 0.25, 0.25)}
with _Patch() as p:
p(ae, "check_all_caps", lambda **kw: [])
p(ae, "_new_name_ctx", lambda caps, c: caps)
cand, why = ae.eval_open(c, params, {"names_count": 0, "max_names": 10}, 500_000)
hn = cand["hard_numbers"]
assert hn["company_review"]["overall"] == "" and hn["company_review_text"] == row["company_review_text"], why
@case("整句送研判且开关可关; 原值字典不送")
def test_judge_whitelist():
hn = {"price": 10.0, "company_review": _cr(), "company_review_text": "公司深度:质地好…", "advice": {"tier": "减半仓"}}
with _Patch() as p:
p(param_store, "get_bool", lambda k, d=None: True)
sent = jd._judge_hard_numbers("OPEN", hn)
assert sent["company_review_text"] == hn["company_review_text"] and "company_review" not in sent and "advice" not in sent, sent
with _Patch() as p:
p(param_store, "get_bool", lambda k, d=None: False if k == "PMS_JUDGE_COMPANY_TEXT" else True)
sent2 = jd._judge_hard_numbers("OPEN", hn)
assert "company_review_text" not in sent2, sent2
assert "company_review_text" in jd.OPEN_JUDGE_KEYS
@case("逐票逻辑状态解析收公司深度; 持仓页 company_view 一行")
def test_positions_view():
payload = {"date": "2026-09-09", "states": [{"code": "SH601126", "state": "逻辑成立", "why": None, "as_of": "2026-09-08",
"reasons": ["a"], "company_review": _cr(), "company_review_text": "公司深度:质地好…"}]}
st = pf.parse_logic_states(payload)["SH601126"]
assert st["company_review"]["overall"] == "" and st["company_review_text"].startswith("公司深度")
v = lss.company_view(st)
assert v["overall"] == "" and "质地好" in v["line"] and v["invalidation"] == "下一期看现金流" and v["report_url"]
assert lss.company_view({"state": "逻辑成立"}) is None
rows = [{"ts_code": "SH601126", "total_qty": 0}]
with _Patch() as p:
p(lss, "state_map", lambda: {"SH601126": st})
lss.decorate_positions(rows)
assert rows[0]["company"]["overall"] == ""
@case("建议方案矩阵六情形与研判补档")
def test_advice_matrix():
params = {"stock_target_default": 0.06, "batch_split": (0.5, 0.25, 0.25)}
val_good = {"neut": 12.0, "pess": 9.5, "price": 10.0} # 上行 20%、下行 5% → 赔率 4
val_bad = {"neut": 10.5, "pess": 8.0, "price": 10.0} # 上行 5%、下行 20% → 赔率 0.25
a = adv.advise({"company_review": _cr("", ""), "logic_state": {"state": "逻辑成立"}, "valuation": val_good}, params)
assert a["tier"] == "标准仓" and a["splits"] == (0.5, 0.25, 0.25) and abs(a["target_pct"] - 0.06) < 1e-9, a
b = adv.advise({"company_review": _cr("", ""), "logic_state": {"state": "逻辑成立"}, "valuation": val_good}, params)
assert b["tier"] == "减半仓" and b["splits"] == (0.5, 0.5) and abs(b["target_pct"] - 0.03) < 1e-9, b
c = adv.advise({"company_review": _cr("", ""), "logic_state": {"state": "逻辑成立"}, "valuation": val_good}, params)
assert c["tier"] == "标准仓" and c["splits"] == (0.3, 0.35, 0.35) and "二十日头看多" in "".join(c["conditions"]), c
d = adv.advise({"company_review": _cr("", ""), "logic_state": {"state": "逻辑成立"}, "valuation": val_bad}, params)
assert d["tier"] == "试探仓" and abs(d["target_pct"] - 0.01) < 1e-9 and "人工确认" in "".join(d["conditions"]), d
e = adv.advise({"company_review": _cr("", ""), "logic_state": {"state": "逻辑成立"}, "valuation": val_good,
"pricing_state": {"state": "高位兑现"}}, params)
assert e["delay_first"] and e["tier"] == "标准仓", e
f = adv.advise({"company_review": _cr("", "", True), "logic_state": {"state": "逻辑存疑"}}, params)
assert f["tier"] == "不建" and f["target_pct"] == 0.0
g = adv.advise({"company_review": None, "valuation": val_good}, params)
assert g["tier"] == "标准仓" and "沿用机械方案" in "".join(g["why"]) and g["splits"] == (0.5, 0.25, 0.25)
# 研判补档
a2 = adv.apply_judge(a, {"verdict": "APPROVE", "confidence": 45}, 60)
assert a2["tier"] == "试探仓" and a2["needs_user_confirm"] and "研判" in "".join(a2["why"]), a2
a3 = adv.apply_judge(a, {"verdict": "APPROVE", "confidence": 80}, 60)
assert a3["tier"] == "标准仓"
a4 = adv.apply_judge(a, {"verdict": "UNAVAILABLE", "confidence": None}, 60)
assert a4["tier"] == "试探仓"
# 评估参数
p2 = adv.params_for(b, params)
assert abs(p2["stock_target_default"] - 0.03) < 1e-9 and p2["batch_split"] == (0.5, 0.5)
assert adv.params_for(f, params) is params
assert "建议方案:减半仓" in adv.text(b) and "机械方案 6.0%" in adv.text(b)
@case("扫描时建议方案进硬数字, 开关开着时按建议仓位评估")
def test_scan_open_uses_advice():
item = {"ts_code": "601126.SH", "code": "601126.SH", "rank": 1, "score": 210, "price": 10.0, "verdict": "候选",
"company_review": _cr("", ""), "company_review_text": "x", "logic_state": {"state": "逻辑成立"},
"valuation": {"neut": 12.0, "pess": 9.5, "price": 10.0}}
params = {"scale": 1_000_000, "stock_target_default": 0.06, "batch_split": (0.5, 0.25, 0.25), "plan_by_advice": True,
"open_route_by_verdict": False, "open_route_by_logic": False}
seen = {}
def fake_eval(c, p, ctx, left):
seen["target"] = p.get("stock_target_default"); seen["split"] = p.get("batch_split")
return {"ts_code": c["ts_code"], "action": ae.A_OPEN, "qty": 100, "hard_numbers": {}}, None
with _Patch() as p:
p(ae, "eval_open", fake_eval)
out = ae.scan_open(candidates=[item], params=params, caps={"names_count": 0, "max_names": 10}, room_amt=500_000, slots=5)
assert abs(seen["target"] - 0.03) < 1e-9 and seen["split"] == (0.5, 0.5), (seen, out.get("skipped"))
cands = out["candidates"]
hn = cands[0]["hard_numbers"]
assert hn["advice"]["tier"] == "减半仓" and hn["advice_text"].startswith("建议方案") and hn["plan_by_advice"] is True
def main():
ok = 0
for name, fn in RESULTS:
try:
fn()
ok += 1
print(f" ok {name}")
except Exception:
print(f" FAIL {name}")
traceback.print_exc()
print("-" * 60)
if ok == len(RESULTS):
print(f"ALL PASS ({ok} cases)")
return 0
print(f"FAILED {len(RESULTS) - ok}/{len(RESULTS)}")
return 1
if __name__ == "__main__":
sys.exit(main())