处理现有问题

This commit is contained in:
zlt 2026-08-03 09:24:29 +08:00
parent d1e0cb9298
commit 680210ebcc
4 changed files with 88 additions and 4 deletions

View File

@ -67,7 +67,7 @@ scripts/
test_batch7_units.py 上游选股计划: 解析/新鲜度/候选筛选/取数守卫 32 例
test_batch8_units.py 榜单变化: 名册指纹/三种语义/尾部闸/落库往返 60 例
test_batch9_units.py 成本价体检 / 对账按日推进 / 行业闸 / 取整记账 47 例
test_batch10_units.py 静默失败专项: 关键路径不许丢返回值 + 八条实例 36
test_batch10_units.py 静默失败专项: 关键路径不许丢返回值 + 八条实例 38
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) 58 例
init_db.py 建表 (应用 ddl_pms_v1.sql, 幂等, 默认演练; 含 DDL 体检)
check_db.py 实机连通性与表结构自检 (需真实 .env)
@ -302,7 +302,7 @@ make t-gate # 随时: 规则闸/研判闸拒了什么、为什么
## 已实现 / 待开发
**已实现**:建表 DDL 与建表脚本配置与运行参数中心仓位规划器与安全垫账命令系统27 类命令全目录 + 双状态机 + 冲突识别);方案生成器(降仓凑额四档、升仓、建仓分批、清仓/减至、行业清仓与限额、暂停买入撤单);账本回放与对账引擎(成交认领、外部成交并入 BASE 告警、以下游为准修正、除权检测、T+1 可用量、连续不一致升级);规则闸终检;择时执行器实现 B分日配额、分笔、VWAP/回踩/不追高、14:45 兜底、停牌一字板顺延、窗口耗尽收口、挂单有效期);动作引擎四类自主动作 + 研判闸客户端 + 提议分流;决策系统信号消化(两条流独立消费组订阅、置信度分档转清仓指令或提议);管理页面四块 + 运维/日报抽屉;调度器九个调度位;**上游选股计划接口接入**`/plan` 取候选池、交易日龄硬校验、`theme` 灌行业映射表、页面预览抽屉与不可用横幅);**榜单变化提示**(名册快照 + 新进/掉榜/档位升降/覆盖翻转/名次跳变,持仓票单列,榜尾截断噪音闸);**ws 直连通道的连接层**(常驻进程 + 出口队列 + 签名 + seq 水位与累积确认,见下);**单测 387 例**。
**已实现**:建表 DDL 与建表脚本配置与运行参数中心仓位规划器与安全垫账命令系统27 类命令全目录 + 双状态机 + 冲突识别);方案生成器(降仓凑额四档、升仓、建仓分批、清仓/减至、行业清仓与限额、暂停买入撤单);账本回放与对账引擎(成交认领、外部成交并入 BASE 告警、以下游为准修正、除权检测、T+1 可用量、连续不一致升级);规则闸终检;择时执行器实现 B分日配额、分笔、VWAP/回踩/不追高、14:45 兜底、停牌一字板顺延、窗口耗尽收口、挂单有效期);动作引擎四类自主动作 + 研判闸客户端 + 提议分流;决策系统信号消化(两条流独立消费组订阅、置信度分档转清仓指令或提议);管理页面四块 + 运维/日报抽屉;调度器九个调度位;**上游选股计划接口接入**`/plan` 取候选池、交易日龄硬校验、`theme` 灌行业映射表、页面预览抽屉与不可用横幅);**榜单变化提示**(名册快照 + 新进/掉榜/档位升降/覆盖翻转/名次跳变,持仓票单列,榜尾截断噪音闸);**ws 直连通道的连接层**(常驻进程 + 出口队列 + 签名 + seq 水位与累积确认,见下);**单测 389 例**。
### 静默失败专项2026-07-31八条已修
@ -377,6 +377,7 @@ git pull → make test → ALL SUITES PASS
- 账本已清空(`reset_ledger --purge-channel --reset-ws --drop-reports`ws seq 水位归零 → pms-ws 一重连对端会从 seq 1 全量补发约 1.2 万条。账本空的时候**不要**点「成交回放」。
- 宿主机放行 docker 网段到 8300 的规则若用 `iptables -I` 加的,**重启就没了**,需 `netfilter-persistent save` 或改 firewalld permanent。不持久化的话服务器重启后候选池会静默变空。
- 自主档位仍是 `propose_only`,下发通道仍是 `shadow`
- **`PMS_TOTAL_SCALE` 与账户实际资金要对得上。** 两个数本来就是不同的东西scale 是「我打算投多少」的命令参数,账户总资产是「现在有多少钱」),允许不等,但差太远会产出一堆执行不了的方案:规划器只认 scale按 200 万 × 总仓上限排出 140 万的买入,而账户只有 98 万,多出来的会在**规则闸**被一条条判 `INSUFFICIENT_CASH`——方案排得出来、下不出去每一步看着都正常。2026-07-31 实测就是这组数。盘前准备(`make t-pre`)现在会把两个数摆在一起,差太多就在 `scale_check` 里说破。
- 静默失败专项修完之后,**几处「以前默默过去」的地方现在会明着报失败**,这是设计如此:开关类命令(`HALT_BUY` / `RESUME_ALL` 等)写不进参数就置 `CANCELLED` 并回 `ok=False`,不再显示「已完成」;`daily_settle` 在对账被拦(爆炸半径 / 两源无应答 / 成本价体检不过)或连续不一致升到 ERROR 时 `ok=False``premarket` 在「该踩刹车却没踩上」时把它记进 `errors` 而不是 warnings。**看到这些报错先别急着改代码——多半是数据库或下游真的出问题了,以前只是没人告诉你。**
- 新增第 15 张表 `pms_plan_snapshot`(名册快照),**部署后要跑一次 `make initdb`**,否则榜单变化那段会一直报「读名册快照失败:表不存在」——候选池不受影响。第一份快照落下之前不报任何变化(`kind=first`),这是设计如此不是坏了。

