处理问题
This commit is contained in:
parent
f03de08ae0
commit
2f1dcf3373
|
|
@ -67,7 +67,7 @@ scripts/
|
||||||
test_batch7_units.py 上游选股计划: 解析/新鲜度/候选筛选/取数守卫 32 例
|
test_batch7_units.py 上游选股计划: 解析/新鲜度/候选筛选/取数守卫 32 例
|
||||||
test_batch8_units.py 榜单变化: 名册指纹/三种语义/尾部闸/落库往返 60 例
|
test_batch8_units.py 榜单变化: 名册指纹/三种语义/尾部闸/落库往返 60 例
|
||||||
test_batch9_units.py 成本价体检 / 对账按日推进 / 行业闸 / 取整记账 49 例
|
test_batch9_units.py 成本价体检 / 对账按日推进 / 行业闸 / 取整记账 49 例
|
||||||
test_batch10_units.py 静默失败专项: 关键路径不许丢返回值 + 八条实例 46 例
|
test_batch10_units.py 静默失败专项: 关键路径不许丢返回值 + 八条实例 48 例
|
||||||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) 58 例
|
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) 58 例
|
||||||
init_db.py 建表 (应用 ddl_pms_v1.sql, 幂等, 默认演练; 含 DDL 体检)
|
init_db.py 建表 (应用 ddl_pms_v1.sql, 幂等, 默认演练; 含 DDL 体检)
|
||||||
check_db.py 实机连通性与表结构自检 (需真实 .env)
|
check_db.py 实机连通性与表结构自检 (需真实 .env)
|
||||||
|
|
@ -304,7 +304,7 @@ make t-gate # 随时: 规则闸/研判闸拒了什么、为什么
|
||||||
|
|
||||||
## 已实现 / 待开发
|
## 已实现 / 待开发
|
||||||
|
|
||||||
**已实现**:建表 DDL 与建表脚本;配置与运行参数中心;仓位规划器与安全垫账;命令系统(27 类命令全目录 + 双状态机 + 冲突识别);方案生成器(降仓凑额四档、升仓、建仓分批、清仓/减至、行业清仓与限额、暂停买入撤单);账本回放与对账引擎(成交认领、外部成交并入 BASE 告警、以下游为准修正、除权检测、T+1 可用量、连续不一致升级);规则闸终检;择时执行器实现 B(分日配额、分笔、VWAP/回踩/不追高、14:45 兜底、停牌一字板顺延、窗口耗尽收口、挂单有效期);动作引擎四类自主动作 + 研判闸客户端 + 提议分流;决策系统信号消化(两条流独立消费组订阅、置信度分档转清仓指令或提议);管理页面四块 + 运维/日报抽屉;调度器九个调度位;**上游选股计划接口接入**(`/plan` 取候选池、交易日龄硬校验、`theme` 灌行业映射表、页面预览抽屉与不可用横幅);**榜单变化提示**(名册快照 + 新进/掉榜/档位升降/覆盖翻转/名次跳变,持仓票单列,榜尾截断噪音闸);**ws 直连通道的连接层**(常驻进程 + 出口队列 + 签名 + seq 水位与累积确认,见下);**单测 399 例**。
|
**已实现**:建表 DDL 与建表脚本;配置与运行参数中心;仓位规划器与安全垫账;命令系统(27 类命令全目录 + 双状态机 + 冲突识别);方案生成器(降仓凑额四档、升仓、建仓分批、清仓/减至、行业清仓与限额、暂停买入撤单);账本回放与对账引擎(成交认领、外部成交并入 BASE 告警、以下游为准修正、除权检测、T+1 可用量、连续不一致升级);规则闸终检;择时执行器实现 B(分日配额、分笔、VWAP/回踩/不追高、14:45 兜底、停牌一字板顺延、窗口耗尽收口、挂单有效期);动作引擎四类自主动作 + 研判闸客户端 + 提议分流;决策系统信号消化(两条流独立消费组订阅、置信度分档转清仓指令或提议);管理页面四块 + 运维/日报抽屉;调度器九个调度位;**上游选股计划接口接入**(`/plan` 取候选池、交易日龄硬校验、`theme` 灌行业映射表、页面预览抽屉与不可用横幅);**榜单变化提示**(名册快照 + 新进/掉榜/档位升降/覆盖翻转/名次跳变,持仓票单列,榜尾截断噪音闸);**ws 直连通道的连接层**(常驻进程 + 出口队列 + 签名 + seq 水位与累积确认,见下);**单测 401 例**。
|
||||||
|
|
||||||
### 静默失败专项(2026-07-31,八条已修)
|
### 静默失败专项(2026-07-31,八条已修)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ from app.core import command_spec as cs
|
||||||
from app.core import exec_timing as et
|
from app.core import exec_timing as et
|
||||||
from app.core import rule_gate
|
from app.core import rule_gate
|
||||||
from app.core import tradedays as td
|
from app.core import tradedays as td
|
||||||
from app.repo import pms_repo
|
from app.repo import pms_repo, qmt_repo
|
||||||
from app.services import (command_service, dispatcher, industry, market, param_store,
|
from app.services import (command_service, dispatcher, industry, market, param_store,
|
||||||
portfolio)
|
portfolio)
|
||||||
|
|
||||||
|
|
@ -153,8 +153,7 @@ def run_tick(*, now=None, dry_run: bool = False) -> dict:
|
||||||
pos = _pos_of(view, code)
|
pos = _pos_of(view, code)
|
||||||
quota = et.daily_quota(remaining, tdays_left,
|
quota = et.daily_quota(remaining, tdays_left,
|
||||||
allow_odd_tail=(ins.get("action") == "EXIT"))
|
allow_odd_tail=(ins.get("action") == "EXIT"))
|
||||||
fired_today = sum(int(c.get("qty") or 0) for c in children
|
fired_today = _consumed_today(children, ymd_today)
|
||||||
if int(c.get("ymd") or 0) == ymd_today)
|
|
||||||
day_ctx = {**day, "support": pos.get("support_ref"),
|
day_ctx = {**day, "support": pos.get("support_ref"),
|
||||||
"limit_up": _limit_up(day), "limit_down": _limit_down(day),
|
"limit_up": _limit_up(day), "limit_down": _limit_down(day),
|
||||||
"halted": not day or not day.get("price")}
|
"halted": not day or not day.get("price")}
|
||||||
|
|
@ -351,6 +350,49 @@ def _child_id(instruction_id: str, n: int) -> str:
|
||||||
return f"{instruction_id}_D{n:02d}"
|
return f"{instruction_id}_D{n:02d}"
|
||||||
|
|
||||||
|
|
||||||
|
def _consumed_today(children: list, ymd_today: int) -> int:
|
||||||
|
"""今日已投放量 —— **到期/被撤/被拒且一股没成的分片不算数**。
|
||||||
|
|
||||||
|
2026-08-03 实机: 002518.SZ 的分片挂到 TTL 到期没成交, 当天配额却显示「已出完」,
|
||||||
|
一股没买到而当日不再尝试。这跟 `exec_timing.slice_deadline` 自己写的设计本意直接
|
||||||
|
冲突 —— 那里说得很清楚:「普通分片只给 ttl_min 个交易分钟, run_tick 每分钟重评一次,
|
||||||
|
**撤掉重下比挂着更好**」。可只要作废的分片仍占着当日额度, 重下就永远轮不上。
|
||||||
|
|
||||||
|
配额的语义是「今天投放多少」, 但**已经作废且零成交的投放不该占额度** —— 那笔钱没花
|
||||||
|
出去, 敞口也没建立, 它什么都没发生。所以按委托的真实结局算:
|
||||||
|
* 还在途 (未终态) → 按下发量算, 它随时可能成交
|
||||||
|
* 已终态 → 只算真正成交的 cum_qty, 未成交部分释放回配额
|
||||||
|
* 查不到出口行 → 按下发量算 (影子模式没有出口行; 宁可少投也不重复投)
|
||||||
|
"""
|
||||||
|
total = 0
|
||||||
|
for c in children or []:
|
||||||
|
if int(c.get("ymd") or 0) != ymd_today:
|
||||||
|
continue
|
||||||
|
qty = int(c.get("qty") or 0)
|
||||||
|
ref = c.get("ref")
|
||||||
|
if not ref or str(ref).startswith("manual:"):
|
||||||
|
total += qty # 影子模式: 没有出口行可查
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
o = qmt_repo.get_order(ref)
|
||||||
|
except Exception:
|
||||||
|
logger.warning("[配额] 反查出口行失败 %s, 本轮按下发量计入", ref)
|
||||||
|
total += qty
|
||||||
|
continue
|
||||||
|
if not o:
|
||||||
|
total += qty
|
||||||
|
continue
|
||||||
|
if str(o.get("status") or "").upper() in qmt_repo.FINAL:
|
||||||
|
done = int(o.get("cum_qty") or 0)
|
||||||
|
if done < qty:
|
||||||
|
logger.info("[配额] %s 已终态 %s, 成交 %s/%s —— 未成交部分释放回当日配额",
|
||||||
|
ref, o.get("status"), done, qty)
|
||||||
|
total += done
|
||||||
|
else:
|
||||||
|
total += qty
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
def _limit_up(day: dict) -> bool:
|
def _limit_up(day: dict) -> bool:
|
||||||
"""一字板粗判: 当日最高=最低=现价 且 涨幅为正 (无涨跌停价字段时的兜底口径)。"""
|
"""一字板粗判: 当日最高=最低=现价 且 涨幅为正 (无涨跌停价字段时的兜底口径)。"""
|
||||||
if not day:
|
if not day:
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@
|
||||||
test_batch7_units.py 上游选股计划: 解析/新鲜度/候选筛选/取数守卫 (32 例)
|
test_batch7_units.py 上游选股计划: 解析/新鲜度/候选筛选/取数守卫 (32 例)
|
||||||
test_batch8_units.py 榜单变化: 名册指纹/三种语义/尾部闸/落库往返 (60 例)
|
test_batch8_units.py 榜单变化: 名册指纹/三种语义/尾部闸/落库往返 (60 例)
|
||||||
test_batch9_units.py 成本价体检 / 对账按日推进 / 行业闸 / 取整记账 (49 例)
|
test_batch9_units.py 成本价体检 / 对账按日推进 / 行业闸 / 取整记账 (49 例)
|
||||||
test_batch10_units.py 静默失败专项: 关键路径不许丢返回值 + 八条实例 (46 例)
|
test_batch10_units.py 静默失败专项: 关键路径不许丢返回值 + 八条实例 (48 例)
|
||||||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) (58 例)
|
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) (58 例)
|
||||||
共 399 例
|
共 401 例
|
||||||
任一子集失败即整体失败 (退出码 1)。
|
任一子集失败即整体失败 (退出码 1)。
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
|
||||||
|
|
@ -910,6 +910,56 @@ def _():
|
||||||
assert "已下发但父指令未更新" in src, "下发成功但更新影响 0 行时没有留出口"
|
assert "已下发但父指令未更新" in src, "下发成功但更新影响 0 行时没有留出口"
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================
|
||||||
|
# [N] 当日配额: 作废且零成交的分片不该占额度
|
||||||
|
# ================================================================
|
||||||
|
@case("[N1] 分片到期未成交 → 未成交部分释放回当日配额 (否则「撤掉重下」永远轮不上)")
|
||||||
|
def _():
|
||||||
|
from test_wiring import install_fakes
|
||||||
|
from app.services import executor
|
||||||
|
from app.repo import qmt_repo
|
||||||
|
install_fakes(prices={})
|
||||||
|
orders = {"P_D01": {"status": "EXPIRED", "cum_qty": 0},
|
||||||
|
"P_D02": {"status": "SUBMITTED", "cum_qty": 0},
|
||||||
|
"P_D03": {"status": "FILLED", "cum_qty": 100},
|
||||||
|
"P_D04": {"status": "EXPIRED", "cum_qty": 40}}
|
||||||
|
orig = qmt_repo.get_order
|
||||||
|
try:
|
||||||
|
qmt_repo.get_order = lambda ref: orders.get(ref)
|
||||||
|
kids = [{"ymd": 20260803, "qty": 100, "ref": r} for r in
|
||||||
|
("P_D01", "P_D02", "P_D03", "P_D04")]
|
||||||
|
# D01 到期零成交 → 0; D02 在途 → 按下发量 100; D03 全成 → 100; D04 到期成 40 → 40
|
||||||
|
assert executor._consumed_today(kids, 20260803) == 240
|
||||||
|
# 不是今天的分片一概不算
|
||||||
|
assert executor._consumed_today(
|
||||||
|
[{"ymd": 20260731, "qty": 999, "ref": "P_D01"}], 20260803) == 0
|
||||||
|
finally:
|
||||||
|
qmt_repo.get_order = orig
|
||||||
|
|
||||||
|
|
||||||
|
@case("[N2] 影子模式 / 查不到出口行 → 按下发量算 (宁可少投也不重复投)")
|
||||||
|
def _():
|
||||||
|
from test_wiring import install_fakes
|
||||||
|
from app.services import executor
|
||||||
|
from app.repo import qmt_repo
|
||||||
|
install_fakes(prices={})
|
||||||
|
orig = qmt_repo.get_order
|
||||||
|
try:
|
||||||
|
qmt_repo.get_order = lambda ref: None
|
||||||
|
assert executor._consumed_today(
|
||||||
|
[{"ymd": 20260803, "qty": 100, "ref": "manual:INS_X"}], 20260803) == 100
|
||||||
|
assert executor._consumed_today(
|
||||||
|
[{"ymd": 20260803, "qty": 100, "ref": "P_D01"}], 20260803) == 100
|
||||||
|
|
||||||
|
def boom(ref):
|
||||||
|
raise RuntimeError("库挂了")
|
||||||
|
qmt_repo.get_order = boom
|
||||||
|
assert executor._consumed_today(
|
||||||
|
[{"ymd": 20260803, "qty": 100, "ref": "P_D01"}], 20260803) == 100
|
||||||
|
finally:
|
||||||
|
qmt_repo.get_order = orig
|
||||||
|
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
def main():
|
def main():
|
||||||
ok = fail = 0
|
ok = fail = 0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue