功能: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 项通过。
This commit is contained in:
2026-10-04 12:47:10 +08:00
parent fce725e13c
commit fb6608193b
21 changed files with 2579 additions and 125 deletions
+59
View File
@@ -560,3 +560,62 @@ def test_dividend_records_include_base_share() -> None:
src = inspect.getsource(Repo.dividend_records)
assert "base_share" in src, "dividend_records 必须选出 base_share"
# ---------------------------------------------------------------------------
# 重跑覆盖同一条记录
# ---------------------------------------------------------------------------
def test_universe_run_id_is_deterministic() -> None:
"""回归:run_id 不得含时间戳,否则同参数重跑会不断累积重复记录。
早期实现把 datetime.now() 编进指纹,同一 asof 最多累积了 11 条内容相同的记录。
现在的语义是「同一份配置 + 同一时点 → 同一个 run_id → 重跑原地覆盖」。
"""
import inspect
from hdiv.universe import selector
src = inspect.getsource(selector.UniverseSelector)
i = src.find("run_id = stable_id(")
assert i != -1, "未找到 run_id 生成处"
# 取到该语句结束的分号行(不能用第一个 ')',那会截断在 config_hash(self.config) 里)
end = src.find("\n )", i)
assert end != -1, "未找到 run_id 语句结尾"
block = src[i:end]
assert "datetime.now" not in block, f"run_id 指纹仍含时间戳:{block}"
for must in ("config_hash", "effective", "self.config.name"):
assert must in block, f"run_id 指纹缺少 {must}:{block}"
@pytest.mark.db
def test_universe_rerun_overwrites_same_record() -> None:
"""同参数重跑不新增记录,且成员行数等于候选数(无重复堆积)。"""
from hdiv.core.config import load_config
from hdiv.data import db
from hdiv.data.sync.base import stable_id
db.load_dotenv_once()
cfg = load_config("datasource")
df = db.read_sql(
"SELECT r.run_id, r.candidate_count, r.asof_date, r.config_hash, r.name, "
" COUNT(m.id) AS member_rows "
"FROM hd_universe_run r LEFT JOIN hd_universe_member m ON m.run_id = r.run_id "
"GROUP BY r.run_id HAVING member_rows > 0 "
"ORDER BY r.created_at DESC LIMIT 5",
cfg=cfg,
)
if df.empty:
pytest.skip("没有筛选记录")
checked = 0
for _, r in df.iterrows():
expect = stable_id("universe", r["name"], str(r["asof_date"]), r["config_hash"])
if r["run_id"] != expect:
continue # 确定化之前的历史记录,跳过
checked += 1
assert int(r["member_rows"]) == int(r["candidate_count"]), (
f"run_id={r['run_id'][:10]} 成员行数 {r['member_rows']} "
f"应等于候选数 {r['candidate_count']}(出现重复堆积)"
)
assert checked > 0, "未找到确定化之后生成的筛选记录,无法验证"