View File

@ -991,10 +991,52 @@ def premarket() -> dict:
(out["errors"] if "刹车未生效" in w else out.setdefault("warnings", [])).append(w)
except Exception as e:
out["errors"].append(f"刹车结算失败: {e}")
try:
s = _scale_vs_account()
out["scale_check"] = s
if s.get("warning"):
out.setdefault("warnings", []).append(s["warning"])
except Exception as e:
out.setdefault("warnings", []).append(f"规模与账户资金比对失败: {e}")
out["ok"] = not out["errors"]
return out
def _scale_vs_account() -> dict:
"""`PMS_TOTAL_SCALE` 与账户实际资金差太多时, 盘前就说出来。
两个数是**不同的东西**, 本来就允许不等 (scale "我打算投多少"的命令参数,
账户总资产是"现在有多少钱"), 但差得太远会产出一堆执行不了的方案:
规划器只认 scale 200 × 总仓上限 70% 排出 140 万的买入方案, 而账户只有 98 ,
多出来那 40 万会在**规则闸**被一条条判 INSUFFICIENT_CASH方案表里躺着一堆 PENDING,
评审账本里刷一屏拒绝, 每一步看着都"正常工作", 只有把两个数摆在一起才看得出病根
2026-07-31 实测: scale=200 , 模拟账户 total_asset=98.1
只在**拿得到真实资金**时判 (估算值是 scale市值, 拿它比 scale 是自己跟自己比)
"""
v = portfolio.positions_view()
t = v["totals"]
scale = float(t.get("scale") or 0)
equity = t.get("total_asset")
out = {"scale": scale, "equity": equity, "cash_source": t.get("cash_source"),
"warning": ""}
if t.get("cash_source") != portfolio.CASH_WS or not equity or scale <= 0:
return out
equity = float(equity)
cap = param_store.get_float("PMS_PORTFOLIO_CAP", 0.6)
plan_ceiling = scale * cap
out.update({"equity": equity, "plan_ceiling": round(plan_ceiling, 2),
"ratio": round(equity / scale, 4)})
if plan_ceiling > equity:
out["warning"] = (
f"规模参数与账户对不上: PMS_TOTAL_SCALE={scale:,.0f} × 总仓上限 {cap:.0%} "
f"= {plan_ceiling:,.0f} 元的方案空间, 而账户总资产只有 {equity:,.0f} 元。"
f"超出的部分会在规则闸被逐条判 INSUFFICIENT_CASH —— 方案排得出来、下不出去。"
f"要么把 PMS_TOTAL_SCALE 调到 {equity / max(cap, 1e-9):,.0f} 以内, 要么先注资")
logger.warning("[盘前] %s", out["warning"])
return out
def _settle_brake() -> dict:
"""组合刹车: 自高水位回撤 ≥ 阈值 → 自主增持停 N 个交易日 (命令类不受限)。

View File

@ -14,9 +14,9 @@
test_batch7_units.py 上游选股计划: 解析/新鲜度/候选筛选/取数守卫 (32 )
test_batch8_units.py 榜单变化: 名册指纹/三种语义/尾部闸/落库往返 (60 )
test_batch9_units.py 成本价体检 / 对账按日推进 / 行业闸 / 取整记账 (47 )
test_batch10_units.py 静默失败专项: 关键路径不许丢返回值 + 八条实例 (36 )
test_batch10_units.py 静默失败专项: 关键路径不许丢返回值 + 八条实例 (38 )
test_wiring.py 装配自检: 服务层核心落表 全链路 (内存桩) (58 )
387
389
任一子集失败即整体失败 (退出码 1)
"""
import os

