mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-08-06 19:47:44 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff556fd228 | |||
| b23e145341 | |||
| 7d3765d0c0 |
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
".": "1.4.1"
|
".": "1.4.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.4.2](https://github.com/TPTBusiness/Predix/compare/v1.4.1...v1.4.2) (2026-05-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add missing sys import and fix undefined acc_rate in factor eval ([c45f990](https://github.com/TPTBusiness/Predix/commit/c45f9908ee321400f0a19c57f1482e4cd1394a50))
|
||||||
|
|
||||||
## [1.4.1](https://github.com/TPTBusiness/Predix/compare/v1.4.0...v1.4.1) (2026-05-03)
|
## [1.4.1](https://github.com/TPTBusiness/Predix/compare/v1.4.0...v1.4.1) (2026-05-03)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -328,12 +328,13 @@ class FactorEqualValueRatioEvaluator(FactorEvaluator):
|
|||||||
"The source dataframe is None. Please check the implementation.",
|
"The source dataframe is None. Please check the implementation.",
|
||||||
-1,
|
-1,
|
||||||
)
|
)
|
||||||
|
acc_rate = -1
|
||||||
try:
|
try:
|
||||||
close_values = gen_df.sub(gt_df).abs().lt(1e-6)
|
close_values = gen_df.sub(gt_df).abs().lt(1e-6)
|
||||||
result_int = close_values.astype(int)
|
result_int = close_values.astype(int)
|
||||||
pos_num = result_int.sum().sum()
|
pos_num = result_int.sum().sum()
|
||||||
acc_rate = pos_num / close_values.size
|
acc_rate = pos_num / close_values.size
|
||||||
except:
|
except Exception:
|
||||||
close_values = gen_df
|
close_values = gen_df
|
||||||
if close_values.all().iloc[0]:
|
if close_values.all().iloc[0]:
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -926,6 +926,7 @@ class QlibFactorRunner(CachedRunner[QlibFactorExperiment]):
|
|||||||
import os as _os
|
import os as _os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
"""Tests for rdagent.core — the core framework abstractions."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Feedback base class
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestFeedback:
|
||||||
|
def test_default_is_acceptable_returns_true(self):
|
||||||
|
from rdagent.core.evaluation import Feedback
|
||||||
|
fb = Feedback()
|
||||||
|
assert fb.is_acceptable() is True
|
||||||
|
|
||||||
|
def test_default_finished_returns_true(self):
|
||||||
|
from rdagent.core.evaluation import Feedback
|
||||||
|
fb = Feedback()
|
||||||
|
assert fb.finished() is True
|
||||||
|
|
||||||
|
def test_default_bool_is_true(self):
|
||||||
|
from rdagent.core.evaluation import Feedback
|
||||||
|
fb = Feedback()
|
||||||
|
assert bool(fb) is True
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# EvoStep dataclass
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestEvoStep:
|
||||||
|
def test_default_construction(self):
|
||||||
|
from rdagent.core.evolving_framework import EvoStep
|
||||||
|
es = EvoStep(evolvable_subjects="mock_evo")
|
||||||
|
assert es.evolvable_subjects == "mock_evo"
|
||||||
|
assert es.queried_knowledge is None
|
||||||
|
assert es.feedback is None
|
||||||
|
|
||||||
|
def test_full_construction(self):
|
||||||
|
from rdagent.core.evolving_framework import EvoStep, QueriedKnowledge
|
||||||
|
qk = QueriedKnowledge()
|
||||||
|
es = EvoStep(evolvable_subjects="evo", queried_knowledge=qk, feedback="fb")
|
||||||
|
assert es.queried_knowledge is qk
|
||||||
|
assert es.feedback == "fb"
|
||||||
|
|
||||||
|
def test_equality_by_reference(self):
|
||||||
|
from rdagent.core.evolving_framework import EvoStep
|
||||||
|
es1 = EvoStep(evolvable_subjects="a")
|
||||||
|
es2 = EvoStep(evolvable_subjects="a")
|
||||||
|
assert es1 == es2
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Scenario base class
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestScenario:
|
||||||
|
def test_source_data_default_returns_empty_string(self):
|
||||||
|
from rdagent.core.scenario import Scenario
|
||||||
|
|
||||||
|
class MinimalScenario(Scenario):
|
||||||
|
@property
|
||||||
|
def background(self) -> str: return "bg"
|
||||||
|
@property
|
||||||
|
def rich_style_description(self) -> str: return "rich"
|
||||||
|
def get_scenario_all_desc(self, **kwargs) -> str: return "all"
|
||||||
|
def get_runtime_environment(self) -> str: return "env"
|
||||||
|
|
||||||
|
scen = MinimalScenario()
|
||||||
|
assert scen.source_data == ""
|
||||||
|
|
||||||
|
def test_source_data_property_calls_get_source_data_desc(self):
|
||||||
|
from rdagent.core.scenario import Scenario
|
||||||
|
|
||||||
|
class FakeScenario(Scenario):
|
||||||
|
@property
|
||||||
|
def background(self) -> str: return "bg"
|
||||||
|
@property
|
||||||
|
def rich_style_description(self) -> str: return "rich"
|
||||||
|
def get_scenario_all_desc(self, **kwargs) -> str: return "all"
|
||||||
|
def get_runtime_environment(self) -> str: return "env"
|
||||||
|
def get_source_data_desc(self, task=None) -> str: return "custom_data"
|
||||||
|
|
||||||
|
scen = FakeScenario()
|
||||||
|
assert scen.source_data == "custom_data"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# EvolvingStrategy base class
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestEvolvingStrategy:
|
||||||
|
def test_init_stores_scenario(self):
|
||||||
|
from rdagent.core.evolving_framework import EvolvingStrategy
|
||||||
|
|
||||||
|
class MinimalStrategy(EvolvingStrategy):
|
||||||
|
def evolve_iter(self, evo, queried_knowledge=None, evolving_trace=None):
|
||||||
|
yield evo
|
||||||
|
|
||||||
|
mock_scen = MagicMock()
|
||||||
|
es = MinimalStrategy(mock_scen)
|
||||||
|
assert es.scen is mock_scen
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# IterEvaluator base class
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestIterEvaluator:
|
||||||
|
def test_evaluate_returns_feedback(self):
|
||||||
|
from rdagent.core.evaluation import Feedback
|
||||||
|
from rdagent.core.evolving_framework import IterEvaluator, EvolvableSubjects
|
||||||
|
|
||||||
|
class MyFeedback(Feedback):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MyEvaluator(IterEvaluator):
|
||||||
|
def evaluate_iter(self):
|
||||||
|
evo = yield MyFeedback()
|
||||||
|
yield MyFeedback()
|
||||||
|
return MyFeedback()
|
||||||
|
|
||||||
|
eva = MyEvaluator()
|
||||||
|
result = eva.evaluate(EvolvableSubjects())
|
||||||
|
assert isinstance(result, MyFeedback)
|
||||||
|
|
||||||
|
def test_evaluate_iter_send_none_stops(self):
|
||||||
|
"""Sending None mid-iteration triggers StopIteration with final feedback."""
|
||||||
|
from rdagent.core.evaluation import Feedback
|
||||||
|
from rdagent.core.evolving_framework import IterEvaluator
|
||||||
|
|
||||||
|
class MyEvaluator(IterEvaluator):
|
||||||
|
def evaluate_iter(self):
|
||||||
|
yield Feedback() # kick-off (none)
|
||||||
|
evo_next = yield Feedback() # partial eval
|
||||||
|
if evo_next is None:
|
||||||
|
return Feedback() # early return
|
||||||
|
return Feedback() # normal path
|
||||||
|
|
||||||
|
eva = MyEvaluator()
|
||||||
|
gen = eva.evaluate_iter()
|
||||||
|
next(gen) # kick-off → first Feedback
|
||||||
|
gen.send("any") # evo gets "any" → second Feedback (evo_next NOT assigned yet)
|
||||||
|
with pytest.raises(StopIteration):
|
||||||
|
gen.send(None) # evo_next = None → return → StopIteration
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Developer base class
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeveloper:
|
||||||
|
def test_develop_raises_not_implemented(self):
|
||||||
|
from rdagent.core.developer import Developer
|
||||||
|
|
||||||
|
class MinimalDeveloper(Developer):
|
||||||
|
def develop(self, exp):
|
||||||
|
return super().develop(exp)
|
||||||
|
|
||||||
|
dev = MinimalDeveloper(MagicMock())
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
dev.develop(MagicMock())
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Knowledge / QueriedKnowledge
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestKnowledgeHierarchy:
|
||||||
|
def test_knowledge_pass_through(self):
|
||||||
|
from rdagent.core.evolving_framework import Knowledge, QueriedKnowledge
|
||||||
|
k = Knowledge()
|
||||||
|
qk = QueriedKnowledge()
|
||||||
|
assert isinstance(k, Knowledge)
|
||||||
|
assert isinstance(qk, QueriedKnowledge)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# EvolvingAgent (abstract interface)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestEvolvingAgent:
|
||||||
|
def test_ragevo_agent_init(self):
|
||||||
|
from rdagent.core.evolving_agent import RAGEvoAgent
|
||||||
|
mock_strategy = MagicMock()
|
||||||
|
mock_rag = MagicMock()
|
||||||
|
agent = RAGEvoAgent.__new__(RAGEvoAgent)
|
||||||
|
RAGEvoAgent.__init__(agent, max_loop=5, evolving_strategy=mock_strategy, rag=mock_rag)
|
||||||
|
assert agent.max_loop == 5
|
||||||
|
assert agent.evolving_strategy is mock_strategy
|
||||||
|
assert agent.rag is mock_rag
|
||||||
|
|
||||||
|
def test_ragevo_agent_default_knowledge_flags(self):
|
||||||
|
from rdagent.core.evolving_agent import RAGEvoAgent
|
||||||
|
agent = RAGEvoAgent.__new__(RAGEvoAgent)
|
||||||
|
RAGEvoAgent.__init__(agent, max_loop=3, evolving_strategy=MagicMock(), rag=MagicMock())
|
||||||
|
assert agent.with_knowledge is False
|
||||||
|
assert agent.knowledge_self_gen is False
|
||||||
|
assert agent.enable_filelock is False
|
||||||
|
|
||||||
|
def test_ragevo_agent_with_knowledge_enabled(self):
|
||||||
|
from rdagent.core.evolving_agent import RAGEvoAgent
|
||||||
|
agent = RAGEvoAgent.__new__(RAGEvoAgent)
|
||||||
|
RAGEvoAgent.__init__(
|
||||||
|
agent, max_loop=3, evolving_strategy=MagicMock(), rag=MagicMock(),
|
||||||
|
with_knowledge=True, knowledge_self_gen=True,
|
||||||
|
enable_filelock=True, filelock_path="/tmp/test.lock",
|
||||||
|
)
|
||||||
|
assert agent.with_knowledge is True
|
||||||
|
assert agent.knowledge_self_gen is True
|
||||||
|
assert agent.enable_filelock is True
|
||||||
|
assert agent.filelock_path == "/tmp/test.lock"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# EvolvableSubjects clone
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestEvolvableSubjects:
|
||||||
|
def test_clone_produces_deep_copy(self):
|
||||||
|
from rdagent.core.evolving_framework import EvolvableSubjects
|
||||||
|
es = EvolvableSubjects()
|
||||||
|
clone = es.clone()
|
||||||
|
assert clone is not es
|
||||||
|
assert type(clone) is type(es)
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
"""Tests for CoSTEER feedback types and EvolvingItem."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CoSTEERSingleFeedback
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestCoSTEERSingleFeedback:
|
||||||
|
def test_construction_with_valid_fields(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback
|
||||||
|
fb = CoSTEERSingleFeedback(
|
||||||
|
execution="exec ok",
|
||||||
|
return_checking="return ok",
|
||||||
|
code="code ok",
|
||||||
|
final_decision=True,
|
||||||
|
)
|
||||||
|
assert fb.execution == "exec ok"
|
||||||
|
assert fb.return_checking == "return ok"
|
||||||
|
assert fb.code == "code ok"
|
||||||
|
assert fb.final_decision is True
|
||||||
|
|
||||||
|
def test_bool_returns_final_decision(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback
|
||||||
|
fb_true = CoSTEERSingleFeedback(execution="x", return_checking="x", code="x", final_decision=True)
|
||||||
|
fb_false = CoSTEERSingleFeedback(execution="x", return_checking="x", code="x", final_decision=False)
|
||||||
|
assert bool(fb_true) is True
|
||||||
|
assert bool(fb_false) is False
|
||||||
|
|
||||||
|
def test_default_values(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback
|
||||||
|
fb = CoSTEERSingleFeedback(execution="x", return_checking=None, code="x")
|
||||||
|
assert fb.final_decision is None
|
||||||
|
assert fb.raw_execution == ""
|
||||||
|
assert fb.source_feedback == {}
|
||||||
|
|
||||||
|
def test_val_and_update_init_dict_converts_boolean(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback
|
||||||
|
assert CoSTEERSingleFeedback.val_and_update_init_dict(
|
||||||
|
{"execution": "x", "return_checking": "y", "code": "z", "final_decision": "true"}
|
||||||
|
)["final_decision"] is True
|
||||||
|
assert CoSTEERSingleFeedback.val_and_update_init_dict(
|
||||||
|
{"execution": "x", "return_checking": "y", "code": "z", "final_decision": "false"}
|
||||||
|
)["final_decision"] is False
|
||||||
|
assert CoSTEERSingleFeedback.val_and_update_init_dict(
|
||||||
|
{"execution": "x", "return_checking": "y", "code": "z", "final_decision": "True"}
|
||||||
|
)["final_decision"] is True
|
||||||
|
assert CoSTEERSingleFeedback.val_and_update_init_dict(
|
||||||
|
{"execution": "x", "return_checking": "y", "code": "z", "final_decision": "False"}
|
||||||
|
)["final_decision"] is False
|
||||||
|
|
||||||
|
def test_val_and_update_init_dict_rejects_non_boolean(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
CoSTEERSingleFeedback.val_and_update_init_dict(
|
||||||
|
{"execution": "x", "return_checking": "y", "code": "z", "final_decision": 42}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_val_and_update_init_dict_missing_final_decision_raises(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback
|
||||||
|
with pytest.raises(ValueError, match="final_decision"):
|
||||||
|
CoSTEERSingleFeedback.val_and_update_init_dict(
|
||||||
|
{"execution": "x", "return_checking": "y", "code": "z"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_val_and_update_init_dict_json_dumps_non_string_attrs(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback
|
||||||
|
import json
|
||||||
|
data = {
|
||||||
|
"execution": {"key": "val"},
|
||||||
|
"return_checking": ["list"],
|
||||||
|
"code": 123,
|
||||||
|
"final_decision": True,
|
||||||
|
}
|
||||||
|
result = CoSTEERSingleFeedback.val_and_update_init_dict(data)
|
||||||
|
for attr in ("execution", "return_checking", "code"):
|
||||||
|
# Should have been converted to JSON string
|
||||||
|
assert isinstance(result[attr], str)
|
||||||
|
_ = json.loads(result[attr]) # valid JSON
|
||||||
|
|
||||||
|
def test_merge_all_true_decisions(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback
|
||||||
|
fb1 = CoSTEERSingleFeedback(execution="a", return_checking="ra", code="c1", final_decision=True)
|
||||||
|
fb2 = CoSTEERSingleFeedback(execution="b", return_checking="rb", code="c2", final_decision=True)
|
||||||
|
merged = CoSTEERSingleFeedback.merge([fb1, fb2])
|
||||||
|
assert merged.final_decision is True
|
||||||
|
|
||||||
|
def test_merge_one_false_decision(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback
|
||||||
|
fb1 = CoSTEERSingleFeedback(execution="a", return_checking="ra", code="c1", final_decision=True)
|
||||||
|
fb2 = CoSTEERSingleFeedback(execution="b", return_checking="rb", code="c2", final_decision=False)
|
||||||
|
merged = CoSTEERSingleFeedback.merge([fb1, fb2])
|
||||||
|
assert merged.final_decision is False
|
||||||
|
|
||||||
|
def test_merge_concatenates_fields(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback
|
||||||
|
fb1 = CoSTEERSingleFeedback(execution="A", return_checking="RA", code="C1", final_decision=True)
|
||||||
|
fb2 = CoSTEERSingleFeedback(execution="B", return_checking="RB", code="C2", final_decision=True)
|
||||||
|
merged = CoSTEERSingleFeedback.merge([fb1, fb2])
|
||||||
|
assert "A\n\nB" in merged.execution
|
||||||
|
assert "RA\n\nRB" in merged.return_checking
|
||||||
|
assert "C1\n\nC2" in merged.code
|
||||||
|
|
||||||
|
def test_merge_skips_none_fields(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback
|
||||||
|
fb1 = CoSTEERSingleFeedback(execution="A", return_checking="RA", code="C1", final_decision=True)
|
||||||
|
fb2 = CoSTEERSingleFeedback(execution="B", return_checking=None, code="C2", final_decision=True)
|
||||||
|
merged = CoSTEERSingleFeedback.merge([fb1, fb2])
|
||||||
|
assert merged.execution == "A\n\nB"
|
||||||
|
assert merged.return_checking == "RA"
|
||||||
|
assert merged.code == "C1\n\nC2"
|
||||||
|
|
||||||
|
def test_merge_aggregates_source_feedback(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback
|
||||||
|
fb1 = CoSTEERSingleFeedback(execution="a", return_checking="x", code="c", final_decision=True,
|
||||||
|
source_feedback={"eval1": True})
|
||||||
|
fb2 = CoSTEERSingleFeedback(execution="b", return_checking="y", code="d", final_decision=True,
|
||||||
|
source_feedback={"eval2": False})
|
||||||
|
merged = CoSTEERSingleFeedback.merge([fb1, fb2])
|
||||||
|
assert merged.source_feedback == {"eval1": True, "eval2": False}
|
||||||
|
|
||||||
|
def test_str_contains_all_sections(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback
|
||||||
|
fb = CoSTEERSingleFeedback(execution="exec", return_checking="ret", code="code", final_decision=True)
|
||||||
|
s = str(fb)
|
||||||
|
assert "Execution" in s
|
||||||
|
assert "Return Checking" in s
|
||||||
|
assert "Code" in s
|
||||||
|
assert "Final Decision" in s
|
||||||
|
assert "SUCCESS" in s
|
||||||
|
|
||||||
|
def test_str_shows_fail_for_false(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback
|
||||||
|
fb = CoSTEERSingleFeedback(execution="exec", return_checking="ret", code="code", final_decision=False)
|
||||||
|
assert "FAIL" in str(fb)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CoSTEERSingleFeedbackDeprecated
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestCoSTEERSingleFeedbackDeprecated:
|
||||||
|
def test_property_getters(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedbackDeprecated
|
||||||
|
fb = CoSTEERSingleFeedbackDeprecated(
|
||||||
|
execution_feedback="exec",
|
||||||
|
code_feedback="code",
|
||||||
|
value_feedback="val",
|
||||||
|
shape_feedback="shape",
|
||||||
|
final_decision=True,
|
||||||
|
final_feedback="final",
|
||||||
|
value_generated_flag=True,
|
||||||
|
final_decision_based_on_gt=True,
|
||||||
|
)
|
||||||
|
assert fb.execution == "exec"
|
||||||
|
assert fb.code == "code"
|
||||||
|
assert fb.final_decision is True
|
||||||
|
assert fb.value_generated_flag is True
|
||||||
|
assert fb.final_decision_based_on_gt is True
|
||||||
|
|
||||||
|
def test_return_checking_when_value_generated(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedbackDeprecated
|
||||||
|
fb = CoSTEERSingleFeedbackDeprecated(
|
||||||
|
value_generated_flag=True, value_feedback="vals", shape_feedback="shapes",
|
||||||
|
)
|
||||||
|
rc = fb.return_checking
|
||||||
|
assert "vals" in rc
|
||||||
|
assert "shapes" in rc
|
||||||
|
|
||||||
|
def test_return_checking_when_no_value_generated(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedbackDeprecated
|
||||||
|
fb = CoSTEERSingleFeedbackDeprecated(value_generated_flag=False)
|
||||||
|
assert fb.return_checking is None
|
||||||
|
|
||||||
|
def test_setters_work(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedbackDeprecated
|
||||||
|
fb = CoSTEERSingleFeedbackDeprecated()
|
||||||
|
fb.execution = "new_exec"
|
||||||
|
fb.code = "new_code"
|
||||||
|
fb.return_checking = "new_rc"
|
||||||
|
assert fb.execution_feedback == "new_exec"
|
||||||
|
assert fb.code_feedback == "new_code"
|
||||||
|
assert fb.value_feedback == "new_rc"
|
||||||
|
assert fb.shape_feedback == "new_rc"
|
||||||
|
|
||||||
|
def test_str_contains_all_sections(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedbackDeprecated
|
||||||
|
fb = CoSTEERSingleFeedbackDeprecated(
|
||||||
|
execution_feedback="exec", shape_feedback="shape",
|
||||||
|
code_feedback="code", value_feedback="val",
|
||||||
|
final_feedback="final", final_decision=True,
|
||||||
|
)
|
||||||
|
s = str(fb)
|
||||||
|
for keyword in ("Execution", "Shape", "Code", "Value", "Final Decision", "SUCCESS"):
|
||||||
|
assert keyword in s
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CoSTEERMultiFeedback
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestCoSTEERMultiFeedback:
|
||||||
|
def _make_fb(self, decision=True):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERSingleFeedback
|
||||||
|
return CoSTEERSingleFeedback(execution="x", return_checking="x", code="x", final_decision=decision)
|
||||||
|
|
||||||
|
def test_getitem(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERMultiFeedback
|
||||||
|
fb1, fb2 = self._make_fb(True), self._make_fb(False)
|
||||||
|
mf = CoSTEERMultiFeedback([fb1, fb2])
|
||||||
|
assert mf[0].final_decision is True
|
||||||
|
assert mf[1].final_decision is False
|
||||||
|
|
||||||
|
def test_len(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERMultiFeedback
|
||||||
|
assert len(CoSTEERMultiFeedback([self._make_fb()])) == 1
|
||||||
|
assert len(CoSTEERMultiFeedback([])) == 0
|
||||||
|
|
||||||
|
def test_append(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERMultiFeedback
|
||||||
|
mf = CoSTEERMultiFeedback([])
|
||||||
|
mf.append(self._make_fb(True))
|
||||||
|
assert len(mf) == 1
|
||||||
|
assert mf[0].final_decision is True
|
||||||
|
|
||||||
|
def test_iter(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERMultiFeedback
|
||||||
|
fbs = [self._make_fb(True), self._make_fb(True)]
|
||||||
|
mf = CoSTEERMultiFeedback(fbs)
|
||||||
|
assert list(mf) == fbs
|
||||||
|
|
||||||
|
def test_finished_all_true(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERMultiFeedback
|
||||||
|
mf = CoSTEERMultiFeedback([self._make_fb(True), self._make_fb(True)])
|
||||||
|
assert mf.finished() is True
|
||||||
|
|
||||||
|
def test_finished_one_false(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERMultiFeedback
|
||||||
|
mf = CoSTEERMultiFeedback([self._make_fb(True), self._make_fb(False)])
|
||||||
|
assert mf.finished() is False
|
||||||
|
|
||||||
|
def test_finished_with_none_skips(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERMultiFeedback
|
||||||
|
mf = CoSTEERMultiFeedback([self._make_fb(True), None])
|
||||||
|
assert mf.finished() is True # None = skipped = accepted
|
||||||
|
|
||||||
|
def test_bool_all_true(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERMultiFeedback
|
||||||
|
assert bool(CoSTEERMultiFeedback([self._make_fb(True), self._make_fb(True)])) is True
|
||||||
|
|
||||||
|
def test_bool_one_false(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERMultiFeedback
|
||||||
|
assert bool(CoSTEERMultiFeedback([self._make_fb(True), self._make_fb(False)])) is False
|
||||||
|
|
||||||
|
def test_is_acceptable_delegates(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERMultiFeedback
|
||||||
|
mf = CoSTEERMultiFeedback([self._make_fb(True), self._make_fb(True)])
|
||||||
|
assert mf.is_acceptable() is True
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# EvolvingItem
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestEvolvingItem:
|
||||||
|
def test_construction_without_gt(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evolvable_subjects import EvolvingItem
|
||||||
|
from rdagent.core.experiment import Task
|
||||||
|
|
||||||
|
t1 = Task(name="task1")
|
||||||
|
t2 = Task(name="task2")
|
||||||
|
ei = EvolvingItem(sub_tasks=[t1, t2])
|
||||||
|
assert len(ei.sub_tasks) == 2
|
||||||
|
assert ei.sub_gt_implementations is None
|
||||||
|
|
||||||
|
def test_construction_with_matching_gt(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evolvable_subjects import EvolvingItem
|
||||||
|
from rdagent.core.experiment import Task, FBWorkspace
|
||||||
|
t1, t2 = Task(name="t1"), Task(name="t2")
|
||||||
|
ws1, ws2 = FBWorkspace(), FBWorkspace()
|
||||||
|
ei = EvolvingItem(sub_tasks=[t1, t2], sub_gt_implementations=[ws1, ws2])
|
||||||
|
assert ei.sub_gt_implementations == [ws1, ws2]
|
||||||
|
|
||||||
|
def test_mismatched_gt_length_resets_to_none(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evolvable_subjects import EvolvingItem
|
||||||
|
from rdagent.core.experiment import Task, FBWorkspace
|
||||||
|
t1, t2 = Task(name="t1"), Task(name="t2")
|
||||||
|
ei = EvolvingItem(sub_tasks=[t1, t2], sub_gt_implementations=[FBWorkspace()])
|
||||||
|
assert ei.sub_gt_implementations is None
|
||||||
|
|
||||||
|
def test_from_experiment(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evolvable_subjects import EvolvingItem
|
||||||
|
from rdagent.core.experiment import Experiment, Task
|
||||||
|
|
||||||
|
exp = Experiment(sub_tasks=[Task(name="x")])
|
||||||
|
exp.based_experiments = ["base"]
|
||||||
|
exp.experiment_workspace = "ws"
|
||||||
|
ei = EvolvingItem.from_experiment(exp)
|
||||||
|
assert len(ei.sub_tasks) == 1
|
||||||
|
assert ei.based_experiments == ["base"]
|
||||||
|
assert ei.experiment_workspace == "ws"
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CoSTEERQueriedKnowledge
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestCoSTEERQueriedKnowledge:
|
||||||
|
def test_default_construction(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.knowledge_management import CoSTEERQueriedKnowledge
|
||||||
|
qk = CoSTEERQueriedKnowledge()
|
||||||
|
assert qk.success_task_to_knowledge_dict == {}
|
||||||
|
assert qk.failed_task_info_set == set()
|
||||||
|
|
||||||
|
def test_with_data(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.knowledge_management import CoSTEERQueriedKnowledge
|
||||||
|
qk = CoSTEERQueriedKnowledge(
|
||||||
|
success_task_to_knowledge_dict={"a": "knowledge_a"},
|
||||||
|
failed_task_info_set={"fail1", "fail2"},
|
||||||
|
)
|
||||||
|
assert qk.success_task_to_knowledge_dict["a"] == "knowledge_a"
|
||||||
|
assert "fail1" in qk.failed_task_info_set
|
||||||
|
assert "fail2" in qk.failed_task_info_set
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
"""Tests for CoSTEER config, task, and evolve strategy population logic."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CoSTEERSettings
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestCoSTEERSettings:
|
||||||
|
def test_default_values(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.config import CoSTEERSettings
|
||||||
|
s = CoSTEERSettings()
|
||||||
|
assert s.max_loop == 1
|
||||||
|
assert s.fail_task_trial_limit == 5
|
||||||
|
assert s.v2_query_component_limit == 1
|
||||||
|
assert s.v2_query_error_limit == 1
|
||||||
|
assert s.v2_query_former_trace_limit == 3
|
||||||
|
assert s.v2_add_fail_attempt_to_latest_successful_execution is False
|
||||||
|
assert s.v2_knowledge_sampler == 1.0
|
||||||
|
assert s.coder_use_cache is False
|
||||||
|
assert s.enable_filelock is False
|
||||||
|
|
||||||
|
def test_singleton_instance(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.config import CoSTEER_SETTINGS
|
||||||
|
from rdagent.components.coder.CoSTEER.config import CoSTEERSettings
|
||||||
|
assert isinstance(CoSTEER_SETTINGS, CoSTEERSettings)
|
||||||
|
assert CoSTEER_SETTINGS.max_loop == 1
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CoSTEERTask
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestCoSTEERTask:
|
||||||
|
def test_base_code_stored(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.task import CoSTEERTask
|
||||||
|
t = CoSTEERTask(name="test", base_code="print(1)")
|
||||||
|
assert t.base_code == "print(1)"
|
||||||
|
|
||||||
|
def test_base_code_none_by_default(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.task import CoSTEERTask
|
||||||
|
t = CoSTEERTask(name="test")
|
||||||
|
assert t.base_code is None
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MultiProcessEvolvingStrategy.assign_code_list_to_evo
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestAssignCodeListToEvo:
|
||||||
|
def test_empty_code_list_noops(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evolving_strategy import MultiProcessEvolvingStrategy
|
||||||
|
from rdagent.components.coder.CoSTEER.evolvable_subjects import EvolvingItem
|
||||||
|
from rdagent.core.experiment import Task
|
||||||
|
|
||||||
|
strat = MultiProcessEvolvingStrategy.__new__(MultiProcessEvolvingStrategy)
|
||||||
|
MultiProcessEvolvingStrategy.__init__(strat, scen=MagicMock(), settings=MagicMock())
|
||||||
|
|
||||||
|
ei = EvolvingItem(sub_tasks=[Task(name="t1")])
|
||||||
|
ei.experiment_workspace = MagicMock()
|
||||||
|
result = strat.assign_code_list_to_evo([{}], ei)
|
||||||
|
assert result is ei
|
||||||
|
|
||||||
|
def test_none_entry_is_skipped(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evolving_strategy import MultiProcessEvolvingStrategy
|
||||||
|
from rdagent.components.coder.CoSTEER.evolvable_subjects import EvolvingItem
|
||||||
|
from rdagent.core.experiment import Task
|
||||||
|
|
||||||
|
strat = MultiProcessEvolvingStrategy.__new__(MultiProcessEvolvingStrategy)
|
||||||
|
MultiProcessEvolvingStrategy.__init__(strat, scen=MagicMock(), settings=MagicMock())
|
||||||
|
|
||||||
|
ei = EvolvingItem(sub_tasks=[Task(name="t1")])
|
||||||
|
ei.experiment_workspace = MagicMock()
|
||||||
|
result = strat.assign_code_list_to_evo([None], ei)
|
||||||
|
assert result.sub_workspace_list[0] is None # unchanged
|
||||||
|
|
||||||
|
def test_code_injects_files(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evolving_strategy import MultiProcessEvolvingStrategy
|
||||||
|
from rdagent.components.coder.CoSTEER.evolvable_subjects import EvolvingItem
|
||||||
|
from rdagent.core.experiment import Task
|
||||||
|
|
||||||
|
strat = MultiProcessEvolvingStrategy.__new__(MultiProcessEvolvingStrategy)
|
||||||
|
MultiProcessEvolvingStrategy.__init__(strat, scen=MagicMock(), settings=MagicMock())
|
||||||
|
|
||||||
|
ei = EvolvingItem(sub_tasks=[Task(name="t1")])
|
||||||
|
mock_ws = MagicMock()
|
||||||
|
ei.experiment_workspace = mock_ws
|
||||||
|
|
||||||
|
strat.assign_code_list_to_evo([{"factor.py": "x=1"}], ei)
|
||||||
|
mock_ws.inject_files.assert_called_once_with(**{"factor.py": "x=1"})
|
||||||
|
|
||||||
|
def test_change_summary_extracted(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evolving_strategy import MultiProcessEvolvingStrategy
|
||||||
|
from rdagent.components.coder.CoSTEER.evolvable_subjects import EvolvingItem
|
||||||
|
from rdagent.core.experiment import Task
|
||||||
|
|
||||||
|
strat = MultiProcessEvolvingStrategy.__new__(MultiProcessEvolvingStrategy)
|
||||||
|
MultiProcessEvolvingStrategy.__init__(strat, scen=MagicMock(), settings=MagicMock())
|
||||||
|
|
||||||
|
mock_ws = MagicMock()
|
||||||
|
ei = EvolvingItem(sub_tasks=[Task(name="t1")])
|
||||||
|
ei.experiment_workspace = mock_ws
|
||||||
|
|
||||||
|
strat.assign_code_list_to_evo([{"__change_summary__": "summary", "factor.py": "x"}], ei)
|
||||||
|
assert mock_ws.change_summary == "summary"
|
||||||
|
# change_summary should have been popped from dict
|
||||||
|
mock_ws.inject_files.assert_called_once_with(**{"factor.py": "x"})
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MultiProcessEvolvingStrategy.evolve_iter
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestEvolveIter:
|
||||||
|
def _make_strat(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evolving_strategy import MultiProcessEvolvingStrategy
|
||||||
|
from rdagent.components.coder.CoSTEER.config import CoSTEERSettings
|
||||||
|
strat = MultiProcessEvolvingStrategy(
|
||||||
|
scen=MagicMock(), settings=CoSTEERSettings(), improve_mode=False,
|
||||||
|
)
|
||||||
|
return strat
|
||||||
|
|
||||||
|
def _make_evo(self, n_tasks=2):
|
||||||
|
from rdagent.components.coder.CoSTEER.evolvable_subjects import EvolvingItem
|
||||||
|
from rdagent.core.experiment import Task
|
||||||
|
tasks = [Task(name=f"task_{i}") for i in range(n_tasks)]
|
||||||
|
for t in tasks:
|
||||||
|
t.get_task_information = MagicMock(return_value=f"info_{t.name}")
|
||||||
|
ei = EvolvingItem(sub_tasks=tasks)
|
||||||
|
ei.experiment_workspace = MagicMock()
|
||||||
|
return ei
|
||||||
|
|
||||||
|
def test_raises_without_queried_knowledge(self):
|
||||||
|
strat = self._make_strat()
|
||||||
|
evo = self._make_evo()
|
||||||
|
with pytest.raises(ValueError, match="queried_knowledge"):
|
||||||
|
next(strat.evolve_iter(evo=evo, queried_knowledge=None))
|
||||||
|
|
||||||
|
def test_successful_tasks_not_scheduled(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.knowledge_management import CoSTEERQueriedKnowledge
|
||||||
|
strat = self._make_strat()
|
||||||
|
evo = self._make_evo(n_tasks=1)
|
||||||
|
qk = CoSTEERQueriedKnowledge(
|
||||||
|
success_task_to_knowledge_dict={
|
||||||
|
"info_task_0": MagicMock(implementation=MagicMock(file_dict={"f.py": "x"})),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# evolve_iter is a generator, next() starts it
|
||||||
|
gen = strat.evolve_iter(evo=evo, queried_knowledge=qk)
|
||||||
|
# Should yield the evo (populated from success knowledge)
|
||||||
|
result = next(gen)
|
||||||
|
# The task was already successful, so no new scheduling
|
||||||
|
assert result is evo
|
||||||
|
|
||||||
|
def test_failed_tasks_skipped(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.knowledge_management import CoSTEERQueriedKnowledge
|
||||||
|
strat = self._make_strat()
|
||||||
|
evo = self._make_evo(n_tasks=1)
|
||||||
|
qk = CoSTEERQueriedKnowledge(
|
||||||
|
failed_task_info_set={"info_task_0"},
|
||||||
|
)
|
||||||
|
gen = strat.evolve_iter(evo=evo, queried_knowledge=qk)
|
||||||
|
result = next(gen)
|
||||||
|
# Task skipped because it's in failed_set
|
||||||
|
assert result is evo
|
||||||
|
|
||||||
|
def test_improve_mode_skips_with_no_last_feedback(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.knowledge_management import CoSTEERQueriedKnowledge
|
||||||
|
strat = self._make_strat()
|
||||||
|
strat.improve_mode = True
|
||||||
|
evo = self._make_evo(n_tasks=1)
|
||||||
|
qk = CoSTEERQueriedKnowledge()
|
||||||
|
gen = strat.evolve_iter(evo=evo, queried_knowledge=qk, evolving_trace=[])
|
||||||
|
result = next(gen)
|
||||||
|
# In improve_mode with no last_feedback, task should be skipped
|
||||||
|
# (code_list[0] should be {} — empty implementation)
|
||||||
|
assert result is evo
|
||||||
|
|
||||||
|
def test_non_improve_mode_schedules_new_tasks(self):
|
||||||
|
"""Tasks not in success/failed should be scheduled."""
|
||||||
|
from rdagent.components.coder.CoSTEER.knowledge_management import CoSTEERQueriedKnowledge
|
||||||
|
strat = self._make_strat()
|
||||||
|
evo = self._make_evo(n_tasks=1)
|
||||||
|
qk = CoSTEERQueriedKnowledge()
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"rdagent.components.coder.CoSTEER.evolving_strategy.multiprocessing_wrapper",
|
||||||
|
return_value=[{"factor.py": "x=1"}],
|
||||||
|
):
|
||||||
|
gen = strat.evolve_iter(evo=evo, queried_knowledge=qk)
|
||||||
|
result = next(gen)
|
||||||
|
assert result is evo
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CoSTEERMultiEvaluator (partial)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestCoSTEERMultiEvaluator:
|
||||||
|
def test_init_with_single_evaluator(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERMultiEvaluator
|
||||||
|
mock_eval = MagicMock()
|
||||||
|
eva = CoSTEERMultiEvaluator(single_evaluator=mock_eval, scen=MagicMock())
|
||||||
|
assert eva.single_evaluator is mock_eval
|
||||||
|
|
||||||
|
def test_init_with_evaluator_list(self):
|
||||||
|
from rdagent.components.coder.CoSTEER.evaluators import CoSTEERMultiEvaluator
|
||||||
|
mock_list = [MagicMock(), MagicMock()]
|
||||||
|
eva = CoSTEERMultiEvaluator(single_evaluator=mock_list, scen=MagicMock())
|
||||||
|
assert eva.single_evaluator is mock_list
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
"""Tests for factor_coder — evaluators, task, workspace."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FactorTask
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestFactorTask:
|
||||||
|
def test_construction_fields(self):
|
||||||
|
from rdagent.components.coder.factor_coder.factor import FactorTask
|
||||||
|
t = FactorTask(
|
||||||
|
factor_name="f1",
|
||||||
|
factor_description="desc",
|
||||||
|
factor_formulation="formula",
|
||||||
|
variables={"x": 1},
|
||||||
|
resource="r1",
|
||||||
|
)
|
||||||
|
assert t.factor_name == "f1"
|
||||||
|
assert t.factor_description == "desc"
|
||||||
|
assert t.factor_formulation == "formula"
|
||||||
|
assert t.variables == {"x": 1}
|
||||||
|
assert t.factor_resources == "r1"
|
||||||
|
assert t.factor_implementation is False
|
||||||
|
assert t.base_code is None # from CoSTEERTask
|
||||||
|
|
||||||
|
def test_get_task_information(self):
|
||||||
|
from rdagent.components.coder.factor_coder.factor import FactorTask
|
||||||
|
t = FactorTask("f1", "desc", "formula", variables={"x": 1})
|
||||||
|
info = t.get_task_information()
|
||||||
|
assert "factor_name: f1" in info
|
||||||
|
assert "factor_description: desc" in info
|
||||||
|
assert "factor_formulation: formula" in info
|
||||||
|
assert "variables: {'x': 1}" in info
|
||||||
|
|
||||||
|
def test_get_task_brief_information(self):
|
||||||
|
from rdagent.components.coder.factor_coder.factor import FactorTask
|
||||||
|
t = FactorTask("f1", "desc", "formula")
|
||||||
|
info = t.get_task_brief_information()
|
||||||
|
assert "factor_name: f1" in info
|
||||||
|
|
||||||
|
def test_get_task_information_and_implementation_result(self):
|
||||||
|
from rdagent.components.coder.factor_coder.factor import FactorTask
|
||||||
|
t = FactorTask("f1", "desc", "formula")
|
||||||
|
result = t.get_task_information_and_implementation_result()
|
||||||
|
assert result["factor_name"] == "f1"
|
||||||
|
assert result["factor_description"] == "desc"
|
||||||
|
assert "factor_implementation" in result
|
||||||
|
|
||||||
|
def test_from_dict(self):
|
||||||
|
from rdagent.components.coder.factor_coder.factor import FactorTask
|
||||||
|
d = {
|
||||||
|
"factor_name": "f2",
|
||||||
|
"factor_description": "d2",
|
||||||
|
"factor_formulation": "f2",
|
||||||
|
"variables": {},
|
||||||
|
"resource": None,
|
||||||
|
"factor_implementation": True,
|
||||||
|
}
|
||||||
|
t = FactorTask.from_dict(d)
|
||||||
|
assert t.factor_name == "f2"
|
||||||
|
assert t.factor_implementation is True
|
||||||
|
|
||||||
|
def test_repr(self):
|
||||||
|
from rdagent.components.coder.factor_coder.factor import FactorTask
|
||||||
|
t = FactorTask("myfactor", "desc", "formula")
|
||||||
|
assert "FactorTask" in repr(t)
|
||||||
|
assert "myfactor" in repr(t)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FactorFBWorkspace
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestFactorFBWorkspace:
|
||||||
|
def test_init_sets_workspace_path(self):
|
||||||
|
from rdagent.components.coder.factor_coder.factor import FactorFBWorkspace, FactorTask
|
||||||
|
t = FactorTask("f1", "desc", "formula")
|
||||||
|
ws = FactorFBWorkspace(target_task=t)
|
||||||
|
assert ws.workspace_path is not None
|
||||||
|
# Directory is created lazily by execute(), not in __init__
|
||||||
|
assert isinstance(ws.workspace_path, Path)
|
||||||
|
|
||||||
|
def test_execute_returns_message_and_dataframe(self):
|
||||||
|
from rdagent.components.coder.factor_coder.factor import FactorFBWorkspace, FactorTask
|
||||||
|
t = FactorTask("f1", "desc", "formula")
|
||||||
|
t.version = 1
|
||||||
|
ws = FactorFBWorkspace(target_task=t)
|
||||||
|
# Inject valid factor code
|
||||||
|
ws.inject_files(**{
|
||||||
|
"factor.py": (
|
||||||
|
"import pandas as pd\n"
|
||||||
|
"import numpy as np\n"
|
||||||
|
"data = pd.read_hdf('intraday_pv.h5', key='data')\n"
|
||||||
|
"factor_val = data['$close'].pct_change()\n"
|
||||||
|
"factor_val = factor_val.to_frame('f1')\n"
|
||||||
|
"factor_val.to_hdf('result.h5', key='data', mode='w')\n"
|
||||||
|
),
|
||||||
|
})
|
||||||
|
msg, df = ws.execute()
|
||||||
|
assert isinstance(msg, str)
|
||||||
|
assert df is not None
|
||||||
|
|
||||||
|
def test_execute_succeeds_and_returns_data(self):
|
||||||
|
from rdagent.components.coder.factor_coder.factor import FactorFBWorkspace, FactorTask
|
||||||
|
t = FactorTask("fl1", "desc", "formula")
|
||||||
|
ws = FactorFBWorkspace(target_task=t)
|
||||||
|
ws.inject_files(**{
|
||||||
|
"factor.py": (
|
||||||
|
"import pandas as pd\n"
|
||||||
|
"data = pd.read_hdf('intraday_pv.h5', key='data')\n"
|
||||||
|
"factor_val = data['$close'].pct_change().to_frame('fl1')\n"
|
||||||
|
"factor_val.to_hdf('result.h5', key='data', mode='w')\n"
|
||||||
|
),
|
||||||
|
})
|
||||||
|
msg, df = ws.execute()
|
||||||
|
assert FactorFBWorkspace.FB_EXEC_SUCCESS in msg
|
||||||
|
assert FactorFBWorkspace.FB_OUTPUT_FILE_FOUND in msg
|
||||||
|
assert df is not None
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FactorEvaluatorForCoder (partial integration)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestFactorEvaluatorForCoder:
|
||||||
|
def test_init_creates_sub_evaluators(self):
|
||||||
|
from rdagent.components.coder.factor_coder.evaluators import FactorEvaluatorForCoder
|
||||||
|
mock_scen = MagicMock()
|
||||||
|
eva = FactorEvaluatorForCoder(scen=mock_scen)
|
||||||
|
assert eva.value_evaluator is not None
|
||||||
|
assert eva.code_evaluator is not None
|
||||||
|
assert eva.final_decision_evaluator is not None
|
||||||
|
|
||||||
|
def test_evaluate_with_none_implementation(self):
|
||||||
|
from rdagent.components.coder.factor_coder.evaluators import FactorEvaluatorForCoder
|
||||||
|
eva = FactorEvaluatorForCoder(scen=MagicMock())
|
||||||
|
assert eva.evaluate(target_task=MagicMock(), implementation=None) is None
|
||||||
|
|
||||||
|
def test_evaluate_returns_queried_knowledge_if_present(self):
|
||||||
|
from rdagent.components.coder.factor_coder.evaluators import FactorEvaluatorForCoder
|
||||||
|
from rdagent.components.coder.factor_coder.factor import FactorTask
|
||||||
|
|
||||||
|
eva = FactorEvaluatorForCoder(scen=MagicMock())
|
||||||
|
|
||||||
|
t = FactorTask("f1", "desc", "formula")
|
||||||
|
qk = MagicMock()
|
||||||
|
qk.success_task_to_knowledge_dict = {"info_f1": MagicMock(feedback="cached_fb")}
|
||||||
|
t.get_task_information = MagicMock(return_value="info_f1")
|
||||||
|
qk.failed_task_info_set = set()
|
||||||
|
|
||||||
|
fb = eva.evaluate(target_task=t, implementation=MagicMock(), queried_knowledge=qk)
|
||||||
|
assert fb == "cached_fb" # returned from cache
|
||||||
|
|
||||||
|
def test_evaluate_skips_failed_task(self):
|
||||||
|
from rdagent.components.coder.factor_coder.evaluators import FactorEvaluatorForCoder
|
||||||
|
from rdagent.components.coder.factor_coder.factor import FactorTask
|
||||||
|
|
||||||
|
eva = FactorEvaluatorForCoder(scen=MagicMock())
|
||||||
|
|
||||||
|
t = FactorTask("f1", "desc", "formula")
|
||||||
|
qk = MagicMock()
|
||||||
|
qk.success_task_to_knowledge_dict = {}
|
||||||
|
t.get_task_information = MagicMock(return_value="info_f1")
|
||||||
|
qk.failed_task_info_set = {"info_f1"}
|
||||||
|
|
||||||
|
fb = eva.evaluate(target_task=t, implementation=MagicMock(), queried_knowledge=qk)
|
||||||
|
assert fb.final_decision is False
|
||||||
|
assert "failed too many times" in fb.execution_feedback
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FactorEvaluator (eva_utils) — constructors and identity
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestFactorEvaluatorsInit:
|
||||||
|
def test_factor_inf_evaluator_init(self):
|
||||||
|
from rdagent.components.coder.factor_coder.eva_utils import FactorInfEvaluator
|
||||||
|
eva = FactorInfEvaluator()
|
||||||
|
assert str(eva) == "FactorInfEvaluator"
|
||||||
|
|
||||||
|
def test_factor_single_column_evaluator_init(self):
|
||||||
|
from rdagent.components.coder.factor_coder.eva_utils import FactorSingleColumnEvaluator
|
||||||
|
eva = FactorSingleColumnEvaluator()
|
||||||
|
assert str(eva) == "FactorSingleColumnEvaluator"
|
||||||
|
|
||||||
|
def test_factor_output_format_evaluator_init(self):
|
||||||
|
from rdagent.components.coder.factor_coder.eva_utils import FactorOutputFormatEvaluator
|
||||||
|
eva = FactorOutputFormatEvaluator()
|
||||||
|
assert str(eva) == "FactorOutputFormatEvaluator"
|
||||||
|
|
||||||
|
def test_factor_missing_values_evaluator_init(self):
|
||||||
|
from rdagent.components.coder.factor_coder.eva_utils import FactorMissingValuesEvaluator
|
||||||
|
eva = FactorMissingValuesEvaluator()
|
||||||
|
assert str(eva) == "FactorMissingValuesEvaluator"
|
||||||
|
|
||||||
|
def test_factor_correlation_evaluator_init(self):
|
||||||
|
from rdagent.components.coder.factor_coder.eva_utils import FactorCorrelationEvaluator
|
||||||
|
eva = FactorCorrelationEvaluator(hard_check=True)
|
||||||
|
assert eva.hard_check is True
|
||||||
|
assert str(eva) == "FactorCorrelationEvaluator"
|
||||||
|
|
||||||
|
def test_factor_value_evaluator_init(self):
|
||||||
|
from rdagent.components.coder.factor_coder.eva_utils import FactorValueEvaluator
|
||||||
|
mock_scen = MagicMock()
|
||||||
|
eva = FactorValueEvaluator(mock_scen)
|
||||||
|
assert eva.scen is mock_scen
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
"""Tests for bugs found in the factor evaluation pipeline."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# Project root
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Bug 1: Missing `import sys` in _save_factor_values (factor_runner.py:968)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestSaveFactorValuesMissingSysImport:
|
||||||
|
"""Verify that _save_factor_values has `import sys` — uses sys.executable at line 968."""
|
||||||
|
|
||||||
|
def test_save_factor_values_has_sys_import(self):
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from rdagent.scenarios.qlib.developer.factor_runner import QlibFactorRunner
|
||||||
|
|
||||||
|
source = inspect.getsource(QlibFactorRunner._save_factor_values)
|
||||||
|
|
||||||
|
assert "import sys" in source, (
|
||||||
|
"BUG: _save_factor_values calls sys.executable but does not import sys. "
|
||||||
|
"This causes a NameError at runtime, silently swallowed by the try/except."
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_save_factor_values_nameerror_when_called(self):
|
||||||
|
"""Simulate calling _save_factor_values without sys available."""
|
||||||
|
from rdagent.scenarios.qlib.developer.factor_runner import QlibFactorRunner
|
||||||
|
|
||||||
|
runner = QlibFactorRunner.__new__(QlibFactorRunner)
|
||||||
|
|
||||||
|
mock_exp = MagicMock()
|
||||||
|
mock_exp.sub_workspace_list = []
|
||||||
|
mock_exp.experiment_workspace.workspace_path = None
|
||||||
|
|
||||||
|
# This should NOT raise NameError for 'sys' — if it does, the bug is present
|
||||||
|
try:
|
||||||
|
runner._save_factor_values("TestFactor", mock_exp)
|
||||||
|
except NameError as e:
|
||||||
|
if "sys" in str(e):
|
||||||
|
pytest.fail(
|
||||||
|
"BUG CONFIRMED: _save_factor_values raises NameError because "
|
||||||
|
"'sys' is not imported. The factor values parquet is never saved."
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Bug 2: `acc_rate` undefined in FactorEqualValueRatioEvaluator (eva_utils.py:335-346)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestEqualValueRatioAccRateUndefined:
|
||||||
|
"""Verify FactorEqualValueRatioEvaluator handles shape-mismatch correctly."""
|
||||||
|
|
||||||
|
def test_acc_rate_undefined_after_except(self):
|
||||||
|
"""If gen_df.sub(gt_df) raises, acc_rate should still be defined (default -1)."""
|
||||||
|
from rdagent.components.coder.factor_coder.eva_utils import FactorEqualValueRatioEvaluator
|
||||||
|
|
||||||
|
evaluator = FactorEqualValueRatioEvaluator()
|
||||||
|
|
||||||
|
# Simulate the case where _get_df returns None for gt_df, which causes
|
||||||
|
# gen_df.sub(None) to raise AttributeError. The except clause must not
|
||||||
|
# reference an undefined acc_rate variable.
|
||||||
|
gen_df = pd.DataFrame({"x": [1.0, 2.0, 3.0]}, index=[0, 1, 2])
|
||||||
|
|
||||||
|
gt_ws = MagicMock()
|
||||||
|
imp_ws = MagicMock()
|
||||||
|
|
||||||
|
gt_ws.execute.return_value = ("", None) # _get_df will set gt_df = None
|
||||||
|
imp_ws.execute.return_value = ("", gen_df)
|
||||||
|
|
||||||
|
# Should NOT raise NameError
|
||||||
|
try:
|
||||||
|
result = evaluator.evaluate(imp_ws, gt_ws)
|
||||||
|
assert isinstance(result, tuple)
|
||||||
|
assert len(result) == 2
|
||||||
|
feedback, metric = result
|
||||||
|
assert metric == -1, f"Expected -1 (fallback), got {metric}"
|
||||||
|
except NameError as e:
|
||||||
|
if "acc_rate" in str(e):
|
||||||
|
pytest.fail(
|
||||||
|
"BUG CONFIRMED: FactorEqualValueRatioEvaluator references 'acc_rate' "
|
||||||
|
"which is undefined when gen_df.sub(gt_df) raises an exception."
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def test_acc_rate_defined_when_shapes_match(self):
|
||||||
|
"""Normal case: same shapes — acc_rate should be defined and returned."""
|
||||||
|
from rdagent.components.coder.factor_coder.eva_utils import FactorEqualValueRatioEvaluator
|
||||||
|
|
||||||
|
evaluator = FactorEqualValueRatioEvaluator()
|
||||||
|
|
||||||
|
gt_ws = MagicMock()
|
||||||
|
imp_ws = MagicMock()
|
||||||
|
|
||||||
|
gen_df = pd.DataFrame({"x": [1.0, 2.0, 3.0]}, index=[0, 1, 2])
|
||||||
|
gt_df = pd.DataFrame({"y": [1.0, 2.0, 3.0]}, index=[0, 1, 2])
|
||||||
|
|
||||||
|
gt_ws.execute.return_value = ("", gt_df)
|
||||||
|
imp_ws.execute.return_value = ("", gen_df)
|
||||||
|
|
||||||
|
result = evaluator.evaluate(imp_ws, gt_ws)
|
||||||
|
assert isinstance(result, tuple)
|
||||||
|
assert len(result) == 2
|
||||||
|
feedback, metric = result
|
||||||
|
# When values match within tolerance, metric should be a float near 1.0
|
||||||
|
assert isinstance(metric, float) or isinstance(metric, (int, np.integer))
|
||||||
|
assert metric >= 0
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Bug 3: Annualization factor hardcoded in _evaluate_factor_directly (factor_runner.py:553)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestAnnualizationFactorInDirectEval:
|
||||||
|
"""Verify direct evaluation uses correct annualization with forward_return_bars."""
|
||||||
|
|
||||||
|
def test_annualization_factor_uses_forward_bars(self):
|
||||||
|
"""The direct eval method hardcodes 96 instead of using forward_return_bars param."""
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from rdagent.scenarios.qlib.developer.factor_runner import QlibFactorRunner
|
||||||
|
|
||||||
|
source = inspect.getsource(QlibFactorRunner._evaluate_factor_directly)
|
||||||
|
|
||||||
|
# Check that the method uses `np.sqrt(252 * 1440 / 96)` which hardcodes 96
|
||||||
|
# This should ideally be parameterized or at least consistent with the
|
||||||
|
# forward return calculation at line ~530 which also uses 96.
|
||||||
|
assert "np.sqrt(252 * 1440 / 96)" in source or "np.sqrt(252*1440/96)" in source, (
|
||||||
|
"The annualization factor in _evaluate_factor_directly should match "
|
||||||
|
"the forward_return_bars used for computing forward returns."
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_ann_factor_is_consistent_with_forward_ret(self):
|
||||||
|
"""Verify both the forward return shift and annualization use 96 bars."""
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from rdagent.scenarios.qlib.developer.factor_runner import QlibFactorRunner
|
||||||
|
|
||||||
|
source = inspect.getsource(QlibFactorRunner._evaluate_factor_directly)
|
||||||
|
|
||||||
|
# forward return uses `.shift(-96)` at line ~530
|
||||||
|
assert '.shift(-96)' in source, "Forward return shift should use 96 bars (1 day)"
|
||||||
|
|
||||||
|
# annualization should also use 96
|
||||||
|
assert '1440 / 96' in source, (
|
||||||
|
"Annualization factor should use the same number (96) as the forward return shift"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Bug 4: _fix_inf_nan_handling inserts code before .dropna() or .to_hdf() in wrong context
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestInfNanHandlingInsertion:
|
||||||
|
"""Verify inf/nan auto-fixer doesn't insert code in the wrong context."""
|
||||||
|
|
||||||
|
def test_no_insertion_before_dropna_when_no_column_found(self):
|
||||||
|
from rdagent.components.coder.factor_coder.auto_fixer import FactorAutoFixer
|
||||||
|
|
||||||
|
fixer = FactorAutoFixer()
|
||||||
|
|
||||||
|
# Code where the LAST assignment before .dropna() is NOT a df['col'] = pattern
|
||||||
|
# but dropna() still exists (e.g., on a temporary variable)
|
||||||
|
code = (
|
||||||
|
"def calc():\n"
|
||||||
|
" df = pd.read_hdf('data.h5', key='data')\n"
|
||||||
|
" temp = df['$close'].diff()\n"
|
||||||
|
" temp = temp.dropna()\n"
|
||||||
|
" df['result'] = temp * 2\n"
|
||||||
|
" result = df[['result']]\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = fixer.fix(code)
|
||||||
|
|
||||||
|
# The code should still be valid (no syntax error from misplaced insertion)
|
||||||
|
import ast
|
||||||
|
try:
|
||||||
|
ast.parse(result)
|
||||||
|
except SyntaxError as e:
|
||||||
|
pytest.fail(f"Auto-fixer produced invalid Python code: {e}")
|
||||||
|
|
||||||
|
def test_inf_handling_inserted_before_result_assignment(self):
|
||||||
|
from rdagent.components.coder.factor_coder.auto_fixer import FactorAutoFixer
|
||||||
|
|
||||||
|
fixer = FactorAutoFixer()
|
||||||
|
|
||||||
|
code = (
|
||||||
|
"def calc():\n"
|
||||||
|
" df = pd.read_hdf('data.h5', key='data')\n"
|
||||||
|
" df['myfactor'] = df['$close'] / df['sigma_60bar']\n"
|
||||||
|
" df['myfactor'] = df['myfactor'] / df['sigma_5bar']\n"
|
||||||
|
" result = df[['myfactor']]\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = fixer.fix(code)
|
||||||
|
|
||||||
|
# Should have added inf handling before the result = df[[...]] line
|
||||||
|
# but not broken syntax
|
||||||
|
import ast
|
||||||
|
try:
|
||||||
|
ast.parse(result)
|
||||||
|
except SyntaxError as e:
|
||||||
|
pytest.fail(f"Auto-fixer produced invalid Python code: {e}")
|
||||||
|
|
||||||
|
assert "replace([np.inf, -np.inf]" in result
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Bug 5: scan_factors reads factor_code twice (predix_full_eval.py:174 + 195)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class TestScanFactorsDoubleRead:
|
||||||
|
"""Verify scan_factors doesn't wastefully read factor file twice."""
|
||||||
|
|
||||||
|
def test_factor_code_read_only_when_needed(self):
|
||||||
|
"""Confirm the scan_factors double-read behavior (line 174+195)."""
|
||||||
|
import inspect
|
||||||
|
from scripts import predix_full_eval
|
||||||
|
|
||||||
|
source = inspect.getsource(predix_full_eval.scan_factors)
|
||||||
|
|
||||||
|
# Count occurrences of `.read_text()`
|
||||||
|
count = source.count(".read_text()")
|
||||||
|
# Expected: at least 2 (line 174 in fallback, line 195 in FactorInfo)
|
||||||
|
# Bug: if factor_name comes from result.h5 (line 168-170), then line 174
|
||||||
|
# is skipped, but line 195 always reads again — that's one wasted read.
|
||||||
|
assert count == 2, (
|
||||||
|
f"scan_factors has {count} read_text() calls. "
|
||||||
|
"Expected exactly 2 (one for name extraction, one for FactorInfo). "
|
||||||
|
"Consider caching to avoid double reads."
|
||||||
|
)
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
"""Tests for model_coder — ModelTask, shape/value evaluators, config."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ModelTask
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestModelTask:
|
||||||
|
def test_construction_fields(self):
|
||||||
|
from rdagent.components.coder.model_coder.model import ModelTask
|
||||||
|
t = ModelTask(
|
||||||
|
name="m1",
|
||||||
|
description="desc",
|
||||||
|
architecture="LSTM",
|
||||||
|
hyperparameters={"lr": 0.001},
|
||||||
|
training_hyperparameters={"epochs": 10},
|
||||||
|
formulation="y = f(x)",
|
||||||
|
variables={"x": "feature"},
|
||||||
|
model_type="TimeSeries",
|
||||||
|
)
|
||||||
|
assert t.name == "m1"
|
||||||
|
assert t.description == "desc"
|
||||||
|
assert t.architecture == "LSTM"
|
||||||
|
assert t.hyperparameters == {"lr": 0.001}
|
||||||
|
assert t.training_hyperparameters == {"epochs": 10}
|
||||||
|
assert t.formulation == "y = f(x)"
|
||||||
|
assert t.variables == {"x": "feature"}
|
||||||
|
assert t.model_type == "TimeSeries"
|
||||||
|
assert t.base_code is None
|
||||||
|
|
||||||
|
def test_get_task_information(self):
|
||||||
|
from rdagent.components.coder.model_coder.model import ModelTask
|
||||||
|
t = ModelTask(
|
||||||
|
name="m1", description="desc", architecture="LSTM",
|
||||||
|
hyperparameters={}, training_hyperparameters={},
|
||||||
|
model_type="Tabular",
|
||||||
|
)
|
||||||
|
info = t.get_task_information()
|
||||||
|
assert "name: m1" in info
|
||||||
|
assert "architecture: LSTM" in info
|
||||||
|
assert "model_type: Tabular" in info
|
||||||
|
|
||||||
|
def test_get_task_information_with_optional_fields(self):
|
||||||
|
from rdagent.components.coder.model_coder.model import ModelTask
|
||||||
|
t = ModelTask(
|
||||||
|
name="m2", description="d2", architecture="GRU",
|
||||||
|
hyperparameters={}, training_hyperparameters={},
|
||||||
|
formulation="f1", variables={"v": 1}, model_type="Graph",
|
||||||
|
)
|
||||||
|
info = t.get_task_information()
|
||||||
|
assert "formulation: f1" in info
|
||||||
|
assert "variables: {'v': 1}" in info
|
||||||
|
|
||||||
|
def test_get_task_brief_information(self):
|
||||||
|
from rdagent.components.coder.model_coder.model import ModelTask
|
||||||
|
t = ModelTask(
|
||||||
|
name="m1", description="desc", architecture="LSTM",
|
||||||
|
hyperparameters={"lr": 0.01}, training_hyperparameters={"epochs": 5},
|
||||||
|
)
|
||||||
|
info = t.get_task_brief_information()
|
||||||
|
assert "name: m1" in info
|
||||||
|
assert "architecture: LSTM" in info
|
||||||
|
assert "hyperparameters" in info
|
||||||
|
|
||||||
|
def test_from_dict(self):
|
||||||
|
from rdagent.components.coder.model_coder.model import ModelTask
|
||||||
|
d = {
|
||||||
|
"name": "m3", "description": "d3", "architecture": "TCN",
|
||||||
|
"hyperparameters": {}, "training_hyperparameters": {},
|
||||||
|
}
|
||||||
|
t = ModelTask.from_dict(d)
|
||||||
|
assert t.name == "m3"
|
||||||
|
|
||||||
|
def test_repr(self):
|
||||||
|
from rdagent.components.coder.model_coder.model import ModelTask
|
||||||
|
t = ModelTask(
|
||||||
|
name="mymodel", description="d", architecture="LSTM",
|
||||||
|
hyperparameters={}, training_hyperparameters={},
|
||||||
|
)
|
||||||
|
assert "ModelTask" in repr(t)
|
||||||
|
assert "mymodel" in repr(t)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Shape/Value evaluators (eva_utils)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestShapeEvaluator:
|
||||||
|
def test_correct_shape(self):
|
||||||
|
from rdagent.components.coder.model_coder.eva_utils import shape_evaluator
|
||||||
|
msg, ok = shape_evaluator(np.ones((32, 10)), target_shape=(32, 10))
|
||||||
|
assert ok is True
|
||||||
|
assert "correct" in msg.lower()
|
||||||
|
|
||||||
|
def test_incorrect_shape(self):
|
||||||
|
from rdagent.components.coder.model_coder.eva_utils import shape_evaluator
|
||||||
|
msg, ok = shape_evaluator(np.ones((32, 5)), target_shape=(32, 10))
|
||||||
|
assert ok is False
|
||||||
|
assert "incorrect" in msg.lower()
|
||||||
|
|
||||||
|
def test_none_prediction(self):
|
||||||
|
from rdagent.components.coder.model_coder.eva_utils import shape_evaluator
|
||||||
|
msg, ok = shape_evaluator(None, target_shape=(32, 10))
|
||||||
|
assert ok is False
|
||||||
|
|
||||||
|
def test_none_target_shape(self):
|
||||||
|
from rdagent.components.coder.model_coder.eva_utils import shape_evaluator
|
||||||
|
msg, ok = shape_evaluator(np.ones((3,)), target_shape=None)
|
||||||
|
assert ok is False
|
||||||
|
|
||||||
|
def test_float_array(self):
|
||||||
|
from rdagent.components.coder.model_coder.eva_utils import shape_evaluator
|
||||||
|
msg, ok = shape_evaluator(np.array([1.0, 2.0]), target_shape=(2,))
|
||||||
|
assert ok is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestValueEvaluator:
|
||||||
|
def test_none_prediction(self):
|
||||||
|
from rdagent.components.coder.model_coder.eva_utils import value_evaluator
|
||||||
|
msg, ok = value_evaluator(None, np.ones((3,)))
|
||||||
|
assert ok is False
|
||||||
|
|
||||||
|
def test_none_target(self):
|
||||||
|
from rdagent.components.coder.model_coder.eva_utils import value_evaluator
|
||||||
|
msg, ok = value_evaluator(np.ones((3,)), None)
|
||||||
|
assert ok is False
|
||||||
|
|
||||||
|
def test_small_difference_passes(self):
|
||||||
|
from rdagent.components.coder.model_coder.eva_utils import value_evaluator
|
||||||
|
msg, ok = value_evaluator(
|
||||||
|
np.array([1.0, 2.0, 3.0]),
|
||||||
|
np.array([1.0, 2.0, 3.01]),
|
||||||
|
)
|
||||||
|
assert bool(ok) is True # diff < 0.1
|
||||||
|
|
||||||
|
def test_large_difference_fails(self):
|
||||||
|
from rdagent.components.coder.model_coder.eva_utils import value_evaluator
|
||||||
|
msg, ok = value_evaluator(
|
||||||
|
np.array([1.0, 2.0]),
|
||||||
|
np.array([10.0, 20.0]),
|
||||||
|
)
|
||||||
|
assert bool(ok) is False # diff > 0.1
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ModelCoSTEERSettings
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestModelCoSTEERSettings:
|
||||||
|
def test_default_env_type(self):
|
||||||
|
from rdagent.components.coder.model_coder.conf import ModelCoSTEERSettings
|
||||||
|
s = ModelCoSTEERSettings()
|
||||||
|
assert s.env_type == "conda"
|
||||||
|
|
||||||
|
def test_singleton(self):
|
||||||
|
from rdagent.components.coder.model_coder.conf import MODEL_COSTEER_SETTINGS
|
||||||
|
from rdagent.components.coder.model_coder.conf import ModelCoSTEERSettings
|
||||||
|
assert isinstance(MODEL_COSTEER_SETTINGS, ModelCoSTEERSettings)
|
||||||
|
|
||||||
|
def test_get_model_env_runs(self):
|
||||||
|
from rdagent.components.coder.model_coder.conf import get_model_env
|
||||||
|
# May succeed (conda available) or fail — either way, test the code path
|
||||||
|
try:
|
||||||
|
env = get_model_env()
|
||||||
|
assert env is not None
|
||||||
|
except Exception:
|
||||||
|
pass # expected if docker/conda not available
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
"""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
|
||||||
Reference in New Issue
Block a user