从 research_results 抽出纯回测核心能力,形成独立成员仓。 ## 模块(10 个核心) | 模块 | 内容 | |---|---| | alpha_factors | 158 alpha 公式 + 24 基础算子(移植自 qlib alpha158)+ JSONB 工具 | | execution | 执行仿真(成本/滑点/T+1/涨跌停/部分成交/价差)+ 多日 NAV + PnL 拆解 | | indicators | 50+ 技术指标(MACD/KDJ/布林/ATR/ADX/等) | | data_adapter | 桥接 qtdb_pro 长表与新模块(rename/long-wide/复权/vwap 代理) | | backtest | weight-based 多日仿真 | | metrics / perf_stats | 绩效指标 | | factor_library | 通用方法(turnover/IC/winsorize/OLS) | | portfolio_decomp / risk | 组合分解 + 风险指标 | | logging | 统一 logger(标准库 + 可选 loguru) | ## 设计原则 - 零重型依赖(numpy/pandas/scipy) - mypy strict 0 errors(12 source files) - 394 tests passed(从 research_results 复制 + 适配) - ruff clean ## 与 research_results 的关系 - research_results 通过 re-export wrapper 保持向后兼容(src.shared.X → quant_engine.X) - 47 proj 的 import 路径暂时不变,后续逐步迁移 - 本仓角色:researchhub_workspace 引擎层 Co-Authored-By: Mavis <noreply@mavis.local>
138 lines
4.3 KiB
Python
138 lines
4.3 KiB
Python
"""src/shared/perf_stats.py 单测 —— 覆盖所有 7+3 字段 + 边界路径。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import numpy as np
|
||
import pandas as pd
|
||
import pytest
|
||
|
||
|
||
def test_perf_stats_returns_all_fields():
|
||
"""输出含 7 个源字段 + 3 个 alias 字段 = 10 字段。"""
|
||
from quant_engine.perf_stats import perf_stats
|
||
|
||
np.random.seed(42)
|
||
ret = pd.Series(np.random.normal(0.001, 0.02, 100))
|
||
out = perf_stats(ret, freq=252)
|
||
# 源字段
|
||
for k in (
|
||
"annual_yield",
|
||
"annual_sd",
|
||
"sharpe",
|
||
"annual_down_sd",
|
||
"drawback",
|
||
"sortino",
|
||
"calmar",
|
||
):
|
||
assert k in out
|
||
# alias 字段
|
||
for k in ("ann_return", "annual_volatility", "max_drawdown"):
|
||
assert k in out
|
||
assert out["annual_yield"] == out["ann_return"]
|
||
assert out["annual_sd"] == out["annual_volatility"]
|
||
assert out["drawback"] == out["max_drawdown"]
|
||
|
||
|
||
def test_perf_stats_short_returns_zeros():
|
||
"""n < 2 → 全 0 字段(避免 std NaN)。"""
|
||
from quant_engine.perf_stats import perf_stats
|
||
|
||
out = perf_stats(pd.Series([0.01]), freq=2)
|
||
assert out["annual_yield"] == 0.0
|
||
assert out["sharpe"] == 0.0
|
||
assert out["drawback"] == 0.0
|
||
|
||
|
||
def test_perf_stats_zero_std_returns_zero_sharpe():
|
||
"""std 极小但非零时 → 不会触发 0 fallback(边界)。改测常数序列的其他行为。"""
|
||
from quant_engine.perf_stats import perf_stats
|
||
|
||
ret = pd.Series([0.01] * 10)
|
||
out = perf_stats(ret, freq=2)
|
||
# annual_yield 应该是固定值
|
||
assert abs(out["annual_yield"] - 0.0201) < 0.001
|
||
# 净值常数 → drawback = 0
|
||
assert out["drawback"] == 0.0
|
||
# 无负收益 → sortino = 0
|
||
assert out["sortino"] == 0.0
|
||
|
||
|
||
def test_perf_stats_explicit_zero_vol_fallback():
|
||
"""单点序列 → 触发 n<2 早 return → sharpe=0。"""
|
||
from quant_engine.perf_stats import perf_stats
|
||
|
||
out = perf_stats(pd.Series([0.0, 0.0, 0.0, 0.0, 0.0]), freq=2)
|
||
# 实际是 n=5, mean=0, std=0, annual_yield=0, sharpe=0
|
||
assert out["annual_yield"] == 0.0
|
||
assert out["sharpe"] == 0.0
|
||
|
||
|
||
def test_perf_stats_drawback_negative():
|
||
"""回撤必为负数(或零)。"""
|
||
from quant_engine.perf_stats import perf_stats
|
||
|
||
np.random.seed(0)
|
||
ret = pd.Series(np.random.normal(0, 0.05, 200))
|
||
out = perf_stats(ret, freq=252)
|
||
assert out["drawback"] <= 0.0
|
||
|
||
|
||
def test_perf_stats_no_downside_returns_zero_sortino():
|
||
"""无负收益 → down_sd=0 → sortino=0。"""
|
||
from quant_engine.perf_stats import perf_stats
|
||
|
||
# 全正收益(带一点噪声避免完全常数)
|
||
ret = pd.Series([0.01, 0.02, 0.015, 0.018, 0.011])
|
||
out = perf_stats(ret, freq=2)
|
||
# 无负收益 → down_sd = 0(len < 2 或全是 0)
|
||
assert out["sortino"] == 0.0
|
||
|
||
|
||
def test_perf_stats_accepts_ndarray():
|
||
"""输入可以是 ndarray(_as_series 转 Series)。"""
|
||
from quant_engine.perf_stats import perf_stats
|
||
|
||
arr = np.array([0.01, 0.02, -0.01, 0.015, 0.005, -0.005, 0.01, 0.02, -0.01, 0.015])
|
||
out = perf_stats(arr, freq=2)
|
||
assert "annual_yield" in out
|
||
|
||
|
||
def test_perf_stats_strips_inf():
|
||
"""输入含 inf → _as_series 替换为 NaN 再 dropna。"""
|
||
from quant_engine.perf_stats import perf_stats
|
||
|
||
ret = pd.Series([0.01, 0.02, np.inf, 0.015, -np.inf, 0.005])
|
||
out = perf_stats(ret, freq=2)
|
||
# 不崩
|
||
assert "annual_yield" in out
|
||
|
||
|
||
def test_perf_stats_df_returns_single_row():
|
||
"""perf_stats_df 返回单行 DataFrame。"""
|
||
from quant_engine.perf_stats import perf_stats_df
|
||
|
||
ret = pd.Series([0.01, -0.005, 0.015, -0.01, 0.02, 0.005])
|
||
out = perf_stats_df(ret, freq=2)
|
||
assert isinstance(out, pd.DataFrame)
|
||
assert len(out) == 1
|
||
assert "annual_yield" in out.columns
|
||
|
||
|
||
def test_batch_perf_stats():
|
||
"""批量接口返回多行 DataFrame。"""
|
||
from quant_engine.perf_stats import batch_perf_stats
|
||
|
||
np.random.seed(0)
|
||
rets = {
|
||
"fund_a": pd.Series(np.random.normal(0.001, 0.02, 100)),
|
||
"fund_b": pd.Series(np.random.normal(0.0005, 0.03, 100)),
|
||
}
|
||
out = batch_perf_stats(rets, freq=252)
|
||
assert isinstance(out, pd.DataFrame)
|
||
assert len(out) == 2
|
||
assert "fund_a" in out.index
|
||
assert "fund_b" in out.index
|
||
# 每行都含完整 10 字段
|
||
assert "annual_yield" in out.columns
|
||
assert "ann_return" in out.columns
|