添加交易逻辑

This commit is contained in:
zlt 2026-08-26 10:11:29 +08:00
parent e03e41a32b
commit e794d8520d
7 changed files with 106 additions and 16 deletions

View File

@ -50,6 +50,9 @@ app/services/strategy_advisor.py新增app/services/strategy_service.py
**真机判收** **真机判收**
部分判收2026-08-25 盘中)。桥机 make deploy + make test 见 ALL SUITES PASSdry-run 试算真机四票读数全部合理688802.SH 想挂网格但「按投入比例折出 47,250 元买不起一手」被挡科创板高价票判定正确002179.SZ 有在途指令主动让路其余两票无信号静默跳过unknown_states 为空(子串归类修对了)。**尚未判收的**:真挂一条(等在途清了重扫或次日 09:40、边三/边四真机走一遍、strategy_runner 对自动网格的逐档发单。 部分判收2026-08-25 盘中)。桥机 make deploy + make test 见 ALL SUITES PASSdry-run 试算真机四票读数全部合理688802.SH 想挂网格但「按投入比例折出 47,250 元买不起一手」被挡科创板高价票判定正确002179.SZ 有在途指令主动让路其余两票无信号静默跳过unknown_states 为空(子串归类修对了)。**尚未判收的**:真挂一条(等在途清了重扫或次日 09:40、边三/边四真机走一遍、strategy_runner 对自动网格的逐档发单。
**次日上午第五批2026-08-26 盘中):把「预演」与「正式跑」在页面上分清**
用户反馈「试算只判断」的提示看不懂到底有用没用——根子是页面只有预演镜没人回答「今天的正式一跳跑没跑、做了什么」而零动作的一跳在台账上没有任何痕迹设计如此台账只记真动作。补法advisor 每次真跑(含总开关关着的那次)在每个出口写一份摘要进运行参数(新登记 PMS_AUTO_LAST_SCAN写失败只告警set_param 返回值按纪律接住),策略接口把总开关状态、今天跑没跑、各类计数一并返回;页面「我的策略」常驻一行仪表:「今天的正式一跳已在 09:41 跑过:查 4 只,挂载 0、被挡 1、跳过 1」或「还没跑09:40 自动执行)」,按钮改名「预演 · 现在系统想做什么」,预演结果头一句明说「什么都没动」。批十七加一例钉住「真跑每个出口都写、预演一笔不写」,总数 560。
**次日上午第四批2026-08-26 盘中):每票阶段可视化** **次日上午第四批2026-08-26 盘中):每票阶段可视化**
用户拍板把状态机补成看得见的:扫描结果新增 stages对**每只**持仓票各出一行——处在五阶段(吸筹震荡、启动拉升、高位派发、深亏修复、中性)的哪一段、一句话理由、热度、浮盈、定性原文;已挂自动策略的票阶段以策略为准(网格即吸筹震荡进行中、止盈即启动拉升保护中),人工策略票按信号判并注明「自动挂载不碰」;信号判定次序与连边一致,派发优先于深亏(保守方向)。页面按钮的结果顶部渲染五阶段视图,不动的票也说清为什么不动。判定函数 stage_of 纯逻辑可测,批十七扩到 40 例,总数 559。同日运维记录桥的计划触发已在北京 07:10 跑通(计划日 T-1判收通过deploy 恰好压掉 09:40 调度位,用试算端点的正式模式手动补跑(补跑与调度位同一份实现);清掉两个三周前 compose run 未带 --rm 的残留容器。 用户拍板把状态机补成看得见的:扫描结果新增 stages对**每只**持仓票各出一行——处在五阶段(吸筹震荡、启动拉升、高位派发、深亏修复、中性)的哪一段、一句话理由、热度、浮盈、定性原文;已挂自动策略的票阶段以策略为准(网格即吸筹震荡进行中、止盈即启动拉升保护中),人工策略票按信号判并注明「自动挂载不碰」;信号判定次序与连边一致,派发优先于深亏(保守方向)。页面按钮的结果顶部渲染五阶段视图,不动的票也说清为什么不动。判定函数 stage_of 纯逻辑可测,批十七扩到 40 例,总数 559。同日运维记录桥的计划触发已在北京 07:10 跑通(计划日 T-1判收通过deploy 恰好压掉 09:40 调度位,用试算端点的正式模式手动补跑(补跑与调度位同一份实现);清掉两个三周前 compose run 未带 --rm 的残留容器。

View File

@ -55,6 +55,9 @@ RUNTIME_EXTRA = {
# 2026-08-18 宏观择时层: 闸状态由 macro_service 每次扫描写入, 扫描层与策略层只读。 # 2026-08-18 宏观择时层: 闸状态由 macro_service 每次扫描写入, 扫描层与策略层只读。
# 用运行参数承载 (不进策略 state、不另建表) —— 与 PMS_STRATEGY_BUYPAUSE 同一手法。 # 用运行参数承载 (不进策略 state、不另建表) —— 与 PMS_STRATEGY_BUYPAUSE 同一手法。
"PMS_MACRO_GATE_STATE": ("", str, "宏观闸状态 (macro_service 维护的 JSON: active/ymd/原因), 页面与扫描层只读, 勿手改"), "PMS_MACRO_GATE_STATE": ("", str, "宏观闸状态 (macro_service 维护的 JSON: active/ymd/原因), 页面与扫描层只读, 勿手改"),
# 2026-08-26: 页面要能回答「今天的正式一跳跑没跑、做了什么」。零动作的一跳在账上没有
# 任何痕迹 (设计如此, 台账只记真动作), 所以由 advisor 每次真跑后写一份摘要在这里。
"PMS_AUTO_LAST_SCAN": ("", str, "自动挂载最近一次正式扫描的摘要 (advisor 真跑后写入的 JSON: 日期/时刻/各类计数), 页面只读, 勿手改"),
} }
# **读不到时必须按"已暂停"处理的键 (fail-closed)。** # **读不到时必须按"已暂停"处理的键 (fail-closed)。**

View File

@ -360,6 +360,28 @@ def _params() -> dict:
# ================================================================ 编排 # ================================================================ 编排
def _record_scan(out: dict, dry_run: bool, now) -> dict:
"""真跑 (非试算) 的每个出口都经过这里: 把本跳摘要写进运行参数 PMS_AUTO_LAST_SCAN,
页面据此回答今天的正式一跳跑没跑做了什么 零动作的一跳在台账上没有痕迹,
没有这份摘要就没人说得清它到底跑过没有写失败只告警不拦返回: 摘要是仪表不是账"""
if dry_run:
return out
try:
summ = {"ymd": td.ymd(now), "at": str(now)[:19], "ok": out.get("ok", True),
"enabled": out.get("enabled"), "checked": out.get("checked", 0),
"counts": {k: len(out.get(k) or []) for k in
("attached", "handoffs", "paused", "resumed",
"blocked", "skipped", "errors")}}
r = param_store.set_param("PMS_AUTO_LAST_SCAN",
json.dumps(summ, ensure_ascii=False), "system")
if not isinstance(r, dict) or not r.get("ok"):
logger.warning("[自动挂载] 本跳摘要没写进参数表: %s",
r.get("error") if isinstance(r, dict) else r)
except Exception as e: # noqa: BLE001
logger.warning("[自动挂载] 本跳摘要没写进参数表: %s", e)
return out
def scan(*, dry_run: bool = False, now=None) -> dict: def scan(*, dry_run: bool = False, now=None) -> dict:
"""每交易日一跳 (09:40)。dry_run=True 只判不写 —— 不挂、不留痕、不动暂停表。""" """每交易日一跳 (09:40)。dry_run=True 只判不写 —— 不挂、不留痕、不动暂停表。"""
now = now or datetime.now() now = now or datetime.now()
@ -371,24 +393,26 @@ def scan(*, dry_run: bool = False, now=None) -> dict:
"unknown_states": [], "stages": []} "unknown_states": [], "stages": []}
if not prm["enabled"]: if not prm["enabled"]:
out["skipped"].append({"why": "策略自动挂载总开关关闭 (PMS_AUTO_STRATEGY_ENABLED)"}) out["skipped"].append({"why": "策略自动挂载总开关关闭 (PMS_AUTO_STRATEGY_ENABLED)"})
return out return _record_scan(out, dry_run, now)
if not param_store.get_bool("PMS_STRATEGY_ENABLED", False): if not param_store.get_bool("PMS_STRATEGY_ENABLED", False):
out["skipped"].append({"why": "策略层总开关关闭 (PMS_STRATEGY_ENABLED) —— " out["skipped"].append({"why": "策略层总开关关闭 (PMS_STRATEGY_ENABLED) —— "
"挂了也不会跑, 本轮不挂"}) "挂了也不会跑, 本轮不挂"})
return out return _record_scan(out, dry_run, now)
if param_store.get_bool("PMS_GLOBAL_EXEC_HALT", False): if param_store.get_bool("PMS_GLOBAL_EXEC_HALT", False):
out["skipped"].append({"why": "全局暂停执行 (休假模式)"}) out["skipped"].append({"why": "全局暂停执行 (休假模式)"})
return out return _record_scan(out, dry_run, now)
from app.services import command_service, portfolio, strategy_service from app.services import command_service, portfolio, strategy_service
try: try:
view = portfolio.positions_view() view = portfolio.positions_view()
except Exception as e: # noqa: BLE001 —— 守成: 读不到持仓不动任何东西 except Exception as e: # noqa: BLE001 —— 守成: 读不到持仓不动任何东西
return {**out, "ok": False, "errors": [f"读持仓失败: {type(e).__name__}: {e}"]} return _record_scan({**out, "ok": False,
"errors": [f"读持仓失败: {type(e).__name__}: {e}"]},
dry_run, now)
held = [p for p in view["held"] if int(p.get("total_qty") or 0) > 0] held = [p for p in view["held"] if int(p.get("total_qty") or 0) > 0]
if not held: if not held:
out["skipped"].append({"why": "当前无持仓"}) out["skipped"].append({"why": "当前无持仓"})
return out return _record_scan(out, dry_run, now)
codes = [p["ts_code"] for p in held] codes = [p["ts_code"] for p in held]
# ---- 信号与既有状态一次取齐 (任一失败只废对应的边, 不废整轮) ---- # ---- 信号与既有状态一次取齐 (任一失败只废对应的边, 不废整轮) ----
@ -419,8 +443,9 @@ def scan(*, dry_run: bool = False, now=None) -> dict:
strat_all = pms_repo.list_strategies(statuses=["ACTIVE", "PAUSED"], limit=500, strat_all = pms_repo.list_strategies(statuses=["ACTIVE", "PAUSED"], limit=500,
include_archived=True) include_archived=True)
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
return {**out, "ok": False, "errors": out["errors"] + return _record_scan({**out, "ok": False, "errors": out["errors"] +
[f"读策略表失败, 整轮守成不动: {type(e).__name__}: {e}"]} [f"读策略表失败, 整轮守成不动: {type(e).__name__}: {e}"]},
dry_run, now)
strat_by_code = {} strat_by_code = {}
for s in strat_all: for s in strat_all:
strat_by_code.setdefault(s.get("ts_code"), s) strat_by_code.setdefault(s.get("ts_code"), s)
@ -494,7 +519,7 @@ def scan(*, dry_run: bool = False, now=None) -> dict:
out["errors"].append(f"{code}: {type(e).__name__}: {e}") out["errors"].append(f"{code}: {type(e).__name__}: {e}")
out.pop("_attached_today", None) out.pop("_attached_today", None)
out["ok"] = not out["errors"] out["ok"] = not out["errors"]
return out return _record_scan(out, dry_run, now)
def _tend_existing(st, p, a, fresh, hv, prm, buypause, handoff_cool, today, def _tend_existing(st, p, a, fresh, hv, prm, buypause, handoff_cool, today,

View File

@ -12,6 +12,7 @@ PMS 管理页面 · Web 入口 (FastAPI + 单页)
""" """
from __future__ import annotations from __future__ import annotations
import json
import logging import logging
import os import os
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -664,8 +665,19 @@ def api_strategies(status: str = Query(None), include_archived: bool = Query(Fal
bp = {} bp = {}
for r in rows: for r in rows:
r["buy_paused"] = bp.get(r.get("ts_code")) or None r["buy_paused"] = bp.get(r.get("ts_code")) or None
# 自动挂载仪表 (2026-08-26): 总开关 + 今天的正式一跳跑没跑。摘要由 advisor 真跑后
# 写进 PMS_AUTO_LAST_SCAN, 这里只读; 解析不了按没跑显示, 不猜。
last, ran_today = None, False
try:
raw = param_store.get("PMS_AUTO_LAST_SCAN") or ""
last = json.loads(raw) if raw else None
ran_today = bool(last and int(last.get("ymd") or 0) == td.ymd())
except (ValueError, TypeError):
last = None
return {"ok": True, "strategies": rows, return {"ok": True, "strategies": rows,
"enabled": param_store.get_bool("PMS_STRATEGY_ENABLED", False)} "enabled": param_store.get_bool("PMS_STRATEGY_ENABLED", False),
"auto": {"enabled": param_store.get_bool("PMS_AUTO_STRATEGY_ENABLED", False),
"ran_today": ran_today, "last_scan": last}}
return ok(_load) return ok(_load)

View File

@ -654,16 +654,32 @@ pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius:
<el-tag size="small" :type="stratEnabled?'primary':'info'" effect="dark" style="margin-left:8px"> <el-tag size="small" :type="stratEnabled?'primary':'info'" effect="dark" style="margin-left:8px">
{{ stratEnabled ? '策略层已启用' : '策略层总开关未开PMS_STRATEGY_ENABLED' }}</el-tag> {{ stratEnabled ? '策略层已启用' : '策略层总开关未开PMS_STRATEGY_ENABLED' }}</el-tag>
<el-checkbox v-model="showAllStrategies" @change="loadStrategies" size="small" style="margin-left:12px;font-weight:normal">显示已撤下/已完成</el-checkbox> <el-checkbox v-model="showAllStrategies" @change="loadStrategies" size="small" style="margin-left:12px;font-weight:normal">显示已撤下/已完成</el-checkbox>
<el-button size="small" style="margin-left:12px" :loading="autoScanBusy" @click="autoScanPreview"> <el-tag v-if="autoInfo" size="small" :type="autoInfo.enabled?'primary':'warning'" effect="dark" style="margin-left:12px">
自动挂载 · 看看今天想挂什么</el-button> {{ autoInfo.enabled ? '自动挂载已开' : '自动挂载总开关未开' }}</el-tag>
<el-button size="small" style="margin-left:8px" :loading="autoScanBusy" @click="autoScanPreview">
预演 · 现在系统想做什么</el-button>
</h3> </h3>
<!-- 自动挂载仪表 (2026-08-26): 正式跑与预演分清 —— 正式跑是 09:40 的调度, 这里回答它今天
跑没跑、做了什么; 旁边按钮只是预演镜, 随便点, 不动任何东西。 -->
<div class="muted" v-if="autoInfo" style="margin-bottom:6px">
<template v-if="autoInfo.ran_today && autoInfo.last_scan">
今天的正式一跳已在 {{ (autoInfo.last_scan.at||'').slice(11,16) }} 跑过:查
{{ autoInfo.last_scan.checked }} 只,挂载 {{ ((autoInfo.last_scan.counts||{}).attached)||0 }}、
接力 {{ ((autoInfo.last_scan.counts||{}).handoffs)||0 }}、停买入 {{ ((autoInfo.last_scan.counts||{}).paused)||0 }}、
被挡 {{ ((autoInfo.last_scan.counts||{}).blocked)||0 }}、跳过 {{ ((autoInfo.last_scan.counts||{}).skipped)||0 }}。
</template>
<template v-else>
今天的正式一跳还没跑(每个交易日 09:40 由调度自动执行,跑完这里会显示结果)。
</template>
「预演」按钮只是看一眼:按此刻的行情和信号算一遍会做什么,不挂载、不留痕,随便点。
</div>
<el-alert v-if="!stratEnabled" type="info" effect="dark" :closable="false" show-icon style="margin-bottom:8px" <el-alert v-if="!stratEnabled" type="info" effect="dark" :closable="false" show-icon style="margin-bottom:8px"
title="策略挂了也不会出手:请在「设置 / 运维」把 PMS_STRATEGY_ENABLED 打开;影子还是实盘由 PMS_DISPATCH_MODE 决定。"></el-alert> title="策略挂了也不会出手:请在「设置 / 运维」把 PMS_STRATEGY_ENABLED 打开;影子还是实盘由 PMS_DISPATCH_MODE 决定。"></el-alert>
<!-- 自动挂载试算结果 (2026-08-25): 只判断不落任何东西, 真跑在每个交易日 09:40 的调度位 --> <!-- 自动挂载试算结果 (2026-08-25): 只判断不落任何东西, 真跑在每个交易日 09:40 的调度位 -->
<div v-if="autoScanRes" style="border:1px solid var(--bd,#dcdfe6);border-radius:6px;padding:8px 12px;margin-bottom:8px"> <div v-if="autoScanRes" style="border:1px solid var(--bd,#dcdfe6);border-radius:6px;padding:8px 12px;margin-bottom:8px">
<div class="muted" style="margin-bottom:4px"> <div class="muted" style="margin-bottom:4px">
试算只判断、不挂载不留痕;真跑在每个交易日 09:40 由调度自动执行。本轮查了 以下为预演结果:按此刻的行情和信号,系统会做什么。只是看一眼,什么都没动。
<b>{{ autoScanRes.checked }}</b> 只持仓票。 本轮查了 <b>{{ autoScanRes.checked }}</b> 只持仓票。
</div> </div>
<!-- 五阶段视图: 每只持仓票当前处在打法状态机的哪个阶段, 不动的票也说清为什么不动 --> <!-- 五阶段视图: 每只持仓票当前处在打法状态机的哪个阶段, 不动的票也说清为什么不动 -->
<div v-for="(x,i) in (autoScanRes.stages||[])" :key="'stg'+i" style="margin:2px 0"> <div v-for="(x,i) in (autoScanRes.stages||[])" :key="'stg'+i" style="margin:2px 0">
@ -1624,6 +1640,7 @@ createApp({
const instructions = ref([]), ledger = ref([]), proposals = ref([]); const instructions = ref([]), ledger = ref([]), proposals = ref([]);
const strategies = ref([]), stratEnabled = ref(false), opLog = ref([]); const strategies = ref([]), stratEnabled = ref(false), opLog = ref([]);
const autoScanRes = ref(null), autoScanBusy = ref(false); // 自动挂载试算 (2026-08-25) const autoScanRes = ref(null), autoScanBusy = ref(false); // 自动挂载试算 (2026-08-25)
const autoInfo = ref(null); // 自动挂载仪表: 总开关 + 今天正式一跳跑没跑 (2026-08-26)
// 「显示已完成/已移除」开关 (默认关: 只看进行中)。命令与策略勾选时会拉全量含已归档 (loadX 传 // 「显示已完成/已移除」开关 (默认关: 只看进行中)。命令与策略勾选时会拉全量含已归档 (loadX 传
// include_archived); 在途指令纯前端过滤, 不重拉。 // include_archived); 在途指令纯前端过滤, 不重拉。
const showAllCommands = ref(false), showAllStrategies = ref(false), showAllInstr = ref(false); const showAllCommands = ref(false), showAllStrategies = ref(false), showAllInstr = ref(false);
@ -2392,6 +2409,7 @@ createApp({
const d = await call('get', q); const d = await call('get', q);
strategies.value = d.strategies || (d.data && d.data.strategies) || []; strategies.value = d.strategies || (d.data && d.data.strategies) || [];
stratEnabled.value = !!(d.enabled != null ? d.enabled : (d.data && d.data.enabled)); stratEnabled.value = !!(d.enabled != null ? d.enabled : (d.data && d.data.enabled));
autoInfo.value = d.auto || (d.data && d.data.auto) || null;
} }
// 五阶段标签配色 (阶段字面量与 strategy_advisor.stage_of 是同一份契约) // 五阶段标签配色 (阶段字面量与 strategy_advisor.stage_of 是同一份契约)
function stageTagType(s) { function stageTagType(s) {
@ -2560,7 +2578,7 @@ createApp({
pctOf, tgtPos, posMoveValid, posMovePreview, doPosMove, heldSectors, secSel, doSectorExit, pctOf, tgtPos, posMoveValid, posMovePreview, doPosMove, heldSectors, secSel, doSectorExit,
pmap, pval, dcaOn, sumPosition, sumDca, sumAutonomy, pmap, pval, dcaOn, sumPosition, sumDca, sumAutonomy,
strategies, stratEnabled, opLog, stratDlg, stratQty, loadStrategies, loadOpLog, strategies, stratEnabled, opLog, stratDlg, stratQty, loadStrategies, loadOpLog,
autoScanRes, autoScanBusy, autoScanPreview, stageTagType, autoScanRes, autoScanBusy, autoScanPreview, stageTagType, autoInfo,
openStrategy, validateStrategy, attachStrategy, setStrategyStatus, stratStateText, resumeBuy, openStrategy, validateStrategy, attachStrategy, setStrategyStatus, stratStateText, resumeBuy,
showLedger, onPosExpand, showLedger, onPosExpand,
openScan, insTab, todayYmd, insLive, insDone, insEnd, dispOf, loadOpenScan, denyToday, openScan, insTab, todayYmd, insLive, insDone, insEnd, dispOf, loadOpenScan, denyToday,

View File

@ -30,9 +30,10 @@
科创板一手200(advisor runner 两侧)/连边矩阵/ 科创板一手200(advisor runner 两侧)/连边矩阵/
五阶段判定与逐票阶段返回/note 约定与冷却推导/ 五阶段判定与逐票阶段返回/note 约定与冷却推导/
接力判定/clear_buypause/编排冒烟(dry_run 滴水不写/ 接力判定/clear_buypause/编排冒烟(dry_run 滴水不写/
名额/暂停买入/接力全链)/判分脚本聚合与对照分组 (40 ) 名额/暂停买入/接力全链)/真跑写一跳摘要而预演不写/
判分脚本聚合与对照分组 (41 )
test_wiring.py 装配自检: 服务层核心落表 全链路 (内存桩) (58 ) test_wiring.py 装配自检: 服务层核心落表 全链路 (内存桩) (58 )
559 560
任一子集失败即整体失败 (退出码 1) 任一子集失败即整体失败 (退出码 1)
""" """
import os import os

View File

@ -376,6 +376,7 @@ class _Rec:
def __init__(self): def __init__(self):
self.attach_calls, self.status_calls, self.pause_calls = [], [], [] self.attach_calls, self.status_calls, self.pause_calls = [], [], []
self.clear_calls, self.ledger, self.upd_calls = [], [], [] self.clear_calls, self.ledger, self.upd_calls = [], [], []
self.param_writes = []
def _scan_stubbed(rec, *, held, accum, heat, strategies=None, cancelled=None, def _scan_stubbed(rec, *, held, accum, heat, strategies=None, cancelled=None,
@ -407,6 +408,8 @@ def _scan_stubbed(rec, *, held, accum, heat, strategies=None, cancelled=None,
keep(param_store, "get_bool", keep(param_store, "get_bool",
lambda k, d=False: {"PMS_STRATEGY_ENABLED": True, lambda k, d=False: {"PMS_STRATEGY_ENABLED": True,
"PMS_GLOBAL_EXEC_HALT": False}.get(k, d)) "PMS_GLOBAL_EXEC_HALT": False}.get(k, d))
keep(param_store, "set_param",
lambda k, v, by="user": rec.param_writes.append((k, v)) or {"ok": True})
keep(portfolio, "positions_view", lambda: view) keep(portfolio, "positions_view", lambda: view)
keep(adv, "accum_of", lambda codes: dict(accum)) keep(adv, "accum_of", lambda codes: dict(accum))
keep(adv, "heat_of", lambda codes: (dict(heat), {"stale": False, "td": TODAY})) keep(adv, "heat_of", lambda codes: (dict(heat), {"stale": False, "td": TODAY}))
@ -679,6 +682,31 @@ def _():
param_store.get_bool, adv._params = orig, orig_p param_store.get_bool, adv._params = orig, orig_p
@case("[仪表] 真跑每个出口都写一跳摘要 (含总开关关着那次), 预演一笔不写")
def _():
import json as _json
assert "PMS_AUTO_LAST_SCAN" in param_store.RUNTIME_EXTRA # 没登记 set_param 会拒写
rec = _Rec()
_scan_stubbed(rec, held=[_pos()], heat={},
accum={"600000.SH": {"cls": adv.CLS_CLEAR, "state": "明确吸筹",
"score": 80, "ymd": TODAY, "age_days": 1}},
dry_run=False)
assert len(rec.param_writes) == 1, rec.param_writes
k, v = rec.param_writes[0]
summ = _json.loads(v)
assert k == "PMS_AUTO_LAST_SCAN" and summ["checked"] == 1
assert summ["counts"]["attached"] == 1 and "ymd" in summ and "at" in summ
# 预演滴水不写
rec2 = _Rec()
_scan_stubbed(rec2, held=[_pos()], accum={}, heat={}, dry_run=True)
assert not rec2.param_writes
# 总开关关着的真跑也要留一笔 —— 不然「跑了但关着」和「根本没跑」分不清
rec3 = _Rec()
_scan_stubbed(rec3, held=[_pos()], accum={}, heat={}, dry_run=False,
prm=_prm(enabled=False))
assert len(rec3.param_writes) == 1 and _json.loads(rec3.param_writes[0][1])["enabled"] is False
@case("[阶段] stage_of 五阶段判定矩阵: 策略优先于信号, 派发优先于深亏, 都不是归中性") @case("[阶段] stage_of 五阶段判定矩阵: 策略优先于信号, 派发优先于深亏, 都不是归中性")
def _(): def _():
def s(**kw): def s(**kw):