功能: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:
@@ -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, "未找到确定化之后生成的筛选记录,无法验证"
|
||||
|
||||
Reference in New Issue
Block a user