mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 23:44:16 +08:00
- 因子平台: /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 通过
111 lines
4.0 KiB
Python
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 # 样本不足
|