按评审修订系统自决包:清仓停买入侧三件、弱表态按强弱、真通过要求、逻辑存疑不送研判

到价自动清仓前改成与命令清仓同样的三件:撤方案、驳回该票待确认买入提议、撤在途买入指令
(原来只撤方案,已下发的网格买单会与清仓单对倒),且不再只在有活动方案时才做。
只有一方表态改按合议强弱打标记,弱表态开关关掉时不再按标准仓采纳。
自动采纳只认带应答体的真通过,动作不在研判范围的 PASS 一律交人。
decline/full 档逻辑存疑的新建仓不送研判直接放弃。到价清仓理由保留现价目标价股数。
页面加「系统已采纳」「系统已放弃」两种处置词。第三十四批加两例、装配自检加一例,共 900 例。
台账 015 补记,上午规格书附录己补真实仓三条。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
zlt 2026-09-14 16:40:22 +08:00
parent 998ebb4ea8
commit 980235cb25
9 changed files with 225 additions and 32 deletions

View File

@ -1095,7 +1095,10 @@ def scan_open(*, candidates: list, params: dict, caps: dict, room_amt: float,
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") == "交人":
# 合议交人的两种, 以及**强弱为弱的一律按只有一方表态** (09-14 评审补): 弱表态开关
# PMS_CONSENSUS_WEAK_CONFIRM 关掉时路由是放行而不是交人, 不看强弱就会按标准仓采纳 ——
# 决定表里没有「放行·弱」这一格, 弱就是试探仓。
if con and (con.get("route") == "交人" or con.get("strength") == ""):
rr = con.get("route_reason") or ""
mark_decide(cand, DK_FUND_BEAR_TECH_BULL if "基本面看空" in rr else DK_WEAK_CONSENSUS)
out.append(cand)

View File

@ -705,7 +705,8 @@ def disposition_snapshot(now=None) -> dict:
for _k in _rejected_today_keys():
_w = _rr.get(_k[0])
skip[_k] = ("未通过合规检查:" + _w) if _w else "今天没通过合规检查,明天开盘会重新评估"
for _k in _system_declined_today_keys(): # 系统自决今日已放弃 (明日重评)
_sd_keys = _system_declined_today_keys() # 系统自决今日已放弃 (明日重评)
for _k in _sd_keys:
skip[_k] = "今日系统已判放弃,明日重评"
for _k in _inflight_keys(): # 在途; 前端多半已按指令/提议标成「已下单/待你确认」
skip[_k] = "系统正在处理这只票(见『待确认提议』或『当日指令』)"
@ -727,6 +728,10 @@ def disposition_snapshot(now=None) -> dict:
# 合议判观察的候选带 disp=wait_tech/wait_confirm, 其余不产出的按 deny。
res["by_code"].setdefault(
code, {"disp": s.get("disp") or "deny", "why": s.get("why") or "未产出候选"})
# 系统自决今日放弃的候选打处置词 sys_decline (09-14 评审补): 页面显示「系统已放弃」而不是「未建仓」。
for (_code, _act) in _sd_keys:
if _act == ae.A_OPEN and _code in res["by_code"]:
res["by_code"][_code]["disp"] = "sys_decline"
# 候选栏三小块 (2026-09-14 页面收尾包): 每只候选的合议摘要, 从 consensus_seen 来。
# 基本面立场与质地档 / 技术面立场与相位 / 合议方向与路由。挂到对应代码的 by_code 行上。
for rec in (sink.get("consensus_seen") or []):
@ -787,7 +792,14 @@ def _route_one(c, view, params, stock_params, brake_active, now, dry_run, out,
# ---- 二级: 研判闸 (补足/加仓/补仓/新建仓; 减持不送研判) ----
verdict = {"verdict": judge.PASS, "reason": "", "degraded": False}
if c.get("judge_required"):
# 系统自决 decline/full 档: 逻辑存疑的新建仓不送研判 (09-14 评审补) —— 自决必然放弃它, 送研判
# 只是白占一次调用与本轮时间预算。record 档不跳: 那一档要照旧进人工队列, 提议上得有真研判结论。
_sd_mode = str((params or {}).get("self_decide") or "off").strip()
if (is_open and c.get("decide_kind") == ae.DK_LOGIC_DOUBT
and _sd_mode in ("decline", "full")):
verdict = {"verdict": "SKIPPED", "reason": "逻辑存疑,不送研判(系统自决直接放弃)",
"degraded": False, "raw": None, "confidence": None}
elif c.get("judge_required"):
if not _judge_budget_left(deadline):
# 本轮研判时间用尽 —— 整条跳过, 下一跳重来。
# **不当成「研判不可用」入人工队列**: 那会在自动档位下凭空造出一个人工确认队列,
@ -955,7 +967,7 @@ def _route_one(c, view, params, stock_params, brake_active, now, dry_run, out,
if auto_exec:
sd_accept = sd_decision == "accept"
strat_cancelled = []
strat_cancelled, buys_cancelled = [], []
if sd_accept and side == "sell":
# 目标价自动清仓两道前置 (2026-09-14): 按当前可卖量夹紧; 该票挂着方案先撤,
# 免得清仓单与网格买入腿对倒 (附录乙那处冲突)。可卖量为零则不落, 明日重评。
@ -966,25 +978,34 @@ def _route_one(c, view, params, stock_params, brake_active, now, dry_run, out,
"why": "系统到价自动清仓: 当前可卖量为零, 本轮不清 (明日重评)"})
return
c["qty"] = clamped
# 停买入侧三件 (09-14 评审补): 撤方案、驳回买入提议、撤在途买单 —— 不只在有活动方案时,
# 一张在途的补仓/网格买单同样会与清仓单对倒。撤不成只记日志, 清仓照落。
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 []
r = strategy_service.cancel_by_code(
code, reason="系统到价自动清仓: 先撤该票交易方案与在途买单, 免得清仓单与买入腿对倒")
strat_cancelled = r.get("cancelled") or []
buys_cancelled = r.get("instructions") or []
if r.get("errors"):
logger.warning("[自决清仓] 停买入侧有未成项 %s: %s", code, r["errors"])
except Exception as e: # noqa: BLE001
logger.warning("[自决清仓] 撤方案失败 %s (清仓照落): %s", code, e)
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"] # 指令与账本理由一致
# 指令与账本理由一致。到价清仓把原句 (现价/目标价/股数) 接在后面, 复盘看得见到的是什么价 (09-14 评审补)。
_why = sd.get("why") or c["reason"]
if sd.get("rule") == ae.DK_TARGET_PRICE and c.get("reason"):
_why = f"{_why}{c['reason']}"
c["reason"] = _why
iid = _make_instruction(c, price, now)
if sd_accept:
reason = sd.get("why") or c["reason"]
reason = c["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)}
"self_decide": _self_decide_hard(c, verdict, sd, params, strat_cancelled, _dc,
buys_cancelled=buys_cancelled)}
else:
reason = verdict.get("reason") or c["reason"]
if auto_why:
@ -1228,6 +1249,12 @@ def _self_decide(c, verdict, params, today_auto_count) -> dict:
# 三, 逻辑存疑: 自动放弃, 评审方 system
if dk == ae.DK_LOGIC_DOUBT:
return _mode({"decision": "decline", "why": "研究证据走弱(逻辑存疑),自动放弃"})
# 四, 真通过 (规格 §3.5): 只认带应答体的 PASS。动作不在研判范围时研判直接回 PASS 且没有应答体,
# 那不是决策系统的结论, 不能据此自动入场 —— 交人 (09-14 评审补; 把 OPEN 从研判范围里拿掉时
# 全部新建仓都会落到这里, 方向是保守)。
if not isinstance(verdict.get("raw"), dict):
return _mode({"decision": "confirm",
"why": "研判没有真正应答(动作不在研判范围或无应答体),交人裁决"})
# 深亏补仓: 研判必答「杀逻辑还是杀情绪」, 契约规定杀逻辑或说不清一律 REJECT (REJECT 已在前面
# 早退当放弃)。所以走到这里的 PASS 即杀情绪: 把握度不低于 60 自动补, 否则放弃。
if is_dca_deep:
@ -1263,8 +1290,10 @@ def _sd_consensus_brief(c) -> dict:
"route": con.get("route")}
def _self_decide_hard(c, verdict, sd, params, strat_cancelled, daily_count) -> dict:
"""账本硬数字的 self_decide 块 (数据契约 §3.8): 决定种类、档、仓位档、合议三项、研判、当日计数、撤掉的方案。"""
def _self_decide_hard(c, verdict, sd, params, strat_cancelled, daily_count, *,
buys_cancelled=None) -> 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"),
@ -1272,7 +1301,8 @@ def _self_decide_hard(c, verdict, sd, params, strat_cancelled, daily_count) -> d
"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 [])}
"strategy_cancelled": list(strat_cancelled or []),
"buys_cancelled": list(buys_cancelled or [])}
def _sd_decline_ledger(code, action, c, verdict, price, sd) -> None:

