公司质地与建议方案接入:公司深度两键透传、整句送研判可关、建议方案矩阵与研判补档、持仓页一行、三个运行参数、单测(台账 053)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
499b1342e4
commit
0724509624
|
|
@ -300,3 +300,8 @@ docker compose run --rm --no-deps pms-web python scripts/probe_bionic.py --base
|
|||
| V1.0 | 2026-08-03 | 首版定稿并双侧落码。择时部分是一套盘中判定规则(资金阈值、动量追买等) |
|
||||
| V1.1 | 2026-08-03 | 按用户审核意见重做择时:删掉 V1.0 的盘中判定规则(违反「提前计算为主、盘中监控为辅、不另做盘中判断、可以接受买不上」),改为由昨夜支撑压力推出执行区间、盘中只做区间比对与当日监控核对;研判留痕不再写 `decision_ledger`(每晚判分全表扫描,已核实);文档与代码注释清理生造词 |
|
||||
| V1.2 | 2026-09-08 | 量价研判链(《量价研判链吸收方案_2026-09-08》):请求侧硬数字白名单多送 `events_text`、`pricing_text` 两句整句(催化事件与定价状态,事件与量价不是产业逻辑);应答侧新建仓研判可并列多带 `pv_heads`(`h5`、`h20` 各含 `score` −1 到 1、`direction` 看多/中性/看空、`justification`),PMS 归一后进提议硬数字 `judge_pv_heads`,只显示不触发;三值 verdict 口径不变,缺 `pv_heads` 就是 None,向下兼容 |
|
||||
|
||||
### 7.x 2026-09-09 公司深度整句(V1.3)
|
||||
- 研判请求的硬数字新增 `company_review_text`(选股系统写好的整句:质地档与三类、十视角计数、买方论点一句、置信度、失效条件、报告链接)。
|
||||
与 `verdict/reasons` 同一性质:上游结论送去让研判对照,研判不重判质地。PMS 侧开关 `PMS_JUDGE_COMPANY_TEXT`(默认开)关掉即不送。
|
||||
- bionic 侧不需要改:硬数字逐键渲染进提示词。应答形状不变。
|
||||
|
|
|
|||
|
|
@ -697,6 +697,9 @@ def eval_open(c: dict, params: dict, caps: dict, room_amt: float):
|
|||
"news": c.get("news"), "news_text": c.get("news_text"),
|
||||
# 行业催化 (2026-09-08 台账 051): 环节级, 原值进账本与提议卡; 产业逻辑不送研判。
|
||||
"industry_catalyst": c.get("industry_catalyst"), "industry_catalyst_text": c.get("industry_catalyst_text"),
|
||||
# 公司深度 (2026-09-09 接入方案): 质地档与评议摘要进账本与提议卡; 整句送研判 (白名单里, 开关可关)。
|
||||
"company_review": c.get("company_review"), "company_review_text": c.get("company_review_text"),
|
||||
"company_gate": c.get("company_gate"),
|
||||
# ---- 仓位口径: 只进评审账本做判分锚。judge.py 送研判时会把这几项过滤掉,
|
||||
# 理由见那边的 OPEN_JUDGE_KEYS —— 决策系统本来就不管仓位, 别送过去带偏它。
|
||||
"target_pct": target_pct, "target_amount": round(full_amt, 2),
|
||||
|
|
@ -799,8 +802,16 @@ def scan_open(*, candidates: list, params: dict, caps: dict, room_amt: float,
|
|||
if why_skip:
|
||||
skipped.append({"ts_code": code, "action": A_OPEN, "why": why_skip})
|
||||
continue
|
||||
# 建议方案 (2026-09-09 接入方案第六之二节): 先按质地、估值、赔率、逻辑状态、定价状态出仓位档与分批;
|
||||
# 开关 PMS_PLAN_BY_ADVICE 开着时它就是评估参数, 关着时只并排显示。研判那一档在提议落账前补。
|
||||
try:
|
||||
cand, why = eval_open(c, params, ctx, left)
|
||||
from app.services import advice_service as _adv
|
||||
adv = _adv.advise(c, params)
|
||||
p_use = _adv.params_for(adv, params) if params.get("plan_by_advice") else params
|
||||
except Exception as e: # noqa: BLE001 —— 建议算不出不拦评估
|
||||
adv, p_use = {"tier": None, "text": f"建议方案算不出: {type(e).__name__}"}, params
|
||||
try:
|
||||
cand, why = eval_open(c, p_use, ctx, left)
|
||||
except Exception as e: # 单票异常不能拖垮整轮扫描 (与 scan() 同口径)
|
||||
skipped.append({"ts_code": code, "action": A_OPEN,
|
||||
"why": f"评估异常 {type(e).__name__}: {e}"})
|
||||
|
|
@ -808,6 +819,15 @@ def scan_open(*, candidates: list, params: dict, caps: dict, room_amt: float,
|
|||
if not cand:
|
||||
skipped.append({"ts_code": code, "action": A_OPEN, "why": why or "未产出候选"})
|
||||
continue
|
||||
try:
|
||||
cand.setdefault("hard_numbers", {})["advice"] = adv
|
||||
cand["hard_numbers"]["advice_text"] = adv.get("text") if isinstance(adv, dict) else None
|
||||
cand["hard_numbers"]["plan_by_advice"] = bool(params.get("plan_by_advice"))
|
||||
if isinstance(adv, dict) and adv.get("tier") == "试探仓":
|
||||
cand["needs_user_confirm"] = True
|
||||
cand["confirm_why"] = (cand.get("confirm_why") or "") + "建议方案为试探仓,只在人工确认后下;"
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
if route:
|
||||
confirm_why = verdict_confirm_why(c.get("verdict"))
|
||||
if confirm_why:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,137 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""建议建仓方案 (2026-09-09 接入方案第六之二节, 台账 053)。纯函数, 不读库不调模型。
|
||||
|
||||
输入六样: 公司质地 (好/中/差, 来自数据基座代码合成)、估值标签 (贵/中/便宜)、赔率 (安全边际三情景)、
|
||||
逻辑状态 (逻辑成立/强化/存疑/无法判断)、研判结论与把握度 (提议落账前才有)、定价状态。
|
||||
输出一份「建议方案」: 仓位档、分批、附加条件、持有期限标签、一句话。矩阵一次定死, 只按复盘读数换档位:
|
||||
|
||||
质地好 + 估值中或便宜 + 逻辑成立或强化 → 标准仓, 5/2.5/2.5 成, 第二批要求二十日头不看空
|
||||
质地好 + 估值贵 → 减半仓, 两批各半, 第二批等下一期财报失效条件未触发
|
||||
质地中 → 标准仓, 第一批 3 成、后两批各 3.5 成, 后两批要求二十日头看多且非高位兑现
|
||||
研判把握度低或不可用、赔率低于 1 → 试探仓 (1%), 一批, 只在人工确认后下
|
||||
定价状态高位兑现 → 档位不变, 首批推迟到回撤至支撑
|
||||
质地差、逻辑存疑 → 不建 (判决与分流已经拦住; 这里只写原因)
|
||||
没有质地读数 (证据不足 / 没报告) → 沿用机械方案, 只标「质地未知」
|
||||
|
||||
开关 PMS_PLAN_BY_ADVICE (模拟侧默认开): 开着时建议方案直接作为提议的仓位档与分批; 关着时只并排显示。
|
||||
止损定性与风控卖出不动; 择时决策系统照旧决定每一批的下单时点。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
TIER_STD, TIER_HALF, TIER_TRIAL, TIER_NONE = "标准仓", "减半仓", "试探仓", "不建"
|
||||
DEFAULT_SPLIT = (0.5, 0.25, 0.25)
|
||||
|
||||
|
||||
def _f(v, d=None):
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return d
|
||||
|
||||
|
||||
def odds_of(valuation: Optional[dict]) -> Optional[float]:
|
||||
"""赔率 = 中性上行 ÷ 悲观下行 (选股系统安全边际三情景的口径)。算不出返回 None。"""
|
||||
if not isinstance(valuation, dict):
|
||||
return None
|
||||
neut, pess, price = _f(valuation.get("neut")), _f(valuation.get("pess")), _f(valuation.get("price"))
|
||||
if not neut or not pess or not price or price <= 0:
|
||||
return None
|
||||
up, down = neut / price - 1.0, pess / price - 1.0
|
||||
if down >= 0:
|
||||
return 9.99 # 悲观情景都在现价之上: 赔率极好, 封顶
|
||||
return round(up / abs(down), 2) if up > 0 else 0.0
|
||||
|
||||
|
||||
def advise(c: dict, params: dict) -> dict[str, Any]:
|
||||
"""候选产出前的建议 (没有研判结论)。返回的 tier / factor / splits 由 params_for 变成评估参数。"""
|
||||
cr = c.get("company_review") if isinstance(c.get("company_review"), dict) else {}
|
||||
overall = cr.get("overall")
|
||||
valuation_label = cr.get("valuation")
|
||||
ls = (c.get("logic_state") or {}).get("state") if isinstance(c.get("logic_state"), dict) else None
|
||||
ps = (c.get("pricing_state") or {}).get("state") if isinstance(c.get("pricing_state"), dict) else None
|
||||
odds = odds_of(c.get("valuation"))
|
||||
base_pct = _f(params.get("stock_target_default"), 0.06)
|
||||
base_split = tuple(params.get("batch_split") or DEFAULT_SPLIT)
|
||||
adv: dict[str, Any] = {"tier": TIER_STD, "factor": 1.0, "splits": base_split, "conditions": [], "why": [],
|
||||
"hold_type": "待定", "delay_first": False, "quality": overall, "valuation_label": valuation_label,
|
||||
"logic_state": ls, "pricing_state": ps, "odds": odds,
|
||||
"mechanical": {"target_pct": base_pct, "splits": base_split}, "version": "1"}
|
||||
if overall == "差" or ls == "逻辑存疑":
|
||||
adv.update(tier=TIER_NONE, factor=0.0, splits=(), hold_type="不适用")
|
||||
adv["why"].append("质地差或逻辑存疑:判决与分流已经拦住,不建")
|
||||
elif overall == "好" and valuation_label in ("中", "便宜") and ls in ("逻辑成立", "逻辑强化", None):
|
||||
adv.update(tier=TIER_STD, factor=1.0, splits=(0.5, 0.25, 0.25), hold_type="长期持有类")
|
||||
adv["conditions"].append("第二批要求二十日头不看空")
|
||||
adv["why"].append(f"质地好、估值{valuation_label}、逻辑{ls or '未读到'}")
|
||||
elif overall == "好" and valuation_label == "贵":
|
||||
adv.update(tier=TIER_HALF, factor=0.5, splits=(0.5, 0.5), hold_type="长期持有类")
|
||||
adv["conditions"].append("第二批等下一期财报失效条件未触发再加")
|
||||
adv["why"].append("质地好但估值贵")
|
||||
elif overall == "中":
|
||||
adv.update(tier=TIER_STD, factor=1.0, splits=(0.3, 0.35, 0.35), hold_type="事件驱动类")
|
||||
adv["conditions"].append("后两批要求二十日头看多且定价状态不是高位兑现")
|
||||
adv["why"].append(f"质地中、估值{valuation_label or '无'}")
|
||||
elif overall == "好":
|
||||
adv.update(tier=TIER_STD, factor=1.0, splits=(0.5, 0.25, 0.25), hold_type="长期持有类")
|
||||
adv["conditions"].append("第二批要求二十日头不看空")
|
||||
adv["why"].append(f"质地好、估值{valuation_label or '无'}、逻辑{ls or '未读到'}")
|
||||
else:
|
||||
adv["why"].append("没有质地读数(证据不足或没报告),沿用机械方案")
|
||||
if odds is not None and odds < 1.0 and adv["tier"] not in (TIER_NONE,):
|
||||
adv.update(tier=TIER_TRIAL, factor=1.0 / 6.0, splits=(1.0,))
|
||||
adv["conditions"].append("只在人工确认后下")
|
||||
adv["why"].append(f"赔率 {odds:.2f} 低于 1")
|
||||
if ps == "高位兑现" and adv["tier"] != TIER_NONE:
|
||||
adv["delay_first"] = True
|
||||
adv["conditions"].append("定价状态高位兑现:首批推迟,等回撤到支撑再下")
|
||||
adv["target_pct"] = round(base_pct * adv["factor"], 4)
|
||||
adv["text"] = text(adv)
|
||||
return adv
|
||||
|
||||
|
||||
def apply_judge(adv: Optional[dict], judge: Optional[dict], conf_min: int = 60) -> Optional[dict]:
|
||||
"""提议落账前补研判一档: 把握度低于 conf_min 或结论不可用 → 试探仓, 只在人工确认后下。不改数量, 数量交人。"""
|
||||
if not isinstance(adv, dict) or not isinstance(judge, dict):
|
||||
return adv
|
||||
verdict = str(judge.get("verdict") or "").upper()
|
||||
conf = _f(judge.get("confidence"))
|
||||
low = (verdict in ("UNAVAILABLE", "不可用")) or (conf is not None and conf < conf_min)
|
||||
if low and adv.get("tier") not in (TIER_NONE, TIER_TRIAL):
|
||||
adv = dict(adv)
|
||||
adv.update(tier=TIER_TRIAL, factor=1.0 / 6.0, splits=(1.0,))
|
||||
adv["conditions"] = list(adv.get("conditions") or []) + ["只在人工确认后下(研判把握度低或不可用)"]
|
||||
adv["why"] = list(adv.get("why") or []) + [f"研判 {verdict or '无结论'}、把握度 {conf if conf is not None else '无'}"]
|
||||
adv["target_pct"] = round(_f((adv.get("mechanical") or {}).get("target_pct"), 0.06) * adv["factor"], 4)
|
||||
adv["needs_user_confirm"] = True
|
||||
adv["text"] = text(adv)
|
||||
return adv
|
||||
|
||||
|
||||
def params_for(adv: dict, params: dict) -> dict:
|
||||
"""把建议方案变成评估参数 (开关开着时用): 目标仓位乘档位系数, 分批按建议。不建的返回原参数 (判决已拦)。"""
|
||||
if not isinstance(adv, dict) or adv.get("tier") in (None, TIER_NONE):
|
||||
return params
|
||||
out = dict(params)
|
||||
out["stock_target_default"] = adv.get("target_pct") or params.get("stock_target_default")
|
||||
if adv.get("splits"):
|
||||
out["batch_split"] = tuple(adv["splits"])
|
||||
return out
|
||||
|
||||
|
||||
def text(adv: dict) -> str:
|
||||
if not isinstance(adv, dict):
|
||||
return ""
|
||||
if adv.get("tier") == TIER_NONE:
|
||||
return "建议方案:不建(" + ";".join(adv.get("why") or []) + ")"
|
||||
sp = "/".join(f"{x:.0%}" for x in (adv.get("splits") or ()))
|
||||
mech = adv.get("mechanical") or {}
|
||||
s = (f"建议方案:{adv.get('tier')}(目标 {adv.get('target_pct', 0):.1%},分批 {sp or '一批'})"
|
||||
f";机械方案 {_f(mech.get('target_pct'), 0):.1%} 分批 {'/'.join(f'{x:.0%}' for x in (mech.get('splits') or ()))}")
|
||||
if adv.get("conditions"):
|
||||
s += ";条件:" + ";".join(adv["conditions"])
|
||||
if adv.get("delay_first"):
|
||||
s += ";首批推迟"
|
||||
s += f";{adv.get('hold_type')};依据:" + ";".join(adv.get("why") or [])
|
||||
return s
|
||||
|
|
@ -51,7 +51,10 @@ OPEN_JUDGE_KEYS = ("price", "score", "theme", "tier", "upside", "heat",
|
|||
"events_text", "pricing_text",
|
||||
# 相关快讯整句 (2026-09-08 台账 050): 财联社电报近 3 天点名这只票的条目。它是市场
|
||||
# 反应与公司快讯, 不是产业逻辑; 正负不由 PMS 判, 交给研判连同事件上下文一起看。
|
||||
"news_text")
|
||||
"news_text",
|
||||
# 公司深度整句 (2026-09-09 接入方案第四节): 质地档、买方论点一句、置信度、失效条件。照 09-02 送判决
|
||||
# 与理由的先例——让研判拿自己的量价结论对照; 质地本身由选股系统判, 研判不重判。开关 PMS_JUDGE_COMPANY_TEXT。
|
||||
"company_review_text")
|
||||
|
||||
# 新建仓送研判时的必答题 (2026-09-03)。候选卡的理由随硬数字送过去了, 研判要逐条回答
|
||||
# 「这条理由到今天还成不成立」—— 这是把上游的静态判决与决策系统的当日观点接起来的那一句。
|
||||
|
|
@ -62,7 +65,13 @@ def _judge_hard_numbers(action: str, hard: dict) -> dict:
|
|||
"""按动作裁剪送出去的硬数字。非 OPEN 一律原样送 (行为与 2026-08-06 之前一字不差)。"""
|
||||
if str(action or "").upper() != "OPEN":
|
||||
return dict(hard or {})
|
||||
return {k: v for k, v in (hard or {}).items() if k in OPEN_JUDGE_KEYS}
|
||||
keys = set(OPEN_JUDGE_KEYS)
|
||||
try:
|
||||
if not param_store.get_bool("PMS_JUDGE_COMPANY_TEXT", True):
|
||||
keys.discard("company_review_text")
|
||||
except Exception: # noqa: BLE001 —— 参数读不到按默认送
|
||||
pass
|
||||
return {k: v for k, v in (hard or {}).items() if k in keys}
|
||||
|
||||
|
||||
def jsonable(v):
|
||||
|
|
|
|||
|
|
@ -176,6 +176,19 @@ def now_view(st) -> dict:
|
|||
"text": (reasons[0] if reasons else (st.get("why") or ""))[:160]}
|
||||
|
||||
|
||||
def company_view(st):
|
||||
"""持仓页「公司质地 · 失效条件」一行 (2026-09-09 接入方案): 质地档、三类、置信度、失效条件、报告链接。没读数返回 None。"""
|
||||
cr = (st or {}).get("company_review") if isinstance(st, dict) else None
|
||||
if not isinstance(cr, dict) or not cr.get("overall"):
|
||||
return None
|
||||
g = cr.get("groups") or {}
|
||||
return {"overall": cr.get("overall"), "valuation": cr.get("valuation"), "period": cr.get("period"),
|
||||
"line": f"质地{cr.get('overall')}(回报{g.get('回报与护城河') or '缺'}、盈余{g.get('盈余质量与财务安全') or '缺'}、成长{g.get('成长与含金量') or '缺'})"
|
||||
f",估值{cr.get('valuation') or '无'}" + (f",置信{cr['confidence']}" if cr.get("confidence") else ""),
|
||||
"invalidation": cr.get("invalidation"), "doubt_hard": bool(cr.get("doubt_hard")),
|
||||
"report_url": cr.get("report_url"), "text": (st or {}).get("company_review_text")}
|
||||
|
||||
|
||||
def ref_target_view(st):
|
||||
"""参考目标价: 第四件的中性情景估值 (中位每股收益乘中位市盈率), 2026-09-07 拍板在持仓页显示,
|
||||
只显示不触发, 不替代用户手设的目标价 (到价提议只认命令表里那个)。算不出就不给。"""
|
||||
|
|
@ -243,6 +256,10 @@ def decorate_positions(rows: list, states: dict | None = None) -> list:
|
|||
r["ref_target"] = ref_target_view(states.get(code))
|
||||
except Exception: # noqa: BLE001
|
||||
r["ref_target"] = None
|
||||
try:
|
||||
r["company"] = company_view(states.get(code))
|
||||
except Exception: # noqa: BLE001
|
||||
r["company"] = None
|
||||
if int(r.get("total_qty") or 0) <= 0:
|
||||
r["entry"] = None
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -72,6 +72,10 @@ RUNTIME_EXTRA = {
|
|||
"PMS_LOGIC_STATE_ROUTE": (True, bool, "按选股系统的逻辑状态分流: 逻辑存疑的持仓停增持侧自主动作并暂停策略买入腿, 判决候选而逻辑存疑的新建仓强制人工确认 (默认开; 关掉即回到只按判决)"),
|
||||
"PMS_LOGIC_DOUBT_TRIM": (False, bool, "逻辑存疑的持仓出减持提议 (必定交人裁决, 绝不自动卖; 默认关, 先跑停增持一个月看误伤再开)"),
|
||||
"PMS_LOGIC_DOUBT_TRIM_RATIO": (0.3333, float, "逻辑存疑减持提议的比例 (占总持仓; 默认与保垫减仓相同的三分之一; 一次定死, 不按复盘读数回调)"),
|
||||
# 2026-09-09 接入方案 (台账 053): 公司深度与建议方案的三个开关。
|
||||
"PMS_JUDGE_COMPANY_TEXT": (True, bool, "新建仓研判送公司深度整句 (质地档、买方论点、置信度、失效条件); 关掉只给人看"),
|
||||
"PMS_PLAN_BY_ADVICE": (True, bool, "建议方案直接作为新建仓提议的仓位档与分批 (模拟侧默认开; 关掉只并排显示, 提议照机械方案)"),
|
||||
"PMS_ADVICE_TRIAL_CONF_MIN": (60, int, "研判把握度低于此值 (或不可用) 时建议方案降为试探仓, 只在人工确认后下"),
|
||||
"PMS_LOGIC_STATE_MAP": ("", str, "在持票的逻辑状态映射 (plan_pull 每早写入的 JSON: 时刻/日期/逐票状态), 扫描层与页面只读, 勿手改"),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -189,6 +189,24 @@ def _news_or_none(v, limit=5):
|
|||
return {"count": count, "latest": _text_or_none(v.get("latest")), "items": items}
|
||||
|
||||
|
||||
def _company_review_or_none(v):
|
||||
"""公司深度 (2026-09-09 接入方案): 数据基座个股深度评析的摘要。固定键, 缺质地档归 None。
|
||||
质地进逻辑状态第五路与判决收敛 (都在选股系统算完); PMS 这里只透传给提议卡、持仓页、建议方案与研判整句。"""
|
||||
if not isinstance(v, dict) or not v.get("overall"):
|
||||
return None
|
||||
gc = v.get("grade_counts") if isinstance(v.get("grade_counts"), dict) else {}
|
||||
groups = v.get("groups") if isinstance(v.get("groups"), dict) else {}
|
||||
return {"period": _text_or_none(v.get("period")), "overall": _text_or_none(v.get("overall")),
|
||||
"valuation": _text_or_none(v.get("valuation")), "doubt_hard": bool(v.get("doubt_hard")),
|
||||
"groups": {k: _text_or_none(groups.get(k)) for k in ("回报与护城河", "盈余质量与财务安全", "成长与含金量")},
|
||||
"grade_counts": {k: _int_or_none(gc.get(k)) or 0 for k in ("好", "中", "差", "证据不足")},
|
||||
"thesis": _text_or_none(v.get("thesis")), "confidence": _text_or_none(v.get("confidence")),
|
||||
"invalidation": _text_or_none(v.get("invalidation")), "antithesis": _text_or_none(v.get("antithesis")),
|
||||
"catalyst": _text_or_none(v.get("catalyst")), "brain_status": _text_or_none(v.get("brain_status")),
|
||||
"ran_at": _text_or_none(v.get("ran_at")), "age_days": _int_or_none(v.get("age_days")),
|
||||
"report_url": _text_or_none(v.get("report_url"))}
|
||||
|
||||
|
||||
def _industry_catalyst_or_none(v, limit=3):
|
||||
"""行业催化 (2026-09-08 台账 051): 上游按环节标出的行业级催化, 只留最多 limit 条固定键。
|
||||
产业逻辑不送研判 (judge.OPEN_JUDGE_KEYS 不收), 只进硬数字给人看与复盘。"""
|
||||
|
|
@ -309,6 +327,11 @@ def _rows(raw, bucket: str) -> list:
|
|||
# 行业催化 (2026-09-08 台账 051): 环节级, 只给人看与复盘, 不送研判、不进判定。
|
||||
"industry_catalyst": _industry_catalyst_or_none(it.get("industry_catalyst")),
|
||||
"industry_catalyst_text": _text_or_none(it.get("industry_catalyst_text")),
|
||||
# 公司深度 (2026-09-09 接入方案): 质地档与评议摘要。整句送研判 (judge.OPEN_JUDGE_KEYS 收 company_review_text,
|
||||
# 开关 PMS_JUDGE_COMPANY_TEXT); 原值进硬数字给提议卡、持仓页与建议方案。质地本身的判定在选股系统已经做完。
|
||||
"company_review": _company_review_or_none(it.get("company_review")),
|
||||
"company_review_text": _text_or_none(it.get("company_review_text")),
|
||||
"company_gate": _text_or_none(it.get("company_gate")),
|
||||
})
|
||||
return out
|
||||
|
||||
|
|
@ -506,7 +529,9 @@ def select_candidates(plan: dict, *, held=(), black=(), top_n: int = 30, tiers=N
|
|||
"pricing_state": r.get("pricing_state"), "pricing_text": r.get("pricing_text"),
|
||||
"news": r.get("news"), "news_text": r.get("news_text"),
|
||||
"industry_catalyst": r.get("industry_catalyst"),
|
||||
"industry_catalyst_text": r.get("industry_catalyst_text")}
|
||||
"industry_catalyst_text": r.get("industry_catalyst_text"),
|
||||
"company_review": r.get("company_review"), "company_review_text": r.get("company_review_text"),
|
||||
"company_gate": r.get("company_gate")}
|
||||
for r in passed[:n]]
|
||||
return {"date": plan.get("date"), "considered": len(pool), "eligible": len(passed),
|
||||
"items": items, "dropped": dropped, "st_unknown": st_unknown,
|
||||
|
|
@ -710,6 +735,9 @@ def parse_logic_states(payload) -> dict:
|
|||
"as_of": _text_or_none(v.get("as_of")), "na": _text_or_none(v.get("na"))}
|
||||
if v else None)
|
||||
st["valuation_text"] = _text_or_none(it.get("valuation_text"))
|
||||
# 公司深度两键随状态回 (2026-09-09 接入方案): 持仓页"证据还在不在"旁显示质地与失效条件。
|
||||
st["company_review"] = _company_review_or_none(it.get("company_review"))
|
||||
st["company_review_text"] = _text_or_none(it.get("company_review_text"))
|
||||
out[code] = st
|
||||
return out
|
||||
|
||||
|
|
|
|||
|
|
@ -743,6 +743,16 @@ def _make_proposal(c, price, verdict) -> str:
|
|||
"judge_verdict": verdict.get("verdict"), "judge_conf": verdict.get("confidence"),
|
||||
# 两个期限的头 (2026-09-08 量价研判链): 决策系统并列给的 5 日与 20 日判断, 只显示不触发。
|
||||
"judge_pv_heads": verdict.get("pv_heads")}
|
||||
# 建议方案补研判一档 (2026-09-09 接入方案): 把握度低或不可用 → 试探仓, 只在人工确认后下 (数量交人, 不自动改)。
|
||||
try:
|
||||
from app.services import advice_service as _adv
|
||||
adv2 = _adv.apply_judge(hn.get("advice"), verdict, param_store.get_int("PMS_ADVICE_TRIAL_CONF_MIN", 60))
|
||||
if isinstance(adv2, dict):
|
||||
hn["advice"], hn["advice_text"] = adv2, adv2.get("text")
|
||||
if adv2.get("needs_user_confirm"):
|
||||
hn["needs_user_confirm"] = True
|
||||
except Exception: # noqa: BLE001 —— 建议方案出错不拦提议
|
||||
pass
|
||||
try:
|
||||
pms_repo.insert_proposal(
|
||||
proposal_id=pid, ts_code=c["ts_code"], action=c["action"], qty=c["qty"],
|
||||
|
|
@ -782,6 +792,8 @@ def _scan_params(view: dict) -> dict:
|
|||
# 减持提议开关 (默认关)、减持比例 (默认与保垫减仓相同的三分之一)。
|
||||
"logic_state_route": param_store.get_bool("PMS_LOGIC_STATE_ROUTE", True),
|
||||
"open_route_by_logic": param_store.get_bool("PMS_LOGIC_STATE_ROUTE", True),
|
||||
# 建议方案直接作为新建仓的仓位档与分批 (2026-09-09 接入方案第六之二节; 模拟侧默认开)
|
||||
"plan_by_advice": param_store.get_bool("PMS_PLAN_BY_ADVICE", True),
|
||||
"logic_doubt_trim": param_store.get_bool("PMS_LOGIC_DOUBT_TRIM", False),
|
||||
"logic_doubt_trim_ratio": param_store.get_float("PMS_LOGIC_DOUBT_TRIM_RATIO", 1.0 / 3),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1036,6 +1036,10 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
|||
<div class="st muted" v-if="propText(p, 'pricing_text')">{{ propText(p, 'pricing_text') }}</div>
|
||||
<div class="st muted" v-if="propText(p, 'news_text')">{{ propText(p, 'news_text') }}</div>
|
||||
<div class="st muted" v-if="propText(p, 'industry_catalyst_text')">{{ propText(p, 'industry_catalyst_text') }}</div>
|
||||
<!-- 公司深度与建议方案 (2026-09-09 接入方案): 质地档、评议摘要、报告链接; 建议方案与机械方案并排。 -->
|
||||
<div class="st" v-if="propText(p, 'company_review_text')"><span class="muted">公司深度:</span>{{ propText(p, 'company_review_text') }}</div>
|
||||
<div class="st" v-if="propText(p, 'company_gate')"><span class="muted">判决收敛:</span>{{ propText(p, 'company_gate') }}</div>
|
||||
<div class="st" v-if="propText(p, 'advice_text')">{{ propText(p, 'advice_text') }}</div>
|
||||
<div class="st" v-if="propHeads(p)"><span class="muted">择时层两个期限:</span>{{ propHeads(p) }}</div>
|
||||
<div class="st muted">{{ propWhy(p) }}</div>
|
||||
<div class="st">约 {{ p.qty }} 股 · 参考价 {{ (p.hard_numbers||{}).price==null ? '—' : (p.hard_numbers||{}).price }}
|
||||
|
|
@ -1602,6 +1606,9 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
|
|||
<div class="muted mono" style="font-size:11px">证据截止 {{ s.row.logic_now.as_of || '—' }}<template
|
||||
v-if="s.row.logic_now.settle_note && s.row.logic_now.settle_note!=='维持'">;{{ s.row.logic_now.settle_note }}</template></div>
|
||||
</template>
|
||||
<!-- 公司质地 · 失效条件 (2026-09-09 接入方案): 数据基座代码合成的质地档与评议给的失效条件, 只显示。 -->
|
||||
<div class="muted" v-if="s.row.company && s.row.company.line" :title="s.row.company.text || ''" style="font-size:11px">
|
||||
{{ s.row.company.line }}<template v-if="s.row.company.invalidation">;失效条件:{{ s.row.company.invalidation }}</template></div>
|
||||
<span v-else class="muted">{{ (s.row.logic_now && s.row.logic_now.text) || '今天没有读数' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@
|
|||
强制入队对卖出同样有效/研判不可用的减持入队/研究证据走弱
|
||||
来源必定交人裁决/风控高置信卖出与命令清仓不经提议分流/
|
||||
到价提议挂着时止损照落并作废该提议 (17 例)
|
||||
test_batch23_units.py 公司质地与建议方案接入 (2026-09-09 接入方案): 解析层收公司深度两键/整句送研判可关/建议
|
||||
方案矩阵与研判补档/持仓页公司块。
|
||||
test_batch22_units.py 逻辑状态四态接入 (2026-09-07 第三件): 解析层收逻辑状态/硬数字带它而研判
|
||||
白名单不收/判决候选而逻辑存疑强制交人/持仓存疑停增持侧/研究走弱
|
||||
减持默认关且开了必交人/同轮减持优先级/早上取回与映射新鲜度/策略
|
||||
|
|
@ -110,7 +112,7 @@ SUITES = ["test_core_units.py", "test_batch2_units.py", "test_batch3_units.py",
|
|||
"test_batch13_units.py", "test_batch14_units.py", "test_batch15_units.py",
|
||||
"test_batch16_units.py", "test_batch17_units.py",
|
||||
"test_batch18_units.py", "test_batch19_units.py", "test_batch20_units.py",
|
||||
"test_batch21_units.py", "test_batch22_units.py",
|
||||
"test_batch21_units.py", "test_batch22_units.py", "test_batch23_units.py",
|
||||
"test_page_enum_guard.py",
|
||||
"test_wiring.py"]
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
# -*- 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())
|
||||
Loading…
Reference in New Issue