Files
ggx/tests/test_format.py
simon fb6608193b 功能:Web 前端与报告格式化(工作区中此前未提交的工作)
说明:本提交**不是本轮会话所做**,而是工作区里此前遗留的未提交改动。
为把历史分开,先单独提交它,再提交本轮会话的修改。

包含:
- Web 前端:web/index.html、web/app.js(统一 SPA,含回测/画像/Walk-forward 页面)
- 后端接口:web/server.py 路由、web/analysis.py(新增个股分析)
- 报告层:report/format.py(新增统一数字格式化 NumFmt)、
  report/{backtest,profile,sensitivity,universe,walkforward}_report.py 接入 NumFmt、
  report/renderer.py
- 股息率口径:factor/dividend_yield.py(毛刺消除 smooth_spikes)
- 筛选:universe/selector.py、universe/filters/dividend.py
- 绩效/敏感性:analysis/performance.py、analysis/sensitivity.py
- 部署:deploy/install-service.sh
- 测试:tests/test_format.py、tests/test_dividend_smoothing.py(新增)、
  tests/test_web.py、tests/test_universe.py

提交时全量测试 403 项通过。
2026-10-04 12:47:10 +08:00

160 lines
5.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""统一数值格式化测试。
**背景**:`config/report.yml: layout.decimals` 长期是摆设 —— `ratio` 与
`money` 从未被读取,各报告模块各自硬编码小数位(7 处),
前端也把百分比写死 2 位。结果是「改了配置不生效」。
本测试钉住「配置必须真的驱动输出」。
"""
from __future__ import annotations
import pytest
from hdiv.report.format import NumFmt
# ---------------------------------------------------------------------------
# 语义:百分比小数位 = ratio - 2
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"ratio,expect",
[(2, "6%"), (4, "6.17%"), (6, "6.1715%"), (8, "6.171491%")],
)
def test_percent_decimals_derive_from_ratio(ratio: int, expect: str) -> None:
"""比率保留 ratio 位后乘 100,恰好少两位 —— 这是与用户确认的语义。"""
f = NumFmt(ratio=ratio)
assert f.pct(0.06171491) == expect
assert f.percent == max(0, ratio - 2)
def test_ratio_str_keeps_ratio_decimals() -> None:
assert NumFmt(ratio=4).ratio_str(0.06171491) == "0.0617"
assert NumFmt(ratio=6).ratio_str(0.06171491) == "0.061715"
def test_ratio_below_two_does_not_go_negative() -> None:
"""ratio=1 时百分比不能出现负小数位(会抛异常)。"""
f = NumFmt(ratio=1)
assert f.percent == 0
assert f.pct(0.0617) == "6%"
def test_missing_values_render_as_dash() -> None:
f = NumFmt()
for v in (None, float("nan")):
assert f.pct(v) == "—"
assert f.ratio_str(v) == "—"
assert f.yi(v) == "—"
def test_non_numeric_falls_back_to_str() -> None:
"""传进来已格式化的字符串不应崩,也不应二次加工。"""
f = NumFmt()
assert f.pct("已格式化") == "已格式化"
def test_pp_and_plus_signs() -> None:
f = NumFmt(ratio=4)
assert f.pct_pp(0.1211) == "+12.11pp"
assert f.pct_pp(-0.0324) == "-3.24pp"
assert f.pct(0.0401, plus=True) == "+4.01%"
def test_by_unit_dispatch() -> None:
f = NumFmt(ratio=4, money=2)
assert f.by_unit(0.0617, "pct") == "6.17%"
assert f.by_unit(123456789.0, "money") == "1.23亿"
assert f.by_unit(16.72, "years") == "17"
assert f.by_unit(0.0617, "ratio") == "0.0617"
assert f.by_unit(2838, "int") == "2,838"
# ---------------------------------------------------------------------------
# 配置必须真的驱动输出
# ---------------------------------------------------------------------------
def test_from_config_reads_report_yml() -> None:
from hdiv.core.config import load_config
cfg = load_config("report")
f = NumFmt.from_config(cfg)
d = cfg.layout.decimals
assert (f.ratio, f.money, f.price) == (d.ratio, d.money, d.price)
assert f.percent == max(0, d.ratio - 2)
def test_from_config_survives_broken_config() -> None:
"""配置不可用时回落默认值,而不是让报告生成崩掉。"""
class Boom:
@property
def layout(self):
raise RuntimeError("配置坏了")
f = NumFmt.from_config(Boom())
assert f.ratio == 4
def test_renderer_uses_config_not_hardcoded_defaults() -> None:
"""回归:渲染器的 fmt_pct 曾默认 2 位且从不读 decimals.ratio。"""
import inspect
from hdiv.report.renderer import Renderer
src = inspect.getsource(Renderer.fmt_pct)
assert "NumFmt" in src or "self.fmt" in src, "fmt_pct 应走统一格式化器"
assert ":.2f}%" not in src, "fmt_pct 不应再硬编码 2 位"
def test_no_hardcoded_percent_format_in_report_modules() -> None:
"""五个报告模块都不应再有硬编码的百分比精度。"""
from pathlib import Path
root = Path(__file__).resolve().parents[1] / "src" / "hdiv" / "report"
offenders = []
for name in ("profile_report", "backtest_report", "universe_report",
"sensitivity_report", "walkforward_report"):
text = (root / f"{name}.py").read_text(encoding="utf-8")
for i, line in enumerate(text.splitlines(), 1):
if ":.2f}%" in line or ":.2f}pp" in line:
offenders.append(f"{name}:{i}")
assert not offenders, f"仍硬编码百分比精度:{offenders}"
def test_frontend_precision_endpoint_exists() -> None:
"""前端也必须由配置驱动(曾把百分比写死 2 位)。"""
from hdiv.web.server import ROUTES
assert any(pat.match("/api/config/display") for _m, pat, _f in ROUTES), \
"缺少 /api/config/display,前端无法获知配置的显示精度"
def test_no_hardcoded_percent_anywhere_in_src() -> None:
"""整个 src/ 都不应再有硬编码的百分比精度(含接口层与 CLI 输出)。
曾散落 20 余处,其中「成交理由里的股息率」是用户直接看到的那种。
唯一允许的例外是 format.py 自身的文档说明。
"""
from pathlib import Path
root = Path(__file__).resolve().parents[1] / "src" / "hdiv"
offenders = []
for f in root.rglob("*.py"):
if f.name == "format.py":
continue
for i, line in enumerate(f.read_text(encoding="utf-8").splitlines(), 1):
if ":.2f}%" in line or "100:.2f" in line:
offenders.append(f"{f.relative_to(root)}:{i}")
assert not offenders, f"仍硬编码百分比精度:{offenders}"
def test_frontend_uses_config_precision() -> None:
from pathlib import Path
js = (Path(__file__).resolve().parents[1] / "web" / "app.js").read_text(encoding="utf-8")
assert "FMT" in js and "config/display" in js, "前端未接入配置驱动精度"
assert "FMT.percent" in js, "百分比应使用配置的 percent"