View File

@ -138,19 +138,26 @@ def set_status(strategy_id: str, status: str, by: str = "user") -> dict:
def cancel_by_code(ts_code: str, reason: str = "") -> dict:
"""撤下某只票当前 ACTIVE 的全部交易方案 (2026-09-14 系统自决包)。
"""停掉某只票的买入侧 (2026-09-14 系统自决包; 同日评审补齐): 与命令清仓那条
command_service._stop_buyside_for_exit 同样的三件事 撤该票 ACTIVE/PAUSED 的全部交易方案
驳回该票待确认的买入提议撤该票当前在途的买入指令
系统自决到价自动清仓前调它: 该票若挂着网格, 网格的买入腿会与清仓单对倒 (附录乙那处冲突),
所以落清仓指令前先撤方案与命令服务清仓覆盖冲突那段 (command_service) 同一手法, 抽到这里
供两处复用返回撤掉的方案编号列表; 读库或撤下失败只记进 errors, 不抛 撤不成也不该拦住清仓
系统自决到价自动清仓前调它: 该票若挂着网格, 网格的买入腿会与清仓单对倒 (规格附录乙那处冲突)
只改方案状态不够 已经下发到券商的那张网格买单还挂着继续成交, 命令清仓那条早就为此补过
撤在途买单这一层 (2026-09-08), 这里照做三步各自幂等失败只记进 errors 不抛
撤不成也不该拦住清仓 (清仓单落表后由执行器照卖; 下游没撤成的买单单独列出来别吞)
返回 {ok, ts_code, cancelled: 方案编号, proposals: 驳回的提议, instructions: 撤掉的在途买单, errors}
"""
out = {"ok": True, "ts_code": ts_code, "cancelled": [], "errors": []}
out = {"ok": True, "ts_code": ts_code, "cancelled": [], "proposals": [], "instructions": [],
"errors": []}
why = reason or "系统到价自动清仓: 先撤该票交易方案与在途买单, 免得清仓单与买入腿对倒"
# 一, 撤方案 (ACTIVE 与 PAUSED 都撤: 暂停的网格恢复后照样买)
try:
actives = pms_repo.list_strategies(ts_code=ts_code, statuses=["ACTIVE"], limit=20)
actives = pms_repo.list_strategies(ts_code=ts_code, statuses=["ACTIVE", "PAUSED"], limit=20)
except Exception as e: # noqa: BLE001
actives = []
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 {}
@ -160,8 +167,7 @@ def cancel_by_code(ts_code: str, reason: str = "") -> dict:
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])
hard_numbers={"strategy_id": s["strategy_id"]}, reason=why[:500])
except Exception as e: # noqa: BLE001
logger.warning("[自决清仓] 撤方案留痕失败 %s: %s", ts_code, e)
else:
@ -170,6 +176,46 @@ def cancel_by_code(ts_code: str, reason: str = "") -> dict:
except Exception as e: # noqa: BLE001
out["ok"] = False
out["errors"].append(f"{s.get('strategy_id')} 撤下失败: {type(e).__name__}: {e}")
# 二, 驳回该票待确认的买入提议 (清仓了还等人点头买, 是自相矛盾)
from app.services import command_service, executor
try:
props = [p for p in pms_repo.list_proposals(statuses=("WAIT_USER",), limit=500)
if p.get("ts_code") == ts_code and p.get("action") in command_service.BUILD_ACTIONS]
except Exception as e: # noqa: BLE001
props = []
out["ok"] = False
out["errors"].append(f"读提议失败: {type(e).__name__}: {e}")
for p in props:
try:
if pms_repo.decide_proposal(p["proposal_id"], "DECLINED"):
out["proposals"].append(p["proposal_id"])
except Exception as e: # noqa: BLE001
out["ok"] = False
out["errors"].append(f"驳回买入提议 {p.get('proposal_id')} 失败: {type(e).__name__}: {e}")
# 三, 撤该票当前在途的买入指令 (已下发到券商的网格/补仓买单, 撤了方案它还挂着)
try:
live_buys = [i for i in pms_repo.list_instructions(
statuses=list(command_service.LIVE_INSTR), side="buy", limit=500)
if i.get("ts_code") == ts_code]
except Exception as e: # noqa: BLE001
live_buys = []
out["ok"] = False
out["errors"].append(f"读在途买单失败: {type(e).__name__}: {e}")
for i in live_buys:
iid = i.get("instruction_id")
try:
r = executor.cancel_instruction(iid, reason=why) or {}
if r.get("ok"):
out["instructions"].append(iid)
else:
out["ok"] = False
out["errors"].append(f"撤在途买单 {iid} 未成: {r.get('error') or r.get('message')}")
except Exception as e: # noqa: BLE001
out["ok"] = False
out["errors"].append(f"撤在途买单 {iid} 失败: {type(e).__name__}: {e}")
if out["cancelled"] or out["proposals"] or out["instructions"]:
logger.warning("[自决清仓] %s 停买入侧: 撤方案 %s, 驳回买入提议 %s, 撤在途买单 %s",
ts_code, out["cancelled"], out["proposals"], out["instructions"])
return out

View File

@ -2937,7 +2937,11 @@ createApp({
function dispOf(code) {
const inst = (instructions.value || []).find(i => i.ts_code === code && i.action === 'OPEN'
&& (_LIVE_INS.includes(i.status) || _sameDayIns(i))); // 旧的终态单不算「已下单」(2026-08-28)
if (inst) return { cls: 'done', label: '已下单', why: '在『当日指令』(' + tx('insStatus', inst.status) + '' };
if (inst) {
// 系统自决自动采纳的指令 (2026-09-14): 进度里 auto 为真且理由以「系统采纳」开头, 显示「系统已采纳」。
const autoAcc = inst.progress && inst.progress.auto && String(inst.progress.reason || '').startsWith('系统采纳');
return { cls: 'done', label: autoAcc ? '系统已采纳' : '已下单', why: '在『当日指令』(' + tx('insStatus', inst.status) + '' };
}
const prop = (proposals.value || []).find(p => p.ts_code === code && p.action === 'OPEN');
if (prop) return { cls: 'queue', label: '待确认', why: '已生成提议,在『待确认提议』等人工裁决' };
const d = (openScan.value.by_code || {})[code];
@ -2950,6 +2954,8 @@ createApp({
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 };
// 系统自决今日已放弃 (2026-09-14): 明日重评, 与「未建仓」分开显示。
if (d.disp === 'sys_decline') return { cls: 'deny', label: '系统已放弃', why: d.why };
return { cls: 'deny', label: '未建仓', why: d.why };
}
return { cls: 'wait', label: '候选观察', why: '' };

View File

@ -514,6 +514,9 @@ consensus.decide 加参数 weak_confirm默认真。_route 里「只有一方
| PMS_TECH_ENABLED | true | 前提是接口根地址指向 188 的决策系统 |
| PMS_FUND_REQUIRED | true | 无评析不建仓 |
| PMS_TOTAL_SCALE | 按真实资金 | 与账户实际资金要对得上 |
| PMS_SELF_DECIDE | off | 系统自决关全部交人2026-09-14 下午加) |
| PMS_SELF_DECIDE_TRIAL | false | 试探仓类不自动采纳2026-09-14 下午加) |
| PMS_SELF_DECIDE_DAILY_MAX | 1 | 每日自动新建仓上限一只2026-09-14 下午加) |
### 脚本

View File

@ -231,3 +231,5 @@
**台账线(只作提示,调阈值先回台账)。** 研判不可用占比应低于两成,超过要查决策系统那侧。上线头几天自动采纳的股数应是试探仓那批,不是标准仓。
**复核日期。** 部署后的下一个交易日盘中先只读试算一次,看 executed 里有没有 route 为 auto、why 含「系统采纳」的条目,或 rejected 里有没有评审方 system 的条目。收盘后跑 make consensus-review 看第五张表系统自决逐条、第六张表研判不可用占比。
**评审补记2026-09-14 下午,同日评审后修订)。** 四处补齐,不改上面的决定。一,到价自动清仓前停买入侧改成与命令清仓同样的三件:撤方案、驳回该票待确认的买入提议、撤该票在途的买入指令;原来只撤方案,已下发的网格买单会与清仓单对倒。不再只在有活动方案时才做,一张在途补仓买单同样要撤。二,只有一方表态改按合议强弱打标记:强弱为弱一律按试探仓,不看路由;原来只在路由为交人时打,弱表态开关 PMS_CONSENSUS_WEAK_CONFIRM 关掉后会按标准仓采纳。三自动采纳只认带应答体的真通过动作不在研判范围时研判直接回通过且没有应答体这种一律交人。四decline 与 full 档下逻辑存疑的新建仓不送研判直接放弃省一次研判调用与时间预算record 档照旧送研判。页面加两种处置词:系统已采纳、系统已放弃。

View File

@ -106,7 +106,8 @@
与拼句 (预设码/其他必填补充/旧自由文本兼容/码串表报错)/决策核心 _self_decide
(off 不管放行强采标准仓低把握转试探仓只有一方表态与异议只在技术面按 TRIAL
开关四类交人逻辑存疑放弃深亏补仓三分支目标价直接采纳每日上限record/decline
降级)/mark_decide 优先级/eval_dca 深档打 decide_kind/三参数登记与范围/两个 repo 查询 (22 )
降级)/mark_decide 优先级/eval_dca 深档打 decide_kind/三参数登记与范围/两个 repo 查询
+ 评审补: 真通过只认带应答体的 PASS弱表态按强弱打标记 (24 )
test_page_copy_guard.py 页面文案守卫·口语化 (2026-09-14 页面专业化包): 禁用词清单
(第一二人称/拟人口语/生造动词) 扫显示文本, 剥三种注释成对豁免
网址不误截自检样例 (1 )
@ -114,14 +115,15 @@
直接印到页面上 / 判据码显示前必须剥前缀 / 不许把整个对象
打给交易员看 / 翻译兜底不许让英文码单独当句子 (1 )
test_wiring.py 装配自检: 服务层核心落表 全链路 (内存桩) +
目标价到价自决关时入队自决开时自动清仓按可卖量夹紧 +
用户设的止损价与目标价单独成列显示 (71 )
894
目标价到价自决关时入队自决开时自动清仓按可卖量夹紧挂着方案且有在途买单时先撤再清 +
用户设的止损价与目标价单独成列显示 (72 )
900
(总数按实跑逐批相加校正过两次: 曾写 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;
09-14 观察读数包第三十一批二十例到 840; 加固包给第二十八批加三例第三十批加三例到 846; 评审修订第三十一批加一例到 847; 09-14 盘中确认包第三十二批十九例到 866; 页面收尾包第三十三批五例到 871;
09-14 系统自决包第三十四批二十二例test_wiring 目标价自动清仓加一例到 894;
09-14 选股打分包 §5.3 来源字段给第七批加两例到 896; 页面专业化包加文案守卫一例, 现为 897)
09-14 选股打分包 §5.3 来源字段给第七批加两例到 896; 页面专业化包加文案守卫一例到 897;
09-14 评审补第三十四批两例test_wiring 撤方案与在途买单一例, 现为 900)
任一子集失败即整体失败 (退出码 1)
哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):

View File

@ -33,8 +33,12 @@ def case(name):
return deco
def _v(verdict=judge.PASS, conf=78, degraded=False):
return {"verdict": verdict, "confidence": conf, "degraded": degraded, "reason": ""}
def _v(verdict=judge.PASS, conf=78, degraded=False, raw="auto"):
"""研判结果桩。raw 默认给一个字典 (带应答体的真通过); 传 None 模拟「动作不在研判范围」那种
没有应答体的 PASS"""
r = {"verdict": verdict, "confidence": conf, "degraded": degraded, "reason": ""}
r["raw"] = {"verdict": verdict} if raw == "auto" else raw
return r
def _cand(action=ae.A_OPEN, decide_kind=None, consensus=True, risk=None):
@ -150,6 +154,20 @@ def _():
assert _self_decide(_cand(decide_kind=ae.DK_VERDICT_UNKNOWN), _v(), fp, 0)["decision"] == "confirm"
@case("C 真通过·PASS 但没有应答体 (动作不在研判范围) → 交人, 不按试探仓采纳")
def _():
fp = {"self_decide": "full", "self_decide_trial": True}
d = _self_decide(_cand(), _v(conf=None, raw=None), fp, 0)
assert d["decision"] == "confirm" and "应答" in d["why"], d
# 深亏补仓同样只认带应答体的 PASS
dca = _cand(action=ae.A_DCA, decide_kind=ae.DK_DCA_DEEP, consensus=False)
assert _self_decide(dca, _v(conf=70, raw=None), fp, 0)["decision"] == "confirm"
# 逻辑存疑排在真通过之前: 不送研判 (raw 为空) 也照样放弃
ld = _cand(decide_kind=ae.DK_LOGIC_DOUBT)
assert _self_decide(ld, {"verdict": "SKIPPED", "raw": None, "degraded": False,
"confidence": None, "reason": ""}, fp, 0)["decision"] == "decline"
@case("C 研判不可用带 judge_unavail 标记 (研判不可用占比复核用)")
def _():
d = _self_decide(_cand(), _v(degraded=True), {"self_decide": "full"}, 0)
@ -233,6 +251,51 @@ def _():
assert on and on.get("decide_kind") == ae.DK_DCA_DEEP, on
def _so_params(**kw):
p = {"scale": 2_000_000.0, "stock_target_default": 0.06,
"batch_split": (0.5, 0.25, 0.25), "min_lot_merge": True,
"consensus_route": True, "self_decide": "full"}
p.update(kw)
return p
def _so_caps():
return {"scale": 2_000_000.0, "portfolio_cap": 0.70, "stock_cap": 0.08,
"max_names": 20, "portfolio_mv": 0.0, "names_count": 0,
"stock_mv": 0.0, "is_new_name": False, "sector": None,
"sector_names": 0, "sector_mv": 0.0, "sector_names_map": {}, "sector_mv_map": {},
"sector_max_names": 4, "sector_max_ratio": 0.40, "cash_reserve": 0.0,
"sector_source_ready": True, "cash_avail": None, "cash_source": "estimate"}
def _so_cand(route, strength, reason):
con = {"route": route, "route_reason": reason, "direction": "看多", "strength": strength,
"votes": {"fund": "中性", "tech": "看多", "timing": "无读数"}, "reason": "一票 → 看多"}
return {"ts_code": "600000.SH", "price": 10.0, "score": 100.0, "sector": None, "theme": None,
"tier": "强传导", "upside": 0.3, "heat": 0.5, "rank": 1, "bucket": "main",
"src": "plan_api", "consensus": con,
"consensus_blocks": {"fund": {"stance": "中性"}, "tech": {"stance": "看多"},
"timing": {"stance": "无读数"}, "consensus": con}}
@case("D scan_open·弱表态开关关掉 (路由放行但强弱为弱) → 仍打 weak_consensus; 关掉自决不打")
def _():
weak_pass = _so_cand("放行", "", "方向看多但只有一方表态(弱表态放行)")
r = ae.scan_open(candidates=[dict(weak_pass)], params=_so_params(), caps=_so_caps(),
room_amt=1e6, slots=10, skip={})
assert r["candidates"], r["skipped"]
assert r["candidates"][0].get("decide_kind") == ae.DK_WEAK_CONSENSUS, r["candidates"][0]
# 弱表态开关开着 (路由交人) 同样是 weak_consensus
weak_confirm = _so_cand("交人", "", "方向看多但只有一方表态")
r2 = ae.scan_open(candidates=[dict(weak_confirm)], params=_so_params(), caps=_so_caps(),
room_amt=1e6, slots=10, skip={})
assert r2["candidates"][0].get("decide_kind") == ae.DK_WEAK_CONSENSUS, r2["candidates"][0]
# 自决 off: 不打 decide_kind (逐字回旧)
r3 = ae.scan_open(candidates=[dict(weak_pass)], params=_so_params(self_decide="off"),
caps=_so_caps(), room_amt=1e6, slots=10, skip={})
assert "decide_kind" not in r3["candidates"][0], r3["candidates"][0]
@case("D 参数登记·三键在 RUNTIME_EXTRA, 合法值与范围校验")
def _():
for k in ("PMS_SELF_DECIDE", "PMS_SELF_DECIDE_TRIAL", "PMS_SELF_DECIDE_DAILY_MAX"):

View File

@ -1819,6 +1819,44 @@ def _():
assert led and "目标价" in (led[0]["reason"] or ""), led
@case("自主提议·目标价到价自决开: 挂着方案且有在途买单时, 先撤方案、撤在途买单再清仓")
def _():
from app.services import proposal_service as ps
# 09-14 评审补: 只撤方案不撤在途买单, 已下发的网格买单会与清仓单对倒。三件事要一起做。
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": 6050,
"base_qty": 6000, "avg_cost": 10.0, "cushion_peak": 0.0,
"target_pct": 0.06}])
fake.insert_strategy(strategy_id="S_GRID_600000", ts_code="600000.SH", stype="GRID",
autonomy="auto", params={}, state={}, status="ACTIVE")
fake.insert_instruction(instruction_id="I_BUY_600000", origin_type="strategy",
origin_id="S_GRID_600000", ts_code="600000.SH", action="ADD",
side="buy", qty=500, status="DISPATCHED", progress={"children": []})
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
assert any(x["action"] == "EXIT" for x in r["executed"]), (r["executed"], r["queued"], r["skipped"])
# 方案撤了, 在途买单撤了, 清仓单落了
assert fake.strategies["S_GRID_600000"]["status"] == "CANCELLED", fake.strategies["S_GRID_600000"]
assert fake.instructions["I_BUY_600000"]["status"] == "CANCELLED", fake.instructions["I_BUY_600000"]
exits = [i for i in fake.instructions.values() if i["action"] == "EXIT"]
assert exits and exits[0]["qty"] == 6050, exits
# 账本 self_decide 块记下撤了什么
led = [x for x in fake.ledger if x["action"] == "EXIT" and x["verdict"] == "PASS"]
assert led, fake.ledger
sd = (led[0].get("hard_numbers") or {}).get("self_decide") or {}
assert sd.get("strategy_cancelled") == ["S_GRID_600000"], sd
assert sd.get("buys_cancelled") == ["I_BUY_600000"], sd
assert "目标价" in (led[0]["reason"] or "") and "12.5" in (led[0]["reason"] or ""), led[0]["reason"]
@case("自主提议·目标价未到 / 没设 一律不产出 (拿不到价绝不折成到价)")
def _():
from app.services import proposal_service as ps