feat(factorminer): 新增 LocalFactorEvaluator 集成到评估管线
- 新增 LocalFactorEvaluator 类封装 FactorEngine,提供 (M,T) 矩阵输出 - evaluate_factors_with_evaluator() 支持新评估方式 - ValidationPipeline 优先使用 evaluator 计算信号 - 新增测试文件验证功能
This commit is contained in:
124
tests/test_factorminer_local_engine.py
Normal file
124
tests/test_factorminer_local_engine.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""Tests for LocalFactorEvaluator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from src.factorminer.evaluation.local_engine import LocalFactorEvaluator
|
||||
|
||||
|
||||
class TestLocalFactorEvaluator:
|
||||
"""测试 LocalFactorEvaluator 的基本功能。"""
|
||||
|
||||
def test_init(self) -> None:
|
||||
"""测试初始化。"""
|
||||
evaluator = LocalFactorEvaluator(
|
||||
start_date="20200101",
|
||||
end_date="20200131",
|
||||
stock_codes=None,
|
||||
)
|
||||
assert evaluator.start_date == "20200101"
|
||||
assert evaluator.end_date == "20200131"
|
||||
assert evaluator.stock_codes is None
|
||||
assert evaluator.engine is not None
|
||||
|
||||
def test_evaluate_empty_specs(self) -> None:
|
||||
"""测试空规格列表。"""
|
||||
evaluator = LocalFactorEvaluator(
|
||||
start_date="20200101",
|
||||
end_date="20200131",
|
||||
)
|
||||
result = evaluator.evaluate([])
|
||||
assert result == {}
|
||||
|
||||
def test_evaluate_returns_shape(self) -> None:
|
||||
"""测试 evaluate_returns 返回矩阵形状。"""
|
||||
evaluator = LocalFactorEvaluator(
|
||||
start_date="20200101",
|
||||
end_date="20200131",
|
||||
)
|
||||
returns = evaluator.evaluate_returns(periods=1)
|
||||
# 验证返回的是 numpy 数组
|
||||
assert isinstance(returns, np.ndarray)
|
||||
|
||||
def test_evaluate_single_basic(self) -> None:
|
||||
"""测试单个因子计算基本功能。"""
|
||||
evaluator = LocalFactorEvaluator(
|
||||
start_date="20200101",
|
||||
end_date="20200131",
|
||||
)
|
||||
# 测试计算 close 因子
|
||||
try:
|
||||
result = evaluator.evaluate_single("close", "close")
|
||||
assert isinstance(result, np.ndarray)
|
||||
# 验证结果是 2D 矩阵
|
||||
assert result.ndim == 2
|
||||
except Exception as e:
|
||||
# 数据可能不存在,跳过
|
||||
pytest.skip(f"数据不存在: {e}")
|
||||
|
||||
def test_evaluate_pct_change(self) -> None:
|
||||
"""测试收益率计算。"""
|
||||
evaluator = LocalFactorEvaluator(
|
||||
start_date="20200101",
|
||||
end_date="20200131",
|
||||
)
|
||||
try:
|
||||
result = evaluator.evaluate_single(
|
||||
"pct_change", "close / ts_delay(close, 1) - 1"
|
||||
)
|
||||
assert isinstance(result, np.ndarray)
|
||||
assert result.ndim == 2
|
||||
except Exception as e:
|
||||
pytest.skip(f"数据不存在: {e}")
|
||||
|
||||
def test_pivot_to_matrix_structure(self) -> None:
|
||||
"""测试 _pivot_to_matrix 的结构。"""
|
||||
import polars as pl
|
||||
|
||||
evaluator = LocalFactorEvaluator(
|
||||
start_date="20200101",
|
||||
end_date="20200131",
|
||||
)
|
||||
|
||||
# 创建测试数据
|
||||
df = pl.DataFrame(
|
||||
{
|
||||
"ts_code": ["000001.SZ", "000001.SZ", "000002.SZ", "000002.SZ"],
|
||||
"trade_date": ["20200101", "20200102", "20200101", "20200102"],
|
||||
"factor1": [1.0, 2.0, 3.0, 4.0],
|
||||
}
|
||||
)
|
||||
|
||||
result = evaluator._pivot_to_matrix(df, ["factor1"])
|
||||
|
||||
assert "factor1" in result
|
||||
assert isinstance(result["factor1"], np.ndarray)
|
||||
assert result["factor1"].ndim == 2
|
||||
|
||||
def test_batch_evaluate(self) -> None:
|
||||
"""测试批量计算。"""
|
||||
evaluator = LocalFactorEvaluator(
|
||||
start_date="20200101",
|
||||
end_date="20200131",
|
||||
)
|
||||
|
||||
specs: List[Tuple[str, str]] = [
|
||||
("close", "close"),
|
||||
("open", "open"),
|
||||
]
|
||||
|
||||
try:
|
||||
result = evaluator.evaluate(specs)
|
||||
assert isinstance(result, dict)
|
||||
assert "close" in result
|
||||
assert "open" in result
|
||||
except Exception as e:
|
||||
pytest.skip(f"数据不存在: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
130
tests/test_factorminer_pipeline_integration.py
Normal file
130
tests/test_factorminer_pipeline_integration.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""Tests for Factorminer pipeline integration with LocalFactorEvaluator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from src.factorminer.core.factor_library import FactorLibrary
|
||||
from src.factorminer.core.library_io import import_from_paper
|
||||
from src.factorminer.evaluation.local_engine import LocalFactorEvaluator
|
||||
from src.factorminer.evaluation.pipeline import (
|
||||
PipelineConfig,
|
||||
ValidationPipeline,
|
||||
run_evaluation_pipeline,
|
||||
)
|
||||
from src.factorminer.evaluation.runtime import (
|
||||
evaluate_factors_with_evaluator,
|
||||
)
|
||||
|
||||
|
||||
class TestLocalFactorEvaluatorIntegration:
|
||||
"""测试 LocalFactorEvaluator 与评估管线的集成。"""
|
||||
|
||||
@pytest.fixture
|
||||
def evaluator(self) -> LocalFactorEvaluator:
|
||||
"""创建评估器 fixture。"""
|
||||
return LocalFactorEvaluator(
|
||||
start_date="20200101",
|
||||
end_date="20200131",
|
||||
stock_codes=None,
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def returns_matrix(self) -> np.ndarray:
|
||||
"""创建模拟收益率矩阵 fixture。"""
|
||||
M, T = 100, 20
|
||||
rng = np.random.default_rng(42)
|
||||
return rng.standard_normal((M, T))
|
||||
|
||||
@pytest.fixture
|
||||
def splits(self) -> Dict[str, object]:
|
||||
"""创建模拟分割 fixture。"""
|
||||
|
||||
class MockSplit:
|
||||
def __init__(self, indices: np.ndarray, returns: np.ndarray):
|
||||
self.indices = indices
|
||||
self.returns = returns
|
||||
self.target_returns = {}
|
||||
|
||||
T = 20
|
||||
indices = np.arange(T)
|
||||
rng = np.random.default_rng(42)
|
||||
returns = rng.standard_normal((100, T))
|
||||
|
||||
return {
|
||||
"train": MockSplit(indices[:15], returns[:, :15]),
|
||||
"val": MockSplit(indices[15:], returns[:, 15:]),
|
||||
}
|
||||
|
||||
def test_evaluate_factors_with_evaluator_deprecated_path(
|
||||
self,
|
||||
evaluator: LocalFactorEvaluator,
|
||||
returns_matrix: np.ndarray,
|
||||
splits: Dict[str, object],
|
||||
) -> None:
|
||||
"""测试 evaluate_factors_with_evaluator 在有 evaluator 时的行为。"""
|
||||
|
||||
# 模拟一个因子对象
|
||||
class MockFactor:
|
||||
def __init__(self, id: str, name: str, formula: str, category: str):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.formula = formula
|
||||
self.category = category
|
||||
|
||||
factors = [
|
||||
MockFactor("f1", "close", "close", "price"),
|
||||
MockFactor("f2", "# TODO: unsupported", "unsupported", "test"),
|
||||
]
|
||||
|
||||
try:
|
||||
artifacts = evaluate_factors_with_evaluator(
|
||||
factors=factors,
|
||||
evaluator=evaluator,
|
||||
returns=returns_matrix,
|
||||
splits=splits,
|
||||
)
|
||||
# 验证返回结果结构
|
||||
assert len(artifacts) == 2
|
||||
assert artifacts[0].name == "close"
|
||||
assert artifacts[1].name == "# TODO: unsupported"
|
||||
# unsupported 因子应该被标记
|
||||
assert artifacts[1].error == "Unsupported operator in formula"
|
||||
except Exception as e:
|
||||
# FactorEngine 可能因为数据不存在而失败
|
||||
pytest.skip(f"FactorEngine 数据不存在: {e}")
|
||||
|
||||
def test_evaluate_factors_fallback_legacy(
|
||||
self,
|
||||
returns_matrix: np.ndarray,
|
||||
splits: Dict[str, object],
|
||||
) -> None:
|
||||
"""测试 evaluator=None 时回退到 legacy 方式。"""
|
||||
|
||||
class MockFactor:
|
||||
def __init__(self, id: str, name: str, formula: str, category: str):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.formula = formula
|
||||
self.category = category
|
||||
|
||||
factors = [
|
||||
MockFactor("f1", "test", "close", "price"),
|
||||
]
|
||||
|
||||
# evaluator=None 应该回退到 legacy
|
||||
artifacts = evaluate_factors_with_evaluator(
|
||||
factors=factors,
|
||||
evaluator=None,
|
||||
returns=returns_matrix,
|
||||
splits=splits,
|
||||
)
|
||||
# Legacy 方式会尝试 compute_tree_signals 但 data_dict 为空
|
||||
assert len(artifacts) == 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user