View File

@ -749,6 +749,47 @@ def _():
ls.positions_source = orig
@case("[L6] 规模参数远大于账户资金 → 盘前就说破 (别等规则闸刷一屏 INSUFFICIENT_CASH)")
def _():
from test_wiring import install_fakes
from app.services import ledger_service as ls, portfolio
install_fakes(prices={})
orig = portfolio.positions_view
try:
# 2026-07-31 实测的那组数: scale 200 万 × 上限 70% = 140 万方案空间, 账户只有 98.1 万
portfolio.positions_view = lambda **kw: {
"held": [], "positions": [], "params": {},
"totals": {"scale": 2000000.0, "portfolio_mv": 0.0, "total_asset": 981448.56,
"cash_source": "ws", "cash_why": ""}}
s = ls._scale_vs_account()
assert s["warning"] and "INSUFFICIENT_CASH" in s["warning"], s
assert s["plan_ceiling"] > s["equity"], s
# 规模压到账户以内就不该再吵
portfolio.positions_view = lambda **kw: {
"held": [], "positions": [], "params": {},
"totals": {"scale": 1000000.0, "portfolio_mv": 0.0, "total_asset": 981448.56,
"cash_source": "ws", "cash_why": ""}}
assert not ls._scale_vs_account()["warning"], ls._scale_vs_account()
finally:
portfolio.positions_view = orig
@case("[L7] 拿不到真实资金时不比 (估算值是 scale市值, 拿它比 scale 是自己跟自己比)")
def _():
from test_wiring import install_fakes
from app.services import ledger_service as ls, portfolio
install_fakes(prices={})
orig = portfolio.positions_view
try:
portfolio.positions_view = lambda **kw: {
"held": [], "positions": [], "params": {},
"totals": {"scale": 2000000.0, "portfolio_mv": 0.0, "total_asset": None,
"cash_source": "estimate", "cash_why": "ws 没回 funds 快照"}}
assert not ls._scale_vs_account()["warning"], ls._scale_vs_account()
finally:
portfolio.positions_view = orig
@case("[L5] 单测清单里的文件缺失不许算通过 (镜像旧了正好整批不存在)")
def _():
import re