diff --git a/test/qlib/test_app_config.py b/test/qlib/test_app_config.py new file mode 100644 index 00000000..a95f2e68 --- /dev/null +++ b/test/qlib/test_app_config.py @@ -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 diff --git a/test/qlib/test_strategy_and_experiment.py b/test/qlib/test_strategy_and_experiment.py new file mode 100644 index 00000000..1b013517 --- /dev/null +++ b/test/qlib/test_strategy_and_experiment.py @@ -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()