Files
tick-stock-panel/backend/tests/test_stats_v2.py
T
shy3130 e0cd625ef4 feat(platform): 因子平台与因子↔策略双向联动 v0.2.3
- 因子平台: /factors 一级页(检验/因子库/编辑器/组合/挖掘), DSL 公式因子(25 算子点选、双语字段、我的因子模板、脏公式守卫), 版本与生命周期, 自动挖掘 L1 统计筛选
- 因子↔策略四条桥: 触发器 Zap 快建因子条件信号、因子一键生成排名策略、自定义信号 AI 提示词接入因子分组、策略回测因子归因(胜/败单入场信号日因子均值, 独立 tab, 双语因子名)
- 回测: 统计卡新增盈亏比(≥1 红/<1 绿), 蒙卡回撤合并为中位/95% 双值卡(自适应字号), 高级设置基础过滤与策略编辑器参数对齐(5 组区间)
- 信号库独立页 /signals(原设置 tab 迁出), 持仓提醒入导航; 挖掘并入因子页第 5 tab, /mining 旧链接重定向
- 研究线配套: 因子目录 61→77(评分/矩阵双内核), stats_v2(Newey-West/BH-FDR/DSR), enriched 管道与异动/报价服务配套调整
- 文档: README 导航与特性表、features.md 因子平台章节、操作说明书 9.2、factor-platform-plan 执行状态与 §5、二开文档桥接说明; 交流与支持节改版
- 版本 0.2.2 → 0.2.3; 后端全量 1625 passed(1 例环境性跳过), 前端 build 通过
2026-09-05 15:41:15 +08:00

111 lines
4.0 KiB
Python

"""metrics_v2 统计函数测试 (P3) — 黄金参考向量 + 性质断言。
NW t 的黄金值由测试内的独立第二实现 (显式循环求 Bartlett 加权长方差) 推导,
与 stats_v2 向量化实现互为对拍。
"""
from __future__ import annotations
import math
import numpy as np
import pytest
from app.backtest.stats_v2 import (
_normal_ppf,
bh_fdr_qvalues,
deflated_sharpe_psr,
expected_max_sharpe,
naive_t,
newey_west_t,
normal_two_sided_p,
)
def _nw_t_reference(values: list[float], lag: int) -> float:
"""独立第二实现: 显式循环按定义计算 Bartlett 核 HAC t 值。"""
n = len(values)
mean = sum(values) / n
centered = [value - mean for value in values]
gamma = [
sum(centered[i] * centered[i + lag_i] for i in range(n - lag_i)) / n
for lag_i in range(lag + 1)
]
long_var = gamma[0]
for lag_i in range(1, lag + 1):
long_var += 2.0 * (1.0 - lag_i / (lag + 1)) * gamma[lag_i]
se = math.sqrt(long_var / n)
return mean / se
def test_newey_west_matches_reference() -> None:
rng = np.random.default_rng(42)
values = np.cumsum(rng.normal(0, 0.01, 60)).tolist() # 高自相关
for lag in (1, 3, 5):
result = newey_west_t(values, lag)
assert result is not None
t_stat, mean, se = result
assert t_stat == pytest.approx(_nw_t_reference(values, lag), rel=1e-9)
assert mean == pytest.approx(float(np.mean(values)))
assert se > 0
def test_newey_west_deflates_autocorrelated_t() -> None:
# 强正自相关序列: NW t 的绝对值必须小于朴素 t (自相关被正确惩罚)
rng = np.random.default_rng(7)
phi = 0.9
values, last = [], 0.0
for shock in rng.normal(0, 0.01, 500):
last = phi * last + shock
values.append(last)
t_naive = naive_t(values)
result = newey_west_t(values, lag=5)
assert t_naive is not None and result is not None
assert abs(result[0]) < abs(t_naive)
def test_newey_west_insufficient_samples() -> None:
assert newey_west_t([0.1, 0.2], lag=1) is None
assert newey_west_t([], lag=1) is None
assert newey_west_t([1.0] * 20, lag=1) is None # 零方差
def test_bh_fdr_golden() -> None:
# 经典 BH 示例 (Wikipedia): q = [.005, .02, .042, .042, .042]
pvalues = [0.001, 0.008, 0.039, 0.041, 0.042]
assert bh_fdr_qvalues(pvalues) == pytest.approx([0.005, 0.02, 0.042, 0.042, 0.042])
# 乱序输入: q 值跟随原位置 (m=3: .042→r3 raw .042; .001→.003; .039→min(.0585,.042)=.042)
assert bh_fdr_qvalues([0.042, 0.001, 0.039]) == pytest.approx([0.042, 0.003, 0.042])
# None 透传
assert bh_fdr_qvalues([None, 0.05]) == [None, 0.05]
def test_normal_p_and_ppf_inverse() -> None:
assert normal_two_sided_p(1.959964) == pytest.approx(0.05, abs=1e-6)
assert normal_two_sided_p(0.0) == pytest.approx(1.0)
assert _normal_ppf(0.975) == pytest.approx(1.959964, abs=1e-6)
assert _normal_ppf(0.5) == pytest.approx(0.0, abs=1e-9)
with pytest.raises(ValueError):
_normal_ppf(0.0)
def test_expected_max_sharpe_monotone() -> None:
assert expected_max_sharpe(1, 0.04) == 0.0 # 单试验不校正
assert expected_max_sharpe(10, 0.0) == 0.0
# 试验数越多期望最大夏普越高 (越难超越)
em_10 = expected_max_sharpe(10, 0.04)
em_100 = expected_max_sharpe(100, 0.04)
assert 0 < em_10 < em_100
def test_deflated_sharpe_psr() -> None:
# 无偏斜无超额峰度时退化为 Φ(SR * sqrt(n-1))
probability = deflated_sharpe_psr(sharpe=0.1, n_obs=2500, skewness=0.0, kurtosis=3.0, expected_max_sharpe=0.0)
assert probability == pytest.approx(0.5 * (1 + math.erf(0.1 * math.sqrt(2499) / math.sqrt(2))))
# 校正项抬高分母会降低 PSR
penalized = deflated_sharpe_psr(0.1, 2500, skewness=0.0, kurtosis=10.0)
assert penalized < probability
# EM 校正降低显著性
deflated = deflated_sharpe_psr(0.1, 2500, 0.0, 3.0, expected_max_sharpe=0.08)
assert deflated < probability
assert deflated_sharpe_psr(0.1, 3) is None # 样本不足