系统自决包:把新建仓/深亏补仓/目标价的该不该做交回合议与研判(台账 015)
PMS_SELF_DECIDE 四档 off/record/decline/full,人只处理四类例外(研判不可用、上游硬风险、 合议装不上、判决词认不出)。合议加择时异议观察(wait_timing);动作引擎打 decide_kind; proposal_service._self_decide 按决定表判采纳/放弃/交人,自动采纳走建议档重算数量、自动清仓 按可卖量夹紧并先撤方案;裁决接口改收预设理由码(decide_reasons);观察读数加七类、复核加 两张表、日报加自决注意项。撤销台账 001 与 008 两句。全量单测 894 例 ALL SUITES PASS (既有 871 例全绿即 off 逐字回旧)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
da272cc1c1
commit
44ec6d4a5d
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
**第六层,个股策略,就是你特意挂的网格、做T和跟踪止盈。** 挂了活跃策略的票,自主提议引擎让开,不再对它自动提议。策略动作按命令口径过规则闸,不过研判闸。做T的指令当日强制平回,绝不过夜。
|
**第六层,个股策略,就是你特意挂的网格、做T和跟踪止盈。** 挂了活跃策略的票,自主提议引擎让开,不再对它自动提议。策略动作按命令口径过规则闸,不过研判闸。做T的指令当日强制平回,绝不过夜。
|
||||||
|
|
||||||
**第七层,自主提议,系统自己的想法,排最末。** 档位有三档,关、只提议、全自动,关是总闸。每条提议过两道闸:规则闸查合规,研判闸问决策系统该不该。分流时减持提议自动执行,增持提议按档位走,浮亏百分之十五及更深的补仓永远要人点头。自主新建仓有独立档位,而且比命令更严:拿不到真实资金快照就不建仓。
|
**第七层,自主提议,系统自己的想法,排最末。** 档位有三档,关、只提议、全自动,关是总闸。每条提议过两道闸:规则闸查合规,研判闸问决策系统该不该。分流时减持提议自动执行,增持提议按档位走。浮亏百分之十五及更深的补仓,自决开关 PMS_SELF_DECIDE 开着时按研判必答题「杀逻辑还是杀情绪」自动判、不再一律要人点头(撤销于 2026-09-14 台账 015;关着时仍要人点头)。自主新建仓有独立档位,而且比命令更严:拿不到真实资金快照就不建仓。
|
||||||
|
|
||||||
## 三、贯穿线:减持畅通,增持重重设卡
|
## 三、贯穿线:减持畅通,增持重重设卡
|
||||||
|
|
||||||
|
|
|
||||||
19
DEVLOG.md
19
DEVLOG.md
|
|
@ -1549,6 +1549,25 @@ app/services/strategy_runner.py(跟踪止盈评估器 _eval_trail 加盘中 SA
|
||||||
**还欠着什么**
|
**还欠着什么**
|
||||||
按规格顺序开发:一,选股打分包(akg-factor-bridge,工作包四,重启桥先经审批);二,系统自决包(后端,部署后页面设 PMS_SELF_DECIDE 为 full、PMS_SELF_DECIDE_TRIAL 开);三,页面专业化包。文档:台账 015、README 313 到 315、COMMAND_PRIORITY 第 25 行撤销、真实仓初始参数清单加三条、选股系统台账 058。
|
按规格顺序开发:一,选股打分包(akg-factor-bridge,工作包四,重启桥先经审批);二,系统自决包(后端,部署后页面设 PMS_SELF_DECIDE 为 full、PMS_SELF_DECIDE_TRIAL 开);三,页面专业化包。文档:台账 015、README 313 到 315、COMMAND_PRIORITY 第 25 行撤销、真实仓初始参数清单加三条、选股系统台账 058。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-09-14 · 系统自决包与选股打分包(开发机完成,未部署)
|
||||||
|
|
||||||
|
**做了什么**
|
||||||
|
把新建仓、深亏补仓、目标价到价三类的「该不该做」交回给已有的三源合议与研判闸,人只处理四类例外(研判不可用、上游硬风险、合议装不上、判决词认不出)。四档开关 PMS_SELF_DECIDE(off 逐字回旧 / record 只记 / decline 只放弃 / full 全自动),试探仓类由 PMS_SELF_DECIDE_TRIAL 控,每日自动新建仓上限 PMS_SELF_DECIDE_DAILY_MAX 默认三。合议路由加择时异议观察(方向看多但择时看空改回观察等择时转多,处置词「择时待转多」)。动作引擎给候选打决定种类 decide_kind,proposal_service 新增 _self_decide 按 §3.5 决定表判采纳/放弃/交人,自动采纳走建议档重算数量、自动清仓落指令前按可卖量夹紧并先撤该票交易方案。裁决接口改收预设理由码(decide_reasons 两张码表加拼句),旧自由文本兼容。观察读数加七个自决类别,复核脚本加第五张系统自决逐条、第六张研判不可用占比,日报加自决注意项。撤销台账 001「重问放行一律交人」与 008「只有一方表态或择时异议交人」两句,落进台账 015。另做选股打分包(akg-factor-bridge 工作包四):计划装配按公司质地重排主榜与观察档的组内名次,不跨档,每行加 score_adj,PLAN_QUALITY_BONUS 默认 8 设 0 逐字回旧,台账 058。
|
||||||
|
|
||||||
|
**动了哪些文件**
|
||||||
|
PMS 侧:app/core/consensus.py(timing_dissent)、app/core/action_engine.py(decide_kind 常量与 mark_decide、scan_open 打标签、eval_dca/eval_target 打 decide_kind)、app/core/decide_reasons.py(新)、app/services/consensus_service.py、app/services/proposal_service.py(_self_decide、_apply_advice_tier、自决块、清仓两前置、放弃记账、跳过集合)、app/services/consensus_stats.py、app/services/strategy_service.py(cancel_by_code)、app/services/ledger_service.py、app/repo/pms_repo.py(system_decided_today / system_pass_count_today)、app/services/param_store.py(三参数)、app/web/main.py(api_decide 收码、decide-reasons 接口)、app/web/static/index.html(dispOf 加 wait_timing)、scripts/consensus_review.py、scripts/test_batch34_units.py(新,22 例)、scripts/test_wiring.py(FakeRepo 两桩、目标价自动清仓一例、路由清单)、scripts/test_batch4_units.py(eval_target 自决断言)、scripts/run_tests.py(登记,共 894 例)、docs/复盘决定台账.md(015)。akg 侧:plan.py、config.py、.env.example、test_plan_quality_rank.py(新)、docs/复盘决定台账.md(058)。
|
||||||
|
|
||||||
|
**部署方式**
|
||||||
|
两个包都在开发机做完、提交。akg 提交 3d3f317 已在因子桥仓库,PMS 侧本次提交见提交信息。**未部署**:PMS 走 make deploy(收盘后、先经审批),部署后页面设 PMS_SELF_DECIDE=full、PMS_SELF_DECIDE_TRIAL 开;akg 走 155 拉码后 docker compose restart(先经审批,避开 07:00-07:30 与 08:30-09:05),重启后预热一次计划接口。
|
||||||
|
|
||||||
|
**真机判收**
|
||||||
|
未判收。开发机全量单测 894 例 ALL SUITES PASS(含既有 871 例全绿即是「off 逐字回旧」的证据);akg test_plan_quality_rank.py 四条不变式全绿。
|
||||||
|
|
||||||
|
**还欠着什么**
|
||||||
|
一,收盘后审批部署 PMS 系统自决包并页面设 full/开 trial;下一交易日盘中只读试算看 executed/rejected,收盘跑 make consensus-review 看第五、六张表。二,审批重启因子桥出选股打分包,次日 07:10 起入池与评析名单按新序,看 §5.4 curl 质地好只数与 rank 连续。三,页面专业化包(第三个包,未做):文案改交易术语、抽屉三段重构、候选可点击、裁决弹窗前后端、文案守卫,以及自决的「系统已采纳/已放弃」显示与来源字段芯片。四,README 313-315、COMMAND_PRIORITY 第 25 行、接口契约 decide 请求体、UI_COPY_AUDIT 第二版词表等文档。
|
||||||
|
|
||||||
---
|
---
|
||||||
<!--
|
<!--
|
||||||
下一条节点从这里往下写,格式照抄上面:
|
下一条节点从这里往下写,格式照抄上面:
|
||||||
|
|
|
||||||
|
|
@ -312,7 +312,9 @@ make t-gate # 随时: 规则闸/研判闸拒了什么、为什么
|
||||||
2. **研判闸**(二级,仅补足/加仓/补仓,委托决策系统)——`PMS_JUDGE_API_BASE` 为空即视为未接通,按设计**自动降级为人工确认**并记 ERROR,绝不把「研判拿不到」当成「研判通过」。
|
2. **研判闸**(二级,仅补足/加仓/补仓,委托决策系统)——`PMS_JUDGE_API_BASE` 为空即视为未接通,按设计**自动降级为人工确认**并记 ERROR,绝不把「研判拿不到」当成「研判通过」。
|
||||||
3. **按自主档位分流**——`full` 执行、`propose_only` 入队(一期默认)、`off` 不扫描。
|
3. **按自主档位分流**——`full` 执行、`propose_only` 入队(一期默认)、`off` 不扫描。
|
||||||
|
|
||||||
两条无条件覆盖档位的规矩:**减持方向不设确认门槛**(TRIM 保垫减仓任何档位都直接落指令);**−15% 及更深的补仓永远需用户确认**(即便档位是 full 也强制入队)。同一只票的同一动作若已有在途提议或在途指令,不重复提。
|
两条无条件覆盖档位的规矩:**减持方向不设确认门槛**(TRIM 保垫减仓任何档位都直接落指令);**−15% 及更深的补仓的裁决由系统自决开关 `PMS_SELF_DECIDE` 决定**——关(off)时永远需用户确认,开(full)时按研判必答题「杀逻辑还是杀情绪」自动判(2026-09-14 台账 015 撤销「永远需用户确认」)。同一只票的同一动作若已有在途提议或在途指令,不重复提。
|
||||||
|
|
||||||
|
系统自决(`PMS_SELF_DECIDE`,四档 off/record/decline/full,默认 off、读不到按 off)把新建仓、深亏补仓、目标价到价这三类的「该不该做」交回三源合议与研判闸:full 时自动采纳或自动放弃,人只处理四类例外——研判不可用、上游硬风险、合议装不上、判决词认不出。试探仓类采纳受 `PMS_SELF_DECIDE_TRIAL` 控,每日自动新建仓上限 `PMS_SELF_DECIDE_DAILY_MAX`(默认 3)。
|
||||||
|
|
||||||
## 榜单变化提示(只提示,不产生指令)
|
## 榜单变化提示(只提示,不产生指令)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,9 @@ DISP_WAIT_TECH = "wait_tech"
|
||||||
# 盘中确认包 (2026-09-14): 开口向上未确认 / 收口等待时段外 的候选进观察, 处置词写这个,
|
# 盘中确认包 (2026-09-14): 开口向上未确认 / 收口等待时段外 的候选进观察, 处置词写这个,
|
||||||
# 页面渲染成「等盘中确认」。与 wait_tech 并列, 走 disposition_snapshot 同一条路。
|
# 页面渲染成「等盘中确认」。与 wait_tech 并列, 走 disposition_snapshot 同一条路。
|
||||||
DISP_WAIT_CONFIRM = "wait_confirm"
|
DISP_WAIT_CONFIRM = "wait_confirm"
|
||||||
|
# 系统自决包 (2026-09-14, 台账 015): 方向看多但择时看空、自决开着时改观察, 处置词写这个,
|
||||||
|
# 页面渲染成「择时待转多」。与 wait_tech / wait_confirm 并列, 走 disposition_snapshot 同一条路。
|
||||||
|
DISP_WAIT_TIMING = "wait_timing"
|
||||||
|
|
||||||
# 观察读数包 (2026-09-14, 台账 013): 合议相关跳过项打的类别标签, 值即 pms_consensus_stat.kind。
|
# 观察读数包 (2026-09-14, 台账 013): 合议相关跳过项打的类别标签, 值即 pms_consensus_stat.kind。
|
||||||
# consensus_stats.record_round 认这些标签落表。**只在合议开着 (con 非 None) 或增持门开着
|
# consensus_stats.record_round 认这些标签落表。**只在合议开着 (con 非 None) 或增持门开着
|
||||||
|
|
@ -64,9 +67,52 @@ TAG_OPEN_SKIP_BEAR = "open_skip_bear" # 合议判跳过, 其余原因 (方向
|
||||||
TAG_OPEN_WAIT_TECH = "open_wait_tech" # 合议判观察 (等技术面开口/转向)
|
TAG_OPEN_WAIT_TECH = "open_wait_tech" # 合议判观察 (等技术面开口/转向)
|
||||||
TAG_OPEN_WAIT_CONFIRM = "open_wait_confirm" # 盘中确认包: 开口未确认/时段外, 等盘中确认
|
TAG_OPEN_WAIT_CONFIRM = "open_wait_confirm" # 盘中确认包: 开口未确认/时段外, 等盘中确认
|
||||||
TAG_GATE_BLOCK = "gate_block" # 持仓增持门拦下 (detail 里带 gate_kind: fill/add/dca)
|
TAG_GATE_BLOCK = "gate_block" # 持仓增持门拦下 (detail 里带 gate_kind: fill/add/dca)
|
||||||
|
TAG_OPEN_WAIT_TIMING = "open_wait_timing" # 系统自决: 方向看多但择时看空进观察, 等择时转多
|
||||||
# open_breakout (盘中收口突破视为开口) 不打在跳过项上 —— 突破的候选会被放行、不进 skipped,
|
# open_breakout (盘中收口突破视为开口) 不打在跳过项上 —— 突破的候选会被放行、不进 skipped,
|
||||||
# 由 proposal_service 记进 consensus_seen 的 intraday 字段, consensus_stats 据此落表。
|
# 由 proposal_service 记进 consensus_seen 的 intraday 字段, consensus_stats 据此落表。
|
||||||
|
|
||||||
|
# 决定种类 (2026-09-14 系统自决包, 台账 015)。候选上的 c["decide_kind"], **只在自决非 off 时写**:
|
||||||
|
# 替代旧的 needs_user_confirm 标记, 让 proposal_service._self_decide 据它查 3.5 节的决定表。
|
||||||
|
# scan_open 尾段写前七种 (新建仓那几类), _route_one 写 reask, eval_dca/eval_target 各写一种。
|
||||||
|
DK_VERDICT_UNKNOWN = "verdict_unknown" # 判决词认不出 → 交人 (最保守, 优先级最高)
|
||||||
|
DK_LOGIC_DOUBT = "logic_doubt" # 逻辑存疑 → 自动放弃
|
||||||
|
DK_VERDICT_WATCH = "verdict_watch" # 判决关注 → 自动试探仓
|
||||||
|
DK_FUND_BEAR_TECH_BULL = "fund_bear_tech_bull" # 合议交人·基本面看空技术面看多 → 试探仓加紧止盈
|
||||||
|
DK_WEAK_CONSENSUS = "weak_consensus" # 合议交人·只有一方表态 → 试探仓
|
||||||
|
DK_TECH_BEAR = "tech_bear" # 异议只在技术面 (advise_v2 那格回观察) → 试探仓
|
||||||
|
DK_ADVICE_TRIAL = "advice_trial" # 建议档位为试探仓 → 试探仓
|
||||||
|
DK_REASK = "reask" # 重问放行 → 试探仓 (在 _route_one 写)
|
||||||
|
DK_DCA_DEEP = "dca_deep" # 深亏补仓 → 按研判必答题自动
|
||||||
|
DK_TARGET_PRICE = "target_price" # 目标价到价 → 自动清仓
|
||||||
|
# scan_open 里多种同时命中时留哪一种: 数字小的优先 (认不出最先, 试探仓最后)。
|
||||||
|
# 逻辑存疑排在判决关注之前 —— 研究证据在走弱要放弃, 比"关注→试探"更保守。
|
||||||
|
_DECIDE_PRIORITY = {DK_VERDICT_UNKNOWN: 0, DK_LOGIC_DOUBT: 1, DK_VERDICT_WATCH: 2,
|
||||||
|
DK_FUND_BEAR_TECH_BULL: 3, DK_WEAK_CONSENSUS: 3, DK_TECH_BEAR: 4,
|
||||||
|
DK_ADVICE_TRIAL: 5}
|
||||||
|
# 决定种类的中文说法 (账本理由与页面用)。
|
||||||
|
DK_CN = {DK_VERDICT_UNKNOWN: "判决词认不出", DK_LOGIC_DOUBT: "研究证据走弱(逻辑存疑)",
|
||||||
|
DK_VERDICT_WATCH: "判决关注", DK_FUND_BEAR_TECH_BULL: "基本面看空技术面看多",
|
||||||
|
DK_WEAK_CONSENSUS: "只有一方表态", DK_TECH_BEAR: "异议只在技术面",
|
||||||
|
DK_ADVICE_TRIAL: "建议试探仓", DK_REASK: "重问放行", DK_DCA_DEEP: "深亏补仓",
|
||||||
|
DK_TARGET_PRICE: "目标价到价"}
|
||||||
|
# 落成试探仓那几类 (受 PMS_SELF_DECIDE_TRIAL 控制; 关掉它们仍交人)。
|
||||||
|
TRIAL_DECIDE_KINDS = {DK_VERDICT_WATCH, DK_FUND_BEAR_TECH_BULL, DK_WEAK_CONSENSUS,
|
||||||
|
DK_TECH_BEAR, DK_ADVICE_TRIAL, DK_REASK}
|
||||||
|
|
||||||
|
|
||||||
|
def self_decide_on(params) -> bool:
|
||||||
|
"""系统自决是否开着 (非 off)。off 或读不到都回 False —— 逐字回旧的判据。"""
|
||||||
|
return str((params or {}).get("self_decide") or "off").strip() != "off"
|
||||||
|
|
||||||
|
|
||||||
|
def mark_decide(cand, kind):
|
||||||
|
"""自决开着时给候选登记决定种类: 高优先级 (数字小) 覆盖低优先级, 同级与低级不覆盖。
|
||||||
|
只写 decide_kind, 不碰 confirm_why —— 交人的原因仍由既有那几处 (关注/逻辑存疑/合议交人)
|
||||||
|
照旧写, 供 record/decline 档队列里的人看; full 档自动决定时用的是 _self_decide 给的理由。"""
|
||||||
|
cur = cand.get("decide_kind")
|
||||||
|
if cur is None or _DECIDE_PRIORITY.get(kind, 9) < _DECIDE_PRIORITY.get(cur, 9):
|
||||||
|
cand["decide_kind"] = kind
|
||||||
|
|
||||||
# 同一轮里出现多条减持时留哪一条 (2026-09-03): 数字小的优先。用户自己设的目标价到价排在
|
# 同一轮里出现多条减持时留哪一条 (2026-09-03): 数字小的优先。用户自己设的目标价到价排在
|
||||||
# 系统按规则算出来的保垫减仓前面 —— 人已经说了到价就清, 这一轮就不该再自作主张先卖一部分。
|
# 系统按规则算出来的保垫减仓前面 —— 人已经说了到价就清, 这一轮就不该再自作主张先卖一部分。
|
||||||
# 只在同一只票的同一轮里比较, 不影响不同票, 也不影响下一轮。
|
# 只在同一只票的同一轮里比较, 不影响不同票, 也不影响下一轮。
|
||||||
|
|
@ -124,6 +170,9 @@ def source_confirm_why(source):
|
||||||
是因为两者问的不是同一件事: 判决是上游对这只票的结论, 词表变了说明我们看不懂它的意思;
|
是因为两者问的不是同一件事: 判决是上游对这只票的结论, 词表变了说明我们看不懂它的意思;
|
||||||
而来源是本系统内部给候选打的标记, 没打标记的就是动作引擎自己那几类老动作, 行为必须
|
而来源是本系统内部给候选打的标记, 没打标记的就是动作引擎自己那几类老动作, 行为必须
|
||||||
与从前逐字一致。新来源要不要强制交人, 由加它的人写进上面那份名单。
|
与从前逐字一致。新来源要不要强制交人, 由加它的人写进上面那份名单。
|
||||||
|
|
||||||
|
目标价到价的自动清仓 (系统自决 full) 不在这里放行: 它照旧回强制入队的原因, 让 force_queue
|
||||||
|
默认交人; 是 _self_decide 在 _route_one 里对采纳的那条显式覆盖 auto_exec, 而不是这里松口。
|
||||||
"""
|
"""
|
||||||
return FORCE_QUEUE_SOURCES.get(str(source or "").strip()) or None
|
return FORCE_QUEUE_SOURCES.get(str(source or "").strip()) or None
|
||||||
|
|
||||||
|
|
@ -383,13 +432,18 @@ def eval_dca(p: dict, params: dict, mkt: dict):
|
||||||
deep = _f(params.get("dca_deep_confirm"), -0.15)
|
deep = _f(params.get("dca_deep_confirm"), -0.15)
|
||||||
is_deep = _f(cushion) <= deep + 1e-12
|
is_deep = _f(cushion) <= deep + 1e-12
|
||||||
depth = sorted(triggers, reverse=True)[stage - 1]
|
depth = sorted(triggers, reverse=True)[stage - 1]
|
||||||
return _cand(p, A_DCA, BUY, qty,
|
c = _cand(p, A_DCA, BUY, qty,
|
||||||
f"补仓评估: 浮亏 {_f(cushion):.2%} 触及 {depth:.0%} 档 (第 {stage} 档), "
|
f"补仓评估: 浮亏 {_f(cushion):.2%} 触及 {depth:.0%} 档 (第 {stage} 档), "
|
||||||
f"拟补 {qty} 股 (≤底仓 {max_ratio:.0%})"
|
f"拟补 {qty} 股 (≤底仓 {max_ratio:.0%})"
|
||||||
+ ("; **深档: 必须用户确认**, 研判须回答杀逻辑还是杀情绪" if is_deep else ""),
|
+ ("; **深档: 必须用户确认**, 研判须回答杀逻辑还是杀情绪" if is_deep else ""),
|
||||||
{"cushion_pct": cushion, "stage": stage, "trigger": depth,
|
{"cushion_pct": cushion, "stage": stage, "trigger": depth,
|
||||||
"base_qty": base_qty, "price": price, "stop_ref": p.get("stop_ref")},
|
"base_qty": base_qty, "price": price, "stop_ref": p.get("stop_ref")},
|
||||||
confirm=is_deep)
|
confirm=is_deep)
|
||||||
|
# 系统自决 (2026-09-14): 深档补仓打上 decide_kind, 由 _self_decide 按研判必答题自动判。
|
||||||
|
# confirm 保持不变 (关掉/record 下仍走人工队列); full 时 _self_decide 覆盖 auto_exec 自动补。
|
||||||
|
if is_deep and self_decide_on(params):
|
||||||
|
c["decide_kind"] = DK_DCA_DEEP
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
def eval_trim(p: dict, params: dict, mkt: dict = None):
|
def eval_trim(p: dict, params: dict, mkt: dict = None):
|
||||||
|
|
@ -462,6 +516,10 @@ def eval_target(p: dict, params: dict = None, mkt: dict = None):
|
||||||
"cushion_pct": p.get("cushion_pct"), "avg_cost": p.get("avg_cost")},
|
"cushion_pct": p.get("cushion_pct"), "avg_cost": p.get("avg_cost")},
|
||||||
confirm=True, source=SRC_TARGET_PRICE)
|
confirm=True, source=SRC_TARGET_PRICE)
|
||||||
c["confirm_why"] = WHY_TARGET_PRICE_CONFIRM
|
c["confirm_why"] = WHY_TARGET_PRICE_CONFIRM
|
||||||
|
# 系统自决 (2026-09-14): 到价清仓打上 decide_kind, confirm/来源标记保持不变 (关掉/record/decline
|
||||||
|
# 下仍进人工队列); full 时 _self_decide 覆盖为自动清仓, 落指令前再按可卖量夹一次、先撤该票方案。
|
||||||
|
if self_decide_on(params):
|
||||||
|
c["decide_kind"] = DK_TARGET_PRICE
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -890,6 +948,9 @@ def scan_open(*, candidates: list, params: dict, caps: dict, room_amt: float,
|
||||||
# 决定这条候选往哪走。开关 PMS_CONSENSUS_ROUTE 关掉时 consensus_on 为假, 下面所有合议相关
|
# 决定这条候选往哪走。开关 PMS_CONSENSUS_ROUTE 关掉时 consensus_on 为假, 下面所有合议相关
|
||||||
# 的代码一行都不执行, 定档退回旧的 advise、分流退回只按选股判决 —— 行为与接入前逐字相同。
|
# 的代码一行都不执行, 定档退回旧的 advise、分流退回只按选股判决 —— 行为与接入前逐字相同。
|
||||||
consensus_on = bool(params.get("consensus_route"))
|
consensus_on = bool(params.get("consensus_route"))
|
||||||
|
# 系统自决 (2026-09-14): 开着时下面五处交人标记改写成 decide_kind、不置 needs_user_confirm,
|
||||||
|
# 由 proposal_service._self_decide 据它自动判; 关掉 (off) 时这些分支一行不走, 逐字回旧。
|
||||||
|
sd_on = self_decide_on(params)
|
||||||
if slots <= 0:
|
if slots <= 0:
|
||||||
return {"candidates": [], "skipped": [
|
return {"candidates": [], "skipped": [
|
||||||
{"ts_code": "*", "action": A_OPEN,
|
{"ts_code": "*", "action": A_OPEN,
|
||||||
|
|
@ -936,10 +997,11 @@ def scan_open(*, candidates: list, params: dict, caps: dict, room_amt: float,
|
||||||
"tag": TAG_OPEN_SKIP_FUND if "买方评析" in _rr else TAG_OPEN_SKIP_BEAR})
|
"tag": TAG_OPEN_SKIP_FUND if "买方评析" in _rr else TAG_OPEN_SKIP_BEAR})
|
||||||
continue
|
continue
|
||||||
if con and con.get("route") == "观察":
|
if con and con.get("route") == "观察":
|
||||||
# 处置词: con 带 disp 就用它 (盘中确认包给 opened_wait/outside_window 写 wait_confirm),
|
# 处置词: con 带 disp 就用它 (盘中确认包给 opened_wait/outside_window 写 wait_confirm;
|
||||||
# 否则用 wait_tech (等技术面开口)。tag 随之分 open_wait_confirm / open_wait_tech。
|
# 系统自决给择时异议写 wait_timing), 否则用 wait_tech (等技术面开口)。tag 随之三分。
|
||||||
_disp = con.get("disp") or DISP_WAIT_TECH
|
_disp = con.get("disp") or DISP_WAIT_TECH
|
||||||
_tag = TAG_OPEN_WAIT_CONFIRM if _disp == DISP_WAIT_CONFIRM else TAG_OPEN_WAIT_TECH
|
_tag = {DISP_WAIT_CONFIRM: TAG_OPEN_WAIT_CONFIRM,
|
||||||
|
DISP_WAIT_TIMING: TAG_OPEN_WAIT_TIMING}.get(_disp, TAG_OPEN_WAIT_TECH)
|
||||||
skipped.append({"ts_code": code, "action": A_OPEN, "disp": _disp,
|
skipped.append({"ts_code": code, "action": A_OPEN, "disp": _disp,
|
||||||
"why": con.get("route_reason") or "三源合议判为观察,等技术面开口",
|
"why": con.get("route_reason") or "三源合议判为观察,等技术面开口",
|
||||||
"tag": _tag})
|
"tag": _tag})
|
||||||
|
|
@ -1017,6 +1079,25 @@ def scan_open(*, candidates: list, params: dict, caps: dict, room_amt: float,
|
||||||
if con and con.get("route") == "交人" and not cand.get("needs_user_confirm"):
|
if con and con.get("route") == "交人" and not cand.get("needs_user_confirm"):
|
||||||
cand["needs_user_confirm"] = True
|
cand["needs_user_confirm"] = True
|
||||||
cand["confirm_why"] = con.get("route_reason") or "三源合议判为交人裁决"
|
cand["confirm_why"] = con.get("route_reason") or "三源合议判为交人裁决"
|
||||||
|
# ---- 系统自决 (2026-09-14 台账 015): 独立算 decide_kind, 不受上面 needs_user_confirm 的
|
||||||
|
# 先来后到影响 (那几道守卫会让靠后的理由不评, 而自决要按优先级取最保守的一种)。
|
||||||
|
# off 时 sd_on 为假, 这一段一行不走, needs_user_confirm 与今天逐字相同; record/decline/full
|
||||||
|
# 时既留着上面的 needs_user_confirm (供 record/decline 排队与显示), 又打上 decide_kind
|
||||||
|
# 供 _self_decide 判。mark_decide 里 verdict_unknown < logic_doubt < 判决关注 < 合议交人 <
|
||||||
|
# 异议只在技术面 < 建议试探仓, 数字小的优先。
|
||||||
|
if sd_on:
|
||||||
|
if isinstance(adv, dict) and adv.get("tier") == "试探仓":
|
||||||
|
mark_decide(cand, DK_ADVICE_TRIAL)
|
||||||
|
if isinstance(adv, dict) and adv.get("route") == "观察":
|
||||||
|
mark_decide(cand, DK_TECH_BEAR) # 异议只在技术面 (advise_v2 那格)
|
||||||
|
if route and verdict_confirm_why(c.get("verdict")):
|
||||||
|
v = str(c.get("verdict") or "").strip()
|
||||||
|
mark_decide(cand, DK_VERDICT_WATCH if v == VERDICT_WATCH else DK_VERDICT_UNKNOWN)
|
||||||
|
if params.get("open_route_by_logic") and logic_confirm_why(c.get("logic_state")):
|
||||||
|
mark_decide(cand, DK_LOGIC_DOUBT)
|
||||||
|
if con and con.get("route") == "交人":
|
||||||
|
rr = con.get("route_reason") or ""
|
||||||
|
mark_decide(cand, DK_FUND_BEAR_TECH_BULL if "基本面看空" in rr else DK_WEAK_CONSENSUS)
|
||||||
out.append(cand)
|
out.append(cand)
|
||||||
used = _f(cand.get("target_amount"))
|
used = _f(cand.get("target_amount"))
|
||||||
left -= used
|
left -= used
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,17 @@ from __future__ import annotations
|
||||||
|
|
||||||
_BULL, _BEAR, _NEUT, _NA = "看多", "看空", "中性", "无读数"
|
_BULL, _BEAR, _NEUT, _NA = "看多", "看空", "中性", "无读数"
|
||||||
|
|
||||||
|
# 方向看多但择时看空时的两种去向 (2026-09-14 系统自决包): 默认交人; 自决开着时改回观察等择时转多。
|
||||||
|
_R_WAIT_TIMING = "等择时转多"
|
||||||
|
|
||||||
|
|
||||||
def _reason(fund, tech, timing, direction, strength):
|
def _reason(fund, tech, timing, direction, strength):
|
||||||
head = "、".join([f"基本面{fund}", f"技术面{tech}", f"择时{timing}"])
|
head = "、".join([f"基本面{fund}", f"技术面{tech}", f"择时{timing}"])
|
||||||
return head + " → " + direction + (("·" + strength) if strength else "")
|
return head + " → " + direction + (("·" + strength) if strength else "")
|
||||||
|
|
||||||
|
|
||||||
def _route(fund, tech, timing, direction, tech_phase, fund_required, weak_confirm=True):
|
def _route(fund, tech, timing, direction, tech_phase, fund_required, weak_confirm=True,
|
||||||
|
timing_dissent="confirm"):
|
||||||
"""附录丙路由表, 从上到下第一个命中。返回 (路由, 原因)。路由取值: 放行/观察/交人/跳过。"""
|
"""附录丙路由表, 从上到下第一个命中。返回 (路由, 原因)。路由取值: 放行/观察/交人/跳过。"""
|
||||||
if fund_required and fund == _NA:
|
if fund_required and fund == _NA:
|
||||||
return "跳过", "没有买方评析"
|
return "跳过", "没有买方评析"
|
||||||
|
|
@ -31,6 +35,9 @@ def _route(fund, tech, timing, direction, tech_phase, fund_required, weak_confir
|
||||||
if direction == _BULL:
|
if direction == _BULL:
|
||||||
voiced = [v for v in (fund, tech, timing) if v in (_BULL, _BEAR)]
|
voiced = [v for v in (fund, tech, timing) if v in (_BULL, _BEAR)]
|
||||||
if timing == _BEAR: # 异议在择时系统
|
if timing == _BEAR: # 异议在择时系统
|
||||||
|
# 默认交人 (逐字回旧); 系统自决开着时改回观察, 等择时转多再走放行 (2026-09-14)。
|
||||||
|
if timing_dissent == "observe":
|
||||||
|
return "观察", _R_WAIT_TIMING
|
||||||
return "交人", "方向看多但择时系统有异议"
|
return "交人", "方向看多但择时系统有异议"
|
||||||
if len(voiced) <= 1: # 只有一方表态
|
if len(voiced) <= 1: # 只有一方表态
|
||||||
# 弱表态: weak_confirm 开着交人, 关掉放行 (2026-09-14 加固, 登记 PMS_CONSENSUS_WEAK_CONFIRM)
|
# 弱表态: weak_confirm 开着交人, 关掉放行 (2026-09-14 加固, 登记 PMS_CONSENSUS_WEAK_CONFIRM)
|
||||||
|
|
@ -43,11 +50,13 @@ def _route(fund, tech, timing, direction, tech_phase, fund_required, weak_confir
|
||||||
return "观察", "方向中性"
|
return "观察", "方向中性"
|
||||||
|
|
||||||
|
|
||||||
def decide(fund, tech, timing, *, tech_phase=None, fund_required=True, weak_confirm=True) -> dict:
|
def decide(fund, tech, timing, *, tech_phase=None, fund_required=True, weak_confirm=True,
|
||||||
|
timing_dissent="confirm") -> dict:
|
||||||
"""fund/tech/timing 是三个立场字符串 (看多/看空/中性/无读数)。tech_phase 是技术面相位
|
"""fund/tech/timing 是三个立场字符串 (看多/看空/中性/无读数)。tech_phase 是技术面相位
|
||||||
(收口等待 等), 路由用。fund_required: 无买方评析是否拦 (开关 PMS_FUND_REQUIRED)。
|
(收口等待 等), 路由用。fund_required: 无买方评析是否拦 (开关 PMS_FUND_REQUIRED)。
|
||||||
weak_confirm: 方向看多但只有一方表态时是否交人 (开关 PMS_CONSENSUS_WEAK_CONFIRM, 默认真;
|
weak_confirm: 方向看多但只有一方表态时是否交人 (开关 PMS_CONSENSUS_WEAK_CONFIRM, 默认真;
|
||||||
关掉则放行)。"""
|
关掉则放行)。timing_dissent: 方向看多但择时看空时的去向, confirm 交人 (默认, 逐字回旧) /
|
||||||
|
observe 改观察等择时转多 (系统自决开着时传, 观察结果带 disp=wait_timing)。"""
|
||||||
votes = {"fund": fund, "tech": tech, "timing": timing}
|
votes = {"fund": fund, "tech": tech, "timing": timing}
|
||||||
voiced = [v for v in (fund, tech, timing) if v in (_BULL, _BEAR)]
|
voiced = [v for v in (fund, tech, timing) if v in (_BULL, _BEAR)]
|
||||||
bulls = sum(1 for v in voiced if v == _BULL)
|
bulls = sum(1 for v in voiced if v == _BULL)
|
||||||
|
|
@ -72,7 +81,11 @@ def decide(fund, tech, timing, *, tech_phase=None, fund_required=True, weak_conf
|
||||||
strength = "中"
|
strength = "中"
|
||||||
|
|
||||||
route, route_reason = _route(fund, tech, timing, direction, tech_phase, fund_required,
|
route, route_reason = _route(fund, tech, timing, direction, tech_phase, fund_required,
|
||||||
weak_confirm)
|
weak_confirm, timing_dissent)
|
||||||
return {"direction": direction, "votes": votes, "strength": strength,
|
out = {"direction": direction, "votes": votes, "strength": strength,
|
||||||
"reason": _reason(fund, tech, timing, direction, strength),
|
"reason": _reason(fund, tech, timing, direction, strength),
|
||||||
"route": route, "route_reason": route_reason}
|
"route": route, "route_reason": route_reason}
|
||||||
|
# 择时异议改观察这一种, 处置词记 wait_timing (页面显示「择时待转多」; 动作引擎观察分支据它选标签)。
|
||||||
|
if route == "观察" and route_reason == _R_WAIT_TIMING:
|
||||||
|
out["disp"] = "wait_timing"
|
||||||
|
return out
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""人工裁决理由的码表与拼句 (2026-09-14 系统自决包, 台账 015)。纯逻辑, 零外部依赖, 可单测。
|
||||||
|
|
||||||
|
系统自决把绝大多数「该不该做」接了过去, 剩下真交人的那几张提议, 人裁决时从预设理由里选一条,
|
||||||
|
选「其他」再补一句 —— 不再每次手写一整段。这里放两张码表 (采纳 / 驳回) 与一个拼句函数,
|
||||||
|
把「决定 + 码 + 补充 + 自由文本」拼成落进评审账本的一句话, 并回出用到的码 (进硬数字备查)。
|
||||||
|
|
||||||
|
页面拿这两张表铺预设按钮 (GET /api/proposals/decide-reasons); 后端在裁决入口再校验一次 ——
|
||||||
|
选「其他」不填补充、或码与决定对不上, 一律回中文错误, 不落账 (与研判不可用交人同一层的严)。
|
||||||
|
旧的自由文本 reason 仍然接受 (向后兼容: 脚本、旧缓存页面照发照收)。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# 驳回理由码 → 中文标签。最后一个是「其他」, 选它必须填补充。
|
||||||
|
DECLINE_REASONS = {
|
||||||
|
"DECLINE_CONSENSUS_MISMATCH": "与研判结论不符",
|
||||||
|
"DECLINE_TECH_WEAK": "技术面已转弱",
|
||||||
|
"DECLINE_POSITION_FULL": "仓位已达上限",
|
||||||
|
"DECLINE_SECTOR_CONCENTRATED": "行业集中度过高",
|
||||||
|
"DECLINE_WAIT_PULLBACK": "等待回调再议",
|
||||||
|
"DECLINE_MARKET_RISK": "大盘风险偏高",
|
||||||
|
"DECLINE_OTHER": "其他",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 采纳理由码 → 中文标签。最后一个是「其他」, 选它必须填补充。
|
||||||
|
ACCEPT_REASONS = {
|
||||||
|
"ACCEPT_CONSENSUS": "合议放行,按系统建议执行",
|
||||||
|
"ACCEPT_MANUAL_JUDGE": "人工判断机会成立",
|
||||||
|
"ACCEPT_PER_COMMAND": "按既有命令执行",
|
||||||
|
"ACCEPT_RISK_CONTROL": "风控需要",
|
||||||
|
"ACCEPT_OTHER": "其他",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def reasons_for(decision) -> dict:
|
||||||
|
"""按决定取对应的码表。ACCEPTED 用采纳表, 其余 (DECLINED) 用驳回表。"""
|
||||||
|
return ACCEPT_REASONS if str(decision or "").upper() == "ACCEPTED" else DECLINE_REASONS
|
||||||
|
|
||||||
|
|
||||||
|
def compose(decision, reason_code=None, reason_note=None, reason=None):
|
||||||
|
"""把裁决理由拼成一句话。返回 (账本句, 用到的码或 None)。
|
||||||
|
|
||||||
|
decision: ACCEPTED / DECLINED, 决定用哪张码表。
|
||||||
|
reason_code: 预设理由码 (可空)。给了就必须落在对应码表里, 否则报错。
|
||||||
|
reason_note: 补充说明 (选「其他」时必填)。
|
||||||
|
reason: 旧的自由文本 (向后兼容)。只有它、没有码时, 原样当账本句。
|
||||||
|
|
||||||
|
码与自由文本都空 → 报错 (空理由不收)。码不在表里 / 选「其他」不填补充 → 报错。
|
||||||
|
错误一律是能直接给人看的中文 ValueError。
|
||||||
|
"""
|
||||||
|
decision = str(decision or "").upper()
|
||||||
|
table = reasons_for(decision)
|
||||||
|
code = (reason_code or "").strip() or None
|
||||||
|
note = (reason_note or "").strip()
|
||||||
|
free = (reason or "").strip()
|
||||||
|
label_side = "采纳" if decision == "ACCEPTED" else "驳回"
|
||||||
|
|
||||||
|
if not code and not free:
|
||||||
|
raise ValueError("采纳或驳回都要给理由:选一个预设理由,或写一句自由说明")
|
||||||
|
if code:
|
||||||
|
if code not in table:
|
||||||
|
raise ValueError(f"理由码 {code} 不适用于本次{label_side}")
|
||||||
|
if code.endswith("_OTHER") and not note:
|
||||||
|
raise ValueError("选「其他」时必须填补充说明")
|
||||||
|
sentence = table[code] + (f";补充:{note}" if note else "")
|
||||||
|
return sentence[:500], code
|
||||||
|
# 只有自由文本 (旧字段兼容): 原样当账本句, 不带码。
|
||||||
|
return free[:500], None
|
||||||
|
|
@ -618,6 +618,37 @@ def judge_rejected_today(since) -> set:
|
||||||
return {(r["ts_code"], r["action"]) for r in rows}
|
return {(r["ts_code"], r["action"]) for r in rows}
|
||||||
|
|
||||||
|
|
||||||
|
# 系统自决自动采纳的账本行 reason 以此开头 (数据契约 §3.8)。system_pass_count_today 按它数,
|
||||||
|
# consensus_stats / 复核脚本也认这个前缀, 集中一处免得各写各的。
|
||||||
|
SYSTEM_ACCEPT_REASON_PREFIX = "系统采纳"
|
||||||
|
|
||||||
|
|
||||||
|
def system_decided_today(since) -> set:
|
||||||
|
"""今天被**系统自决放弃**过的 (代码, 动作) —— 自主扫描据此当日不再重评 (明日重评)。
|
||||||
|
|
||||||
|
评审方 system 的 REJECT 就是自决放弃 (逻辑存疑、合议引起的放弃)。研判 REJECT 引起的
|
||||||
|
放弃写评审方 judge, 走 judge_rejected_today 那条去重, 不在此列 —— 那条还要参与解锁重问。
|
||||||
|
只碰账本一张表。读不到返回空 (去重是降噪不是纪律, 同 rule/judge 两条)。
|
||||||
|
"""
|
||||||
|
rows = fetch_all("SELECT DISTINCT ts_code, action FROM pms_action_ledger "
|
||||||
|
"WHERE verdict = 'REJECT' AND arbiter = 'system' AND decided_at >= :d",
|
||||||
|
{"d": since})
|
||||||
|
return {(r["ts_code"], r["action"]) for r in rows}
|
||||||
|
|
||||||
|
|
||||||
|
def system_pass_count_today(since) -> int:
|
||||||
|
"""今天系统自决**自动采纳**的新建仓只数, 供每日上限 PMS_SELF_DECIDE_DAILY_MAX 判断。
|
||||||
|
|
||||||
|
自动采纳的账本行评审方是 judge/rule 的 PASS (它确实过了研判与规则), 与普通自动执行
|
||||||
|
同评审方, 只能靠 reason 前缀区分 —— 采纳理由一律以「系统采纳」开头。只碰账本一张表。
|
||||||
|
"""
|
||||||
|
r = fetch_one("SELECT COUNT(*) AS n FROM pms_action_ledger "
|
||||||
|
"WHERE action = 'OPEN' AND verdict = 'PASS' AND decided_at >= :d "
|
||||||
|
"AND reason LIKE :pfx",
|
||||||
|
{"d": since, "pfx": SYSTEM_ACCEPT_REASON_PREFIX + "%"})
|
||||||
|
return int((r or {}).get("n") or 0)
|
||||||
|
|
||||||
|
|
||||||
def judge_rejected_open_rows(since) -> dict:
|
def judge_rejected_open_rows(since) -> dict:
|
||||||
"""今天被研判闸驳回的**新建仓**, 每只票取当日**最新一条** → {代码: {at, price, hard}}。
|
"""今天被研判闸驳回的**新建仓**, 每只票取当日**最新一条** → {代码: {at, price, hard}}。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,16 @@ logger = logging.getLogger("pms.consensus")
|
||||||
|
|
||||||
def _params() -> dict:
|
def _params() -> dict:
|
||||||
gi, gb = param_store.get_int, param_store.get_bool
|
gi, gb = param_store.get_int, param_store.get_bool
|
||||||
|
# 系统自决非 off 时, 方向看多但择时看空的合议改回观察 (等择时转多), 不再交人 (2026-09-14)。
|
||||||
|
self_decide = str(param_store.get("PMS_SELF_DECIDE", "off") or "off").strip()
|
||||||
return {
|
return {
|
||||||
"fund_required": gb("PMS_FUND_REQUIRED", True),
|
"fund_required": gb("PMS_FUND_REQUIRED", True),
|
||||||
"fund_stale_days": gi("PMS_FUND_STALE_DAYS", 120),
|
"fund_stale_days": gi("PMS_FUND_STALE_DAYS", 120),
|
||||||
"fund_consensus_good_min": gi("PMS_FUND_CONSENSUS_GOOD_MIN", 8),
|
"fund_consensus_good_min": gi("PMS_FUND_CONSENSUS_GOOD_MIN", 8),
|
||||||
"consensus_route": gb("PMS_CONSENSUS_ROUTE", True),
|
"consensus_route": gb("PMS_CONSENSUS_ROUTE", True),
|
||||||
"weak_confirm": gb("PMS_CONSENSUS_WEAK_CONFIRM", True),
|
"weak_confirm": gb("PMS_CONSENSUS_WEAK_CONFIRM", True),
|
||||||
|
"self_decide": self_decide,
|
||||||
|
"timing_dissent": "observe" if self_decide != "off" else "confirm",
|
||||||
"timing_stale_tdays": gi("PMS_REF_STALE_TDAYS", 3),
|
"timing_stale_tdays": gi("PMS_REF_STALE_TDAYS", 3),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,7 +78,8 @@ def assemble(row, *, nightly=None, tech_states=None, flip_at=None, params=None)
|
||||||
con = consensus.decide(fund["stance"], tech.get("stance") or "无读数", tm["stance"],
|
con = consensus.decide(fund["stance"], tech.get("stance") or "无读数", tm["stance"],
|
||||||
tech_phase=tech.get("phase"),
|
tech_phase=tech.get("phase"),
|
||||||
fund_required=(p["fund_required"] and p["consensus_route"]),
|
fund_required=(p["fund_required"] and p["consensus_route"]),
|
||||||
weak_confirm=p.get("weak_confirm", True))
|
weak_confirm=p.get("weak_confirm", True),
|
||||||
|
timing_dissent=p.get("timing_dissent", "confirm"))
|
||||||
return {"fund": fund, "tech": tech, "timing": tm, "consensus": con}
|
return {"fund": fund, "tech": tech, "timing": tm, "consensus": con}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,14 @@ K_TIMING_NOREAD = "timing_noread"
|
||||||
K_TECH_EXIT = "tech_exit"
|
K_TECH_EXIT = "tech_exit"
|
||||||
K_TECH_NOREAD = "tech_noread"
|
K_TECH_NOREAD = "tech_noread"
|
||||||
K_MAP_COVER = "map_cover"
|
K_MAP_COVER = "map_cover"
|
||||||
|
# 系统自决 (2026-09-14 台账 015): 从 out["self_decide_seen"] 派生, 随时写 (不受检查点限制)。
|
||||||
|
K_AUTO_ACCEPT = "auto_accept" # full 档自动采纳
|
||||||
|
K_AUTO_DECLINE = "auto_decline" # full/decline 档自动放弃
|
||||||
|
K_AUTO_CONFIRM = "auto_confirm" # 自决判仍交人 (研判不可用/硬风险/上限等)
|
||||||
|
K_AUTO_WOULD_ACCEPT = "auto_would_accept" # record 档: 本会采纳
|
||||||
|
K_AUTO_WOULD_DECLINE = "auto_would_decline" # record 档: 本会放弃
|
||||||
|
K_JUDGE_UNAVAIL = "judge_unavail" # 研判不可用 (研判不可用占比复核用)
|
||||||
|
K_OPEN_WAIT_TIMING = ae.TAG_OPEN_WAIT_TIMING # 方向看多但择时看空, 进观察等择时转多 (跳过项 tag)
|
||||||
|
|
||||||
|
|
||||||
# ================================================================ 时刻与行
|
# ================================================================ 时刻与行
|
||||||
|
|
@ -141,6 +149,32 @@ def _skip_tag_rows(out, day, now) -> list:
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _self_decide_rows(out, day, now) -> list:
|
||||||
|
"""从 out["self_decide_seen"] 生成自决读数 (2026-09-14, 随时写, 不受检查点限制)。
|
||||||
|
|
||||||
|
full 档记 auto_accept / auto_decline / auto_confirm; record 档本会采纳/放弃记 auto_would_*;
|
||||||
|
研判不可用的另记一行 judge_unavail (研判不可用占比复核用)。seen 由 _route_one 逐条追加。"""
|
||||||
|
rows = []
|
||||||
|
for s in out.get("self_decide_seen") or []:
|
||||||
|
code = s.get("ts_code")
|
||||||
|
if not code:
|
||||||
|
continue
|
||||||
|
decision, would, why = s.get("decision"), s.get("would"), s.get("why")
|
||||||
|
detail = {"rule": s.get("decide_kind"), "would": would,
|
||||||
|
"tier": s.get("tier"), "action": s.get("action"),
|
||||||
|
"direction": s.get("direction"), "route": s.get("route"),
|
||||||
|
"conf": s.get("conf")}
|
||||||
|
kind = {"accept": K_AUTO_ACCEPT, "decline": K_AUTO_DECLINE,
|
||||||
|
"confirm": K_AUTO_CONFIRM}.get(decision)
|
||||||
|
if kind is None and decision == "none": # record 档: 只记本会怎么判
|
||||||
|
kind = {"accept": K_AUTO_WOULD_ACCEPT, "decline": K_AUTO_WOULD_DECLINE}.get(would)
|
||||||
|
if kind:
|
||||||
|
rows.append(_row(day, code, kind, reason=why, detail=detail, now=now))
|
||||||
|
if (why or "").startswith("研判不可用"):
|
||||||
|
rows.append(_row(day, code, K_JUDGE_UNAVAIL, reason=why, now=now))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def _tech_exit_rows(out, scanned, day, now) -> list:
|
def _tech_exit_rows(out, scanned, day, now) -> list:
|
||||||
"""从 scanned["candidates"] 里来源是 tech_exit 且没被闸拒的项生成 tech_exit 行。
|
"""从 scanned["candidates"] 里来源是 tech_exit 且没被闸拒的项生成 tech_exit 行。
|
||||||
确认转空清仓走 EXIT, 未确认减三分之一走 TRIM —— 据动作名记 confirm。"""
|
确认转空清仓走 EXIT, 未确认减三分之一走 TRIM —— 据动作名记 confirm。"""
|
||||||
|
|
@ -169,7 +203,9 @@ def record_round(out, scanned, params, now=None) -> dict:
|
||||||
if not isinstance(out, dict) or out.get("dry_run"):
|
if not isinstance(out, dict) or out.get("dry_run"):
|
||||||
return {"ok": True, "rows": 0, "note": "dry_run 或无 out, 不落表"}
|
return {"ok": True, "rows": 0, "note": "dry_run 或无 out, 不落表"}
|
||||||
day = int(now.strftime("%Y%m%d")) # 北京日期 (容器时钟)
|
day = int(now.strftime("%Y%m%d")) # 北京日期 (容器时钟)
|
||||||
|
# tech_exit 与自决读数随时写 (稀有且要看全); 路由与装配两类只在检查点写 (每分钟太密)。
|
||||||
rows = list(_tech_exit_rows(out, scanned, day, now))
|
rows = list(_tech_exit_rows(out, scanned, day, now))
|
||||||
|
rows += _self_decide_rows(out, day, now)
|
||||||
times = param_store.get(STAT_TIMES_KEY, "0935,1030,1330,1445")
|
times = param_store.get(STAT_TIMES_KEY, "0935,1030,1330,1445")
|
||||||
if _at_checkpoint(now, times):
|
if _at_checkpoint(now, times):
|
||||||
rows += _seen_rows(out, day, now)
|
rows += _seen_rows(out, day, now)
|
||||||
|
|
|
||||||
|
|
@ -1484,10 +1484,20 @@ def build_daily_report(ymd: int = None) -> dict:
|
||||||
# 合议观察小节 (2026-09-14 观察读数包, 台账 013): 当天按类别的计数。取不到写空, 不拖垮日报。
|
# 合议观察小节 (2026-09-14 观察读数包, 台账 013): 当天按类别的计数。取不到写空, 不拖垮日报。
|
||||||
try:
|
try:
|
||||||
from app.services import consensus_stats
|
from app.services import consensus_stats
|
||||||
report["consensus"] = consensus_stats.daily_summary(ymd)
|
cstat = consensus_stats.daily_summary(ymd)
|
||||||
|
report["consensus"] = cstat
|
||||||
|
# 系统自决注意项 (2026-09-14 台账 015): 今天自动采纳/放弃/交人各几只, 研判不可用几只。
|
||||||
|
def _crows(k):
|
||||||
|
return int((cstat.get(k) or {}).get("rows", 0))
|
||||||
|
report["self_decide"] = {
|
||||||
|
"accept": _crows(consensus_stats.K_AUTO_ACCEPT),
|
||||||
|
"decline": _crows(consensus_stats.K_AUTO_DECLINE),
|
||||||
|
"confirm": _crows(consensus_stats.K_AUTO_CONFIRM),
|
||||||
|
"judge_unavail": _crows(consensus_stats.K_JUDGE_UNAVAIL)}
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
logger.warning("[日报] 合议观察小节取数失败 (写空): %s", e)
|
logger.warning("[日报] 合议观察小节取数失败 (写空): %s", e)
|
||||||
report["consensus"] = {}
|
report["consensus"] = {}
|
||||||
|
report["self_decide"] = {}
|
||||||
try:
|
try:
|
||||||
pms_repo.upsert_report(ymd, report)
|
pms_repo.upsert_report(ymd, report)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,10 @@ RUNTIME_EXTRA = {
|
||||||
"PMS_TECH_INTRADAY_BREAKOUT": (True, bool, "收口突破规则开关 (关掉只剩开口确认规则)"),
|
"PMS_TECH_INTRADAY_BREAKOUT": (True, bool, "收口突破规则开关 (关掉只剩开口确认规则)"),
|
||||||
"PMS_TECH_VOL_CONFIRM": (1.5, float, "盘中确认的时段折算量比阈值 (与解锁重问 PMS_OPEN_REASK_VOL_MIN 同值同义)"),
|
"PMS_TECH_VOL_CONFIRM": (1.5, float, "盘中确认的时段折算量比阈值 (与解锁重问 PMS_OPEN_REASK_VOL_MIN 同值同义)"),
|
||||||
"PMS_TECH_INTRADAY_WINDOW": ("0945-1430", str, "盘中确认时段 (与解锁重问 PMS_OPEN_REASK_WINDOW 同义; 时段外开口向上候选进观察等确认)"),
|
"PMS_TECH_INTRADAY_WINDOW": ("0945-1430", str, "盘中确认时段 (与解锁重问 PMS_OPEN_REASK_WINDOW 同义; 时段外开口向上候选进观察等确认)"),
|
||||||
|
# ── 2026-09-14 系统自决包 (台账 015): 把「该不该做」交回三源合议与研判闸。四档留作回退。
|
||||||
|
"PMS_SELF_DECIDE": ("off", str, "系统自决档: off 逐字回旧 / record 只记不做 / decline 只自动放弃 / full 自动采纳加自动放弃 (155 上线设 full; 读不到按 off)"),
|
||||||
|
"PMS_SELF_DECIDE_TRIAL": (False, bool, "试探仓类情形是否自动采纳 (只有一方表态/基本面看空技术面看多/异议只在技术面/判决关注/建议试探仓/重问放行); 关掉这几类仍交人"),
|
||||||
|
"PMS_SELF_DECIDE_DAILY_MAX": (3, int, "每日自动新建仓只数上限 (超过转交人; 0 = 不限)"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# **读不到时必须按"已暂停"处理的键 (fail-closed)。**
|
# **读不到时必须按"已暂停"处理的键 (fail-closed)。**
|
||||||
|
|
@ -159,6 +163,9 @@ FAIL_CLOSED = {
|
||||||
# 2026-08-25 策略自动挂载: 同宏观层的理由 —— 文件初值 True (模拟仓拍板全自动),
|
# 2026-08-25 策略自动挂载: 同宏观层的理由 —— 文件初值 True (模拟仓拍板全自动),
|
||||||
# 表读不到时退文件初值等于故障期间照常自动挂策略。安全方向是关, 本轮不挂不撤。
|
# 表读不到时退文件初值等于故障期间照常自动挂策略。安全方向是关, 本轮不挂不撤。
|
||||||
"PMS_AUTO_STRATEGY_ENABLED": False,
|
"PMS_AUTO_STRATEGY_ENABLED": False,
|
||||||
|
# 系统自决读不到 → off (逐字回旧, 只交人不自动)。与新建仓档位同一个安全方向:
|
||||||
|
# 参数表抖一下就自动采纳新建仓、自动放弃候选, 都是读不到约束时不该发生的动作。
|
||||||
|
"PMS_SELF_DECIDE": "off",
|
||||||
}
|
}
|
||||||
|
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
|
|
@ -534,6 +541,8 @@ _RANGES = {
|
||||||
"PMS_AUTO_GRID_CAP_RATIO": (0, 1), "PMS_AUTO_HEAT_TH": (0, 1),
|
"PMS_AUTO_GRID_CAP_RATIO": (0, 1), "PMS_AUTO_HEAT_TH": (0, 1),
|
||||||
"PMS_AUTO_TRAIL_GIVEBACK": (0.01, 0.5), "PMS_AUTO_TRAIL_SELL_RATIO": (0.1, 1),
|
"PMS_AUTO_TRAIL_GIVEBACK": (0.01, 0.5), "PMS_AUTO_TRAIL_SELL_RATIO": (0.1, 1),
|
||||||
"PMS_AUTO_HANDOFF_COOLDOWN_TDAYS": (0, 60), "PMS_AUTO_OPTOUT_COOLDOWN_TDAYS": (0, 60),
|
"PMS_AUTO_HANDOFF_COOLDOWN_TDAYS": (0, 60), "PMS_AUTO_OPTOUT_COOLDOWN_TDAYS": (0, 60),
|
||||||
|
# 系统自决 (2026-09-14)
|
||||||
|
"PMS_SELF_DECIDE_DAILY_MAX": (0, 50),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -547,6 +556,8 @@ def _range_check(key, v):
|
||||||
return f"{key} 只能是 zone_enter (进区即动) / zone_exit (极值回落再动)"
|
return f"{key} 只能是 zone_enter (进区即动) / zone_exit (极值回落再动)"
|
||||||
if key == "PMS_OPEN_AUTONOMY" and v not in ("full", "propose_only", "off"):
|
if key == "PMS_OPEN_AUTONOMY" and v not in ("full", "propose_only", "off"):
|
||||||
return "PMS_OPEN_AUTONOMY 只能是 full / propose_only / off"
|
return "PMS_OPEN_AUTONOMY 只能是 full / propose_only / off"
|
||||||
|
if key == "PMS_SELF_DECIDE" and v not in ("off", "record", "decline", "full"):
|
||||||
|
return "PMS_SELF_DECIDE 只能是 off / record / decline / full"
|
||||||
if key == "PMS_EXEC_IMPL" and str(v).strip().upper() not in ("A", "B"):
|
if key == "PMS_EXEC_IMPL" and str(v).strip().upper() not in ("A", "B"):
|
||||||
return "PMS_EXEC_IMPL 只能是 A (委托决策系统) / B (内置保守择时)"
|
return "PMS_EXEC_IMPL 只能是 A (委托决策系统) / B (内置保守择时)"
|
||||||
if key == "PMS_SECTOR_SOURCE" and v not in ("", "gp_hybk", "custom_table",
|
if key == "PMS_SECTOR_SOURCE" and v not in ("", "gp_hybk", "custom_table",
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ from app.core import tradedays as td
|
||||||
from app.repo import pms_repo
|
from app.repo import pms_repo
|
||||||
from app.services import (candidate_pub, command_service, consensus_service, executor, industry,
|
from app.services import (candidate_pub, command_service, consensus_service, executor, industry,
|
||||||
judge, market, param_store, reask_service,
|
judge, market, param_store, reask_service,
|
||||||
plan_feed, portfolio)
|
plan_feed, portfolio, strategy_service)
|
||||||
|
|
||||||
logger = logging.getLogger("pms.proposal")
|
logger = logging.getLogger("pms.proposal")
|
||||||
|
|
||||||
|
|
@ -68,7 +68,9 @@ def scan_and_route(*, now=None, dry_run: bool = False) -> dict:
|
||||||
now = now or datetime.now()
|
now = now or datetime.now()
|
||||||
out = {"ok": True, "autonomy": None, "open_autonomy": None, "candidates": 0,
|
out = {"ok": True, "autonomy": None, "open_autonomy": None, "candidates": 0,
|
||||||
"open_candidates": 0, "executed": [], "queued": [],
|
"open_candidates": 0, "executed": [], "queued": [],
|
||||||
"rejected": [], "skipped": [], "errors": [], "degraded": False, "dry_run": dry_run}
|
"rejected": [], "skipped": [], "errors": [], "degraded": False, "dry_run": dry_run,
|
||||||
|
# 系统自决本轮自动采纳的新建仓只数 (每日上限累计用; observe 读数从 self_decide_seen 取)
|
||||||
|
"_sd_accepts": 0}
|
||||||
|
|
||||||
autonomy = param_store.get("PMS_AUTONOMY", AUTONOMY_PROPOSE)
|
autonomy = param_store.get("PMS_AUTONOMY", AUTONOMY_PROPOSE)
|
||||||
out["autonomy"] = autonomy
|
out["autonomy"] = autonomy
|
||||||
|
|
@ -89,6 +91,15 @@ def scan_and_route(*, now=None, dry_run: bool = False) -> dict:
|
||||||
stock_params = command_service.effective_stock_params()
|
stock_params = command_service.effective_stock_params()
|
||||||
mkt = _market_ctx(view["held"], now)
|
mkt = _market_ctx(view["held"], now)
|
||||||
params["_mkt"] = mkt # 规则闸要用同一份 MA5, 不再重取
|
params["_mkt"] = mkt # 规则闸要用同一份 MA5, 不再重取
|
||||||
|
# 系统自决每日新建仓上限的基数: 今天已自动采纳几只 (评审账本按前缀数)。只读一次,
|
||||||
|
# 本轮内 _route_one 每采纳一只再往 out["_sd_accepts"] 加, 两者相加即当日累计。off 时不查库。
|
||||||
|
if ae.self_decide_on(params):
|
||||||
|
try:
|
||||||
|
_t0 = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
params["_sd_base_count"] = pms_repo.system_pass_count_today(_t0)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.warning("[系统自决] 读当日已采纳数失败 (本轮按 0 起算): %s", e)
|
||||||
|
params["_sd_base_count"] = 0
|
||||||
# 跳过三类: ①已有在途提议或指令的 ②今天已被规则闸拒过的 ③今天已被研判闸驳回的新建仓。
|
# 跳过三类: ①已有在途提议或指令的 ②今天已被规则闸拒过的 ③今天已被研判闸驳回的新建仓。
|
||||||
# ② 是 2026-07-29 的教训 —— 组合已超总仓上限时, 16 只深亏票的补仓候选每分钟被拒
|
# ② 是 2026-07-29 的教训 —— 组合已超总仓上限时, 16 只深亏票的补仓候选每分钟被拒
|
||||||
# 一次, 一天往评审账本灌几千行一模一样的记录。闸门结论当天基本不会变, 记一次就够。
|
# 一次, 一天往评审账本灌几千行一模一样的记录。闸门结论当天基本不会变, 记一次就够。
|
||||||
|
|
@ -112,8 +123,11 @@ def scan_and_route(*, now=None, dry_run: bool = False) -> dict:
|
||||||
_jk.pop(_k, None)
|
_jk.pop(_k, None)
|
||||||
out["reask_unlocked"] = [{"ts_code": c, "action": a, "why": v.get("why"),
|
out["reask_unlocked"] = [{"ts_code": c, "action": a, "why": v.get("why"),
|
||||||
"hits": v.get("hits")} for (c, a), v in _unlocked.items()]
|
"hits": v.get("hits")} for (c, a), v in _unlocked.items()]
|
||||||
|
# 系统自决当日已放弃的新建仓也并入跳过集合 (改动六): 排在用户驳回之后、在途之前 ——
|
||||||
|
# 「今日系统已判放弃,明日重评」。off 时 _system_declined_today_keys 返回空, 逐字回旧。
|
||||||
skip = {**_jk, **_rejected_today_keys(),
|
skip = {**_jk, **_rejected_today_keys(),
|
||||||
**_declined_today_keys(), **_inflight_keys()}
|
**_declined_today_keys(), **_system_declined_today_keys(),
|
||||||
|
**_inflight_keys()}
|
||||||
# 挂了 ACTIVE 交易方案(策略)的票交策略层接管, 动作引擎不再对它自动提议
|
# 挂了 ACTIVE 交易方案(策略)的票交策略层接管, 动作引擎不再对它自动提议
|
||||||
# (读库失败按空集 —— 宁可这轮不排除, 也不能因读不到把全体持仓都排除)
|
# (读库失败按空集 —— 宁可这轮不排除, 也不能因读不到把全体持仓都排除)
|
||||||
try:
|
try:
|
||||||
|
|
@ -457,7 +471,8 @@ def _apply_intraday(code, blocks, cfg, now, cache) -> str | None:
|
||||||
(blocks.get("timing") or {}).get("stance") or "无读数",
|
(blocks.get("timing") or {}).get("stance") or "无读数",
|
||||||
tech_phase=new_tech.get("phase"),
|
tech_phase=new_tech.get("phase"),
|
||||||
fund_required=(cp.get("fund_required") and cp.get("consensus_route")),
|
fund_required=(cp.get("fund_required") and cp.get("consensus_route")),
|
||||||
weak_confirm=cp.get("weak_confirm", True))
|
weak_confirm=cp.get("weak_confirm", True),
|
||||||
|
timing_dissent=cp.get("timing_dissent", "confirm"))
|
||||||
new_tech["intraday"] = {"kind": kind, "reads": r["reads"]}
|
new_tech["intraday"] = {"kind": kind, "reads": r["reads"]}
|
||||||
elif kind in ("opened_wait", "outside_window"):
|
elif kind in ("opened_wait", "outside_window"):
|
||||||
con = blocks.get("consensus") or {}
|
con = blocks.get("consensus") or {}
|
||||||
|
|
@ -690,6 +705,8 @@ def disposition_snapshot(now=None) -> dict:
|
||||||
for _k in _rejected_today_keys():
|
for _k in _rejected_today_keys():
|
||||||
_w = _rr.get(_k[0])
|
_w = _rr.get(_k[0])
|
||||||
skip[_k] = ("未通过合规检查:" + _w) if _w else "今天没通过合规检查,明天开盘会重新评估"
|
skip[_k] = ("未通过合规检查:" + _w) if _w else "今天没通过合规检查,明天开盘会重新评估"
|
||||||
|
for _k in _system_declined_today_keys(): # 系统自决今日已放弃 (明日重评)
|
||||||
|
skip[_k] = "今日系统已判放弃,明日重评"
|
||||||
for _k in _inflight_keys(): # 在途; 前端多半已按指令/提议标成「已下单/待你确认」
|
for _k in _inflight_keys(): # 在途; 前端多半已按指令/提议标成「已下单/待你确认」
|
||||||
skip[_k] = "系统已经在处理这只票(见『等我拍板』或『今日在办』)"
|
skip[_k] = "系统已经在处理这只票(见『等我拍板』或『今日在办』)"
|
||||||
sink = {"skipped": []} # 丢弃用: _scan_open 只往里 append/extend, 不读它
|
sink = {"skipped": []} # 丢弃用: _scan_open 只往里 append/extend, 不读它
|
||||||
|
|
@ -798,7 +815,9 @@ def _route_one(c, view, params, stock_params, brake_active, now, dry_run, out,
|
||||||
# 「证据不足以判断 → 不可用」, 但模型未必每次守得住; 它给的把握度是现成的读数,
|
# 「证据不足以判断 → 不可用」, 但模型未必每次守得住; 它给的把握度是现成的读数,
|
||||||
# 低于阈值的驳回按「不可用」处理 —— 进人的待确认队列, 不记驳回、不杀提议。
|
# 低于阈值的驳回按「不可用」处理 —— 进人的待确认队列, 不记驳回、不杀提议。
|
||||||
# 只对新建仓: 加仓类的驳回口径不动。阈值一次定死, 不按复盘读数回调。
|
# 只对新建仓: 加仓类的驳回口径不动。阈值一次定死, 不按复盘读数回调。
|
||||||
if is_open and verdict["verdict"] == judge.REJECT:
|
# **系统自决开着时不做这个转换** (2026-09-14, §3.5): 自决下 REJECT 一律放弃并记账 judge,
|
||||||
|
# 不再"低把握改不可用交人"——低把握的 REJECT 也走下面的放弃路, 解锁重问对它照常生效。
|
||||||
|
if is_open and verdict["verdict"] == judge.REJECT and not ae.self_decide_on(params):
|
||||||
conf = verdict.get("confidence")
|
conf = verdict.get("confidence")
|
||||||
floor_conf = param_store.get_int("PMS_JUDGE_REJECT_CONF_MIN", 60)
|
floor_conf = param_store.get_int("PMS_JUDGE_REJECT_CONF_MIN", 60)
|
||||||
if conf is not None and float(conf) < floor_conf:
|
if conf is not None and float(conf) < floor_conf:
|
||||||
|
|
@ -886,6 +905,38 @@ def _route_one(c, view, params, stock_params, brake_active, now, dry_run, out,
|
||||||
force_queue = (bool(c.get("needs_user_confirm")) or bool(src_why)
|
force_queue = (bool(c.get("needs_user_confirm")) or bool(src_why)
|
||||||
or bool(verdict.get("degraded")) or bool(reask_why) or bool(risk_why))
|
or bool(verdict.get("degraded")) or bool(reask_why) or bool(risk_why))
|
||||||
auto_exec = (not force_queue) and (side == "sell" or autonomy == AUTONOMY_FULL)
|
auto_exec = (not force_queue) and (side == "sell" or autonomy == AUTONOMY_FULL)
|
||||||
|
# ---- 系统自决 (2026-09-14 台账 015): 新建仓/深亏补仓/目标价由合议+研判自动决定 ----
|
||||||
|
# 位置在 force_queue 之后、四条件旁路之前: 自决非 off 时它先决定, 旁路不再插手。
|
||||||
|
# 重问放行也归自决 (试探仓): 先给它打上 decide_kind, 再交 _self_decide 判。
|
||||||
|
if c.get("reask") and ae.self_decide_on(params) and not c.get("decide_kind"):
|
||||||
|
c["decide_kind"] = ae.DK_REASK
|
||||||
|
sd = _self_decide(c, verdict, params, params.get("_sd_base_count", 0) + out.get("_sd_accepts", 0))
|
||||||
|
if sd.get("would") and sd["would"] != "none":
|
||||||
|
_con = _sd_consensus_brief(c)
|
||||||
|
out.setdefault("self_decide_seen", []).append(
|
||||||
|
{"ts_code": code, "action": action, "decide_kind": sd.get("rule"),
|
||||||
|
"would": sd["would"], "decision": sd.get("decision"),
|
||||||
|
"tier": sd.get("tier"), "why": sd.get("why"),
|
||||||
|
"direction": _con.get("direction"), "route": _con.get("route"),
|
||||||
|
"conf": verdict.get("confidence")})
|
||||||
|
sd_decision = sd.get("decision", "none")
|
||||||
|
if sd_decision == "accept":
|
||||||
|
auto_exec, force_queue = True, False
|
||||||
|
if is_open:
|
||||||
|
out["_sd_accepts"] = out.get("_sd_accepts", 0) + 1
|
||||||
|
elif sd_decision == "decline":
|
||||||
|
# 自动放弃: 落账 (评审方 system) + 追加 rejected by system + 当日不重评 + 返回
|
||||||
|
if dry_run:
|
||||||
|
out["rejected"].append({**_brief(c), "by": "system", "route": "decline",
|
||||||
|
"dry_run": True, "why": sd.get("why")})
|
||||||
|
return
|
||||||
|
_sd_decline_ledger(code, action, c, verdict, price, sd)
|
||||||
|
out["rejected"].append({"ts_code": code, "action": action, "by": "system",
|
||||||
|
"failed": [sd.get("why") or "系统自决放弃"]})
|
||||||
|
return
|
||||||
|
elif sd_decision == "confirm":
|
||||||
|
auto_exec, force_queue = False, True
|
||||||
|
c["confirm_why"] = sd.get("why") or c.get("confirm_why")
|
||||||
# 自动执行开关 (2026-09-03, PMS_OPEN_AUTO_EXEC_ON_VERDICT): 新建仓档位是 propose_only 时,
|
# 自动执行开关 (2026-09-03, PMS_OPEN_AUTO_EXEC_ON_VERDICT): 新建仓档位是 propose_only 时,
|
||||||
# 「判决候选 + 决策系统研判真回了通过 + 规则闸通过 (走到这里就是通过了) + 上游风险列表
|
# 「判决候选 + 决策系统研判真回了通过 + 规则闸通过 (走到这里就是通过了) + 上游风险列表
|
||||||
# 为空」四条齐, 这一条按 full 处理。强制入队的 (关注 / 深档补仓 / 研判不可用) 永远不走。
|
# 为空」四条齐, 这一条按 full 处理。强制入队的 (关注 / 深档补仓 / 研判不可用) 永远不走。
|
||||||
|
|
@ -903,19 +954,52 @@ def _route_one(c, view, params, stock_params, brake_active, now, dry_run, out,
|
||||||
return
|
return
|
||||||
|
|
||||||
if auto_exec:
|
if auto_exec:
|
||||||
|
sd_accept = sd_decision == "accept"
|
||||||
|
strat_cancelled = []
|
||||||
|
if sd_accept and side == "sell":
|
||||||
|
# 目标价自动清仓两道前置 (2026-09-14): 按当前可卖量夹紧; 该票挂着方案先撤,
|
||||||
|
# 免得清仓单与网格买入腿对倒 (附录乙那处冲突)。可卖量为零则不落, 明日重评。
|
||||||
|
posrow = pms_repo.get_position(code) or {}
|
||||||
|
clamped = ae.clamp_sell_qty(int(c.get("qty") or 0), posrow)
|
||||||
|
if clamped <= 0:
|
||||||
|
out["skipped"].append({"ts_code": code, "action": action,
|
||||||
|
"why": "系统到价自动清仓: 当前可卖量为零, 本轮不清 (明日重评)"})
|
||||||
|
return
|
||||||
|
c["qty"] = clamped
|
||||||
|
try:
|
||||||
|
if code in (pms_repo.active_strategy_codes() or set()):
|
||||||
|
r = strategy_service.cancel_by_code(
|
||||||
|
code, reason="系统到价自动清仓: 先撤该票交易方案, 免得清仓单与方案买入腿对倒")
|
||||||
|
strat_cancelled = r.get("cancelled") or []
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.warning("[自决清仓] 撤方案失败 %s (清仓照落): %s", code, e)
|
||||||
|
elif sd_accept and is_open:
|
||||||
|
# 试探仓类采纳: 把数量重算成建议档 (弱基本面那类还带上紧止盈标记 advice.tight_trail)。
|
||||||
|
_apply_advice_tier(c, c.setdefault("hard_numbers", {}), verdict,
|
||||||
|
force_trial=(sd.get("tier") == "trial"))
|
||||||
|
if sd_accept:
|
||||||
|
c["reason"] = sd.get("why") or c["reason"] # 指令与账本理由一致
|
||||||
iid = _make_instruction(c, price, now)
|
iid = _make_instruction(c, price, now)
|
||||||
reason = verdict.get("reason") or c["reason"]
|
if sd_accept:
|
||||||
if auto_why:
|
reason = sd.get("why") or c["reason"]
|
||||||
reason = f"判决候选自动执行: {reason}"
|
_dc = params.get("_sd_base_count", 0) + out.get("_sd_accepts", 0)
|
||||||
|
hard = {**(c.get("hard_numbers") or {}),
|
||||||
|
"self_decide": _self_decide_hard(c, verdict, sd, params, strat_cancelled, _dc)}
|
||||||
|
else:
|
||||||
|
reason = verdict.get("reason") or c["reason"]
|
||||||
|
if auto_why:
|
||||||
|
reason = f"判决候选自动执行: {reason}"
|
||||||
|
hard = c["hard_numbers"]
|
||||||
pms_repo.insert_ledger(ts_code=code, action=action,
|
pms_repo.insert_ledger(ts_code=code, action=action,
|
||||||
arbiter="judge" if c.get("judge_required") else "rule",
|
arbiter="judge" if c.get("judge_required") else "rule",
|
||||||
verdict="PASS", price_at=price, hard_numbers=c["hard_numbers"],
|
verdict="PASS", price_at=price, hard_numbers=hard,
|
||||||
ref_id=iid, reason=reason[:500])
|
ref_id=iid, reason=reason[:500])
|
||||||
out["executed"].append({**_brief(c), "instruction_id": iid,
|
out["executed"].append({**_brief(c), "instruction_id": iid,
|
||||||
"why": ("减持自动执行 (规则触发, 来源没有要求交人裁决)"
|
"why": (sd.get("why") if sd_accept else
|
||||||
if side == "sell"
|
("减持自动执行 (规则触发, 来源没有要求交人裁决)"
|
||||||
else (auto_why or ("新建仓档位 full" if is_open
|
if side == "sell"
|
||||||
else "档位 full")))})
|
else (auto_why or ("新建仓档位 full" if is_open
|
||||||
|
else "档位 full"))))})
|
||||||
else:
|
else:
|
||||||
pid = _make_proposal(c, price, verdict)
|
pid = _make_proposal(c, price, verdict)
|
||||||
# 交人的原因按从具体到笼统取: 重问放行 → 上游标的硬风险 → 候选自带的 (关注判决等,
|
# 交人的原因按从具体到笼统取: 重问放行 → 上游标的硬风险 → 候选自带的 (关注判决等,
|
||||||
|
|
@ -1056,6 +1140,154 @@ def bump_once_guards(ts_code: str, action: str, hard_numbers=None, now=None):
|
||||||
return {"ok": True, "fields": fields}
|
return {"ok": True, "fields": fields}
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_advice_tier(c, hn, verdict, *, force_trial=False):
|
||||||
|
"""按建议档位补研判一档、重算数量与仓位 (原 _make_proposal 内联那段抽出, 2026-09-14)。
|
||||||
|
|
||||||
|
自动路在落指令前也调它, 让自动采纳的试探仓真下的是试探仓的股数与分批, 弱基本面那类还带上
|
||||||
|
紧止盈标记 (advice.tight_trail)。force_trial=True 时把建议直接压到试探仓 —— 系统自决按
|
||||||
|
decide_kind 判要试探仓时, 研判把握度未必低, apply_judge 不会降档, 所以显式压一次。
|
||||||
|
原地改 hn 与 c["qty"], 返回 (hn, qty, is_trial)。建议算不出不拦 (与从前一样吞掉异常)。
|
||||||
|
"""
|
||||||
|
is_trial = False
|
||||||
|
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 force_trial and isinstance(adv2, dict) \
|
||||||
|
and adv2.get("tier") not in (_adv.TIER_NONE, _adv.TIER_TRIAL):
|
||||||
|
adv2 = {**adv2, "tier": _adv.TIER_TRIAL, "factor": 1.0 / 6.0, "splits": (1.0,)}
|
||||||
|
if isinstance(adv2, dict):
|
||||||
|
hn["advice"], hn["advice_text"] = adv2, adv2.get("text")
|
||||||
|
is_trial = adv2.get("tier") == _adv.TIER_TRIAL
|
||||||
|
if adv2.get("needs_user_confirm"):
|
||||||
|
hn["needs_user_confirm"] = True
|
||||||
|
fix, resize_why = _adv.resize_to_advice(adv2, hn, c["ts_code"])
|
||||||
|
if fix:
|
||||||
|
old_qty = c.get("qty")
|
||||||
|
c["qty"] = fix["qty"]
|
||||||
|
hn["target_pct"] = fix["target_pct"]
|
||||||
|
hn["target_amount"] = fix["target_amount"]
|
||||||
|
hn["base_amount"] = fix["base_amount"]
|
||||||
|
hn["batch_scheme"] = fix["batch_scheme"]
|
||||||
|
hn["advice_resized"] = {"from_qty": old_qty, "to_qty": fix["qty"],
|
||||||
|
"tier": adv2.get("tier"), "note": fix.get("note") or ""}
|
||||||
|
logger.info("提议按建议档位重算数量 %s: %s → %s 股 (%s)%s",
|
||||||
|
c["ts_code"], old_qty, fix["qty"], adv2.get("tier"),
|
||||||
|
"; " + fix["note"] if fix.get("note") else "")
|
||||||
|
elif resize_why and "档位没变" not in resize_why:
|
||||||
|
# 算不出来要留痕, 让人在卡上看得见「为什么数量还是原来那个」,
|
||||||
|
# 而不是默默按旧数量下单。
|
||||||
|
hn["advice_resize_note"] = resize_why
|
||||||
|
except Exception: # noqa: BLE001 —— 建议方案出错不拦
|
||||||
|
pass
|
||||||
|
return hn, c.get("qty"), is_trial
|
||||||
|
|
||||||
|
|
||||||
|
def _self_decide(c, verdict, params, today_auto_count) -> dict:
|
||||||
|
"""系统自决 (2026-09-14 台账 015): 一条候选归不归自决管、自决怎么判。
|
||||||
|
|
||||||
|
返回 {decision, tier, why, rule, would}。decision 取 accept / decline / confirm / none:
|
||||||
|
none 表示不归自决管 (走旧路)。would 是未经档位降级 (record/decline) 的原判, 供观察读数记
|
||||||
|
「本会怎么判」。判定次序按规格书 §3.6 改动三, 决定表见 §3.5。
|
||||||
|
"""
|
||||||
|
mode = str((params or {}).get("self_decide") or "off").strip()
|
||||||
|
action, dk = c.get("action"), c.get("decide_kind")
|
||||||
|
is_open = action == ae.A_OPEN
|
||||||
|
is_dca_deep = action == ae.A_DCA and dk == ae.DK_DCA_DEEP
|
||||||
|
is_target = action == ae.A_EXIT and dk == ae.DK_TARGET_PRICE
|
||||||
|
# 一, 不归自决管: 关掉, 或不是新建仓/深亏补仓/目标价这三类
|
||||||
|
if mode == "off" or not (is_open or is_dca_deep or is_target):
|
||||||
|
return {"decision": "none"}
|
||||||
|
conf = verdict.get("confidence")
|
||||||
|
trial_min = param_store.get_int("PMS_ADVICE_TRIAL_CONF_MIN", 60)
|
||||||
|
dk_cn = ae.DK_CN.get(dk, "")
|
||||||
|
|
||||||
|
def _mode(res):
|
||||||
|
"""按档位把原判降级: record 只记不做 (全变 none); decline 只放弃、不采纳 (accept 变 none)。
|
||||||
|
would 存原判供读数。"""
|
||||||
|
res = {"tier": None, "rule": dk or "consensus_strong", **res}
|
||||||
|
res["would"] = res["decision"]
|
||||||
|
if mode == "record":
|
||||||
|
res["decision"] = "none"
|
||||||
|
elif mode == "decline" and res["decision"] == "accept":
|
||||||
|
res["decision"] = "none"
|
||||||
|
return res
|
||||||
|
|
||||||
|
# 目标价到价: 不进研判范围, 直接自动清仓 (卖出方向, 落指令前再按可卖量夹紧、先撤方案)
|
||||||
|
if is_target:
|
||||||
|
return _mode({"decision": "accept", "rule": ae.DK_TARGET_PRICE, "why": "目标价到价自动清仓"})
|
||||||
|
# 二, 前置四种交人: 研判不可用 / 上游硬风险 / 合议装不上 / 判决词认不出
|
||||||
|
if verdict.get("degraded"):
|
||||||
|
return _mode({"decision": "confirm", "why": "研判不可用,降级人工确认"})
|
||||||
|
if (c.get("hard_numbers") or {}).get("risk"):
|
||||||
|
return _mode({"decision": "confirm", "why": "上游标了硬风险,交人裁决"})
|
||||||
|
if is_open and not (c.get("consensus") or (c.get("hard_numbers") or {}).get("consensus")):
|
||||||
|
return _mode({"decision": "confirm", "why": "三源合议装不上,交人裁决"})
|
||||||
|
if dk == ae.DK_VERDICT_UNKNOWN:
|
||||||
|
return _mode({"decision": "confirm", "why": "判决词认不出,交人裁决"})
|
||||||
|
# 三, 逻辑存疑: 自动放弃, 评审方 system
|
||||||
|
if dk == ae.DK_LOGIC_DOUBT:
|
||||||
|
return _mode({"decision": "decline", "why": "研究证据走弱(逻辑存疑),自动放弃"})
|
||||||
|
# 深亏补仓: 研判必答「杀逻辑还是杀情绪」, 契约规定杀逻辑或说不清一律 REJECT (REJECT 已在前面
|
||||||
|
# 早退当放弃)。所以走到这里的 PASS 即杀情绪: 把握度不低于 60 自动补, 否则放弃。
|
||||||
|
if is_dca_deep:
|
||||||
|
if verdict.get("verdict") == judge.PASS and conf is not None and float(conf) >= trial_min:
|
||||||
|
return _mode({"decision": "accept", "rule": ae.DK_DCA_DEEP,
|
||||||
|
"why": f"深亏补仓:研判通过(把握度 {int(conf)}),按研判必答题自动补仓"})
|
||||||
|
return _mode({"decision": "decline", "rule": ae.DK_DCA_DEEP,
|
||||||
|
"why": "深亏补仓:把握度不足,自动放弃"})
|
||||||
|
# 新建仓 (走到这: verdict 是真 PASS —— REJECT 与 degraded 都已在前面处理)
|
||||||
|
conf_ok = conf is not None and float(conf) >= trial_min
|
||||||
|
is_trial = (dk in ae.TRIAL_DECIDE_KINDS) or (not conf_ok)
|
||||||
|
if is_trial:
|
||||||
|
if not (params or {}).get("self_decide_trial"):
|
||||||
|
return _mode({"decision": "confirm",
|
||||||
|
"why": f"{dk_cn or '把握度偏低'}:试探仓自动采纳未开,交人裁决"})
|
||||||
|
why = ("系统采纳试探仓:" + (dk_cn or "把握度偏低")
|
||||||
|
+ (f";研判通过(把握度 {int(conf)})" if conf is not None else ""))
|
||||||
|
res = {"decision": "accept", "tier": "trial", "why": why}
|
||||||
|
else:
|
||||||
|
res = {"decision": "accept", "tier": "standard",
|
||||||
|
"why": "系统采纳:合议放行" + (f",研判通过(把握度 {int(conf)})" if conf is not None else "")}
|
||||||
|
# 五, 每日自动新建仓上限: 超过转交人 (0 = 不限)
|
||||||
|
dmax = int((params or {}).get("self_decide_daily_max") or 0)
|
||||||
|
if res["decision"] == "accept" and is_open and dmax and today_auto_count >= dmax:
|
||||||
|
return _mode({"decision": "confirm", "why": f"今日自动新建仓已达上限 {dmax} 只,本只转交人裁决"})
|
||||||
|
return _mode(res)
|
||||||
|
|
||||||
|
|
||||||
|
def _sd_consensus_brief(c) -> dict:
|
||||||
|
"""从候选取合议方向/强弱/路由三项 (self_decide 块用)。硬数字里的合议六键或候选自带的都行。"""
|
||||||
|
con = (c.get("hard_numbers") or {}).get("consensus") or c.get("consensus") or {}
|
||||||
|
return {"direction": con.get("direction"), "strength": con.get("strength"),
|
||||||
|
"route": con.get("route")}
|
||||||
|
|
||||||
|
|
||||||
|
def _self_decide_hard(c, verdict, sd, params, strat_cancelled, daily_count) -> dict:
|
||||||
|
"""账本硬数字的 self_decide 块 (数据契约 §3.8): 决定种类、档、仓位档、合议三项、研判、当日计数、撤掉的方案。"""
|
||||||
|
tier_cn = {"trial": "试探仓", "standard": "标准仓", "half": "减半仓"}.get(sd.get("tier"))
|
||||||
|
return {"rule": sd.get("rule"), "mode": str((params or {}).get("self_decide") or "off"),
|
||||||
|
"tier": tier_cn or sd.get("tier"),
|
||||||
|
"consensus": _sd_consensus_brief(c),
|
||||||
|
"judge": {"verdict": verdict.get("verdict"), "confidence": verdict.get("confidence")},
|
||||||
|
"daily_count": daily_count,
|
||||||
|
"daily_max": int((params or {}).get("self_decide_daily_max") or 0),
|
||||||
|
"strategy_cancelled": list(strat_cancelled or [])}
|
||||||
|
|
||||||
|
|
||||||
|
def _sd_decline_ledger(code, action, c, verdict, price, sd) -> None:
|
||||||
|
"""系统自决放弃落账 (改动六): 评审方 system, 结论 REJECT, 硬数字带原硬数字、合议六键、
|
||||||
|
研判把握度与 self_decide 块。研判 REJECT 引起的放弃走的是研判闸那条 (评审方 judge), 不到这里。"""
|
||||||
|
hard = {**(c.get("hard_numbers") or {}), "judge_conf": verdict.get("confidence"),
|
||||||
|
"self_decide": {"rule": sd.get("rule"), "decision": "decline",
|
||||||
|
"consensus": _sd_consensus_brief(c),
|
||||||
|
"judge": {"verdict": verdict.get("verdict"),
|
||||||
|
"confidence": verdict.get("confidence")}}}
|
||||||
|
pms_repo.insert_ledger(ts_code=code, action=action, arbiter="system", verdict="REJECT",
|
||||||
|
price_at=price, hard_numbers=hard,
|
||||||
|
reason=(sd.get("why") or "系统自决放弃")[:500])
|
||||||
|
|
||||||
|
|
||||||
def _make_proposal(c, price, verdict) -> str:
|
def _make_proposal(c, price, verdict) -> str:
|
||||||
ttl = param_store.get_int("PMS_PROPOSAL_TTL_HOURS", 24)
|
ttl = param_store.get_int("PMS_PROPOSAL_TTL_HOURS", 24)
|
||||||
pid = f"PRP_{td.ymd()}_{c['ts_code'].replace('.', '')}_{c['action']}"
|
pid = f"PRP_{td.ymd()}_{c['ts_code'].replace('.', '')}_{c['action']}"
|
||||||
|
|
@ -1076,32 +1308,7 @@ def _make_proposal(c, price, verdict) -> str:
|
||||||
# 而采纳后真下的是标准仓 6% 的第一批。实测 8 张待确认提议里 7 张是这个样子, 差三倍。
|
# 而采纳后真下的是标准仓 6% 的第一批。实测 8 张待确认提议里 7 张是这个样子, 差三倍。
|
||||||
# 采纳按钮那边直接抄提议里的股数原样落单 (只有卖出侧会按可卖量重算), 所以不在这里
|
# 采纳按钮那边直接抄提议里的股数原样落单 (只有卖出侧会按可卖量重算), 所以不在这里
|
||||||
# 改, 后面就没有第二次机会了。
|
# 改, 后面就没有第二次机会了。
|
||||||
try:
|
_apply_advice_tier(c, hn, verdict)
|
||||||
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
|
|
||||||
fix, resize_why = _adv.resize_to_advice(adv2, hn, c["ts_code"])
|
|
||||||
if fix:
|
|
||||||
old_qty = c.get("qty")
|
|
||||||
c["qty"] = fix["qty"]
|
|
||||||
hn["target_pct"] = fix["target_pct"]
|
|
||||||
hn["target_amount"] = fix["target_amount"]
|
|
||||||
hn["base_amount"] = fix["base_amount"]
|
|
||||||
hn["batch_scheme"] = fix["batch_scheme"]
|
|
||||||
hn["advice_resized"] = {"from_qty": old_qty, "to_qty": fix["qty"],
|
|
||||||
"tier": adv2.get("tier"), "note": fix.get("note") or ""}
|
|
||||||
logger.info("提议按建议档位重算数量 %s: %s → %s 股 (%s)%s",
|
|
||||||
c["ts_code"], old_qty, fix["qty"], adv2.get("tier"),
|
|
||||||
"; " + fix["note"] if fix.get("note") else "")
|
|
||||||
elif resize_why and "档位没变" not in resize_why:
|
|
||||||
# 算不出来要留痕, 让人在卡上看得见「为什么数量还是原来那个」,
|
|
||||||
# 而不是默默按旧数量下单。
|
|
||||||
hn["advice_resize_note"] = resize_why
|
|
||||||
except Exception: # noqa: BLE001 —— 建议方案出错不拦提议
|
|
||||||
pass
|
|
||||||
try:
|
try:
|
||||||
pms_repo.insert_proposal(
|
pms_repo.insert_proposal(
|
||||||
proposal_id=pid, ts_code=c["ts_code"], action=c["action"], qty=c["qty"],
|
proposal_id=pid, ts_code=c["ts_code"], action=c["action"], qty=c["qty"],
|
||||||
|
|
@ -1149,6 +1356,11 @@ def _scan_params(view: dict) -> dict:
|
||||||
# 两个都关掉时, 合议既不装配也不分流、增持门不设 —— 行为与接入前逐字相同。
|
# 两个都关掉时, 合议既不装配也不分流、增持门不设 —— 行为与接入前逐字相同。
|
||||||
"consensus_route": param_store.get_bool("PMS_CONSENSUS_ROUTE", True),
|
"consensus_route": param_store.get_bool("PMS_CONSENSUS_ROUTE", True),
|
||||||
"tech_gate_increase": param_store.get_bool("PMS_TECH_GATE_INCREASE", True),
|
"tech_gate_increase": param_store.get_bool("PMS_TECH_GATE_INCREASE", True),
|
||||||
|
# 系统自决 (2026-09-14 台账 015): 档位 off/record/decline/full 由 action_engine 与
|
||||||
|
# _self_decide 读; off 时 scan_open/eval_dca/eval_target 的自决分支一行不走, 逐字回旧。
|
||||||
|
"self_decide": str(param_store.get("PMS_SELF_DECIDE", "off") or "off").strip(),
|
||||||
|
"self_decide_trial": param_store.get_bool("PMS_SELF_DECIDE_TRIAL", False),
|
||||||
|
"self_decide_daily_max": param_store.get_int("PMS_SELF_DECIDE_DAILY_MAX", 3),
|
||||||
# 技术面转空自动离场 (2026-09-11 工作包三, 台账 010)。档位 off/propose_only/full:
|
# 技术面转空自动离场 (2026-09-11 工作包三, 台账 010)。档位 off/propose_only/full:
|
||||||
# off 不评 (tech_exit_on 为假)、propose_only 交人、full 自动执行 (与保垫减仓同档)。
|
# off 不评 (tech_exit_on 为假)、propose_only 交人、full 自动执行 (与保垫减仓同档)。
|
||||||
# tech_exit_done 去重集在 scan_and_route 里按当轮技术面映射填 (这里给空占位)。
|
# tech_exit_done 去重集在 scan_and_route 里按当轮技术面映射填 (这里给空占位)。
|
||||||
|
|
@ -1245,6 +1457,21 @@ def _judge_rejected_open_keys() -> dict:
|
||||||
for code, act in keys if act == ae.A_OPEN}
|
for code, act in keys if act == ae.A_OPEN}
|
||||||
|
|
||||||
|
|
||||||
|
def _system_declined_today_keys() -> dict:
|
||||||
|
"""今天已被**系统自决放弃**过的新建仓等 {(代码, 动作): 原因} —— 当日不再重评, 明日重评。
|
||||||
|
|
||||||
|
评审方 system 的 REJECT (逻辑存疑、合议引起的自动放弃) 就是这类。研判 REJECT 引起的放弃
|
||||||
|
评审方是 judge, 走 _judge_rejected_open_keys 那条 (还要参与解锁重问), 不在此列。
|
||||||
|
off 时账本里没有 system 放弃行, 自然返回空 —— 逐字回旧。读不到就返回空 (去重是降噪)。"""
|
||||||
|
try:
|
||||||
|
keys = pms_repo.system_decided_today(datetime.now().replace(
|
||||||
|
hour=0, minute=0, second=0, microsecond=0))
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.warning("读当日系统自决放弃记录失败 (按未放弃过继续扫描): %s", e)
|
||||||
|
return {}
|
||||||
|
return {k: "今日系统已判放弃,明日重评" for k in keys}
|
||||||
|
|
||||||
|
|
||||||
def _declined_today_keys() -> dict:
|
def _declined_today_keys() -> dict:
|
||||||
"""今天已被用户**驳回**的提议 {(代码, 动作): 原因} —— 当日不再重提 (2026-08-28)。
|
"""今天已被用户**驳回**的提议 {(代码, 动作): 原因} —— 当日不再重提 (2026-08-28)。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,42 @@ def set_status(strategy_id: str, status: str, by: str = "user") -> dict:
|
||||||
return {"ok": True, "strategy_id": strategy_id, "ts_code": st.get("ts_code"), "status": status}
|
return {"ok": True, "strategy_id": strategy_id, "ts_code": st.get("ts_code"), "status": status}
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_by_code(ts_code: str, reason: str = "") -> dict:
|
||||||
|
"""撤下某只票当前 ACTIVE 的全部交易方案 (2026-09-14 系统自决包)。
|
||||||
|
|
||||||
|
系统自决到价自动清仓前调它: 该票若挂着网格, 网格的买入腿会与清仓单对倒 (附录乙那处冲突),
|
||||||
|
所以落清仓指令前先撤方案。与命令服务清仓覆盖冲突那段 (command_service) 同一手法, 抽到这里
|
||||||
|
供两处复用。返回撤掉的方案编号列表; 读库或撤下失败只记进 errors, 不抛 —— 撤不成也不该拦住清仓。
|
||||||
|
"""
|
||||||
|
out = {"ok": True, "ts_code": ts_code, "cancelled": [], "errors": []}
|
||||||
|
try:
|
||||||
|
actives = pms_repo.list_strategies(ts_code=ts_code, statuses=["ACTIVE"], limit=20)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
out["ok"] = False
|
||||||
|
out["errors"].append(f"读策略失败: {type(e).__name__}: {e}")
|
||||||
|
return out
|
||||||
|
for s in actives:
|
||||||
|
try:
|
||||||
|
r = set_status(s["strategy_id"], "CANCELLED", by="system") or {}
|
||||||
|
if r.get("ok"):
|
||||||
|
out["cancelled"].append(s["strategy_id"])
|
||||||
|
try:
|
||||||
|
pms_repo.insert_ledger(
|
||||||
|
ts_code=ts_code, action="CLEANUP", arbiter="system", verdict="PASS",
|
||||||
|
price_at=0, ref_id=s["strategy_id"],
|
||||||
|
hard_numbers={"strategy_id": s["strategy_id"]},
|
||||||
|
reason=(reason or "系统到价自动清仓: 先撤该票交易方案, 免得清仓单与方案买入腿对倒")[:500])
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.warning("[自决清仓] 撤方案留痕失败 %s: %s", ts_code, e)
|
||||||
|
else:
|
||||||
|
out["ok"] = False
|
||||||
|
out["errors"].append(f"{s.get('strategy_id')} 撤下未成: {r.get('error')}")
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
out["ok"] = False
|
||||||
|
out["errors"].append(f"{s.get('strategy_id')} 撤下失败: {type(e).__name__}: {e}")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
# ================================================================ 买入暂停表 (决策系统风控预警)
|
# ================================================================ 买入暂停表 (决策系统风控预警)
|
||||||
# 挂了策略的票, 决策系统的风控卖出只提示、不自动清仓 (强制离场会推翻你特意设的策略);
|
# 挂了策略的票, 决策系统的风控卖出只提示、不自动清仓 (强制离场会推翻你特意设的策略);
|
||||||
# 这里把该票策略的**买入这一侧**暂停 —— 不平仓、不动卖出、可页面恢复。
|
# 这里把该票策略的**买入这一侧**暂停 —— 不平仓、不动卖出、可页面恢复。
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ from fastapi.responses import FileResponse, JSONResponse
|
||||||
|
|
||||||
from config.settings import settings
|
from config.settings import settings
|
||||||
from app.core import command_spec as cs
|
from app.core import command_spec as cs
|
||||||
|
from app.core import decide_reasons
|
||||||
from app.core import tradedays as td
|
from app.core import tradedays as td
|
||||||
from app.db import session as dbs
|
from app.db import session as dbs
|
||||||
from app.repo import downstream_repo, pms_repo
|
from app.repo import downstream_repo, pms_repo
|
||||||
|
|
@ -454,6 +455,13 @@ def api_proposals(status: str = Query("WAIT_USER"), limit: int = 100,
|
||||||
return ok(_list)
|
return ok(_list)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/proposals/decide-reasons")
|
||||||
|
def api_decide_reasons():
|
||||||
|
"""裁决预设理由的两张码表 (采纳 / 驳回), 页面据此铺按钮。台账 015。"""
|
||||||
|
return {"ok": True, "accept": decide_reasons.ACCEPT_REASONS,
|
||||||
|
"decline": decide_reasons.DECLINE_REASONS}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/proposals/{proposal_id}/decide")
|
@app.post("/api/proposals/{proposal_id}/decide")
|
||||||
def api_decide(request: Request, proposal_id: str, payload: dict = Body(default={})):
|
def api_decide(request: Request, proposal_id: str, payload: dict = Body(default={})):
|
||||||
"""采纳/驳回一条自主提议。采纳 = 先落指令表 (先记账后动作), 下发由择时执行器负责。"""
|
"""采纳/驳回一条自主提议。采纳 = 先落指令表 (先记账后动作), 下发由择时执行器负责。"""
|
||||||
|
|
@ -463,9 +471,14 @@ def api_decide(request: Request, proposal_id: str, payload: dict = Body(default=
|
||||||
# 理由在服务端强制 (2026-09-07): 页面 09-03 起就弹必填框, 但只在浏览器里成立 ——
|
# 理由在服务端强制 (2026-09-07): 页面 09-03 起就弹必填框, 但只在浏览器里成立 ——
|
||||||
# 任何脚本、旧缓存页面都能发一条没有理由的裁决, 账本上落一句「页面人工裁决」的
|
# 任何脚本、旧缓存页面都能发一条没有理由的裁决, 账本上落一句「页面人工裁决」的
|
||||||
# 默认文案, 与真裁决看不出区别。复盘要的是「人为什么这么判」, 没理由的不收。
|
# 默认文案, 与真裁决看不出区别。复盘要的是「人为什么这么判」, 没理由的不收。
|
||||||
reason = str(payload.get("reason") or "").strip()
|
# 2026-09-14 台账 015: 改收预设理由码 reason_code 加补充 reason_note; 旧的自由文本 reason
|
||||||
if not reason:
|
# 继续接受 (向后兼容)。三者全空、码不合法、选「其他」不填补充, 由 decide_reasons.compose 拒。
|
||||||
return {"ok": False, "error": "采纳或驳回都要写理由, 空理由不收"}
|
try:
|
||||||
|
reason, reason_code = decide_reasons.compose(
|
||||||
|
decision, payload.get("reason_code"), payload.get("reason_note"),
|
||||||
|
payload.get("reason"))
|
||||||
|
except ValueError as e:
|
||||||
|
return {"ok": False, "error": str(e)}
|
||||||
|
|
||||||
def _decide():
|
def _decide():
|
||||||
from app.core import action_engine as ae
|
from app.core import action_engine as ae
|
||||||
|
|
@ -477,9 +490,11 @@ def api_decide(request: Request, proposal_id: str, payload: dict = Body(default=
|
||||||
if not pms_repo.decide_proposal(proposal_id, decision):
|
if not pms_repo.decide_proposal(proposal_id, decision):
|
||||||
return {"ok": False, "error": "提议状态已变更, 请刷新"}
|
return {"ok": False, "error": "提议状态已变更, 请刷新"}
|
||||||
hn = p.get("hard_numbers") or {}
|
hn = p.get("hard_numbers") or {}
|
||||||
|
# 用到的预设理由码进硬数字备查 (复盘要能按码聚合"人常用哪条理由驳回")。
|
||||||
|
hn_led = {**hn, "decide_reason_code": reason_code} if reason_code else hn
|
||||||
pms_repo.insert_ledger(ts_code=p["ts_code"], action=p["action"], arbiter="user",
|
pms_repo.insert_ledger(ts_code=p["ts_code"], action=p["action"], arbiter="user",
|
||||||
verdict="PASS" if decision == "ACCEPTED" else "REJECT",
|
verdict="PASS" if decision == "ACCEPTED" else "REJECT",
|
||||||
price_at=float(hn.get("price") or 0), hard_numbers=hn,
|
price_at=float(hn.get("price") or 0), hard_numbers=hn_led,
|
||||||
ref_id=proposal_id, reason=reason)
|
ref_id=proposal_id, reason=reason)
|
||||||
instruction_id = None
|
instruction_id = None
|
||||||
if decision == "ACCEPTED":
|
if decision == "ACCEPTED":
|
||||||
|
|
|
||||||
|
|
@ -2922,6 +2922,8 @@ createApp({
|
||||||
if (d.disp === 'wait_tech') return { cls: 'wait', label: '等技术面开口', why: d.why };
|
if (d.disp === 'wait_tech') return { cls: 'wait', label: '等技术面开口', why: d.why };
|
||||||
// 盘中确认包 (2026-09-14): 开口未确认/时段外, 等盘中放量确认, 同样是「在盯」不是「被否决」。
|
// 盘中确认包 (2026-09-14): 开口未确认/时段外, 等盘中放量确认, 同样是「在盯」不是「被否决」。
|
||||||
if (d.disp === 'wait_confirm') return { cls: 'wait', label: '等盘中确认', why: d.why };
|
if (d.disp === 'wait_confirm') return { cls: 'wait', label: '等盘中确认', why: d.why };
|
||||||
|
// 系统自决 (2026-09-14): 方向看多但择时看空, 进观察等择时转多, 也是「在盯」不是「被否决」。
|
||||||
|
if (d.disp === 'wait_timing') return { cls: 'wait', label: '择时待转多', why: d.why };
|
||||||
return { cls: 'deny', label: '被否决', why: d.why };
|
return { cls: 'deny', label: '被否决', why: d.why };
|
||||||
}
|
}
|
||||||
return { cls: 'wait', label: '在盯', why: '' };
|
return { cls: 'wait', label: '在盯', why: '' };
|
||||||
|
|
|
||||||
|
|
@ -205,3 +205,29 @@
|
||||||
**台账线(只作提示,调阈值先回台账)。** 技术面无读数占比应低于百分之三。每天等技术面开口应在一到十五只。转空退出每周零到三条。合议链上线第一天转空离场应为零。
|
**台账线(只作提示,调阈值先回台账)。** 技术面无读数占比应低于百分之三。每天等技术面开口应在一到十五只。转空退出每周零到三条。合议链上线第一天转空离场应为零。
|
||||||
|
|
||||||
**复核日期。** 观察读数包部署后的下一个交易日先跑一次 consensus-review 看有没有读数落下来。台账 006 到 009 的第一次正式复核在合议分流上线满三个交易日,也就是 09-17。
|
**复核日期。** 观察读数包部署后的下一个交易日先跑一次 consensus-review 看有没有读数落下来。台账 006 到 009 的第一次正式复核在合议分流上线满三个交易日,也就是 09-17。
|
||||||
|
|
||||||
|
## 015 · 2026-09-14 · 系统自决:把「该不该做」交回三源合议与研判闸
|
||||||
|
|
||||||
|
**改动。** 新建仓、深亏补仓、目标价到价这三类,原来大多要人在页面上点采纳或驳回。现在系统按已有的三源合议与研判闸自己决定、自己写理由、自己记账,当天不重复评估。人只处理四类真交人的例外:研判不可用、上游标了硬风险、合议装不上、判决词认不出。
|
||||||
|
|
||||||
|
四档开关 PMS_SELF_DECIDE 留作回退:off 逐字回旧,record 只记不做(把本会怎么判落进观察读数),decline 只自动放弃、不自动采纳,full 自动采纳加自动放弃。模拟仓 155 上线设 full,真实仓 188 初始设 off。
|
||||||
|
|
||||||
|
**决定表。** 合议放行且研判通过,把握度不低于六十按仓位矩阵档位采纳,低于六十采纳试探仓一批。只有一方表态、基本面看空技术面看多、异议只在技术面、判决关注、建议试探仓、重问放行这几类,研判通过则采纳试探仓;研判驳回一律放弃并记账。深亏补仓靠研判必答题「下跌是杀逻辑还是杀情绪」,契约规定杀逻辑或说不清一律驳回,所以研判通过即杀情绪,把握度不低于六十自动补仓。目标价到价不进研判范围,直接自动清仓,落指令前按可卖量夹紧,该票挂着交易方案的先撤方案再落指令,免得清仓单与网格买入腿对倒。
|
||||||
|
|
||||||
|
**撤销两条旧决定。** 台账 001 里「重问放行一律交人」这一句撤销:自决开着时重问放行改自动试探仓(研判引起的放弃仍记评审方 judge,解锁重问对它照常生效)。台账 008 里「只有一方表态或择时有异议交人」这一句撤销:只有一方表态改自动试探仓;方向看多但择时看空不再交人,改在合议路由层回观察、等择时转多(处置词「择时待转多」)。
|
||||||
|
|
||||||
|
**一处既有口径冲突的落法。** 仓位矩阵里「基本面看多、择时看多、技术面看空」那一格,advise_v2 早就标了「观察」,但动作引擎从来没读这个字段——卡面写着「技术面看空进观察」,实际却按标准仓出了提议。自决开着时这一格落成决定种类 tech_bear,采纳试探仓;自决关着时保持今天的行为不动。
|
||||||
|
|
||||||
|
**试探仓与每日上限。** 试探仓类的自动采纳受第二个开关 PMS_SELF_DECIDE_TRIAL 控制,关掉则这几类仍交人。每日自动新建仓只数上限 PMS_SELF_DECIDE_DAILY_MAX,默认三只,超过转交人并在原因里写明,设零表示不限。
|
||||||
|
|
||||||
|
**记账与不重评。** 自动采纳先落指令再进账本,评审方按动作走 judge 或 rule,理由以「系统采纳」开头(每日上限的计数按这个前缀数)。自动放弃先进账本再进跳过集合,评审方 system,当天不再重评、明日重评。账本硬数字带一个 self_decide 块,记决定种类、档、仓位档、合议三项、研判把握度、当日累计、撤掉的方案编号。
|
||||||
|
|
||||||
|
**裁决理由改预设码。** 仍交人的那几张提议,人裁决时从两张预设码表里选一条(采纳五条、驳回七条),选「其他」再补一句。旧的自由文本理由继续接受。用到的码进账本硬数字,复盘能按码聚合。
|
||||||
|
|
||||||
|
**依据。** 采纳驳回都要人手写理由,偏离了三源合议自决的设计目标——既然合议与研判已经把「该不该做」判清楚了,剩下的机械动作不该每次都等人。off 逐字回旧由全量单测守(第三十四批与既有八百多例全绿即是逐字回旧的证据)。
|
||||||
|
|
||||||
|
**参数。** PMS_SELF_DECIDE 默认 off、读不到按 off(fail-closed)、合法值 off/record/decline/full。PMS_SELF_DECIDE_TRIAL 默认关。PMS_SELF_DECIDE_DAILY_MAX 默认三、范围零到五十。
|
||||||
|
|
||||||
|
**台账线(只作提示,调阈值先回台账)。** 研判不可用占比应低于两成,超过要查决策系统那侧。上线头几天自动采纳的股数应是试探仓那批,不是标准仓。
|
||||||
|
|
||||||
|
**复核日期。** 部署后的下一个交易日盘中先只读试算一次,看 executed 里有没有 route 为 auto、why 含「系统采纳」的条目,或 rejected 里有没有评审方 system 的条目。收盘后跑 make consensus-review 看第五张表系统自决逐条、第六张表研判不可用占比。
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""三源合议观察读数复核 (2026-09-14 观察读数包, 台账 013)。只读, 不写任何表, 随时可跑。
|
"""三源合议观察读数复核 (2026-09-14 观察读数包, 台账 013)。只读, 不写任何表, 随时可跑。
|
||||||
|
|
||||||
回答台账 006 到 011 复核要的四个读数, 打四张表:
|
回答台账 006 到 011、015 复核要的读数, 打六张表:
|
||||||
一, 逐日: 相关票数、技术面无读数占比、基本面无读数只数、择时无读数只数 (占比用 map_cover 算)。
|
一, 逐日: 相关票数、技术面无读数占比、基本面无读数只数、择时无读数只数 (占比用 map_cover 算)。
|
||||||
二, 逐日: 等开口只数、等盘中确认只数、跳过原因分布、交人只数、放行只数、增持门拦截次数。
|
二, 逐日: 等开口只数、等盘中确认只数、跳过原因分布、交人只数、放行只数、增持门拦截次数。
|
||||||
三, 转空离场逐条: 日期、代码、确认与否、数量。
|
三, 转空离场逐条: 日期、代码、确认与否、数量。
|
||||||
四, 合议方向与人工裁决比对: 带合议六键且已被人裁决的提议里, 合议看多而人驳回、
|
四, 合议方向与人工裁决比对: 带合议六键且已被人裁决的提议里, 合议看多而人驳回、
|
||||||
合议交人而人采纳的, 逐只列出。
|
合议交人而人采纳的, 逐只列出。
|
||||||
|
五, 系统自决逐条 (台账 015): 日期、代码、决定 (采纳/放弃/交人)、档位、规则、合议、研判把握度。
|
||||||
|
六, 研判不可用占比: 自决触及的候选里研判不可用的比例 (台账线低于两成)。
|
||||||
|
|
||||||
台账里的对照阈值 (只作提示, 不下结论 —— 调阈值要先回台账):
|
台账里的对照阈值 (只作提示, 不下结论 —— 调阈值要先回台账):
|
||||||
技术面无读数占比应低于 3%; 每天等技术面开口应在 1 到 15 只; 转空退出每周 0 到 3 条。
|
技术面无读数占比应低于 3%; 每天等技术面开口应在 1 到 15 只; 转空退出每周 0 到 3 条。
|
||||||
|
|
@ -185,7 +187,48 @@ def main():
|
||||||
for d, code, con in sorted(handoff_ok):
|
for d, code, con in sorted(handoff_ok):
|
||||||
print(" %-8s %-10s (合议: %s)" % (d, code, con.get("reason") or ""))
|
print(" %-8s %-10s (合议: %s)" % (d, code, con.get("reason") or ""))
|
||||||
|
|
||||||
print("\n复核口径提醒: 本报告只陈述读数, 阈值调整要先回台账 006 到 011 写一条, "
|
# ---- 五、系统自决逐条 (2026-09-14 台账 015) ----
|
||||||
|
print("\n【五】系统自决逐条 (日期 代码 决定 档位 规则 合议 研判把握度)")
|
||||||
|
try:
|
||||||
|
sd_rows = consensus_stat_repo.list_range(
|
||||||
|
a, b, kinds=[cst.K_AUTO_ACCEPT, cst.K_AUTO_DECLINE, cst.K_AUTO_CONFIRM,
|
||||||
|
cst.K_AUTO_WOULD_ACCEPT, cst.K_AUTO_WOULD_DECLINE])
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f" ✗ 自决读数读取失败: {type(e).__name__}: {e}")
|
||||||
|
sd_rows = []
|
||||||
|
_DEC_CN = {cst.K_AUTO_ACCEPT: "采纳", cst.K_AUTO_DECLINE: "放弃", cst.K_AUTO_CONFIRM: "交人",
|
||||||
|
cst.K_AUTO_WOULD_ACCEPT: "本会采纳", cst.K_AUTO_WOULD_DECLINE: "本会放弃"}
|
||||||
|
if not sd_rows:
|
||||||
|
print(" 窗口内没有系统自决记录 (自决还没开, 或没有归自决管的候选)。")
|
||||||
|
for r in sd_rows:
|
||||||
|
d = _loads(r.get("detail"))
|
||||||
|
con = "%s/%s" % (d.get("direction") or "—", d.get("route") or "—")
|
||||||
|
conf = d.get("conf")
|
||||||
|
print(" %-8s %-10s %-6s %-5s %-16s %-7s 把握度 %s (记到 %s 次)"
|
||||||
|
% (r["stat_date"], r["ts_code"], _DEC_CN.get(r["kind"], r["kind"]),
|
||||||
|
d.get("tier") or "—", d.get("rule") or "—", con,
|
||||||
|
conf if conf is not None else "—", r.get("rounds")))
|
||||||
|
|
||||||
|
# ---- 六、研判不可用占比 (2026-09-14; 台账线: 超两成报警) ----
|
||||||
|
print("\n【六】研判不可用占比 (judge_unavail / 自决触及的候选; 台账线: < 20%)")
|
||||||
|
print(" 日期 研判不可用 自决触及 占比")
|
||||||
|
for d in _dates_in(counts):
|
||||||
|
c = counts.get(d) or {}
|
||||||
|
|
||||||
|
def _n2(k):
|
||||||
|
return (c.get(k) or {}).get("rows", 0)
|
||||||
|
un = _n2(cst.K_JUDGE_UNAVAIL)
|
||||||
|
touched = sum(_n2(k) for k in (cst.K_AUTO_ACCEPT, cst.K_AUTO_DECLINE, cst.K_AUTO_CONFIRM,
|
||||||
|
cst.K_AUTO_WOULD_ACCEPT, cst.K_AUTO_WOULD_DECLINE))
|
||||||
|
ratio = (un / touched) if touched else None
|
||||||
|
flag = " ⚠ 超 20%" if (ratio is not None and ratio > 0.2) else ""
|
||||||
|
if un or touched:
|
||||||
|
print(" %-8s %8d %8d %6s%s" % (d, un, touched, _fmt_pct(ratio), flag))
|
||||||
|
if not any((counts.get(d) or {}).get(cst.K_JUDGE_UNAVAIL) or
|
||||||
|
(counts.get(d) or {}).get(cst.K_AUTO_CONFIRM) for d in _dates_in(counts)):
|
||||||
|
print(" 窗口内没有自决读数 (自决还没开)。")
|
||||||
|
|
||||||
|
print("\n复核口径提醒: 本报告只陈述读数, 阈值调整要先回台账 006 到 011、015 写一条, "
|
||||||
"不按单次复盘读数回调 (原则一)。")
|
"不按单次复盘读数回调 (原则一)。")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,16 +101,22 @@
|
||||||
test_batch33_units.py 页面收尾包·单票研究面接口 (2026-09-14): _research_st 来源解析 (持仓/候选/都没有)/
|
test_batch33_units.py 页面收尾包·单票研究面接口 (2026-09-14): _research_st 来源解析 (持仓/候选/都没有)/
|
||||||
api_research 六块齐全且来源逐路标 ok / 无读数票 company 为 None 且来源标明。
|
api_research 六块齐全且来源逐路标 ok / 无读数票 company 为 None 且来源标明。
|
||||||
前端三处 (抽屉四芯片、候选栏芯片、管理研究面列) 由两道页面守卫与真机视觉判收兜 (5 例)
|
前端三处 (抽屉四芯片、候选栏芯片、管理研究面列) 由两道页面守卫与真机视觉判收兜 (5 例)
|
||||||
|
test_batch34_units.py 系统自决包 (2026-09-14 台账 015): 合议择时异议改观察带 wait_timing/裁决理由码表
|
||||||
|
与拼句 (预设码/其他必填补充/旧自由文本兼容/码串表报错)/决策核心 _self_decide
|
||||||
|
(off 不管、放行强采标准仓、低把握转试探仓、只有一方表态与异议只在技术面按 TRIAL
|
||||||
|
开关、四类交人、逻辑存疑放弃、深亏补仓三分支、目标价直接采纳、每日上限、record/decline
|
||||||
|
降级)/mark_decide 优先级/eval_dca 深档打 decide_kind/三参数登记与范围/两个 repo 查询 (22 例)
|
||||||
test_page_enum_guard.py 页面文案守卫 (静态扫描, 不连库不起浏览器): 枚举字段不许
|
test_page_enum_guard.py 页面文案守卫 (静态扫描, 不连库不起浏览器): 枚举字段不许
|
||||||
直接印到页面上 / 判据码显示前必须剥前缀 / 不许把整个对象
|
直接印到页面上 / 判据码显示前必须剥前缀 / 不许把整个对象
|
||||||
打给交易员看 / 翻译兜底不许让英文码单独当句子 (1 例)
|
打给交易员看 / 翻译兜底不许让英文码单独当句子 (1 例)
|
||||||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) +
|
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) +
|
||||||
目标价到价必定入队 (档位 full 也不自动卖) +
|
目标价到价自决关时入队、自决开时自动清仓按可卖量夹紧 +
|
||||||
用户设的止损价与目标价单独成列显示 (70 例)
|
用户设的止损价与目标价单独成列显示 (71 例)
|
||||||
共 871 例
|
共 894 例
|
||||||
(总数按实跑逐批相加校正过两次: 曾写 649 是笔误, 实为 650; 09-03 先后加了同轮只发一条
|
(总数按实跑逐批相加校正过两次: 曾写 649 是笔误, 实为 650; 09-03 先后加了同轮只发一条
|
||||||
减持与研究理由两键各一例, 到 652; 09-04 加了仅展示跳过原因与空候选说明各一例, 到 654; 又加了页面文案守卫一例, 到 655; 09-07 审查修复加了跨轮减持等五例, 到 660; 第二件低把握驳回交人一例, 到 661; 第三件逻辑状态接入第二十二批十五例, 到 676; 第四件安全边际整句透传一例, 到 677; 参考目标价一例, 到 678; 催化事件与定价状态透传一例, 到 679; 两个期限的头一例, 现为 680; 09-10 建议档位对齐第二十六批十七例到 697; 09-11 技术面接入工作包一第二十七批二十七例到 724; 三源合议工作包二纯逻辑第二十八批三十七例到 763; 接入下单链路第二十九批二十四例到 787; 工作包三离场纪律第三十批十八例到 805; part3 盘中 SAR 止损线补九例到 814; part4 弱基本面紧止盈补六例到 820;
|
减持与研究理由两键各一例, 到 652; 09-04 加了仅展示跳过原因与空候选说明各一例, 到 654; 又加了页面文案守卫一例, 到 655; 09-07 审查修复加了跨轮减持等五例, 到 660; 第二件低把握驳回交人一例, 到 661; 第三件逻辑状态接入第二十二批十五例, 到 676; 第四件安全边际整句透传一例, 到 677; 参考目标价一例, 到 678; 催化事件与定价状态透传一例, 到 679; 两个期限的头一例, 现为 680; 09-10 建议档位对齐第二十六批十七例到 697; 09-11 技术面接入工作包一第二十七批二十七例到 724; 三源合议工作包二纯逻辑第二十八批三十七例到 763; 接入下单链路第二十九批二十四例到 787; 工作包三离场纪律第三十批十八例到 805; part3 盘中 SAR 止损线补九例到 814; part4 弱基本面紧止盈补六例到 820;
|
||||||
09-14 观察读数包第三十一批二十例到 840; 加固包给第二十八批加三例、第三十批加三例到 846; 评审修订第三十一批加一例到 847; 09-14 盘中确认包第三十二批十九例到 866; 页面收尾包第三十三批五例, 现为 871)
|
09-14 观察读数包第三十一批二十例到 840; 加固包给第二十八批加三例、第三十批加三例到 846; 评审修订第三十一批加一例到 847; 09-14 盘中确认包第三十二批十九例到 866; 页面收尾包第三十三批五例到 871;
|
||||||
|
09-14 系统自决包第三十四批二十二例、test_wiring 目标价自动清仓加一例, 现为 894)
|
||||||
任一子集失败即整体失败 (退出码 1)。
|
任一子集失败即整体失败 (退出码 1)。
|
||||||
|
|
||||||
哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):
|
哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):
|
||||||
|
|
@ -167,6 +173,7 @@ SUITES = ["test_core_units.py", "test_batch2_units.py", "test_batch3_units.py",
|
||||||
"test_batch31_units.py",
|
"test_batch31_units.py",
|
||||||
"test_batch32_units.py",
|
"test_batch32_units.py",
|
||||||
"test_batch33_units.py",
|
"test_batch33_units.py",
|
||||||
|
"test_batch34_units.py",
|
||||||
"test_page_enum_guard.py",
|
"test_page_enum_guard.py",
|
||||||
"test_page_wiring_guard.py",
|
"test_page_wiring_guard.py",
|
||||||
"test_wiring.py"]
|
"test_wiring.py"]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,275 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""系统自决包 (2026-09-14 台账 015)。后端离线纯逻辑单测, 不连库。
|
||||||
|
|
||||||
|
把「该不该做」交回三源合议与研判闸。人只处理四类例外: 研判不可用、上游硬风险、合议装不上、
|
||||||
|
判决词认不出。这里测四块:
|
||||||
|
A 合议路由的择时异议观察 (consensus.decide timing_dissent)。
|
||||||
|
B 裁决理由码表与拼句 (decide_reasons.compose)。
|
||||||
|
C 决策核心 _self_decide: 决定表逐格 (放行强/试探仓/逻辑存疑/深亏补仓/目标价/上限/四类交人)、
|
||||||
|
四档 off/record/decline/full 的降级。
|
||||||
|
D 动作引擎 scan_open 打 decide_kind (关掉不打, 逐字回旧) 与 mark_decide 优先级、参数与哨兵。
|
||||||
|
|
||||||
|
端到端的自动清仓/自动采纳落表见 test_wiring.py (目标价那两例)。
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
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.core import consensus # noqa: E402
|
||||||
|
from app.core import decide_reasons as dr # noqa: E402
|
||||||
|
from app.services import judge # noqa: E402
|
||||||
|
from app.services import param_store # noqa: E402
|
||||||
|
from app.services.proposal_service import _self_decide # noqa: E402
|
||||||
|
|
||||||
|
RESULTS = []
|
||||||
|
|
||||||
|
|
||||||
|
def case(name):
|
||||||
|
def deco(fn):
|
||||||
|
RESULTS.append((name, fn))
|
||||||
|
return fn
|
||||||
|
return deco
|
||||||
|
|
||||||
|
|
||||||
|
def _v(verdict=judge.PASS, conf=78, degraded=False):
|
||||||
|
return {"verdict": verdict, "confidence": conf, "degraded": degraded, "reason": ""}
|
||||||
|
|
||||||
|
|
||||||
|
def _cand(action=ae.A_OPEN, decide_kind=None, consensus=True, risk=None):
|
||||||
|
c = {"ts_code": "600000.SH", "action": action, "side": "buy",
|
||||||
|
"qty": 1000, "reason": "x", "hard_numbers": {}}
|
||||||
|
if action in (ae.A_EXIT, ae.A_TRIM):
|
||||||
|
c["side"] = "sell"
|
||||||
|
if decide_kind:
|
||||||
|
c["decide_kind"] = decide_kind
|
||||||
|
if consensus:
|
||||||
|
c["consensus"] = {"direction": "看多", "route": "放行", "strength": "强"}
|
||||||
|
if risk:
|
||||||
|
c["hard_numbers"]["risk"] = risk
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================ A 合议择时异议
|
||||||
|
@case("A 合议·方向看多但择时看空: 默认交人 (逐字回旧)")
|
||||||
|
def _():
|
||||||
|
d = consensus.decide("看多", "看多", "看空")
|
||||||
|
assert d["route"] == "交人" and "择时" in d["route_reason"], d
|
||||||
|
assert "disp" not in d, d
|
||||||
|
|
||||||
|
|
||||||
|
@case("A 合议·自决开 (timing_dissent=observe): 改观察, 等择时转多, disp=wait_timing")
|
||||||
|
def _():
|
||||||
|
d = consensus.decide("看多", "看多", "看空", timing_dissent="observe")
|
||||||
|
assert d["route"] == "观察" and d["route_reason"] == "等择时转多", d
|
||||||
|
assert d.get("disp") == "wait_timing", d
|
||||||
|
# 择时不看空时 observe 不改别的格 (三方看多仍放行)
|
||||||
|
assert consensus.decide("看多", "看多", "看多", timing_dissent="observe")["route"] == "放行"
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================ B 裁决理由拼句
|
||||||
|
@case("B decide_reasons·采纳预设码 → 句子与码")
|
||||||
|
def _():
|
||||||
|
s, code = dr.compose("ACCEPTED", "ACCEPT_CONSENSUS", None, None)
|
||||||
|
assert code == "ACCEPT_CONSENSUS" and "合议放行" in s, (s, code)
|
||||||
|
|
||||||
|
|
||||||
|
@case("B decide_reasons·选其他不填补充 → 报错; 填了 → 带补充")
|
||||||
|
def _():
|
||||||
|
try:
|
||||||
|
dr.compose("DECLINED", "DECLINE_OTHER", "", None)
|
||||||
|
assert False, "空补充应报错"
|
||||||
|
except ValueError as e:
|
||||||
|
assert "其他" in str(e) or "补充" in str(e), e
|
||||||
|
s, code = dr.compose("DECLINED", "DECLINE_OTHER", "估值太贵", None)
|
||||||
|
assert code == "DECLINE_OTHER" and "估值太贵" in s, (s, code)
|
||||||
|
|
||||||
|
|
||||||
|
@case("B decide_reasons·旧自由文本兼容 / 全空报错 / 码串表报错")
|
||||||
|
def _():
|
||||||
|
s, code = dr.compose("ACCEPTED", None, None, "我自己判断成立")
|
||||||
|
assert code is None and s == "我自己判断成立", (s, code)
|
||||||
|
try:
|
||||||
|
dr.compose("ACCEPTED", None, None, None)
|
||||||
|
assert False
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
dr.compose("ACCEPTED", "DECLINE_TECH_WEAK", None, None) # 采纳用了驳回码
|
||||||
|
assert False
|
||||||
|
except ValueError as e:
|
||||||
|
assert "不适用" in str(e), e
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================ C 决策核心
|
||||||
|
@case("C 不变式·off 一律不归自决管 (decision none)")
|
||||||
|
def _():
|
||||||
|
d = _self_decide(_cand(), _v(), {"self_decide": "off"}, 0)
|
||||||
|
assert d["decision"] == "none", d
|
||||||
|
|
||||||
|
|
||||||
|
@case("C 放行强·PASS 把握度 78, full → 采纳标准仓")
|
||||||
|
def _():
|
||||||
|
d = _self_decide(_cand(), _v(conf=78), {"self_decide": "full"}, 0)
|
||||||
|
assert d["decision"] == "accept" and d["tier"] == "standard", d
|
||||||
|
assert d["rule"] == "consensus_strong", d
|
||||||
|
assert d["why"].startswith("系统采纳"), d
|
||||||
|
|
||||||
|
|
||||||
|
@case("C 放行强·PASS 把握度 40: TRIAL 关转交人 / 开则采纳试探仓")
|
||||||
|
def _():
|
||||||
|
off = _self_decide(_cand(), _v(conf=40), {"self_decide": "full", "self_decide_trial": False}, 0)
|
||||||
|
assert off["decision"] == "confirm", off
|
||||||
|
on = _self_decide(_cand(), _v(conf=40), {"self_decide": "full", "self_decide_trial": True}, 0)
|
||||||
|
assert on["decision"] == "accept" and on["tier"] == "trial", on
|
||||||
|
|
||||||
|
|
||||||
|
@case("C 只有一方表态 (weak_consensus): TRIAL 开采纳试探 / 关交人")
|
||||||
|
def _():
|
||||||
|
c = _cand(decide_kind=ae.DK_WEAK_CONSENSUS)
|
||||||
|
on = _self_decide(c, _v(conf=78), {"self_decide": "full", "self_decide_trial": True}, 0)
|
||||||
|
assert on["decision"] == "accept" and on["tier"] == "trial", on
|
||||||
|
off = _self_decide(c, _v(conf=78), {"self_decide": "full", "self_decide_trial": False}, 0)
|
||||||
|
assert off["decision"] == "confirm", off
|
||||||
|
|
||||||
|
|
||||||
|
@case("C 异议只在技术面 (tech_bear): 采纳试探仓")
|
||||||
|
def _():
|
||||||
|
d = _self_decide(_cand(decide_kind=ae.DK_TECH_BEAR), _v(conf=78),
|
||||||
|
{"self_decide": "full", "self_decide_trial": True}, 0)
|
||||||
|
assert d["decision"] == "accept" and d["tier"] == "trial", d
|
||||||
|
|
||||||
|
|
||||||
|
@case("C 四类交人·研判不可用 / 硬风险 / 合议装不上 / 判决认不出 → confirm")
|
||||||
|
def _():
|
||||||
|
fp = {"self_decide": "full", "self_decide_trial": True}
|
||||||
|
assert _self_decide(_cand(), _v(degraded=True), fp, 0)["decision"] == "confirm"
|
||||||
|
assert _self_decide(_cand(risk=["高位派发"]), _v(), fp, 0)["decision"] == "confirm"
|
||||||
|
assert _self_decide(_cand(consensus=False), _v(), fp, 0)["decision"] == "confirm"
|
||||||
|
assert _self_decide(_cand(decide_kind=ae.DK_VERDICT_UNKNOWN), _v(), fp, 0)["decision"] == "confirm"
|
||||||
|
|
||||||
|
|
||||||
|
@case("C 研判不可用带 judge_unavail 标记 (研判不可用占比复核用)")
|
||||||
|
def _():
|
||||||
|
d = _self_decide(_cand(), _v(degraded=True), {"self_decide": "full"}, 0)
|
||||||
|
assert d["why"].startswith("研判不可用"), d
|
||||||
|
|
||||||
|
|
||||||
|
@case("C 逻辑存疑 (logic_doubt): 自动放弃 decline")
|
||||||
|
def _():
|
||||||
|
d = _self_decide(_cand(decide_kind=ae.DK_LOGIC_DOUBT), _v(), {"self_decide": "full"}, 0)
|
||||||
|
assert d["decision"] == "decline", d
|
||||||
|
|
||||||
|
|
||||||
|
@case("C 深亏补仓: PASS≥60 自动补 / PASS<60 放弃 / 不可用交人")
|
||||||
|
def _():
|
||||||
|
dca = _cand(action=ae.A_DCA, decide_kind=ae.DK_DCA_DEEP, consensus=False)
|
||||||
|
dca["side"] = "buy"
|
||||||
|
fp = {"self_decide": "full"}
|
||||||
|
assert _self_decide(dca, _v(conf=70), fp, 0)["decision"] == "accept"
|
||||||
|
assert _self_decide(dca, _v(conf=40), fp, 0)["decision"] == "decline"
|
||||||
|
assert _self_decide(dca, _v(degraded=True), fp, 0)["decision"] == "confirm"
|
||||||
|
|
||||||
|
|
||||||
|
@case("C 目标价到价: EXIT 直接采纳 (不进研判范围)")
|
||||||
|
def _():
|
||||||
|
ex = _cand(action=ae.A_EXIT, decide_kind=ae.DK_TARGET_PRICE, consensus=False)
|
||||||
|
d = _self_decide(ex, _v(verdict=judge.PASS, conf=None), {"self_decide": "full"}, 0)
|
||||||
|
assert d["decision"] == "accept" and d["rule"] == ae.DK_TARGET_PRICE, d
|
||||||
|
|
||||||
|
|
||||||
|
@case("C 每日上限: 达上限 3 时第四只转交人")
|
||||||
|
def _():
|
||||||
|
fp = {"self_decide": "full", "self_decide_daily_max": 3}
|
||||||
|
assert _self_decide(_cand(), _v(conf=78), fp, 2)["decision"] == "accept" # 第三只
|
||||||
|
over = _self_decide(_cand(), _v(conf=78), fp, 3) # 第四只
|
||||||
|
assert over["decision"] == "confirm" and "上限" in over["why"], over
|
||||||
|
|
||||||
|
|
||||||
|
@case("C 档位降级·record 只记不做 (would 留原判); decline 只放弃不采纳")
|
||||||
|
def _():
|
||||||
|
# record: 本会采纳但 decision 降为 none, would 留 accept
|
||||||
|
rec = _self_decide(_cand(), _v(conf=78), {"self_decide": "record"}, 0)
|
||||||
|
assert rec["decision"] == "none" and rec["would"] == "accept", rec
|
||||||
|
# decline: 采纳降 none, 放弃仍放弃
|
||||||
|
dec_acc = _self_decide(_cand(), _v(conf=78), {"self_decide": "decline"}, 0)
|
||||||
|
assert dec_acc["decision"] == "none" and dec_acc["would"] == "accept", dec_acc
|
||||||
|
dec_dec = _self_decide(_cand(decide_kind=ae.DK_LOGIC_DOUBT), _v(),
|
||||||
|
{"self_decide": "decline"}, 0)
|
||||||
|
assert dec_dec["decision"] == "decline", dec_dec
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================ D 动作引擎与参数
|
||||||
|
@case("D mark_decide·按优先级取最保守: 认不出 < 逻辑存疑 < 关注 < 试探仓")
|
||||||
|
def _():
|
||||||
|
c = {}
|
||||||
|
ae.mark_decide(c, ae.DK_ADVICE_TRIAL)
|
||||||
|
assert c["decide_kind"] == ae.DK_ADVICE_TRIAL
|
||||||
|
ae.mark_decide(c, ae.DK_VERDICT_WATCH) # 更高优先, 覆盖
|
||||||
|
assert c["decide_kind"] == ae.DK_VERDICT_WATCH
|
||||||
|
ae.mark_decide(c, ae.DK_LOGIC_DOUBT) # 再高, 覆盖
|
||||||
|
assert c["decide_kind"] == ae.DK_LOGIC_DOUBT
|
||||||
|
ae.mark_decide(c, ae.DK_ADVICE_TRIAL) # 更低, 不覆盖
|
||||||
|
assert c["decide_kind"] == ae.DK_LOGIC_DOUBT
|
||||||
|
|
||||||
|
|
||||||
|
@case("D self_decide_on: off/空 → False; record/decline/full → True")
|
||||||
|
def _():
|
||||||
|
assert ae.self_decide_on({"self_decide": "off"}) is False
|
||||||
|
assert ae.self_decide_on({}) is False
|
||||||
|
assert ae.self_decide_on({"self_decide": "record"}) is True
|
||||||
|
assert ae.self_decide_on({"self_decide": "full"}) is True
|
||||||
|
|
||||||
|
|
||||||
|
@case("D eval_dca 深档·自决关不打 decide_kind、confirm 真; 自决开打 dca_deep")
|
||||||
|
def _():
|
||||||
|
p = {"ts_code": "688001.SH", "cushion_pct": -0.20, "dca_qty": 0, "dca_count": 0,
|
||||||
|
"price": 10.0, "base_qty": 2000}
|
||||||
|
pr = {"dca_triggers": (-0.08, -0.15), "dca_deep_confirm": -0.15, "dca_max_ratio": 0.5}
|
||||||
|
off = ae.eval_dca(p, pr, {})
|
||||||
|
assert off and off["needs_user_confirm"] is True and "decide_kind" not in off, off
|
||||||
|
on = ae.eval_dca(p, {**pr, "self_decide": "full"}, {})
|
||||||
|
assert on and on.get("decide_kind") == ae.DK_DCA_DEEP, on
|
||||||
|
|
||||||
|
|
||||||
|
@case("D 参数登记·三键在 RUNTIME_EXTRA, 合法值与范围校验")
|
||||||
|
def _():
|
||||||
|
for k in ("PMS_SELF_DECIDE", "PMS_SELF_DECIDE_TRIAL", "PMS_SELF_DECIDE_DAILY_MAX"):
|
||||||
|
assert k in param_store.RUNTIME_EXTRA, k
|
||||||
|
assert param_store.RUNTIME_EXTRA["PMS_SELF_DECIDE"][0] == "off" # 代码默认 off
|
||||||
|
assert param_store.FAIL_CLOSED.get("PMS_SELF_DECIDE") == "off" # 读不到按 off
|
||||||
|
assert param_store._range_check("PMS_SELF_DECIDE", "full") is None
|
||||||
|
assert param_store._range_check("PMS_SELF_DECIDE", "wtf") is not None # 非法值被拒
|
||||||
|
assert param_store._range_check("PMS_SELF_DECIDE_DAILY_MAX", 3) is None
|
||||||
|
assert param_store._range_check("PMS_SELF_DECIDE_DAILY_MAX", 99) is not None
|
||||||
|
|
||||||
|
|
||||||
|
@case("D repo 两个自决查询函数存在且单表")
|
||||||
|
def _():
|
||||||
|
from app.repo import pms_repo
|
||||||
|
assert callable(pms_repo.system_decided_today) and callable(pms_repo.system_pass_count_today)
|
||||||
|
assert pms_repo.SYSTEM_ACCEPT_REASON_PREFIX == "系统采纳"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ok = 0
|
||||||
|
for name, fn in RESULTS:
|
||||||
|
try:
|
||||||
|
fn()
|
||||||
|
ok += 1
|
||||||
|
print(" ok " + name)
|
||||||
|
except Exception:
|
||||||
|
import traceback
|
||||||
|
print(" FAIL " + name)
|
||||||
|
traceback.print_exc()
|
||||||
|
print("-" * 60)
|
||||||
|
if ok == len(RESULTS):
|
||||||
|
print("ALL PASS (%d cases)" % ok)
|
||||||
|
return 0
|
||||||
|
print("FAILED %d/%d" % (len(RESULTS) - ok, len(RESULTS)))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
@ -153,6 +153,13 @@ def _():
|
||||||
assert c["source"] == ae.SRC_TARGET_PRICE, c
|
assert c["source"] == ae.SRC_TARGET_PRICE, c
|
||||||
assert c["confirm_why"] == ae.WHY_TARGET_PRICE_CONFIRM, c
|
assert c["confirm_why"] == ae.WHY_TARGET_PRICE_CONFIRM, c
|
||||||
assert c["hard_numbers"]["target_price"] == 12.5, c["hard_numbers"]
|
assert c["hard_numbers"]["target_price"] == 12.5, c["hard_numbers"]
|
||||||
|
assert c.get("decide_kind") is None, c # 自决关: 不打 decide_kind (逐字回旧)
|
||||||
|
# 系统自决开着时 (2026-09-14 台账 015): 候选多打 decide_kind=target_price, confirm 与来源不变;
|
||||||
|
# 是否真自动清仓由 _self_decide 在分流层决定 (自决关时必定入队, 见 test_wiring 那两例)。
|
||||||
|
c2 = ae.eval_target(pos(price=13.0, target_price=12.5, total_qty=6050, avail_qty=6050,
|
||||||
|
cushion_pct=0.30), {**PARAMS, "self_decide": "full"})
|
||||||
|
assert c2.get("decide_kind") == ae.DK_TARGET_PRICE, c2
|
||||||
|
assert c2["needs_user_confirm"] is True and c2["source"] == ae.SRC_TARGET_PRICE, c2
|
||||||
# 恰好等于目标价也算到价 (不设缓冲、不做平滑 —— 那个数是用户自己填的)
|
# 恰好等于目标价也算到价 (不设缓冲、不做平滑 —— 那个数是用户自己填的)
|
||||||
assert ae.eval_target(pos(price=12.5, target_price=12.5), PARAMS) is not None
|
assert ae.eval_target(pos(price=12.5, target_price=12.5), PARAMS) is not None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -406,6 +406,17 @@ class FakeRepo:
|
||||||
return {s["ts_code"] for s in self.strategies.values()
|
return {s["ts_code"] for s in self.strategies.values()
|
||||||
if s["status"] == "ACTIVE" and s.get("ts_code")}
|
if s["status"] == "ACTIVE" and s.get("ts_code")}
|
||||||
|
|
||||||
|
def system_decided_today(self, since):
|
||||||
|
# 系统自决放弃过的 (代码, 动作): 账本里评审方 system 的 REJECT (2026-09-14 台账 015)
|
||||||
|
return {(r["ts_code"], r["action"]) for r in self.ledger
|
||||||
|
if r.get("verdict") == "REJECT" and r.get("arbiter") == "system"}
|
||||||
|
|
||||||
|
def system_pass_count_today(self, since):
|
||||||
|
# 今天系统自决自动采纳的新建仓只数: 账本里 OPEN 的 PASS 且 reason 以「系统采纳」开头
|
||||||
|
return sum(1 for r in self.ledger
|
||||||
|
if r.get("action") == "OPEN" and r.get("verdict") == "PASS"
|
||||||
|
and str(r.get("reason") or "").startswith("系统采纳"))
|
||||||
|
|
||||||
def insert_strategy(self, *, strategy_id, ts_code, stype, autonomy="auto", params=None,
|
def insert_strategy(self, *, strategy_id, ts_code, stype, autonomy="auto", params=None,
|
||||||
state=None, status="ACTIVE", note=None):
|
state=None, status="ACTIVE", note=None):
|
||||||
if strategy_id in self.strategies: # 真表 strategy_id 唯一键
|
if strategy_id in self.strategies: # 真表 strategy_id 唯一键
|
||||||
|
|
@ -643,7 +654,9 @@ def _():
|
||||||
"/api/commands/catalog", "/api/commands/active", "/api/commands/{command_id}",
|
"/api/commands/catalog", "/api/commands/active", "/api/commands/{command_id}",
|
||||||
"/api/commands/{command_id}/cancel", "/api/plans", "/api/positions",
|
"/api/commands/{command_id}/cancel", "/api/plans", "/api/positions",
|
||||||
"/api/positions/{ts_code}/lots", "/api/instructions", "/api/ledger", "/api/report",
|
"/api/positions/{ts_code}/lots", "/api/instructions", "/api/ledger", "/api/report",
|
||||||
"/api/proposals", "/api/proposals/{proposal_id}/decide", "/api/ops/replay",
|
"/api/proposals", "/api/proposals/{proposal_id}/decide",
|
||||||
|
"/api/proposals/decide-reasons", # 2026-09-14 系统自决包: 裁决预设理由码表
|
||||||
|
"/api/ops/replay",
|
||||||
"/api/ops/reconcile", "/api/ops/daily-settle", "/api/ops/report",
|
"/api/ops/reconcile", "/api/ops/daily-settle", "/api/ops/report",
|
||||||
"/api/ops/downstream-schema", "/api/industry",
|
"/api/ops/downstream-schema", "/api/industry",
|
||||||
"/api/upstream/plan", "/api/ops/plan-refresh",
|
"/api/upstream/plan", "/api/ops/plan-refresh",
|
||||||
|
|
@ -1741,10 +1754,10 @@ def _target_fakes(price, target, **kw):
|
||||||
return fake, restore
|
return fake, restore
|
||||||
|
|
||||||
|
|
||||||
@case("自主提议·目标价到价: 必定入队等人拍板, 档位 full 也绝不自动卖")
|
@case("自主提议·目标价到价: 系统自决关时必定入队 (档位 full 也不自动卖)")
|
||||||
def _():
|
def _():
|
||||||
from app.services import proposal_service as ps
|
from app.services import proposal_service as ps
|
||||||
# 档位 full = 「闸门与研判通过即执行」, 而目标价这条必须无视档位入队
|
# 档位 full = 「闸门与研判通过即执行」, 而目标价这条 (自决关) 必须无视档位入队
|
||||||
fake, restore = _target_fakes(13.0, 12.5, params={"PMS_AUTONOMY": "full"})
|
fake, restore = _target_fakes(13.0, 12.5, params={"PMS_AUTONOMY": "full"})
|
||||||
try:
|
try:
|
||||||
r = ps.scan_and_route()
|
r = ps.scan_and_route()
|
||||||
|
|
@ -1777,6 +1790,35 @@ def _():
|
||||||
assert any("在途提议" in (s.get("why") or "") for s in r2["skipped"]), r2["skipped"]
|
assert any("在途提议" in (s.get("why") or "") for s in r2["skipped"]), r2["skipped"]
|
||||||
|
|
||||||
|
|
||||||
|
@case("自主提议·目标价到价: 系统自决开 (full) 时自动清仓, 按可卖量夹紧")
|
||||||
|
def _():
|
||||||
|
from app.services import proposal_service as ps
|
||||||
|
# 自决 full: 目标价到价不再入队, 直接落 EXIT 指令 (卖出方向); 按可卖量夹紧 (avail<total)
|
||||||
|
fake = install_fakes(
|
||||||
|
prices={"600000.SH": 13.0}, high5={"600000.SH": 19.5},
|
||||||
|
params={"PMS_TOTAL_SCALE": "2000000", "PMS_AUTONOMY": "full",
|
||||||
|
"PMS_SELF_DECIDE": "full", "PMS_SELF_DECIDE_TRIAL": "1"},
|
||||||
|
positions=[{"ts_code": "600000.SH", "total_qty": 6050, "avail_qty": 3000,
|
||||||
|
"base_qty": 6000, "avg_cost": 10.0, "cushion_peak": 0.0,
|
||||||
|
"target_pct": 0.06}])
|
||||||
|
from app.services import command_service as csvc
|
||||||
|
restore = _stub_sp(csvc, {"600000.SH": {"target_price": 12.5}})
|
||||||
|
try:
|
||||||
|
r = ps.scan_and_route()
|
||||||
|
finally:
|
||||||
|
restore()
|
||||||
|
assert r["ok"], r
|
||||||
|
# 自动清仓: 落进 executed 而不是 queued, 生成 EXIT 指令
|
||||||
|
assert any(x["action"] == "EXIT" for x in r["executed"]), r["executed"]
|
||||||
|
assert not any(x["action"] == "EXIT" for x in r["queued"]), r["queued"]
|
||||||
|
exits = [i for i in fake.instructions.values() if i["action"] == "EXIT"]
|
||||||
|
assert exits, fake.instructions
|
||||||
|
assert exits[0]["qty"] == 3000, exits[0] # 按可卖量 3000 夹紧, 不是总持仓 6050
|
||||||
|
# 账本一行 system 自动清仓 (理由含目标价)
|
||||||
|
led = [x for x in fake.ledger if x["action"] == "EXIT" and x["verdict"] == "PASS"]
|
||||||
|
assert led and "目标价" in (led[0]["reason"] or ""), led
|
||||||
|
|
||||||
|
|
||||||
@case("自主提议·目标价未到 / 没设 一律不产出 (拿不到价绝不折成到价)")
|
@case("自主提议·目标价未到 / 没设 一律不产出 (拿不到价绝不折成到价)")
|
||||||
def _():
|
def _():
|
||||||
from app.services import proposal_service as ps
|
from app.services import proposal_service as ps
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue