mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-07-27 23:47:46 +00:00
187 lines
6.9 KiB
Python
187 lines
6.9 KiB
Python
"""Tests for qlib pipeline — feedback, bandit, quant_loop_factory."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
|
|
# =============================================================================
|
|
# process_results (feedback.py)
|
|
# =============================================================================
|
|
|
|
|
|
class TestProcessResults:
|
|
def test_process_results_handles_named_series(self):
|
|
"""process_results renames column "0" to "Current Result" — this works
|
|
when the Series name is '0' (string), which matches the rename dict."""
|
|
from rdagent.scenarios.qlib.developer.feedback import process_results
|
|
import pandas as pd
|
|
|
|
# process_results expects the Series to produce a DataFrame column named "0" (string)
|
|
# This happens when the Series has name '0'
|
|
current = pd.Series(
|
|
{"IC": 0.05, "1day.excess_return_with_cost.annualized_return": 0.12,
|
|
"1day.excess_return_with_cost.max_drawdown": -0.08},
|
|
name="0",
|
|
)
|
|
sota = pd.Series(
|
|
{"IC": 0.03, "1day.excess_return_with_cost.annualized_return": 0.10,
|
|
"1day.excess_return_with_cost.max_drawdown": -0.05},
|
|
name="0",
|
|
)
|
|
|
|
result = process_results(current, sota)
|
|
assert "IC of Current Result is" in result
|
|
assert "of SOTA Result is" in result
|
|
|
|
def test_raises_on_missing_metrics(self):
|
|
from rdagent.scenarios.qlib.developer.feedback import process_results
|
|
|
|
current = pd.Series({"IC": 0.05})
|
|
sota = pd.Series({"IC": 0.03})
|
|
with pytest.raises(KeyError):
|
|
process_results(current, sota)
|
|
|
|
|
|
# =============================================================================
|
|
# bandit.py — Metrics and extract_metrics_from_experiment
|
|
# =============================================================================
|
|
|
|
|
|
class TestBanditMetrics:
|
|
def test_default_values_are_zero(self):
|
|
from rdagent.scenarios.qlib.proposal.bandit import Metrics
|
|
m = Metrics()
|
|
assert m.ic == 0.0
|
|
assert m.sharpe == 0.0
|
|
assert m.mdd == 0.0
|
|
|
|
def test_as_vector_length(self):
|
|
from rdagent.scenarios.qlib.proposal.bandit import Metrics
|
|
m = Metrics(ic=0.1, sharpe=1.5)
|
|
v = m.as_vector()
|
|
assert len(v) == 8
|
|
assert v[0] == 0.1
|
|
assert v[7] == 1.5
|
|
|
|
def test_mdd_negated_in_vector(self):
|
|
from rdagent.scenarios.qlib.proposal.bandit import Metrics
|
|
m = Metrics(mdd=0.15)
|
|
v = m.as_vector()
|
|
assert v[6] == -0.15 # -self.mdd
|
|
|
|
def test_extract_metrics_from_experiment(self):
|
|
from rdagent.scenarios.qlib.proposal.bandit import extract_metrics_from_experiment
|
|
|
|
mock_exp = MagicMock()
|
|
mock_exp.result = {
|
|
"IC": 0.04,
|
|
"ICIR": 0.5,
|
|
"Rank IC": 0.03,
|
|
"Rank ICIR": 0.4,
|
|
"1day.excess_return_with_cost.annualized_return ": 0.10,
|
|
"1day.excess_return_with_cost.information_ratio": 0.6,
|
|
"1day.excess_return_with_cost.max_drawdown": -0.12,
|
|
}
|
|
m = extract_metrics_from_experiment(mock_exp)
|
|
assert m.ic == 0.04
|
|
assert m.rank_ic == 0.03
|
|
assert m.mdd == -0.12
|
|
|
|
def test_extract_metrics_returns_default_on_error(self):
|
|
from rdagent.scenarios.qlib.proposal.bandit import extract_metrics_from_experiment
|
|
|
|
mock_exp = MagicMock()
|
|
mock_exp.result = None # Will cause AttributeError
|
|
m = extract_metrics_from_experiment(mock_exp)
|
|
assert m.ic == 0.0
|
|
assert m.sharpe == 0.0
|
|
|
|
def test_sharpe_computation(self):
|
|
from rdagent.scenarios.qlib.proposal.bandit import extract_metrics_from_experiment
|
|
|
|
mock_exp = MagicMock()
|
|
mock_exp.result = {
|
|
"IC": 0.0, "ICIR": 0.0, "Rank IC": 0.0, "Rank ICIR": 0.0,
|
|
"1day.excess_return_with_cost.annualized_return ": 0.15,
|
|
"1day.excess_return_with_cost.information_ratio": 0.0,
|
|
"1day.excess_return_with_cost.max_drawdown": -0.10,
|
|
}
|
|
m = extract_metrics_from_experiment(mock_exp)
|
|
assert m.sharpe == pytest.approx(1.5) # 0.15 / 0.10
|
|
|
|
|
|
# =============================================================================
|
|
# LinearThompsonTwoArm
|
|
# =============================================================================
|
|
|
|
|
|
class TestLinearThompsonTwoArm:
|
|
def test_initialization(self):
|
|
from rdagent.scenarios.qlib.proposal.bandit import LinearThompsonTwoArm
|
|
bandit = LinearThompsonTwoArm(dim=5)
|
|
assert bandit.dim == 5
|
|
assert bandit.noise_var == 1.0
|
|
assert bandit.mean["factor"].shape == (5,)
|
|
assert bandit.mean["model"].shape == (5,)
|
|
assert bandit.precision["factor"].shape == (5, 5)
|
|
|
|
def test_sample_reward_returns_float(self):
|
|
from rdagent.scenarios.qlib.proposal.bandit import LinearThompsonTwoArm
|
|
bandit = LinearThompsonTwoArm(dim=3)
|
|
x = np.ones(3)
|
|
reward = bandit.sample_reward("factor", x)
|
|
assert isinstance(reward, float)
|
|
|
|
def test_arms_are_initialized_identically(self):
|
|
from rdagent.scenarios.qlib.proposal.bandit import LinearThompsonTwoArm
|
|
bandit = LinearThompsonTwoArm(dim=4)
|
|
assert np.array_equal(bandit.mean["factor"], bandit.mean["model"])
|
|
assert np.array_equal(bandit.precision["factor"], bandit.precision["model"])
|
|
|
|
|
|
# =============================================================================
|
|
# quant_loop_factory.py
|
|
# =============================================================================
|
|
|
|
|
|
class TestHasLocalComponents:
|
|
def test_returns_bool(self):
|
|
from rdagent.scenarios.qlib.quant_loop_factory import has_local_components
|
|
result = has_local_components()
|
|
assert isinstance(result, bool)
|
|
|
|
def test_returns_false_with_no_local_dir(self, monkeypatch):
|
|
from rdagent.scenarios.qlib import quant_loop_factory
|
|
monkeypatch.setattr(quant_loop_factory.Path, "exists", lambda self: False)
|
|
assert quant_loop_factory.has_local_components() is False
|
|
|
|
|
|
class TestCountValidFactors:
|
|
def test_returns_zero_when_no_dir(self):
|
|
from rdagent.scenarios.qlib.quant_loop_factory import count_valid_factors
|
|
with patch("rdagent.scenarios.qlib.quant_loop_factory.Path.exists", return_value=False):
|
|
assert count_valid_factors() == 0
|
|
|
|
def test_returns_int(self):
|
|
from rdagent.scenarios.qlib.quant_loop_factory import count_valid_factors
|
|
result = count_valid_factors()
|
|
assert isinstance(result, int)
|
|
assert result >= 0
|
|
|
|
|
|
class TestAdvancedLoopThreshold:
|
|
def test_constant_is_defined(self):
|
|
from rdagent.scenarios.qlib.quant_loop_factory import ADVANCED_LOOP_FACTOR_THRESHOLD
|
|
assert ADVANCED_LOOP_FACTOR_THRESHOLD == 5000
|