@@ -1721,12 +1724,17 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
+
+
- {{ tx('frozen', s.row.frozen_reason) }}
- —
+
+ {{ tx('posStatus', s.row.status) }}
+ {{ tx('stype', stratOf(s.row.ts_code).type) }}
+ 只卖不买
+
+ {{ nextStep(s.row) }}
@@ -2327,7 +2335,8 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
- 计划日 {{ planStatus.date }} (日龄 {{ planStatus.age_tdays }} 交易日) ·
+ 数据日 {{ planStatus.date }}(上游每个交易日早上用上一交易日收盘后的证据出计划,日龄 {{ planStatus.age_tdays }} 交易日,属正常) · {{ planFetchedText }} ·
热度日 {{ planStatus.heat_date || '-' }} · 快照 {{ (planStatus.market_snapshot_days||[]).join(',') }} ·
上游主题上限 {{ planStatus.theme_cap }} · theme 灌映射 {{ JSON.stringify(planStatus.theme_sync||{}) }}
请求 {{ JSON.stringify(planStatus.query||{}) }} · 漏斗: 打分池
@@ -2586,6 +2595,15 @@ createApp({
const planStatus = computed(() => planRaw.value.status || {});
const planRows = computed(() => planRaw.value.rows || []);
const planCand = computed(() => planRaw.value.candidates_raw || null);
+ // 计划拉取时刻 (2026-09-16): fetched_at 是持仓系统自己记的时间戳, 按浏览器本地时区显示, 不猜上游容器的时区。
+ const planFetchedText = computed(() => {
+ const ts = Number((planStatus.value || {}).fetched_at || 0); if (!ts) return '';
+ const d = new Date(ts * 1000); if (isNaN(d.getTime())) return '';
+ const hm = String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
+ const t = new Date();
+ const same = d.getFullYear() === t.getFullYear() && d.getMonth() === t.getMonth() && d.getDate() === t.getDate();
+ return (same ? '今日 ' : (d.getMonth() + 1) + '-' + String(d.getDate()).padStart(2, '0') + ' ') + hm + ' 拉取';
+ });
// 技术面模块状态 (2026-09-11): 顶栏新鲜度芯片。只读 /api/tech/status, 交易员也看得到。
const techStatus = ref({});
const techChipText = computed(() => {
@@ -4574,7 +4592,7 @@ createApp({
wsRaw, wsAuto, wsHidePong, wsc, wst, wsMode, wsOrders, wsInbox, wsWarnings, loadWs,
loadAll, loadParams, saveParams, loadPlans, loadLots, onCmdChange, issue,
cancelCmd, replan, decide, decideDlg, decideCodes, submitDecide, ops, loadSchema, openReport, cancelIns,
- planDrawer, planLoading, planStatus, planRows, planCand, planResult,
+ planDrawer, planLoading, planStatus, planFetchedText, planRows, planCand, planResult,
loadPlan, openPlan, refreshPlan,
techStatus, techChipText, techChipCls,
chgRaw, chgSt, chgLoading, chgTab, chg, chgHeld, chgLog, chgRevised, chgWatch,
diff --git a/config/settings.py b/config/settings.py
index 4c1823f..ae409e0 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -104,7 +104,7 @@ class Settings(BaseSettings):
PMS_BUILD_WINDOW_TDAYS: int = 10 # 建仓期窗口 (交易日)
PMS_FILL_MAX_LOSS: float = -0.03 # 浮亏深于此不走回踩补足 (转补仓域)
PMS_WEAK_NEG_DAYS: int = 5 # 降仓「清弱票」判定: 安全垫连续为负 N 日 (设计 §3.2)
- PMS_PROPOSAL_TTL_HOURS: int = 24 # 自主提议待确认有效期 (超时置 EXPIRED)
+ PMS_PROPOSAL_TTL_HOURS: int = 24 # 自主提议待确认有效期 (超时置 EXPIRED); 不跨自然日, 最晚当天 23:59:59
# --- 自主新建仓 (动作引擎第五类动作 OPEN, 2026-08-06) ---
# 让自主提议除了管已有持仓, 也能从上游候选池里挑新票建底仓, 走同一条
diff --git a/scripts/run_tests.py b/scripts/run_tests.py
index 1316230..3a76e2f 100644
--- a/scripts/run_tests.py
+++ b/scripts/run_tests.py
@@ -52,7 +52,8 @@
仅展示的跳过原因写出这一只自己的判决依据/
候选一只不剩时的说明不打印计数字典/
新建仓低把握驳回按证据不足交人/
- 页面静态守卫 (18 例)
+ 提议失效不跨自然日 (2026-09-16 两例)/
+ 页面静态守卫 (20 例)
test_batch21_units.py 减持的自动执行边界 (2026-09-03 安全修复): 保垫减仓照旧自动/
强制入队对卖出同样有效/研判不可用的减持入队/研究证据走弱
来源必定交人裁决/风控高置信卖出与命令清仓不经提议分流/
@@ -117,13 +118,14 @@
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) +
目标价到价自决关时入队、自决开时自动清仓按可卖量夹紧、挂着方案且有在途买单时先撤再清 +
用户设的止损价与目标价单独成列显示 (72 例)
- 共 900 例
+ 共 902 例
(总数按实跑逐批相加校正过两次: 曾写 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 评审补第三十四批两例、test_wiring 撤方案与在途买单一例, 现为 900)
+ 09-14 评审补第三十四批两例、test_wiring 撤方案与在途买单一例到 900;
+ 09-16 提议失效不跨自然日给第二十批加两例, 现为 902)
任一子集失败即整体失败 (退出码 1)。
哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):
diff --git a/scripts/test_batch20_units.py b/scripts/test_batch20_units.py
index 4cdcf6a..a9d7a74 100644
--- a/scripts/test_batch20_units.py
+++ b/scripts/test_batch20_units.py
@@ -546,6 +546,42 @@ def _():
assert got[0]["judge_verdict"] == "PASS" and got[0]["judge_reason"] == "都成立"
+@case("提议失效时刻不跨自然日 (2026-09-16): TTL 24 小时也在当天 23:59:59 失效; TTL 短于当天剩余时照旧按 TTL; "
+ "过期判定认 datetime 与字符串, 没有失效时刻按未过期")
+def _():
+ from datetime import timedelta
+ from app.services import proposal_service as psvc
+ now = datetime(2026, 9, 15, 9, 31, 57)
+ assert psvc.proposal_expire_at(now, ttl_hours=24) == datetime(2026, 9, 15, 23, 59, 59)
+ assert psvc.proposal_expire_at(now, ttl_hours=2) == now + timedelta(hours=2)
+ late = datetime(2026, 9, 15, 23, 0, 0)
+ assert psvc.proposal_expire_at(late, ttl_hours=2) == datetime(2026, 9, 15, 23, 59, 59)
+ at = datetime(2026, 9, 16, 9, 20, 0)
+ assert psvc.is_expired({"expire_at": datetime(2026, 9, 15, 23, 59, 59)}, now=at) is True
+ assert psvc.is_expired({"expire_at": "2026-09-16 09:31:57"}, now=at) is False
+ assert psvc.is_expired({"expire_at": "2026-09-15 23:59:59"}, now=at) is True
+ assert psvc.is_expired({"expire_at": None}, now=at) is False
+ assert psvc.is_expired({}, now=at) is False
+
+
+@case("_make_proposal 落表的失效时刻不晚于当天 23:59:59 (2026-09-16 用户拍板: 昨天的提议不留到今天开盘)")
+def _():
+ from datetime import timedelta
+ from app.repo import pms_repo
+ from app.services import param_store, proposal_service as psvc
+ got = []
+ c = _open_cand()
+ with _Patch() as p:
+ p(pms_repo, "insert_proposal", lambda **kw: got.append(kw) or 1)
+ _patch_params(p, param_store, {"PMS_PROPOSAL_TTL_HOURS": 24})
+ psvc._make_proposal(c, 10.0, {"verdict": "PASS", "reason": "都成立", "degraded": False,
+ "raw": {}, "confidence": 72.0})
+ exp = got[0]["expire_at"]
+ now = datetime.now()
+ assert exp <= now.replace(hour=23, minute=59, second=59, microsecond=0), exp
+ assert exp >= now - timedelta(minutes=1), exp
+
+
# ================================================================ 页面静态守卫
@case("页面守卫·提议卡按 plan_rank 取候选榜名次并显示判决与理由; 采纳/驳回都弹必填理由并随请求体发")
def _():