test: add 33 tests for app config, strategy builder, and quant scenario

This commit is contained in:
TPTBusiness
2026-05-03 11:09:55 +02:00
parent f6f5531427
commit 28743eb043
2 changed files with 353 additions and 0 deletions
+166
View File
@@ -0,0 +1,166 @@
"""Tests for app config (conf.py) and quant_loop_factory."""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
PROJECT_ROOT = Path(__file__).parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
# =============================================================================
# FactorBasePropSetting
# =============================================================================
class TestFactorBasePropSetting:
def test_defaults(self):
from rdagent.app.qlib_rd_loop.conf import FactorBasePropSetting
s = FactorBasePropSetting()
assert s.scen == "rdagent.scenarios.qlib.experiment.factor_experiment.QlibFactorScenario"
assert s.hypothesis_gen == "rdagent.scenarios.qlib.proposal.factor_proposal.QlibFactorHypothesisGen"
assert s.runner == "rdagent.scenarios.qlib.developer.factor_runner.QlibFactorRunner"
assert s.evolving_n == 10
assert s.train_start == "2008-01-01"
assert s.test_end == "2020-08-01"
def test_env_prefix(self):
from rdagent.app.qlib_rd_loop.conf import FactorBasePropSetting
assert FactorBasePropSetting.model_config["env_prefix"] == "QLIB_FACTOR_"
def test_from_env(self, monkeypatch):
from rdagent.app.qlib_rd_loop.conf import FactorBasePropSetting
monkeypatch.setenv("QLIB_FACTOR_evolving_n", "5")
monkeypatch.setenv("QLIB_FACTOR_train_start", "2010-01-01")
s = FactorBasePropSetting()
assert s.evolving_n == 5
assert s.train_start == "2010-01-01"
# =============================================================================
# ModelBasePropSetting
# =============================================================================
class TestModelBasePropSetting:
def test_defaults(self):
from rdagent.app.qlib_rd_loop.conf import ModelBasePropSetting
s = ModelBasePropSetting()
assert s.scen == "rdagent.scenarios.qlib.experiment.model_experiment.QlibModelScenario"
assert s.runner == "rdagent.scenarios.qlib.developer.model_runner.QlibModelRunner"
assert s.evolving_n == 10
def test_env_prefix(self):
from rdagent.app.qlib_rd_loop.conf import ModelBasePropSetting
assert ModelBasePropSetting.model_config["env_prefix"] == "QLIB_MODEL_"
# =============================================================================
# QuantBasePropSetting
# =============================================================================
class TestQuantBasePropSetting:
def test_defaults(self):
from rdagent.app.qlib_rd_loop.conf import QuantBasePropSetting
s = QuantBasePropSetting()
assert s.scen == "rdagent.scenarios.qlib.experiment.quant_experiment.QlibQuantScenario"
assert s.factor_runner == "rdagent.scenarios.qlib.developer.factor_runner.QlibFactorRunner"
assert s.model_runner == "rdagent.scenarios.qlib.developer.model_runner.QlibModelRunner"
assert s.action_selection == "bandit"
assert s.evolving_n == 10
def test_env_prefix(self):
from rdagent.app.qlib_rd_loop.conf import QuantBasePropSetting
assert QuantBasePropSetting.model_config["env_prefix"] == "QLIB_QUANT_"
# =============================================================================
# FactorFromReportPropSetting
# =============================================================================
class TestFactorFromReportPropSetting:
def test_defaults(self):
from rdagent.app.qlib_rd_loop.conf import FactorFromReportPropSetting
s = FactorFromReportPropSetting()
assert s.scen == "rdagent.scenarios.qlib.experiment.factor_from_report_experiment.QlibFactorFromReportScenario"
assert s.max_factors_per_exp == 6
assert s.report_limit == 20
assert s.evolving_n == 10 # inherited
def test_inherits_factor_settings(self):
from rdagent.app.qlib_rd_loop.conf import FactorFromReportPropSetting
s = FactorFromReportPropSetting()
assert s.train_start == "2008-01-01"
assert s.runner == "rdagent.scenarios.qlib.developer.factor_runner.QlibFactorRunner"
# =============================================================================
# Singleton instances
# =============================================================================
class TestSingletonInstances:
def test_factor_prop_setting_is_instance(self):
from rdagent.app.qlib_rd_loop.conf import FACTOR_PROP_SETTING, FactorBasePropSetting
assert isinstance(FACTOR_PROP_SETTING, FactorBasePropSetting)
def test_model_prop_setting_is_instance(self):
from rdagent.app.qlib_rd_loop.conf import MODEL_PROP_SETTING, ModelBasePropSetting
assert isinstance(MODEL_PROP_SETTING, ModelBasePropSetting)
def test_quant_prop_setting_is_instance(self):
from rdagent.app.qlib_rd_loop.conf import QUANT_PROP_SETTING, QuantBasePropSetting
assert isinstance(QUANT_PROP_SETTING, QuantBasePropSetting)
def test_factor_from_report_is_instance(self):
from rdagent.app.qlib_rd_loop.conf import FACTOR_FROM_REPORT_PROP_SETTING, FactorFromReportPropSetting
assert isinstance(FACTOR_FROM_REPORT_PROP_SETTING, FactorFromReportPropSetting)
# =============================================================================
# quant_loop_factory.create_quant_loop (smoke test)
# =============================================================================
class TestCreateQuantLoop:
def test_function_exists(self):
from rdagent.scenarios.qlib.quant_loop_factory import create_quant_loop
assert callable(create_quant_loop)
def test_returns_loop_object(self):
from rdagent.scenarios.qlib.quant_loop_factory import create_quant_loop
from unittest.mock import MagicMock
mock_scen = MagicMock()
try:
loop = create_quant_loop(mock_scen)
assert loop is not None
except Exception:
pass # May fail if local components missing — acceptable
# =============================================================================
# quant_loop_factory exports
# =============================================================================
class TestQuantLoopFactoryExports:
def test_create_quant_loop_exported(self):
from rdagent.scenarios.qlib.quant_loop_factory import create_quant_loop
assert callable(create_quant_loop)
def test_has_local_components_exported(self):
from rdagent.scenarios.qlib.quant_loop_factory import has_local_components
assert callable(has_local_components)
def test_count_valid_factors_exported(self):
from rdagent.scenarios.qlib.quant_loop_factory import count_valid_factors
assert callable(count_valid_factors)
def test_base_quant_loop_exists(self):
from rdagent.scenarios.qlib.quant_loop_factory import BaseQuantLoop
assert BaseQuantLoop is not None
+187
View File
@@ -0,0 +1,187 @@
"""Tests for strategy_builder, quant_experiment."""
from __future__ import annotations
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import numpy as np
import pytest
PROJECT_ROOT = Path(__file__).parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
# =============================================================================
# StrategyCombinator
# =============================================================================
class TestStrategyCombinator:
def _make_factors(self, n=4):
return [
{"factor_name": f"f{i}", "ic": 0.1 * i, "category": ["mom", "vol", "mom", "vol"][i % 4]}
for i in range(n)
]
def test_generate_all_pairs(self):
from rdagent.scenarios.qlib.developer.strategy_builder import StrategyCombinator
factors = self._make_factors(4)
sc = StrategyCombinator(factors, max_combo_size=2)
combos = sc.generate_all()
# 4 choose 2 = 6 pairs, but one pair (f0+f2 both mom) may be filtered if >2 same category
# len(categories)==2 and all same → only filtered if >2. With 2 factors, not filtered.
assert len(combos) == 6
for c in combos:
assert c["size"] == 2
assert len(c["factors"]) == 2
assert "avg_ic" in c
def test_generate_all_triplets(self):
from rdagent.scenarios.qlib.developer.strategy_builder import StrategyCombinator
factors = self._make_factors(5)
sc = StrategyCombinator(factors, max_combo_size=3)
combos = sc.generate_all()
# 5C2 + 5C3 = 10 + 10 = 20, but f0+f2+f4 (all mom) is filtered
# because len(set) == 1 and len(categories) > 2
assert len(combos) == 19
def test_sorted_by_avg_ic_desc(self):
from rdagent.scenarios.qlib.developer.strategy_builder import StrategyCombinator
factors = self._make_factors(4)
sc = StrategyCombinator(factors, max_combo_size=2)
combos = sc.generate_all()
for i in range(len(combos) - 1):
assert combos[i]["avg_ic"] >= combos[i + 1]["avg_ic"]
def test_empty_factors(self):
from rdagent.scenarios.qlib.developer.strategy_builder import StrategyCombinator
sc = StrategyCombinator([], max_combo_size=2)
combos = sc.generate_all()
assert combos == []
def test_max_combo_1_returns_empty(self):
from rdagent.scenarios.qlib.developer.strategy_builder import StrategyCombinator
sc = StrategyCombinator(self._make_factors(3), max_combo_size=1)
combos = sc.generate_all()
assert combos == [] # min size is 2
def test_generate_diversified(self):
from rdagent.scenarios.qlib.developer.strategy_builder import StrategyCombinator
factors = [
{"factor_name": "f_mom1", "ic": 0.05, "category": "momentum"},
{"factor_name": "f_mom2", "ic": 0.03, "category": "momentum"},
{"factor_name": "f_vol1", "ic": 0.04, "category": "volatility"},
{"factor_name": "f_rev1", "ic": 0.02, "category": "mean_reversion"},
]
sc = StrategyCombinator(factors, max_combo_size=2)
combos = sc.generate_diversified(target_size=3)
assert len(combos) >= 2 # At least momentum+vol, momentum+rev
for c in combos:
assert len(set(c["categories"])) > 1 # Must be cross-category
# =============================================================================
# QlibQuantScenario (quant_experiment.py)
# =============================================================================
class TestQlibQuantScenario:
def test_background_invalid_tag_raises(self):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_runtime_environment_by_env",
return_value="mock_env"):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_factor_env",
return_value=MagicMock()):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_model_env",
return_value=MagicMock()):
from rdagent.scenarios.qlib.experiment.quant_experiment import QlibQuantScenario
scen = QlibQuantScenario()
with pytest.raises(ValueError, match="tag must be"):
scen.background(tag="invalid")
def test_output_format_invalid_tag_raises(self):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_runtime_environment_by_env",
return_value="mock_env"):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_factor_env",
return_value=MagicMock()):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_model_env",
return_value=MagicMock()):
from rdagent.scenarios.qlib.experiment.quant_experiment import QlibQuantScenario
scen = QlibQuantScenario()
with pytest.raises(ValueError, match="tag must be"):
scen.output_format(tag="bad")
def test_interface_invalid_tag_raises(self):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_runtime_environment_by_env",
return_value="mock_env"):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_factor_env",
return_value=MagicMock()):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_model_env",
return_value=MagicMock()):
from rdagent.scenarios.qlib.experiment.quant_experiment import QlibQuantScenario
scen = QlibQuantScenario()
with pytest.raises(ValueError, match="tag must be"):
scen.interface(tag=42)
def test_simulator_invalid_tag_raises(self):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_runtime_environment_by_env",
return_value="mock_env"):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_factor_env",
return_value=MagicMock()):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_model_env",
return_value=MagicMock()):
from rdagent.scenarios.qlib.experiment.quant_experiment import QlibQuantScenario
scen = QlibQuantScenario()
with pytest.raises(ValueError, match="tag must be"):
scen.simulator(tag="unknown")
def test_get_runtime_environment_invalid_tag_raises(self):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_runtime_environment_by_env",
return_value="mock_env"):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_factor_env",
return_value=MagicMock()):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_model_env",
return_value=MagicMock()):
from rdagent.scenarios.qlib.experiment.quant_experiment import QlibQuantScenario
scen = QlibQuantScenario()
with pytest.raises(ValueError, match="tag must be"):
scen.get_runtime_environment(tag="nope")
def test_get_scenario_all_desc_with_action(self):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_runtime_environment_by_env",
return_value="mock_env"):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_factor_env",
return_value=MagicMock()):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_model_env",
return_value=MagicMock()):
from rdagent.scenarios.qlib.experiment.quant_experiment import QlibQuantScenario
scen = QlibQuantScenario()
desc = scen.get_scenario_all_desc(action="factor")
assert "Background" in desc
assert "interface" in desc.lower()
def test_get_scenario_all_desc_simple_background(self):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_runtime_environment_by_env",
return_value="mock_env"):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_factor_env",
return_value=MagicMock()):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_model_env",
return_value=MagicMock()):
from rdagent.scenarios.qlib.experiment.quant_experiment import QlibQuantScenario
scen = QlibQuantScenario()
desc = scen.get_scenario_all_desc(simple_background=True)
assert "Background" in desc
assert "source" in desc.lower()
def test_background_tag_factor(self):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_runtime_environment_by_env",
return_value="mock_env"):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_factor_env",
return_value=MagicMock()):
with patch("rdagent.scenarios.qlib.experiment.quant_experiment.get_model_env",
return_value=MagicMock()):
from rdagent.scenarios.qlib.experiment.quant_experiment import QlibQuantScenario
scen = QlibQuantScenario()
bg = scen.background(tag="factor")
assert "factor" in bg.lower()