mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 16:44:15 +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 通过
148 lines
6.0 KiB
Python
148 lines
6.0 KiB
Python
"""metrics_v2 统计函数 (P3) — Newey-West HAC t 值 / BH-FDR q 值 / DSR。
|
|
|
|
运行时零新增第三方依赖 (后端无 scipy/statsmodels), 全部 numpy 手写;
|
|
数值测试用固定黄金参考向量锁定 (tests/test_stats_v2.py)。
|
|
|
|
口径 (设计文档 factor-system-design.md §6):
|
|
- IC 序列因 h 日前瞻收益存在 h-1 阶移动平均自相关, 主口径 t 值取 NW HAC, 滞后 L=h。
|
|
- 多因子批量检验按 Benjamini-Hochberg 步进法控制 FDR。
|
|
- DSR (Deflated Sharpe Ratio, Bailey & Lopez de Prado 2014) 用于多重试验校正后的
|
|
夏普显著性; 期望最大夏普 EM = sqrt(V[SR]) * ((1-gamma)Φ^-1(1-1/N) + gammaΦ^-1(1-1/(Ne)))
|
|
其中 gamma 为欧拉-马歇罗尼常数。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
import numpy as np
|
|
|
|
EULER_GAMMA = 0.5772156649015329
|
|
|
|
|
|
def _clean_values(values) -> np.ndarray:
|
|
array = np.asarray([value for value in values if value is not None and np.isfinite(value)], dtype=float)
|
|
return array
|
|
|
|
|
|
def newey_west_t(values, lag: int) -> tuple[float, float, float] | None:
|
|
"""Newey-West HAC 稳健 t 统计量 (Bartlett 核)。
|
|
|
|
返回 (t值, 均值, NW标准误); 样本不足 (n <= lag+2) 或方差为零返回 None。
|
|
"""
|
|
array = _clean_values(values)
|
|
n = array.size
|
|
if n <= lag + 2 or n < 3:
|
|
return None
|
|
mean = float(array.mean())
|
|
centered = array - mean
|
|
# 长方差 S = gamma0 + 2 Σ_l w_l gamma_l, w_l = 1 - l/(lag+1) (Bartlett)
|
|
gamma = [float(np.dot(centered[: n - lag_i], centered[lag_i:]) / n) for lag_i in range(lag + 1)]
|
|
long_variance = gamma[0]
|
|
for lag_i in range(1, lag + 1):
|
|
weight = 1.0 - lag_i / (lag + 1)
|
|
long_variance += 2.0 * weight * gamma[lag_i]
|
|
long_variance = max(long_variance, 0.0)
|
|
nw_se = math.sqrt(long_variance / n)
|
|
if nw_se == 0:
|
|
return None
|
|
return (mean - 0.0) / nw_se, mean, nw_se
|
|
|
|
|
|
def naive_t(values) -> float | None:
|
|
array = _clean_values(values)
|
|
n = array.size
|
|
if n < 3:
|
|
return None
|
|
std = float(array.std(ddof=1))
|
|
if std == 0:
|
|
return None
|
|
return float(array.mean()) / (std / math.sqrt(n))
|
|
|
|
|
|
def normal_two_sided_p(t_stat: float) -> float:
|
|
"""标准正态双侧 p 值: erfc(|t|/sqrt(2))。"""
|
|
return math.erfc(abs(t_stat) / math.sqrt(2.0))
|
|
|
|
|
|
def bh_fdr_qvalues(pvalues: list[float | None]) -> list[float | None]:
|
|
"""Benjamini-Hochberg 步进法 q 值 (与输入等长, None 透传)。
|
|
|
|
m 取可检验假设数 (None 不计入); q_i = min over j>=rank_i { p_j * m / rank_j },
|
|
从大到小单调回填保证递增约束。
|
|
"""
|
|
indexed = [
|
|
(index, p) for index, p in enumerate(pvalues)
|
|
if p is not None and np.isfinite(p)
|
|
]
|
|
qvalues: list[float | None] = [None] * len(pvalues)
|
|
if not indexed:
|
|
return qvalues
|
|
m = len(indexed)
|
|
indexed.sort(key=lambda pair: pair[1])
|
|
running_min = float("inf")
|
|
for reverse_rank in range(len(indexed) - 1, -1, -1):
|
|
index, p = indexed[reverse_rank]
|
|
rank = reverse_rank + 1
|
|
candidate = p * m / rank
|
|
running_min = min(running_min, candidate)
|
|
qvalues[index] = min(1.0, running_min)
|
|
return qvalues
|
|
|
|
|
|
def _normal_ppf(probability: float) -> float:
|
|
"""标准正态分位数 Acklam 逆逼近 (相对误差 < 1.15e-9), 零依赖替代 scipy.stats.norm.ppf。"""
|
|
if not (0.0 < probability < 1.0):
|
|
raise ValueError("probability 必须在 (0,1) 开区间")
|
|
a = (-3.969683028665376e+01, 2.209460984245205e+02, -2.759285104469687e+02,
|
|
1.383577518672690e+02, -3.066479806614716e+01, 2.506628277459239e+00)
|
|
b = (-5.447609879822406e+01, 1.615858368580409e+02, -1.556989798598866e+02,
|
|
6.680131188771972e+01, -1.328068155288572e+01)
|
|
c = (-7.784894002430293e-03, -3.223964580411365e-01, -2.400758277161838e+00,
|
|
-2.549732539343734e+00, 4.374664141464968e+00, 2.938163982698783e+00)
|
|
d = (7.784695709041462e-03, 3.224671290700398e-01, 2.445134137142996e+00,
|
|
3.754408661907416e+00)
|
|
p_low, p_high = 0.02425, 1 - 0.02425
|
|
if probability < p_low:
|
|
q_value = math.sqrt(-2 * math.log(probability))
|
|
return (((((c[0] * q_value + c[1]) * q_value + c[2]) * q_value + c[3]) * q_value + c[4]) * q_value + c[5]) / \
|
|
((((d[0] * q_value + d[1]) * q_value + d[2]) * q_value + d[3]) * q_value + 1)
|
|
if probability <= p_high:
|
|
q_value = probability - 0.5
|
|
r = q_value * q_value
|
|
return (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q_value / \
|
|
(((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1)
|
|
q_value = math.sqrt(-2 * math.log(1 - probability))
|
|
return -(((((c[0] * q_value + c[1]) * q_value + c[2]) * q_value + c[3]) * q_value + c[4]) * q_value + c[5]) / \
|
|
((((d[0] * q_value + d[1]) * q_value + d[2]) * q_value + d[3]) * q_value + 1)
|
|
|
|
|
|
def expected_max_sharpe(n_trials: int, variance_sharpes: float) -> float:
|
|
"""N 次独立试验的期望最大夏普 EM (方差>0 时); 单次试验不校正。"""
|
|
if n_trials <= 1 or variance_sharpes <= 0:
|
|
return 0.0
|
|
z1 = _normal_ppf(1.0 - 1.0 / n_trials)
|
|
z2 = _normal_ppf(1.0 - 1.0 / (n_trials * math.e))
|
|
return math.sqrt(variance_sharpes) * ((1.0 - EULER_GAMMA) * z1 + EULER_GAMMA * z2)
|
|
|
|
|
|
def deflated_sharpe_psr(
|
|
sharpe: float,
|
|
n_obs: int,
|
|
skewness: float | None = None,
|
|
kurtosis: float | None = None,
|
|
expected_max_sharpe: float = 0.0,
|
|
) -> float | None:
|
|
"""Deflated Sharpe (PSR 对 EM 校正) 概率; 参数不足或退化返回 None。
|
|
|
|
PSR = Φ( (SR - SR*) * sqrt(n-1) / sqrt(1 - gamma3 SR + (gamma4-1)/4 SR^2) )
|
|
"""
|
|
if n_obs < 5 or not np.isfinite(sharpe):
|
|
return None
|
|
skewness = 0.0 if skewness is None else skewness
|
|
kurtosis = 3.0 if kurtosis is None else kurtosis
|
|
denominator = 1.0 - skewness * sharpe + (kurtosis - 1.0) / 4.0 * sharpe * sharpe
|
|
if denominator <= 0:
|
|
return None
|
|
statistic = (sharpe - expected_max_sharpe) * math.sqrt(n_obs - 1) / math.sqrt(denominator)
|
|
return 0.5 * (1.0 + math.erf(statistic / math.sqrt(2.0)))
|