diff --git a/BIONIC_PMS_INTERFACE.md b/BIONIC_PMS_INTERFACE.md index d5e52d2..a0e5fe3 100644 --- a/BIONIC_PMS_INTERFACE.md +++ b/BIONIC_PMS_INTERFACE.md @@ -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 侧不需要改:硬数字逐键渲染进提示词。应答形状不变。 diff --git a/app/core/action_engine.py b/app/core/action_engine.py index a995d6a..56b8112 100644 --- a/app/core/action_engine.py +++ b/app/core/action_engine.py @@ -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: diff --git a/app/services/advice_service.py b/app/services/advice_service.py new file mode 100644 index 0000000..544871c --- /dev/null +++ b/app/services/advice_service.py @@ -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 diff --git a/app/services/judge.py b/app/services/judge.py index 6c1d832..0b16682 100644 --- a/app/services/judge.py +++ b/app/services/judge.py @@ -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): diff --git a/app/services/logic_state_service.py b/app/services/logic_state_service.py index 7a36ea1..54c7dd8 100644 --- a/app/services/logic_state_service.py +++ b/app/services/logic_state_service.py @@ -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 diff --git a/app/services/param_store.py b/app/services/param_store.py index 6c9c27d..ffe1387 100644 --- a/app/services/param_store.py +++ b/app/services/param_store.py @@ -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: 时刻/日期/逐票状态), 扫描层与页面只读, 勿手改"), } diff --git a/app/services/plan_feed.py b/app/services/plan_feed.py index 445658c..fcfcaeb 100644 --- a/app/services/plan_feed.py +++ b/app/services/plan_feed.py @@ -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 diff --git a/app/services/proposal_service.py b/app/services/proposal_service.py index 033612d..f0283fe 100644 --- a/app/services/proposal_service.py +++ b/app/services/proposal_service.py @@ -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), }) diff --git a/app/web/static/index.html b/app/web/static/index.html index eb0890d..8799c3c 100644 --- a/app/web/static/index.html +++ b/app/web/static/index.html @@ -1036,6 +1036,10 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}