fix(dupont): reject out-of-domain window + bidirectional clip + suffix discipline (R1-R4/M2)
ci/woodpecker/push/main Pipeline failed
ci/woodpecker/push/main Pipeline failed
Per finance ruling 2026-10-05 (project-20261005-dupont-oob-window-fix): - R1: no intersection with available month domain => explicit error (endpoint 400), no window fields - R2: with intersection => intersection window, months>=1 (kills inversion and months<=0) - R3: quarter/half/... suffix kept only when returned window == nominal window (clipped flag) - R4: symmetric lower-bound clip note; M2: TTM nominal window = [anchor-11, anchor] (12 months) - M1: docstring correction (lower-bound overflow is clipped, not 400) No push / no deploy / no restart / no DB write.
This commit is contained in:
@@ -23,7 +23,7 @@ from app.utils.deviation_engine import calc_period_deviation, calc_period_diff,
|
||||
from app.utils.alert_entity_scope import month_period_clause
|
||||
from app.utils.profitability_params import load_profitability_params
|
||||
from app.utils.dupont_basis import (
|
||||
annualize_factors, available_months, build_window_meta, check_identity,
|
||||
annualize_factors, apply_period_suffix, available_months, build_window_meta, check_identity,
|
||||
compute_factors, latest_stock_month, load_dupont_params, months_between, prev_month,
|
||||
resolve_window, stock_balance, sum_flows,
|
||||
)
|
||||
@@ -2330,9 +2330,9 @@ def get_dupont_analysis(
|
||||
name_row = db.execute(
|
||||
text("SELECT name FROM entities WHERE id = :eid"), {"eid": entity_id}).first()
|
||||
entity_name = (name_row[0] if name_row else None) or ("陕西酣客文化传媒" if slug == "hanke" else "陕西博海网络科技")
|
||||
suffix = {"ytd": "(YTD 年初至今)", "ttm": "(TTM 滚动 12 月)", "month": "(单月)",
|
||||
"quarter": "(季度)", "half": "(半年)", "year": "(全年)", "custom": "(自定义区间)"}
|
||||
label = win["label"] + suffix.get(win["period_key"], "")
|
||||
# R3 后缀纪律:仅在「返回窗口 == 名义窗口」(未裁剪)时保留「(季度)/(半年)…」后缀;
|
||||
# 发生任何裁剪 ⇒ 去后缀,period = 实际窗口标签(判据由 resolve_window 回传 `clipped`)。
|
||||
label = apply_period_suffix(win["label"], win["period_key"], bool(win.get("clipped")))
|
||||
|
||||
cur = _dupont_window_values(db, entity_id, caliber, win["period_start"], win["period_end"])
|
||||
ann = annualize_factors(dict(cur["factors"]), win["months"], annualize)
|
||||
|
||||
@@ -31,7 +31,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
@@ -152,6 +152,24 @@ def _fmt_cn(start: str, end: str) -> str:
|
||||
return f"{start[:4]}年{int(start[5:7])}-{int(end[5:7])}月"
|
||||
|
||||
|
||||
#: 区间后缀(R3:仅在「返回窗口 == 名义窗口」时保留 —— 统一经 `apply_period_suffix` 施加)
|
||||
PERIOD_SUFFIX: Dict[str, str] = {
|
||||
"ytd": "(YTD 年初至今)", "ttm": "(TTM 滚动 12 月)", "month": "(单月)",
|
||||
"quarter": "(季度)", "half": "(半年)", "year": "(全年)", "custom": "(自定义区间)",
|
||||
}
|
||||
|
||||
|
||||
def apply_period_suffix(label: str, period_key: Optional[str], clipped: bool = False) -> str:
|
||||
"""R3 后缀纪律:只在**未发生裁剪**(返回窗口 == 名义窗口)时保留「(季度)/(半年)…」。
|
||||
|
||||
发生任何裁剪 ⇒ 去后缀(`label` 已是实际窗口的 `_fmt_cn`)。`period_key` 只表**请求形态**,
|
||||
**不等于**返回窗口类型(返回窗口类型由 `months` + `period` 标签表达)。
|
||||
"""
|
||||
if clipped:
|
||||
return label
|
||||
return f"{label}{PERIOD_SUFFIX.get(period_key or '', '')}"
|
||||
|
||||
|
||||
def resolve_window(period: Optional[str], start: Optional[str], end: Optional[str],
|
||||
available: List[str], preset: Optional[str] = None,
|
||||
today: Optional[str] = None, anchor: Optional[str] = None) -> Dict[str, Any]:
|
||||
@@ -161,21 +179,28 @@ def resolve_window(period: Optional[str], start: Optional[str], end: Optional[st
|
||||
可用月域 `available`(升序 YYYY-MM 列表)用于:① 缺省锚定;② 区间裁剪。
|
||||
`anchor` = 默认区间的锚月(端点传入「存量最新月」`latest_stock_month`,缺省取 available 末月)。
|
||||
|
||||
⚠️ 裁剪纪律:区间上界**只裁不扩**——超出可用月域即裁到可用域(并在 `clip_note` 留痕),
|
||||
绝不因裁剪而静默改变用户已选下界(下界越界则显式 400,见 `validate`)。
|
||||
裁剪纪律(财务侧 2026-10-05 口径裁定 R1–R4,件
|
||||
`_reports/project-20261005-dupont-oob-ruling-finance.md`):
|
||||
· **R1 无交集 ⇒ 显式拒绝**:`e < avail_start` 或 `s > latest` ⇒ 返回 `error`
|
||||
(调用侧转 HTTP 400),**不返回任何窗口/数值字段**(不得以 null/空串占位);
|
||||
· **R2 有交集 ⇒ 双向裁剪**:窗口 = 交集 `[max(s, avail_start), min(e, latest)]`(只裁不扩),
|
||||
`months` = 实际月数 ⇒ 恒 ≥ 1(消灭 `months = 0/负数` 与窗口倒挂);
|
||||
· **R3 后缀纪律**:是否允许调用侧挂「(季度)/(半年)…」后缀 = `clipped` 字段
|
||||
(未裁剪才保留;裁剪后 `period` = 实际窗口标签);
|
||||
· **R4 裁剪留痕**:见 `clip_note`(上界沿用既有文案;下界为对称文案;两端同时裁剪则并存)。
|
||||
|
||||
⚠️ 更正(2026-10-05,同裁定 M1):下界越界**不是**无条件 400 —— 「有交集但下界越界」
|
||||
= 裁到 `avail_start` + 留痕 + 去后缀(R2/R3/R4);**只有「整区间与可用月域无交集」才 400**。
|
||||
旧 docstring 声称「下界越界则显式 400,见 `validate`」,而本模块从未有 `validate` 函数。
|
||||
"""
|
||||
avail = sorted(available or [])
|
||||
latest = avail[-1] if avail else None
|
||||
avail_start = avail[0] if avail else None
|
||||
today = today or datetime.now().strftime("%Y-%m")
|
||||
anchor = min(anchor or latest or today, today)
|
||||
if latest:
|
||||
anchor = min(anchor, latest)
|
||||
|
||||
def clip_end(e: str) -> Tuple[str, Optional[str]]:
|
||||
if latest and e > latest:
|
||||
return latest, f"上界 {e} 超出可用月域(最新 {latest}),已裁至可用域"
|
||||
return e, None
|
||||
|
||||
p = (period or "").strip().lower()
|
||||
if preset:
|
||||
p = preset.strip().lower()
|
||||
@@ -188,8 +213,12 @@ def resolve_window(period: Optional[str], start: Optional[str], end: Optional[st
|
||||
elif p in ("ttm",):
|
||||
if not anchor:
|
||||
return {"error": "无可用数据月域,无法解析 TTM 区间"}
|
||||
# M2(同裁定 §4):TTM 名义区间 = [anchor−11, anchor](**12 个月**)。
|
||||
# 历史实现取 anchor 的「去年同月」(如 anchor=2026-08 ⇒ 2025-08)= 13 个月,与后缀
|
||||
# 「(TTM 滚动 12 月)」自相矛盾,且 `months` 进年化分母 ⇒ 年化被低估 12/13 ≈ 7.7%。
|
||||
y, m = int(anchor[:4]), int(anchor[5:7])
|
||||
s = f"{y - 1}-{m:02d}" if m != 12 else f"{y}-01"
|
||||
idx = y * 12 + (m - 1) - 11
|
||||
s = f"{idx // 12}-{idx % 12 + 1:02d}"
|
||||
e = anchor
|
||||
key = "ttm"
|
||||
elif p in ("custom",) or start or end:
|
||||
@@ -214,13 +243,32 @@ def resolve_window(period: Optional[str], start: Optional[str], end: Optional[st
|
||||
else:
|
||||
return {"error": f"无法识别的区间参数 period={period!r}(支持 ytd/ttm/YYYY-MM/YYYY-Qn/YYYY-Hn/YYYY/custom)"}
|
||||
|
||||
e, cn = clip_end(e)
|
||||
note = cn
|
||||
months = months_between(s, e)
|
||||
# ── R1/R2:与可用月域求交集(无交集 ⇒ 显式拒绝;有交集 ⇒ 双向裁剪) ──────────
|
||||
if not avail:
|
||||
return {"error": "无可用数据月域,无法计算区间指标"}
|
||||
latest = avail[-1]
|
||||
avail_start = avail[0]
|
||||
if e < avail_start or s > latest:
|
||||
return {"error": f"请求区间 {s}~{e} 与可用月域 {avail_start}~{latest} 无交集,无法计算区间指标"}
|
||||
notes: List[str] = []
|
||||
lo, hi = s, e
|
||||
if s < avail_start:
|
||||
lo = avail_start
|
||||
notes.append(f"下界 {s} 早于可用月域起点({avail_start}),已裁至可用域")
|
||||
if e > latest:
|
||||
hi = latest
|
||||
notes.append(f"上界 {e} 超出可用月域(最新 {latest}),已裁至可用域")
|
||||
if lo > hi: # 名义区间自身颠倒(如 custom start > end)——不得产出倒挂窗口
|
||||
return {"error": f"请求区间 {s}~{e} 与可用月域 {avail_start}~{latest} 无交集,无法计算区间指标"}
|
||||
clipped = (lo, hi) != (s, e)
|
||||
months = months_between(lo, hi) # R2 ⇒ 恒 ≥ 1
|
||||
return {
|
||||
"period_key": key, "period_start": s, "period_end": e, "months": months,
|
||||
"label": _fmt_cn(s, e), "clip_note": note,
|
||||
"available_start": avail[0] if avail else None, "available_end": latest,
|
||||
"period_key": key, "period_start": lo, "period_end": hi, "months": months,
|
||||
"label": _fmt_cn(lo, hi),
|
||||
"nominal_start": s, "nominal_end": e, "nominal_months": months_between(s, e),
|
||||
"clipped": clipped,
|
||||
"clip_note": ";".join(notes) if notes else None,
|
||||
"available_start": avail_start, "available_end": latest,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""杜邦区间窗口「完全越出可用月域」回归用例(2026-10-05 口径裁定 R1–R4 + M2)。
|
||||
|
||||
裁定件(财物侧口径,执行前必读):
|
||||
/srv/hermes/mailbox/finance/pending-tasks/_reports/project-20261005-dupont-oob-ruling-finance.md
|
||||
施工单: project-20261005-dupont-oob-window-fix-fullstack.md
|
||||
缺陷(改前实测,部署树 HEAD 3ed9fa0):
|
||||
· `?period=2026-Q4` → 200 / period_start=2026-10 > period_end=2026-09 / months=0 /
|
||||
label「2026年10-9月(季度)」/ 四因子全 None(**窗口倒挂 + 名号不符 + 静默空数据**);
|
||||
· `?period=2026-H2` → 裁至 2026-07..09(实 3 个月)却仍标「(半年)」。
|
||||
|
||||
裁定后口径(本文件逐条断言):
|
||||
· R1 整区间与可用月域 D 无交集 ⇒ `error`(端点转 400),**不返回任何窗口/数值字段**;
|
||||
· R2 有交集 ⇒ 窗口 = 交集(双向裁剪),`months` = 实际月数 ≥ 1(不倒挂);
|
||||
· R3 后缀((季度)/(半年)…)仅在「返回窗口 == 名义窗口」时保留 ⇒ 由 `clipped` 判;
|
||||
· R4 裁剪留痕:上界沿用既有文案;下界为对称文案;
|
||||
· M2 TTM 名义区间 = [anchor−11, anchor] = 12 个月。
|
||||
|
||||
可用月域取裁定 §2 场景:D = 2026-01 ~ 2026-09(latest=2026-09)。
|
||||
本文件只测纯函数(`resolve_window` / `apply_period_suffix`),不连库、不写盘、不碰服务。
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from app.utils import dupont_basis as _du
|
||||
from app.utils.dupont_basis import months_between, resolve_window
|
||||
|
||||
|
||||
def apply_period_suffix(label, period_key, clipped=False):
|
||||
"""R3 判据入口(经模块属性取,便于负对照)。
|
||||
|
||||
改前件(origin/main 3ed9fa0)**没有** `apply_period_suffix` ⇒ 走显式断言失败
|
||||
(AssertionError,而不是 import 期 ImportError 导致整个用例文件收集失败),
|
||||
这样「把实现换回改前版本」时这些断言会以**可读的失败**呈现。
|
||||
"""
|
||||
fn = getattr(_du, "apply_period_suffix", None)
|
||||
assert fn is not None, ("resolve_window/dupont_basis 未落地 R3 后缀纪律(clipped 判据)"
|
||||
"—— 改前件预期失败")
|
||||
return fn(label, period_key, clipped)
|
||||
|
||||
|
||||
#: 裁定 §2/§4 场景可用月域(latest = 2026-09,博海账套实测形态)
|
||||
D_AVAIL = [f"2026-{m:02d}" for m in range(1, 10)]
|
||||
|
||||
#: 整区间无交集的统一 400 文案(端点直接把它作为 `detail`,前端零改动可正确呈现)
|
||||
Q4_DETAIL = "请求区间 2026-10~2026-12 与可用月域 2026-01~2026-09 无交集,无法计算区间指标"
|
||||
|
||||
|
||||
def rwd(period, start=None, end=None, **kw):
|
||||
kw.setdefault("today", "2026-10")
|
||||
return resolve_window(period, start, end, D_AVAIL, **kw)
|
||||
|
||||
|
||||
# ── R1:整区间与可用月域无交集 ⇒ 显式拒绝(不返回窗口/数值字段) ──────────────
|
||||
|
||||
def test_q4_out_of_domain_rejected_with_exact_detail():
|
||||
"""`?period=2026-Q4`(A=[2026-10,2026-12] 与 D 无交集)⇒ error,文案逐字符合裁定 §2.1。"""
|
||||
w = rwd("2026-Q4")
|
||||
assert w.get("error") == Q4_DETAIL, w
|
||||
# 不得以 null/空串占位:窗口/月数/标签/裁剪留痕一律**不存在**
|
||||
for k in ("period_start", "period_end", "months", "months_label", "label",
|
||||
"period", "clip_note", "nominal_months", "clipped"):
|
||||
assert k not in w, (k, w)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("period,start,end", [
|
||||
("2026-Q4", None, None), # 未来侧整季度越界
|
||||
("2026-10", None, None), # 未来侧单月越界
|
||||
("2027", None, None), # 未来侧整年越界
|
||||
("2025-Q1", None, None), # 历史侧整季度(域前)
|
||||
("2025-Q4", None, None), # 历史侧整季度(域前)
|
||||
("custom", "2026-10", "2026-12"), # custom 未来侧整段
|
||||
("custom", "2026-11", "2026-11"), # custom 未来侧单月(改前 months=-1)
|
||||
("custom", "2025-01", "2025-12"), # custom 历史侧整段
|
||||
])
|
||||
def test_all_no_intersection_forms_rejected(period, start, end):
|
||||
w = rwd(period, start, end)
|
||||
assert w.get("error"), (period, start, end, w)
|
||||
assert "无交集" in w["error"]
|
||||
assert "period_start" not in w and "months" not in w
|
||||
|
||||
|
||||
def test_nominal_inverted_custom_never_returns_inverted_window():
|
||||
"""名义区间自身颠倒(custom start > end)⇒ 拒绝,绝不产出倒挂窗口。"""
|
||||
w = rwd("custom", "2026-05", "2026-03")
|
||||
assert w.get("error"), w
|
||||
assert "period_start" not in w
|
||||
|
||||
|
||||
# ── R2 + R3 + R4:有交集 ⇒ 双向裁剪 + 月数/标签如实 ──────────────────────────
|
||||
|
||||
def test_h2_clips_upper_bound_label_and_months():
|
||||
"""`?period=2026-H2` ⇒ 窗口 [2026-07,2026-09]、months=3、标签无「(半年)」、上界留痕。"""
|
||||
w = rwd("2026-H2")
|
||||
assert "error" not in w
|
||||
assert w["period_key"] == "half" # 请求形态不变(向后兼容)
|
||||
assert (w["period_start"], w["period_end"]) == ("2026-07", "2026-09")
|
||||
assert w["months"] == 3
|
||||
assert w["label"] == "2026年7-9月"
|
||||
assert w["nominal_start"] == "2026-07" and w["nominal_end"] == "2026-12"
|
||||
assert w["nominal_months"] == 6
|
||||
assert w["clipped"] is True
|
||||
assert w["clip_note"] == "上界 2026-12 超出可用月域(最新 2026-09),已裁至可用域"
|
||||
# R3:发生裁剪 ⇒ 去后缀(裁定 §2.2 期望标签「2026年7-9月」,不得带「(半年)」)
|
||||
assert apply_period_suffix(w["label"], w["period_key"], w["clipped"]) == "2026年7-9月"
|
||||
assert "半年" not in apply_period_suffix(w["label"], w["period_key"], w["clipped"])
|
||||
|
||||
|
||||
def test_h2_window_equals_q3_window():
|
||||
"""强判据(裁定 §2.2):同一返回窗口 [2026-07,2026-09] ⇒ H2 与 Q3 窗口逐项相同。"""
|
||||
h2, q3 = rwd("2026-H2"), rwd("2026-Q3")
|
||||
assert "error" not in h2 and "error" not in q3
|
||||
for k in ("period_start", "period_end", "months", "label"):
|
||||
assert h2[k] == q3[k], (k, h2[k], q3[k])
|
||||
|
||||
|
||||
def test_full_year_clips_lower_upper_and_label():
|
||||
"""`?period=2026`(全年)⇒ [2026-01,2026-09]、months=9、标签无「(全年)」。"""
|
||||
w = rwd("2026")
|
||||
assert (w["period_start"], w["period_end"], w["months"]) == ("2026-01", "2026-09", 9)
|
||||
assert w["label"] == "2026年1-9月"
|
||||
assert w["clipped"] is True
|
||||
assert apply_period_suffix(w["label"], w["period_key"], w["clipped"]) == "2026年1-9月"
|
||||
|
||||
|
||||
def test_custom_lower_bound_clip_note_symmetric():
|
||||
"""R4:下界越界(域起点晚于名义起点)⇒ 裁到 avail_start + 对称留痕 + 去后缀。"""
|
||||
w = rwd("custom", "2025-01", "2026-06")
|
||||
assert (w["period_start"], w["period_end"], w["months"]) == ("2026-01", "2026-06", 6)
|
||||
assert w["clipped"] is True
|
||||
assert w["clip_note"] == "下界 2025-01 早于可用月域起点(2026-01),已裁至可用域"
|
||||
assert apply_period_suffix(w["label"], w["period_key"], w["clipped"]) == "2026年1-6月"
|
||||
|
||||
|
||||
def test_ttm_nominal_12_months_and_lower_clip():
|
||||
"""M2:TTM 名义 12 个月;域内 ⇒ 下界裁剪 + 去「(TTM 滚动 12 月)」后缀。"""
|
||||
nom = resolve_window("ttm", None, None, [f"2025-{m:02d}" for m in range(1, 13)]
|
||||
+ [f"2026-{m:02d}" for m in range(1, 13)], anchor="2026-08")
|
||||
assert (nom["period_start"], nom["period_end"], nom["nominal_months"]) == (
|
||||
"2025-09", "2026-08", 12)
|
||||
|
||||
w = rwd("ttm", anchor="2026-08")
|
||||
assert (w["period_start"], w["period_end"], w["months"]) == ("2026-01", "2026-08", 8)
|
||||
assert w["clipped"] is True
|
||||
assert w["clip_note"] == "下界 2025-09 早于可用月域起点(2026-01),已裁至可用域"
|
||||
assert apply_period_suffix(w["label"], w["period_key"], w["clipped"]) == "2026年1-8月"
|
||||
|
||||
|
||||
def test_ytd_unclipped_keeps_suffix():
|
||||
"""未裁剪(返回窗口 == 名义窗口)⇒ 后缀保留(R3 的另一半)。"""
|
||||
w = rwd("ytd", anchor="2026-08")
|
||||
assert w["clipped"] is False
|
||||
assert w["clip_note"] is None
|
||||
assert apply_period_suffix(w["label"], w["period_key"], w["clipped"]) == "2026年1-8月(YTD 年初至今)"
|
||||
|
||||
|
||||
# ── R3 后缀纪律(纯函数) ─────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.parametrize("label,key,unclipped,clipped", [
|
||||
("2026年1-6月", "half", "2026年1-6月(半年)", "2026年1-6月"),
|
||||
("2026年1-3月", "quarter", "2026年1-3月(季度)", "2026年1-3月"),
|
||||
("2026年1-12月", "year", "2026年1-12月(全年)", "2026年1-12月"),
|
||||
("2026年8月", "month", "2026年8月(单月)", "2026年8月"),
|
||||
("2026年1-8月", "ytd", "2026年1-8月(YTD 年初至今)", "2026年1-8月"),
|
||||
("2025年9月-2026年8月", "ttm", "2025年9月-2026年8月(TTM 滚动 12 月)", "2025年9月-2026年8月"),
|
||||
("2026年6-9月", "custom", "2026年6-9月(自定义区间)", "2026年6-9月"),
|
||||
])
|
||||
def test_apply_period_suffix_discipline(label, key, unclipped, clipped):
|
||||
"""R3:未裁剪 ⇒ 保留后缀;发生任何裁剪 ⇒ 去后缀。"""
|
||||
assert apply_period_suffix(label, key, False) == unclipped
|
||||
assert apply_period_suffix(label, key, True) == clipped
|
||||
|
||||
|
||||
# ── 全形态矩阵:任意入参下不得倒挂、months 与标签自洽 ────────────────────────
|
||||
|
||||
@pytest.mark.parametrize("period,start,end", [
|
||||
("2026-Q1", None, None), ("2026-Q3", None, None), ("2026-Q4", None, None),
|
||||
("2026-H1", None, None), ("2026-H2", None, None),
|
||||
("2026-08", None, None), ("2026-10", None, None),
|
||||
("2026", None, None), ("2026-01", None, None), ("2027", None, None),
|
||||
("2025-Q1", None, None), ("2025-Q4", None, None),
|
||||
("ytd", None, None), ("ttm", None, None),
|
||||
("custom", "2026-06", "2026-12"), ("custom", "2026-10", "2026-12"),
|
||||
("custom", "2026-11", "2026-11"), ("custom", "2025-01", "2026-06"),
|
||||
])
|
||||
def test_matrix_no_inversion_and_self_consistency(period, start, end):
|
||||
w = rwd(period, start, end, anchor="2026-08")
|
||||
if w.get("error"):
|
||||
assert "无交集" in w["error"] or "无可用" in w["error"], w
|
||||
return
|
||||
assert w["period_start"] <= w["period_end"], w
|
||||
assert w["months"] == months_between(w["period_start"], w["period_end"]), w
|
||||
assert w["months"] >= 1, w
|
||||
assert D_AVAIL[0] <= w["period_start"] <= w["period_end"] <= D_AVAIL[-1], w
|
||||
assert w["clipped"] is (w["nominal_months"] != w["months"]), w
|
||||
|
||||
|
||||
def test_clip_note_present_iff_clipped():
|
||||
for period, start, end in [("2026-Q3", None, None), ("2026-H2", None, None),
|
||||
("2026", None, None), ("2025-Q1", None, None),
|
||||
("custom", "2025-01", "2026-06")]:
|
||||
w = rwd(period, start, end)
|
||||
if w.get("error"):
|
||||
assert "clip_note" not in w
|
||||
continue
|
||||
assert (w["clip_note"] is not None) is w["clipped"], (period, w)
|
||||
@@ -100,10 +100,14 @@ def test_ytd_ttm_unchanged():
|
||||
assert (ytd["period_start"], ytd["period_end"]) == ("2026-01", "2026-10")
|
||||
assert ytd["months"] == 10
|
||||
|
||||
# M2(2026-10-05 裁定 §4):TTM 名义区间 = [anchor−11, anchor] = **12 个月**。
|
||||
# 历史断言写的是 13 个月(anchor 的「去年同月」),即被裁定为缺陷的口径,见
|
||||
# test_dupont_oob_window_ruling_20261005.py / 本文件 §裁定回归。
|
||||
ttm = rw("ttm", anchor="2026-10")
|
||||
assert ttm["period_key"] == "ttm"
|
||||
assert (ttm["period_start"], ttm["period_end"]) == ("2025-10", "2026-10")
|
||||
assert ttm["months"] == 13
|
||||
assert (ttm["period_start"], ttm["period_end"]) == ("2025-11", "2026-10")
|
||||
assert ttm["nominal_months"] == 12
|
||||
assert ttm["months"] == 12
|
||||
|
||||
|
||||
def test_custom_unchanged():
|
||||
|
||||
Reference in New Issue
Block a user