66 lines
2.5 KiB
Python
66 lines
2.5 KiB
Python
"""持有期 → 赎回费率查表(架构 §7 · D11 数据驱动)。
|
||||
|
|
|
|||
|
|
规则来自 `core_fee_rule`(`fee_type='redeem'`),**本模块不查库** ——
|
|||
|
|
调用方(`core_ro.get_redeem_fee_rules(product_id)`)取回规则行后传入。
|
|||
|
|
|
|||
|
|
**区间口径:左闭右开 `[min_hold_days, max_hold_days)`**,`max_hold_days = NULL` 表示无上限。
|
|||
|
|
与 `scripts/core/07-seed-fee-rule.sql`、架构 §7 分档表三处一致:
|
|||
|
|
|
|||
|
|
| 档 | 区间 | 费率(22 号文 §10 下限) |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| < 7 日 | `[0, 7)` | 0.0150 |
|
|||
|
|
| 7–30 日 | `[7, 30)` | 0.0100 |
|
|||
|
|
| 30–180 日 | `[30, 180)` | 0.0050 |
|
|||
|
|
| 180–365 日 | `[180, 365)` | 0.0025 |
|
|||
|
|
| ≥ 365 日 | `[365, NULL)` | 0.0000 |
|
|||
|
|
|
|||
|
|
边界语义举例(PRD §12:**满 7 日归 7–30 档**):`hold_days = 6` → 1.5%;
|
|||
|
|
`hold_days = 7` → 1.0%;`hold_days = 365` → 0。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from decimal import Decimal
|
|||
|
|
from typing import Iterable, Sequence
|
|||
|
|
|
|||
|
|
from app.service.convert.errors import FeeRuleMissing
|
|||
|
|
from app.service.convert.types import FeeRule, to_decimal
|
|||
|
|
|
|||
|
|
|
|||
|
|
def matches(rule: FeeRule, hold_days: int) -> bool:
|
|||
|
|
"""单条规则是否命中(左闭右开;`max_hold_days is None` 表示无上限)。"""
|
|||
|
|
if hold_days < rule.min_hold_days:
|
|||
|
|
return False
|
|||
|
|
return rule.max_hold_days is None or hold_days < rule.max_hold_days
|
|||
|
|
|
|||
|
|
|
|||
|
|
def pick_fee_rate(
|
|||
|
|
rules: Sequence[FeeRule] | Iterable[FeeRule],
|
|||
|
|
hold_days: int,
|
|||
|
|
*,
|
|||
|
|
product_id: str | None = None,
|
|||
|
|
) -> Decimal:
|
|||
|
|
"""按持有天数取赎回费率;**无命中即数据缺失,直接失败**(不降级为 0)。
|
|||
|
|
|
|||
|
|
多档同时命中时取 `min_hold_days` **最大**的一档(最具体的区间)——
|
|||
|
|
真实费率表本不应重叠,这条规则是防止种子误配出重叠区间时"取到哪档看运气"。
|
|||
|
|
|
|||
|
|
⚠️ 为什么无命中不能返回 0:静默按 0 计费会**少收赎回费且不留痕**
|
|||
|
|
(客户与基金财产双向损失),比直接失败危险得多。费率表必含 `[0, 7)` 档,
|
|||
|
|
走到这里说明种子漏灌或产品未配档 → `FeeRuleMissing`(500,内部兜底)。
|
|||
|
|
"""
|
|||
|
|
candidates = [
|
|||
|
|
rule
|
|||
|
|
for rule in rules
|
|||
|
|
if rule.fee_type == "redeem" and matches(rule, hold_days)
|
|||
|
|
]
|
|||
|
|
if not candidates:
|
|||
|
|
raise FeeRuleMissing(
|
|||
|
|
f"未匹配到赎回费率档:product_id={product_id!r},持有 {hold_days} 天"
|
|||
|
|
)
|
|||
|
|
best = max(candidates, key=lambda rule: rule.min_hold_days)
|
|||
|
|
return to_decimal(best.rate)
|
|||
|
|
|
|||
|
|
|
|||
|
|
__all__ = ["matches", "pick_fee_rate"]
|