Files
quant_engine/tests/test_perf_stats.py
George BerkshireandMavis c04acf0ab6 feat: quant_engine 独立量化引擎(v1.2.0 重构 bootstrap)
从 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>
2026-08-19 15:50:01 +08:00

138 lines
4.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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