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>
This commit is contained in:
George Berkshire
2026-08-19 15:50:01 +08:00
co-authored by Mavis
commit c04acf0ab6
29 changed files with 9814 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
"""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