处理现有问题

This commit is contained in:
zlt 2026-08-03 09:53:18 +08:00
parent d0398b9331
commit cf99e1446c
4 changed files with 63 additions and 9 deletions

View File

@ -66,7 +66,7 @@ scripts/
test_batch6_units.py ws 通道: 测试向量/签名/公钥/水位/DDL/逐笔入账 65 例 test_batch6_units.py ws 通道: 测试向量/签名/公钥/水位/DDL/逐笔入账 65 例
test_batch7_units.py 上游选股计划: 解析/新鲜度/候选筛选/取数守卫 32 例 test_batch7_units.py 上游选股计划: 解析/新鲜度/候选筛选/取数守卫 32 例
test_batch8_units.py 榜单变化: 名册指纹/三种语义/尾部闸/落库往返 60 例 test_batch8_units.py 榜单变化: 名册指纹/三种语义/尾部闸/落库往返 60 例
test_batch9_units.py 成本价体检 / 对账按日推进 / 行业闸 / 取整记账 47 test_batch9_units.py 成本价体检 / 对账按日推进 / 行业闸 / 取整记账 49
test_batch10_units.py 静默失败专项: 关键路径不许丢返回值 + 八条实例 40 例 test_batch10_units.py 静默失败专项: 关键路径不许丢返回值 + 八条实例 40 例
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) 58 例 test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) 58 例
init_db.py 建表 (应用 ddl_pms_v1.sql, 幂等, 默认演练; 含 DDL 体检) init_db.py 建表 (应用 ddl_pms_v1.sql, 幂等, 默认演练; 含 DDL 体检)
@ -302,7 +302,7 @@ make t-gate # 随时: 规则闸/研判闸拒了什么、为什么
## 已实现 / 待开发 ## 已实现 / 待开发
**已实现**:建表 DDL 与建表脚本配置与运行参数中心仓位规划器与安全垫账命令系统27 类命令全目录 + 双状态机 + 冲突识别);方案生成器(降仓凑额四档、升仓、建仓分批、清仓/减至、行业清仓与限额、暂停买入撤单);账本回放与对账引擎(成交认领、外部成交并入 BASE 告警、以下游为准修正、除权检测、T+1 可用量、连续不一致升级);规则闸终检;择时执行器实现 B分日配额、分笔、VWAP/回踩/不追高、14:45 兜底、停牌一字板顺延、窗口耗尽收口、挂单有效期);动作引擎四类自主动作 + 研判闸客户端 + 提议分流;决策系统信号消化(两条流独立消费组订阅、置信度分档转清仓指令或提议);管理页面四块 + 运维/日报抽屉;调度器九个调度位;**上游选股计划接口接入**`/plan` 取候选池、交易日龄硬校验、`theme` 灌行业映射表、页面预览抽屉与不可用横幅);**榜单变化提示**(名册快照 + 新进/掉榜/档位升降/覆盖翻转/名次跳变,持仓票单列,榜尾截断噪音闸);**ws 直连通道的连接层**(常驻进程 + 出口队列 + 签名 + seq 水位与累积确认,见下);**单测 391 例**。 **已实现**:建表 DDL 与建表脚本配置与运行参数中心仓位规划器与安全垫账命令系统27 类命令全目录 + 双状态机 + 冲突识别);方案生成器(降仓凑额四档、升仓、建仓分批、清仓/减至、行业清仓与限额、暂停买入撤单);账本回放与对账引擎(成交认领、外部成交并入 BASE 告警、以下游为准修正、除权检测、T+1 可用量、连续不一致升级);规则闸终检;择时执行器实现 B分日配额、分笔、VWAP/回踩/不追高、14:45 兜底、停牌一字板顺延、窗口耗尽收口、挂单有效期);动作引擎四类自主动作 + 研判闸客户端 + 提议分流;决策系统信号消化(两条流独立消费组订阅、置信度分档转清仓指令或提议);管理页面四块 + 运维/日报抽屉;调度器九个调度位;**上游选股计划接口接入**`/plan` 取候选池、交易日龄硬校验、`theme` 灌行业映射表、页面预览抽屉与不可用横幅);**榜单变化提示**(名册快照 + 新进/掉榜/档位升降/覆盖翻转/名次跳变,持仓票单列,榜尾截断噪音闸);**ws 直连通道的连接层**(常驻进程 + 出口队列 + 签名 + seq 水位与累积确认,见下);**单测 393 例**。
### 静默失败专项2026-07-31八条已修 ### 静默失败专项2026-07-31八条已修

View File

@ -22,6 +22,8 @@
""" """
from __future__ import annotations from __future__ import annotations
import re
from app.core.sizer import LOT, lot_qty, split_batches, check_caps from app.core.sizer import LOT, lot_qty, split_batches, check_caps
from app.core.cushion import sell_allocation # noqa: F401 (供调用方做核销预览) from app.core.cushion import sell_allocation # noqa: F401 (供调用方做核销预览)
@ -631,6 +633,26 @@ def _open_batch_items(ts_code, sp, price, reason_prefix="建仓"):
# ================================================================ # ================================================================
# 四、拒绝原因聚合 # 四、拒绝原因聚合
# ================================================================ # ================================================================
_REJ_CODE = re.compile(r"^([A-Z][A-Z0-9_]{2,}):")
_REJ_NUM = re.compile(r"\d+(?:[,\d]*)(?:\.\d+)?%?")
def _reason_key(text: str) -> str:
"""把一条拒绝原因归一成"同类原因同一个键"
有判据码 (`SECTOR_RATIO: `) 就用码; 没有码的把**数字抹成 ** 再当键 价格金额
只数每只票都不一样, 不抹掉就永远聚不到一起括号里的尾注 ((已尝试: CUSTOM ))
也去掉, 它是排查细节不是分类依据, 原始 rejects 清单里一个字没少
"""
s = str(text or "").strip()
m = _REJ_CODE.match(s)
if m:
return m.group(1)
s = s.split("(")[0].split("")[0].strip()
s = _REJ_NUM.sub("", s)
return s[:40].strip() or "(无原因)"
def summarize_rejects(rejects) -> str: def summarize_rejects(rejects) -> str:
"""把 rejects 清单聚合成一行人话。 """把 rejects 清单聚合成一行人话。
@ -640,7 +662,13 @@ def summarize_rejects(rejects) -> str:
端到面上**"拒了 10 只""没有候选"是两件完全不同的事, 长得却一样** 端到面上**"拒了 10 只""没有候选"是两件完全不同的事, 长得却一样**
输出按拒绝码归类计数, 多的排前面: 输出按拒绝码归类计数, 多的排前面:
"SECTOR_RATIO 10 只 (600000.SH…); STOCK_CAP 2 只 (600519.SH…)" "SECTOR_RATIO 10 只 (600000.SH…); 目标金额 … 元按现价 … 买不足一手 8 只 (600764.SH…)"
**没有判据码的原因必须先把数字抹掉再归类** (2026-08-03 实机修): 原来对无码原因是
`text[:20]` 硬截 20 个字, 结果两件事同时坏掉 一是价格在截断范围内, 8 只票 8 个价,
于是聚成 8 一组一只, 聚合等于没做, 输出比原始清单还长; 二是截断落在词中间, 印出
"…按现价 18.68 买" / "…按现价 6.47 买不" 这种半句话聚合本来是为了让人一眼看懂
"为什么这么少", 结果自己成了噪音源
""" """
if not rejects: if not rejects:
return "" return ""
@ -648,10 +676,7 @@ def summarize_rejects(rejects) -> str:
for r in rejects or []: for r in rejects or []:
code = r.get("ts_code") code = r.get("ts_code")
for reason in (r.get("reasons") or []): for reason in (r.get("reasons") or []):
# 判据码是冒号前那一段 (SECTOR_RATIO / STOCK_CAP / MAX_NAMES …); key = _reason_key(str(reason))
# 一手不可行那类没有码, 整句截断当分类名
text = str(reason)
key = text.split(":", 1)[0].strip() if ":" in text[:24] else text[:20]
slot = by.setdefault(key, {"n": 0, "codes": []}) slot = by.setdefault(key, {"n": 0, "codes": []})
slot["n"] += 1 slot["n"] += 1
if code and code not in slot["codes"]: if code and code not in slot["codes"]:

View File

