T-0 / T-0b(门禁 · 2026-09-10) - T-0:sqlite 与 MySQL 结构对齐 —— core_holding 统一为 qty/cost_amount/as_of/pnl_pct + PK + UNIQUE(customer_id, product_id);补 core_product_nav;新增建库自校验 _assert_ddl_aligned()(R-g);test_db.py 增 3 条门禁用例(含反向验证门禁失效) - T-0b:DB 账号分离(D20)—— 新增 scripts/core/00-grant.sql(三账号逐表授权); settings.py 增 3 组账号;db.py 改 get_engine(db, role),缓存键改为 (库名, 角色), 账号未配置回退单账号;core_ro→ro / gateway_repository→rw / risk·session_repository→rw; tests/conftest.py 四处显式 role="admin"(R-e) T-1(数据层) - scripts/core/01-ddl.sql:新建 core_fee_rule / core_share_lot / core_convert_lot_detail; core_trade 加 convert_group_id + idx_convert_group;core_product 加 8 列 + fee_rate 补 COMMENT - 新增 07-seed-fee-rule.sql(赎回费 5 档 × 14 产品,按 22 号文 §10)/ 08-seed-share-lot.sql (58 行持仓 → 61 行批次,Σ remain_qty 恒等于 qty)/ 09-seed-org.sql(管理人 + TA + 申购费率 + 最低持有余额,v1.1 按「管理人全产品线」重排) - reset.ps1 追加 07/08/09;02-mysql-agent专用.sql 追加 risk_convert_detail - tests/_ddl.py 同步 4 表 + 新增 REQUIRED_CONVERT_TABLES 建库门禁 - 新增 scripts/dev/verify_convert_seed.py(pymysql 等价 reset 流程 + 8 条 DoD 断言, 含断言 ⑧「费率档 ↔ product_type 匹配」,越档即 FAIL) T-2 / T-2b(纯函数包 + 示例实算回填) - 新增 app/service/convert/ 7 文件:__init__ / types / calc / fee / nav / lot_bootstrap / errors (纯函数,不查库、不碰 SQL;所有量化显式 ROUND_HALF_UP;lot_bootstrap 用 zlib.crc32 保证 D18 跨进程同源) - 新增 tests/test_convert_calc.py 93 用例(12 类:HALF_UP 反向自证 / 分档边界 / FIFO 含同 confirmed_at 兜底 / 双口径 / 强制全转与强制赎回 / PRD §5.3 全链自证 / 纯函数零 IO 依赖断言) - 重写 scripts/dev/calc_convert_demo.py:去掉脚本内公式副本,改为调用生产 calc.py, 末尾与 PRD §5.3 逐项比对(不一致即退出码 1),兼作一致性门禁 验证 - pytest 609 passed / 3 skipped(516 → +93,零回归) - verify_convert_seed.py 8/8 PASS;calc_convert_demo.py 15/15 与 PRD §5.3 一致 文档:PRD v0.9.1(费率分类修正)· 架构 §7 签名回填 / §8.3 错误码注 / §15 T-2 完成 · 开发计划 §1.5 新增 R-h + §4.2·§4.3 执行记录 · AGENTS.md · docs/memory
75 lines
3.0 KiB
Python
75 lines
3.0 KiB
Python
"""净值口径判定(架构 §7 · D10 · PRD §2.2/§8.3)。
|
||
|
||
**本模块不查库** —— 调用方先 `core_ro.get_nav_as_of(product_id, trade_date)` 取回
|
||
`nav_date <= 交易日` 的最近一行,再传入此处判定。取值本身(T 日未知价法、
|
||
降序取 1)由 SQL 保证(D10:**不改** `get_latest_nav`,它会取到未来日期的净值)。
|
||
|
||
两个判定必须分清——它们对应**完全不同的处置**:
|
||
|
||
| 判定 | 含义 | 处置 |
|
||
| --- | --- | --- |
|
||
| `ensure_nav_ready()` | 该产品**无任何净值记录** | **503 `NAV_NOT_READY`**,不落占位(PRD §8.3) |
|
||
| `is_stale()` | 有净值但距交易日过久 | **不阻断**,仅额外落一条 `nav_stale` 审计(PRD §2.2) |
|
||
|
||
把"净值过期"误当"无净值"会拦掉本可完成的转换;反之会让演示库的过期净值
|
||
静默按新鲜净值折算。故两者是**两个独立函数**,不合并成一个布尔返回。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import date
|
||
|
||
from app.service.convert.errors import NavNotReady
|
||
from app.service.convert.types import to_date
|
||
|
||
#: 净值过期阈值(天)—— 与 `settings.convert_nav_stale_days` 默认值一致。
|
||
#: 纯函数不读配置,故此处只是**默认参数**,运行时以调用方传入为准。
|
||
DEFAULT_STALE_DAYS = 3
|
||
|
||
|
||
def ensure_nav_ready(nav_date: date | None, *, product_id: str | None = None) -> None:
|
||
"""无净值记录 → `NavNotReady`(503);有则原样通过。"""
|
||
if to_date(nav_date) is None:
|
||
raise NavNotReady(f"基金 {product_id!r} 尚无可用净值(无任何净值记录)")
|
||
|
||
|
||
def is_stale(
|
||
nav_date: date | None,
|
||
trade_date: date,
|
||
stale_days: int = DEFAULT_STALE_DAYS,
|
||
) -> bool:
|
||
"""净值是否过期:`(交易日 − 净值日期).days > stale_days`。
|
||
|
||
注意是 **`>`**:恰好等于阈值不算过期(与 PRD §2.2「超过此值」一致)。
|
||
|
||
`nav_date` 为 `None` 时抛 `ValueError` 而**不是**返回 `True`——
|
||
"无净值"必须走 `ensure_nav_ready()` 得到 503,混进 `stale` 分支会
|
||
变成"不阻断 + 落审计",性质完全不同。调用顺序:先 `ensure_nav_ready` 再 `is_stale`。
|
||
"""
|
||
d = to_date(nav_date)
|
||
t = to_date(trade_date)
|
||
if d is None:
|
||
raise ValueError("nav_date 为 None(无净值记录),应先调用 ensure_nav_ready()")
|
||
if t is None:
|
||
raise ValueError("trade_date 不能为 None")
|
||
return (t - d).days > stale_days
|
||
|
||
|
||
def evaluate_nav(
|
||
nav_date: date | None,
|
||
trade_date: date,
|
||
stale_days: int = DEFAULT_STALE_DAYS,
|
||
*,
|
||
product_id: str | None = None,
|
||
) -> bool:
|
||
"""便捷入口:先确保有净值(否则 503),再返回是否过期。
|
||
|
||
返回值即响应体 `nav_stale` 字段(PRD §5.3),也是 `risk_convert_detail.nav_stale`
|
||
的取值(PRD §4.1)。
|
||
"""
|
||
ensure_nav_ready(nav_date, product_id=product_id)
|
||
return is_stale(nav_date, trade_date, stale_days)
|
||
|
||
|
||
__all__ = ["DEFAULT_STALE_DAYS", "ensure_nav_ready", "is_stale", "evaluate_nav"]
|