From e794d8520d5b31c51fde9ee043ef835e9f3d5050 Mon Sep 17 00:00:00 2001 From: zlt Date: Wed, 26 Aug 2026 10:11:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=A4=E6=98=93=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DEVLOG.md | 3 +++ app/services/param_store.py | 3 +++ app/services/strategy_advisor.py | 41 +++++++++++++++++++++++++------- app/web/main.py | 14 ++++++++++- app/web/static/index.html | 28 ++++++++++++++++++---- scripts/run_tests.py | 5 ++-- scripts/test_batch17_units.py | 28 ++++++++++++++++++++++ 7 files changed, 106 insertions(+), 16 deletions(-) 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: {{ stratEnabled ? '策略层已启用' : '策略层总开关未开(PMS_STRATEGY_ENABLED)' }} 显示已撤下/已完成 - - 自动挂载 · 看看今天想挂什么 + + {{ autoInfo.enabled ? '自动挂载已开' : '自动挂载总开关未开' }} + + 预演 · 现在系统想做什么 + +
+ + + 「预演」按钮只是看一眼:按此刻的行情和信号算一遍会做什么,不挂载、不留痕,随便点。 +
- 试算只判断、不挂载不留痕;真跑在每个交易日 09:40 由调度自动执行。本轮查了 - {{ autoScanRes.checked }} 只持仓票。 + 以下为预演结果:按此刻的行情和信号,系统会做什么。只是看一眼,什么都没动。 + 本轮查了 {{ autoScanRes.checked }} 只持仓票。
@@ -1624,6 +1640,7 @@ createApp({ const instructions = ref([]), ledger = ref([]), proposals = ref([]); const strategies = ref([]), stratEnabled = ref(false), opLog = ref([]); const autoScanRes = ref(null), autoScanBusy = ref(false); // 自动挂载试算 (2026-08-25) + const autoInfo = ref(null); // 自动挂载仪表: 总开关 + 今天正式一跳跑没跑 (2026-08-26) // 「显示已完成/已移除」开关 (默认关: 只看进行中)。命令与策略勾选时会拉全量含已归档 (loadX 传 // include_archived); 在途指令纯前端过滤, 不重拉。 const showAllCommands = ref(false), showAllStrategies = ref(false), showAllInstr = ref(false); @@ -2392,6 +2409,7 @@ createApp({ const d = await call('get', q); strategies.value = d.strategies || (d.data && d.data.strategies) || []; 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 是同一份契约) function stageTagType(s) { @@ -2560,7 +2578,7 @@ createApp({ pctOf, tgtPos, posMoveValid, posMovePreview, doPosMove, heldSectors, secSel, doSectorExit, pmap, pval, dcaOn, sumPosition, sumDca, sumAutonomy, strategies, stratEnabled, opLog, stratDlg, stratQty, loadStrategies, loadOpLog, - autoScanRes, autoScanBusy, autoScanPreview, stageTagType, + autoScanRes, autoScanBusy, autoScanPreview, stageTagType, autoInfo, openStrategy, validateStrategy, attachStrategy, setStrategyStatus, stratStateText, resumeBuy, showLedger, onPosExpand, openScan, insTab, todayYmd, insLive, insDone, insEnd, dispOf, loadOpenScan, denyToday, diff --git a/scripts/run_tests.py b/scripts/run_tests.py index c817d72..1c3a60d 100644 --- a/scripts/run_tests.py +++ b/scripts/run_tests.py @@ -30,9 +30,10 @@ 科创板一手200(advisor 与 runner 两侧)/连边矩阵/ 五阶段判定与逐票阶段返回/note 约定与冷却推导/ 接力判定/clear_buypause/编排冒烟(dry_run 滴水不写/ - 名额/暂停买入/接力全链)/判分脚本聚合与对照分组 (40 例) + 名额/暂停买入/接力全链)/真跑写一跳摘要而预演不写/ + 判分脚本聚合与对照分组 (41 例) test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) (58 例) - 共 559 例 + 共 560 例 任一子集失败即整体失败 (退出码 1)。 """ import os diff --git a/scripts/test_batch17_units.py b/scripts/test_batch17_units.py index d1cfc07..326253c 100644 --- a/scripts/test_batch17_units.py +++ b/scripts/test_batch17_units.py @@ -376,6 +376,7 @@ class _Rec: def __init__(self): self.attach_calls, self.status_calls, self.pause_calls = [], [], [] self.clear_calls, self.ledger, self.upd_calls = [], [], [] + self.param_writes = [] 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", lambda k, d=False: {"PMS_STRATEGY_ENABLED": True, "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(adv, "accum_of", lambda codes: dict(accum)) 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 +@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 五阶段判定矩阵: 策略优先于信号, 派发优先于深亏, 都不是归中性") def _(): def s(**kw):