@ -13,10 +13,10 @@
test_batch6_units.py ws 通道: 测试向量/签名/公钥/水位/DDL/逐笔入账 (65 ) test_batch6_units.py ws 通道: 测试向量/签名/公钥/水位/DDL/逐笔入账 (65 )
test_batch7_units.py 上游选股计划: 解析/新鲜度/候选筛选/取数守卫 (32 ) test_batch7_units.py 上游选股计划: 解析/新鲜度/候选筛选/取数守卫 (32 )
test_batch8_units.py 榜单变化: 名册指纹/三种语义/尾部闸/落库往返 (60 ) test_batch8_units.py 榜单变化: 名册指纹/三种语义/尾部闸/落库往返 (60 )
test_batch9_units.py 成本价体检 / 对账按日推进 / 行业闸 / 取整记账 (47 ) test_batch9_units.py 成本价体检 / 对账按日推进 / 行业闸 / 取整记账 (49 )
test_batch10_units.py 静默失败专项: 关键路径不许丢返回值 + 八条实例 (40 ) test_batch10_units.py 静默失败专项: 关键路径不许丢返回值 + 八条实例 (40 )
test_wiring.py 装配自检: 服务层核心落表 全链路 (内存桩) (58 ) test_wiring.py 装配自检: 服务层核心落表 全链路 (内存桩) (58 )
391 393
任一子集失败即整体失败 (退出码 1) 任一子集失败即整体失败 (退出码 1)
""" """
import os import os

View File

@ -426,6 +426,35 @@ def t_g4():
assert "1 只" in s and "股A" in s assert "1 只" in s and "股A" in s
@case("[G5] 无码原因**先抹掉数字再归类** —— 价格各不相同也要聚成一组")
def t_g5():
# 2026-08-03 实机: 8 只票同一个原因、8 个不同价格, 原来按 text[:20] 硬截,
# 价格落在截断范围内 → 聚成 8 组一组一只 (聚合等于没做), 而且截断落在词中间,
# 印出「…按现价 18.68 买」「…按现价 6.47 买不」这种半句话。
rej = [{"ts_code": c, "reasons": [f"目标金额 60 元按现价 {p} 买不足一手 "
f"(已尝试: CUSTOM → BASE/FILL → BASE)"]}
for c, p in (("600764.SH", "18.68"), ("000800.SZ", "6.47"),
("600104.SH", "11.22"), ("002594.SZ", "95.77"))]
s = summarize_rejects(rej)
assert s.count("") == 1, f"没聚成一组: {s}"
assert "4 只" in s, s
assert "买不足一手" in s, f"截断落在词中间了: {s}"
assert "18.68" not in s and "95.77" not in s, f"具体价格不该出现在聚合标签里: {s}"
assert "已尝试" not in s, f"括号尾注是排查细节, 不该进分类标签: {s}"
@case("[G6] 有码与无码混在一起时各归各的, 仍按条数降序")
def t_g6():
rej = [{"ts_code": "A", "reasons": ["目标金额 60 元按现价 1.00 买不足一手"]},
{"ts_code": "B", "reasons": ["目标金额 60 元按现价 2.00 买不足一手"]},
{"ts_code": "C", "reasons": ["目标金额 60 元按现价 3.00 买不足一手"]},
{"ts_code": "D", "reasons": ["SECTOR_RATIO: 行业[白酒]占组合将达 55.0% > 上限 40%"]},
{"ts_code": "E", "reasons": ["STOCK_CAP: 单股占规模将达 9.1% > 上限 8%"]}]
s = summarize_rejects(rej)
assert s.index("3 只") < s.index("SECTOR_RATIO"), f"没按条数降序: {s}"
assert "SECTOR_RATIO 1 只" in s and "STOCK_CAP 1 只" in s, s
# ================================================================ [H] 一手取整的记账口径 # ================================================================ [H] 一手取整的记账口径
# 2026-07-31 实机: 命令进度写着 planned_amount=600,000 / gap=0 ("完全满足"), 而 15 条方案 # 2026-07-31 实机: 命令进度写着 planned_amount=600,000 / gap=0 ("完全满足"), 而 15 条方案
# 的金额合计只有 573,883 —— 少买 26,117 元 (4.4%) 且账面上完全看不出来。 # 的金额合计只有 573,883 —— 少买 26,117 元 (4.4%) 且账面上完全看不出来。