diff --git a/DEVLOG.md b/DEVLOG.md
index e64db81..9cbdc25 100644
--- a/DEVLOG.md
+++ b/DEVLOG.md
@@ -50,6 +50,9 @@ app/services/strategy_advisor.py(新增);app/services/strategy_service.py
**真机判收**
部分判收(2026-08-25 盘中)。桥机 make deploy + make test 见 ALL SUITES PASS;dry-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 盘中):每票阶段可视化**
用户拍板把状态机补成看得见的:扫描结果新增 stages,对**每只**持仓票各出一行——处在五阶段(吸筹震荡、启动拉升、高位派发、深亏修复、中性)的哪一段、一句话理由、热度、浮盈、定性原文;已挂自动策略的票阶段以策略为准(网格即吸筹震荡进行中、止盈即启动拉升保护中),人工策略票按信号判并注明「自动挂载不碰」;信号判定次序与连边一致,派发优先于深亏(保守方向)。页面按钮的结果顶部渲染五阶段视图,不动的票也说清为什么不动。判定函数 stage_of 纯逻辑可测,批十七扩到 40 例,总数 559。同日运维记录:桥的计划触发已在北京 07:10 跑通(计划日 T-1,判收通过);deploy 恰好压掉 09:40 调度位,用试算端点的正式模式手动补跑(补跑与调度位同一份实现);清掉两个三周前 compose run 未带 --rm 的残留容器。
diff --git a/app/services/param_store.py b/app/services/param_store.py
index e2e65b9..34b6733 100644
--- a/app/services/param_store.py
+++ b/app/services/param_store.py
@@ -55,6 +55,9 @@ RUNTIME_EXTRA = {
# 2026-08-18 宏观择时层: 闸状态由 macro_service 每次扫描写入, 扫描层与策略层只读。
# 用运行参数承载 (不进策略 state、不另建表) —— 与 PMS_STRATEGY_BUYPAUSE 同一手法。
"PMS_MACRO_GATE_STATE": ("", str, "宏观闸状态 (macro_service 维护的 JSON: active/ymd/原因), 页面与扫描层只读, 勿手改"),
+ # 2026-08-26: 页面要能回答「今天的正式一跳跑没跑、做了什么」。零动作的一跳在账上没有
+ # 任何痕迹 (设计如此, 台账只记真动作), 所以由 advisor 每次真跑后写一份摘要在这里。
+ "PMS_AUTO_LAST_SCAN": ("", str, "自动挂载最近一次正式扫描的摘要 (advisor 真跑后写入的 JSON: 日期/时刻/各类计数), 页面只读, 勿手改"),
}
# **读不到时必须按"已暂停"处理的键 (fail-closed)。**
diff --git a/app/services/strategy_advisor.py b/app/services/strategy_advisor.py
index 80057b4..1eaba6c 100644
--- a/app/services/strategy_advisor.py
+++ b/app/services/strategy_advisor.py
@@ -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:
"""每交易日一跳 (09:40)。dry_run=True 只判不写 —— 不挂、不留痕、不动暂停表。"""
now = now or datetime.now()
@@ -371,24 +393,26 @@ def scan(*, dry_run: bool = False, now=None) -> dict:
"unknown_states": [], "stages": []}
if not prm["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):
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):
out["skipped"].append({"why": "全局暂停执行 (休假模式)"})
- return out
+ return _record_scan(out, dry_run, now)
from app.services import command_service, portfolio, strategy_service
try:
view = portfolio.positions_view()
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]
if not held:
out["skipped"].append({"why": "当前无持仓"})
- return out
+ return _record_scan(out, dry_run, now)
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,
include_archived=True)
except Exception as e: # noqa: BLE001
- return {**out, "ok": False, "errors": out["errors"] +
- [f"读策略表失败, 整轮守成不动: {type(e).__name__}: {e}"]}
+ return _record_scan({**out, "ok": False, "errors": out["errors"] +
+ [f"读策略表失败, 整轮守成不动: {type(e).__name__}: {e}"]},
+ dry_run, now)
strat_by_code = {}
for s in strat_all:
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.pop("_attached_today", None)
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,
diff --git a/app/web/main.py b/app/web/main.py
index ac70d16..276d9aa 100644
--- a/app/web/main.py
+++ b/app/web/main.py
@@ -12,6 +12,7 @@ PMS 管理页面 · Web 入口 (FastAPI + 单页)
"""
from __future__ import annotations
+import json
import logging
import os
from datetime import datetime, timedelta
@@ -664,8 +665,19 @@ def api_strategies(status: str = Query(None), include_archived: bool = Query(Fal
bp = {}
for r in rows:
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,
- "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)
diff --git a/app/web/static/index.html b/app/web/static/index.html
index ea33ee3..bee8ede 100644
--- a/app/web/static/index.html
+++ b/app/web/static/index.html
@@ -654,16 +654,32 @@ pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius: