feat: Complete P6-P9 implementation (73 tests)

P6: ML Feedback Integrator (18 tests)
- MLFeedbackMixin for QuantRDLoop
- Auto-trigger ML training every 500 factors
- Feature importance → prompt feedback

P7: Portfolio Optimizer (28 tests)
- Mean-Variance optimization (max Sharpe)
- Risk Parity (equal risk contribution)
- Correlation analysis (max 0.3)
- Portfolio backtest with weighted signals

P8: Integration Tests (27 tests)
- End-to-end pipeline test
- Parallelization test (4 workers)
- RiskMgmt compliance test
- Error handling test

P9: Documentation
- QWEN.md updated with all new modules
- Project status updated
- Architecture diagram expanded

73 tests passing in 0.48s
This commit is contained in:
TPTBusiness
2026-04-09 10:09:20 +02:00
parent 03536af000
commit 0b168fd3e4
5 changed files with 1750 additions and 9 deletions
+490
View File
@@ -0,0 +1,490 @@
"""
Tests for ML Feedback Integrator
Tests the MLFeedbackMixin class for correct trigger logic,
factor counting, and prompt feedback generation.
15 tests covering:
- Initialization and configuration
- Trigger condition logic
- Factor counting methods
- Feature importance extraction
- Prompt suggestion generation
- Graceful error handling
"""
import json
import os
import sys
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def mock_project_root(tmp_path: Path) -> Path:
"""Create a temporary project structure for testing."""
# Create directory structure
(tmp_path / "results" / "factors").mkdir(parents=True)
(tmp_path / "results" / "models").mkdir(parents=True)
(tmp_path / "prompts" / "local").mkdir(parents=True)
return tmp_path
@pytest.fixture
def mock_factor_data() -> dict:
"""Return sample factor data for testing."""
return {
"name": "test_momentum_factor",
"status": "success",
"ic": 0.15,
"sharpe_ratio": 1.8,
"max_drawdown": -0.12,
"win_rate": 0.55,
"code": "def factor(): ...",
}
@pytest.fixture
def mock_importance_data() -> dict:
"""Return sample feature importance data."""
return {
"importance": {
"momentum_5d": 0.25,
"volatility_10d": 0.18,
"mean_reversion_3d": 0.12,
"volume_spike": 0.08,
"trend_strength": 0.05,
},
"model_type": "lightgbm",
"n_factors": 50,
}
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestMLFeedbackMixinInit:
"""Test MLFeedbackMixin initialization."""
def test_default_initialization(self, mock_project_root):
"""Test default configuration values."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
# Create a mock parent class
class MockParent:
def __init__(self):
pass
class TestMixin(MLFeedbackMixin, MockParent):
pass
mixin = TestMixin(ml_feedback=True)
assert mixin.ml_feedback_enabled is True
assert mixin.ml_train_interval == 500
assert mixin.strategy_gen_interval == 1000
assert mixin.portfolio_opt_interval == 2000
def test_custom_intervals(self, mock_project_root):
"""Test custom interval configuration."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
class MockParent:
def __init__(self):
pass
class TestMixin(MLFeedbackMixin, MockParent):
pass
mixin = TestMixin(
ml_feedback=True,
ml_train_interval=1000,
strategy_gen_interval=2000,
portfolio_opt_interval=4000,
)
assert mixin.ml_train_interval == 1000
assert mixin.strategy_gen_interval == 2000
assert mixin.portfolio_opt_interval == 4000
def test_disabled_feedback(self, mock_project_root):
"""Test disabled feedback mode."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
class MockParent:
def __init__(self):
pass
class TestMixin(MLFeedbackMixin, MockParent):
pass
mixin = TestMixin(ml_feedback=False)
assert mixin.ml_feedback_enabled is False
class TestTriggerConditions:
"""Test trigger condition logic."""
def test_should_trigger_ml_train_at_threshold(self):
"""Test ML train trigger at exact threshold."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
class MockParent:
def __init__(self):
pass
class TestMixin(MLFeedbackMixin, MockParent):
pass
mixin = TestMixin(ml_train_interval=500)
mixin._last_ml_train_factor = 0
assert mixin._should_trigger_ml_train(500) is True
assert mixin._should_trigger_ml_train(499) is False
assert mixin._should_trigger_ml_train(1000) is True
def test_no_duplicate_trigger(self):
"""Test that duplicate triggers are prevented."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
class MockParent:
def __init__(self):
pass
class TestMixin(MLFeedbackMixin, MockParent):
pass
mixin = TestMixin(ml_train_interval=500)
mixin._last_ml_train_factor = 500
# Should not trigger again at 500
assert mixin._should_trigger_ml_train(500) is False
# Should trigger at 1000
assert mixin._should_trigger_ml_train(1000) is True
def test_strategy_gen_trigger(self):
"""Test strategy generation trigger logic."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
class MockParent:
def __init__(self):
pass
class TestMixin(MLFeedbackMixin, MockParent):
pass
mixin = TestMixin(strategy_gen_interval=1000)
mixin._last_strategy_gen_factor = 0
assert mixin._should_trigger_strategy_gen(1000) is True
assert mixin._should_trigger_strategy_gen(999) is False
def test_portfolio_opt_trigger(self):
"""Test portfolio optimization trigger logic."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
class MockParent:
def __init__(self):
pass
class TestMixin(MLFeedbackMixin, MockParent):
pass
mixin = TestMixin(portfolio_opt_interval=2000)
mixin._last_portfolio_opt_factor = 0
assert mixin._should_trigger_portfolio_opt(2000) is True
assert mixin._should_trigger_portfolio_opt(1999) is False
class TestFactorCounting:
"""Test factor counting methods."""
def test_count_from_results_dir(self, mock_project_root, mock_factor_data):
"""Test counting factors from results directory."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
# Write test factor files
for i in range(5):
factor_file = mock_project_root / "results" / "factors" / f"factor_{i}.json"
data = mock_factor_data.copy()
data["name"] = f"factor_{i}"
data["ic"] = 0.1 + i * 0.01
with open(factor_file, "w") as f:
json.dump(data, f)
class MockParent:
def __init__(self):
pass
class TestMixin(MLFeedbackMixin, MockParent):
def _get_project_root(self):
return mock_project_root
mixin = TestMixin()
count = mixin._count_factors_from_results()
assert count == 5
def test_count_skips_failed_factors(self, mock_project_root, mock_factor_data):
"""Test that failed factors are not counted."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
# Write successful factors
for i in range(3):
factor_file = mock_project_root / "results" / "factors" / f"success_{i}.json"
data = mock_factor_data.copy()
with open(factor_file, "w") as f:
json.dump(data, f)
# Write failed factors
for i in range(2):
factor_file = mock_project_root / "results" / "factors" / f"failed_{i}.json"
data = mock_factor_data.copy()
data["status"] = "failed"
data["ic"] = None
with open(factor_file, "w") as f:
json.dump(data, f)
class MockParent:
def __init__(self):
pass
class TestMixin(MLFeedbackMixin, MockParent):
def _get_project_root(self):
return mock_project_root
mixin = TestMixin()
count = mixin._count_factors_from_results()
assert count == 3 # Only successful factors
def test_count_empty_directory(self, mock_project_root):
"""Test counting with empty factors directory."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
class MockParent:
def __init__(self):
pass
class TestMixin(MLFeedbackMixin, MockParent):
def _get_project_root(self):
return mock_project_root
mixin = TestMixin()
count = mixin._count_factors_from_results()
assert count == 0
class TestFeatureImportance:
"""Test feature importance extraction and prompt suggestions."""
def test_generate_prompt_suggestions_top_features(self, mock_importance_data):
"""Test prompt suggestions from feature importance."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
class MockParent:
def __init__(self):
pass
class TestMixin(MLFeedbackMixin, MockParent):
pass
mixin = TestMixin()
suggestions = mixin._generate_prompt_suggestions(mock_importance_data)
assert len(suggestions) >= 1
# Should mention top features
assert any("momentum_5d" in s for s in suggestions)
def test_generate_suggestions_low_performing_features(self, mock_importance_data):
"""Test suggestions for avoiding low-performing features."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
class MockParent:
def __init__(self):
pass
class TestMixin(MLFeedbackMixin, MockParent):
pass
mixin = TestMixin()
suggestions = mixin._generate_prompt_suggestions(mock_importance_data)
# Should suggest avoiding low-performing features
assert any("Avoid" in s or "avoid" in s or "reduce" in s.lower() for s in suggestions)
def test_suggestions_empty_importance(self):
"""Test suggestions with empty importance data."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
class MockParent:
def __init__(self):
pass
class TestMixin(MLFeedbackMixin, MockParent):
pass
mixin = TestMixin()
suggestions = mixin._generate_prompt_suggestions({"importance": {}})
assert len(suggestions) == 1
assert "No feature importance" in suggestions[0]
def test_suggestions_low_diversity(self):
"""Test suggestions when factor diversity is low."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
class MockParent:
def __init__(self):
pass
class TestMixin(MLFeedbackMixin, MockParent):
pass
mixin = TestMixin()
importance = {
"importance": {
"momentum_1d": 0.3,
"momentum_2d": 0.25,
"momentum_3d": 0.2,
"momentum_4d": 0.15,
}
}
suggestions = mixin._generate_prompt_suggestions(importance)
# Should suggest more diversity
assert any("diversity" in s.lower() or "Diversity" in s for s in suggestions)
class TestLoadTopFactors:
"""Test loading top factors by IC."""
def test_load_top_factors(self, mock_project_root, mock_factor_data):
"""Test loading top N factors."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
# Write factor files with varying IC
for i in range(10):
factor_file = mock_project_root / "results" / "factors" / f"factor_{i}.json"
data = mock_factor_data.copy()
data["name"] = f"factor_{i}"
data["ic"] = 0.01 * (i + 1) # IC from 0.01 to 0.10
with open(factor_file, "w") as f:
json.dump(data, f)
class MockParent:
def __init__(self):
pass
class TestMixin(MLFeedbackMixin, MockParent):
def _get_project_root(self):
return mock_project_root
mixin = TestMixin()
top_factors = mixin._load_top_factors(n=5)
assert len(top_factors) == 5
# Should be sorted by IC (descending)
assert top_factors[0]["ic"] >= top_factors[-1]["ic"]
def test_load_top_factors_empty_dir(self, mock_project_root):
"""Test loading from empty directory."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
class MockParent:
def __init__(self):
pass
class TestMixin(MLFeedbackMixin, MockParent):
def _get_project_root(self):
return mock_project_root
mixin = TestMixin()
top_factors = mixin._load_top_factors(n=5)
assert top_factors == []
class TestErrorHandling:
"""Test graceful error handling."""
def test_feedback_with_exception(self):
"""Test that feedback handles exceptions gracefully."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
class MockParent:
def __init__(self):
pass
def feedback(self, prev_out):
return "parent_feedback"
def _get_factor_count(self):
raise RuntimeError("Simulated error")
class TestMixin(MLFeedbackMixin, MockParent):
pass
mixin = TestMixin(ml_feedback=True)
# Should not raise exception
result = mixin.feedback({})
assert result == "parent_feedback"
class TestIntegration:
"""Integration tests for full workflow."""
def test_full_feedback_cycle(self, mock_project_root, mock_factor_data, mock_importance_data):
"""Test complete feedback cycle with triggers."""
from rdagent.scenarios.qlib.local.feedback_integrator import MLFeedbackMixin
# Write importance file
importance_file = mock_project_root / "results" / "models" / "feature_importance.json"
with open(importance_file, "w") as f:
json.dump(mock_importance_data, f)
call_log = []
class MockParent:
def __init__(self):
pass
def feedback(self, prev_out):
call_log.append("parent_feedback")
return "feedback_result"
def _get_factor_count(self):
return 500
class TestMixin(MLFeedbackMixin, MockParent):
def _get_project_root(self):
return mock_project_root
def _count_factors_from_results(self):
return 500
def _trigger_ml_training(self, factor_count):
call_log.append(f"ml_train_{factor_count}")
self._last_ml_train_factor = factor_count
mixin = TestMixin(ml_feedback=True, ml_train_interval=500)
mixin._last_ml_train_factor = 0
result = mixin.feedback({})
assert result == "feedback_result"
assert "parent_feedback" in call_log
assert "ml_train_500" in call_log
+440
View File
@@ -0,0 +1,440 @@
"""
Tests for Portfolio Optimizer
Tests the PortfolioOptimizer class for:
- Mean-Variance Optimization
- Risk Parity Optimization
- Correlation Analysis
- Portfolio Backtesting
- Strategy Selection
20 tests covering all optimization methods and edge cases.
"""
import json
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
import numpy as np
import pandas as pd
import pytest
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def mock_strategies_dir(tmp_path: Path) -> Path:
"""Create a temporary strategies directory with mock data."""
strategies_dir = tmp_path / "results" / "strategies_new"
strategies_dir.mkdir(parents=True)
# Create 5 mock strategy files
strategies = [
{
"name": "MomentumStrategy",
"sharpe_ratio": 2.1,
"ic": 0.15,
"max_drawdown": -0.10,
"backtest": {
"returns": np.random.randn(252) * 0.01 + 0.0005,
"equity_curve": np.cumprod(1 + np.random.randn(252) * 0.01 + 0.0005).tolist(),
},
},
{
"name": "MeanReversionStrategy",
"sharpe_ratio": 1.8,
"ic": 0.12,
"max_drawdown": -0.15,
"backtest": {
"returns": np.random.randn(252) * 0.012 + 0.0004,
},
},
{
"name": "VolatilityTargetStrategy",
"sharpe_ratio": 1.5,
"ic": 0.10,
"max_drawdown": -0.12,
"backtest": {
"returns": np.random.randn(252) * 0.009 + 0.0003,
},
},
{
"name": "TrendFollowingStrategy",
"sharpe_ratio": 1.2,
"ic": 0.08,
"max_drawdown": -0.18,
"backtest": {
"returns": np.random.randn(252) * 0.011 + 0.0002,
},
},
{
"name": "BreakoutStrategy",
"sharpe_ratio": 0.9,
"ic": 0.06,
"max_drawdown": -0.22,
"backtest": {
"returns": np.random.randn(252) * 0.013 + 0.0001,
},
},
]
for strategy in strategies:
filepath = strategies_dir / f"{strategy['name']}.json"
with open(filepath, "w") as f:
json.dump(strategy, f, default=lambda x: x.tolist() if isinstance(x, np.ndarray) else x)
return tmp_path
@pytest.fixture
def mock_returns_data() -> pd.DataFrame:
"""Create mock strategy returns DataFrame."""
np.random.seed(42)
n_days = 252
return pd.DataFrame(
{
"StrategyA": np.random.randn(n_days) * 0.01 + 0.0005,
"StrategyB": np.random.randn(n_days) * 0.01 + 0.0004,
"StrategyC": np.random.randn(n_days) * 0.009 + 0.0003,
}
)
@pytest.fixture
def portfolio_optimizer(mock_strategies_dir):
"""Create a PortfolioOptimizer instance with mock data."""
from rdagent.scenarios.qlib.local.portfolio_optimizer import PortfolioOptimizer
return PortfolioOptimizer(project_root=mock_strategies_dir)
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestPortfolioOptimizerInit:
"""Test PortfolioOptimizer initialization."""
def test_default_initialization(self):
"""Test default configuration values."""
from rdagent.scenarios.qlib.local.portfolio_optimizer import PortfolioOptimizer
optimizer = PortfolioOptimizer()
assert optimizer.max_correlation == 0.3
assert optimizer.top_strategies == 30
assert optimizer.risk_free_rate == 0.02
def test_custom_configuration(self):
"""Test custom configuration values."""
from rdagent.scenarios.qlib.local.portfolio_optimizer import PortfolioOptimizer
optimizer = PortfolioOptimizer(
max_correlation=0.5,
top_strategies=20,
risk_free_rate=0.03,
)
assert optimizer.max_correlation == 0.5
assert optimizer.top_strategies == 20
assert optimizer.risk_free_rate == 0.03
class TestLoadStrategyData:
"""Test strategy data loading."""
def test_load_strategy_data(self, portfolio_optimizer):
"""Test loading strategy data from directory."""
result = portfolio_optimizer._load_strategy_data()
assert result is True
assert portfolio_optimizer._strategy_returns is not None
assert portfolio_optimizer._strategy_expected_returns is not None
assert portfolio_optimizer._cov_matrix is not None
assert portfolio_optimizer._corr_matrix is not None
def test_load_specific_strategies(self, portfolio_optimizer):
"""Test loading specific strategies by name."""
result = portfolio_optimizer._load_strategy_data(
strategies=["MomentumStrategy", "MeanReversionStrategy"]
)
assert result is True
assert len(portfolio_optimizer._strategy_expected_returns) == 2
def test_load_no_strategies_dir(self, tmp_path):
"""Test loading when no strategies directory exists."""
from rdagent.scenarios.qlib.local.portfolio_optimizer import PortfolioOptimizer
optimizer = PortfolioOptimizer(project_root=tmp_path)
result = optimizer._load_strategy_data()
assert result is False
class TestExtractReturns:
"""Test returns extraction from backtest data."""
def test_extract_returns_from_array(self):
"""Test extracting returns from array."""
from rdagent.scenarios.qlib.local.portfolio_optimizer import PortfolioOptimizer
optimizer = PortfolioOptimizer()
returns = np.array([0.01, -0.005, 0.008])
data = {"returns": returns.tolist()}
result = optimizer._extract_returns(data)
assert result is not None
assert len(result) == 3
def test_extract_returns_from_equity_curve(self):
"""Test extracting returns from equity curve."""
from rdagent.scenarios.qlib.local.portfolio_optimizer import PortfolioOptimizer
optimizer = PortfolioOptimizer()
equity = [100, 101, 100.5, 101.5, 102]
data = {"equity_curve": equity}
result = optimizer._extract_returns(data)
assert result is not None
assert len(result) == 4 # One less than equity points
def test_extract_returns_no_data(self):
"""Test extracting returns when no data available."""
from rdagent.scenarios.qlib.local.portfolio_optimizer import PortfolioOptimizer
optimizer = PortfolioOptimizer()
result = optimizer._extract_returns({})
assert result is None
class TestMeanVarianceOptimization:
"""Test mean-variance optimization."""
def test_mean_variance_basic(self, portfolio_optimizer):
"""Test basic mean-variance optimization."""
portfolio_optimizer._load_strategy_data()
result = portfolio_optimizer._optimize_mean_variance()
assert result is not None
assert "weights" in result
assert "expected_return" in result
assert "volatility" in result
assert "sharpe" in result
assert result["method"] == "mean_variance"
# Weights should sum to 1
total_weight = sum(result["weights"].values())
assert abs(total_weight - 1.0) < 0.01
def test_mean_variance_weights_positive(self, portfolio_optimizer):
"""Test that all weights are non-negative."""
portfolio_optimizer._load_strategy_data()
result = portfolio_optimizer._optimize_mean_variance()
assert result is not None
for name, weight in result["weights"].items():
assert weight >= 0
def test_mean_variance_insufficient_strategies(self, portfolio_optimizer):
"""Test optimization with insufficient strategies."""
portfolio_optimizer._strategy_expected_returns = pd.Series({"OnlyOne": 0.1})
portfolio_optimizer._cov_matrix = pd.DataFrame([[0.0001]], index=["OnlyOne"], columns=["OnlyOne"])
result = portfolio_optimizer._optimize_mean_variance()
assert result is None
class TestRiskParityOptimization:
"""Test risk parity optimization."""
def test_risk_parity_basic(self, portfolio_optimizer):
"""Test basic risk parity optimization."""
portfolio_optimizer._load_strategy_data()
result = portfolio_optimizer._optimize_risk_parity()
assert result is not None
assert "weights" in result
assert result["method"] == "risk_parity"
def test_risk_parity_weights_positive(self, portfolio_optimizer):
"""Test that all weights are non-negative."""
portfolio_optimizer._load_strategy_data()
result = portfolio_optimizer._optimize_risk_parity()
assert result is not None
for name, weight in result["weights"].items():
assert weight >= 0
def test_risk_parity_insufficient_strategies(self, portfolio_optimizer):
"""Test optimization with insufficient strategies."""
portfolio_optimizer._strategy_expected_returns = pd.Series({"OnlyOne": 0.1})
portfolio_optimizer._cov_matrix = pd.DataFrame([[0.0001]], index=["OnlyOne"], columns=["OnlyOne"])
result = portfolio_optimizer._optimize_risk_parity()
assert result is None
class TestICWeightedOptimization:
"""Test IC-weighted optimization."""
def test_ic_weighted_basic(self, portfolio_optimizer):
"""Test basic IC-weighted optimization."""
result = portfolio_optimizer._optimize_ic_weighted()
assert result is not None
assert "weights" in result
assert result["method"] == "ic_weighted"
def test_ic_weighted_weights_proportional(self, portfolio_optimizer):
"""Test that weights are proportional to IC."""
result = portfolio_optimizer._optimize_ic_weighted()
assert result is not None
total_weight = sum(result["weights"].values())
assert abs(total_weight - 1.0) < 0.01
class TestCorrelationAnalysis:
"""Test correlation analysis."""
def test_analyze_correlations(self, portfolio_optimizer):
"""Test correlation analysis."""
portfolio_optimizer._load_strategy_data()
result = portfolio_optimizer.analyze_correlations()
assert result is not None
assert "correlation_matrix" in result
assert "uncorrelated_strategies" in result
assert "high_corr_pairs" in result
def test_select_uncorrelated_strategies(self, portfolio_optimizer):
"""Test selecting uncorrelated strategies."""
portfolio_optimizer._load_strategy_data()
uncorrelated = portfolio_optimizer.select_uncorrelated_strategies(target_count=3)
assert len(uncorrelated) <= 3
def test_correlation_no_data(self, tmp_path):
"""Test correlation analysis with no data."""
from rdagent.scenarios.qlib.local.portfolio_optimizer import PortfolioOptimizer
optimizer = PortfolioOptimizer(project_root=tmp_path)
result = optimizer.analyze_correlations()
assert result is None
class TestPortfolioBacktest:
"""Test portfolio backtesting."""
def test_backtest_portfolio(self, portfolio_optimizer):
"""Test backtesting a portfolio."""
portfolio_optimizer._load_strategy_data()
# Use equal weights
n = len(portfolio_optimizer._strategy_returns.columns)
weights = {col: 1.0 / n for col in portfolio_optimizer._strategy_returns.columns}
result = portfolio_optimizer.backtest_portfolio(weights)
assert result is not None
assert "total_return" in result
assert "annualized_return" in result
assert "annualized_volatility" in result
assert "sharpe_ratio" in result
assert "max_drawdown" in result
assert "win_rate" in result
def test_backtest_default_weights(self, portfolio_optimizer):
"""Test backtesting with default (equal) weights."""
portfolio_optimizer._load_strategy_data()
result = portfolio_optimizer.backtest_portfolio()
assert result is not None
def test_backtest_no_data(self, tmp_path):
"""Test backtesting with no data."""
from rdagent.scenarios.qlib.local.portfolio_optimizer import PortfolioOptimizer
optimizer = PortfolioOptimizer(project_root=tmp_path)
result = optimizer.backtest_portfolio()
assert result is None
class TestOptimizePortfolio:
"""Test the main optimize_portfolio method."""
def test_optimize_mean_variance(self, portfolio_optimizer):
"""Test optimize_portfolio with mean_variance method."""
result = portfolio_optimizer.optimize_portfolio(method="mean_variance")
assert result is not None
assert result["method"] == "mean_variance"
def test_optimize_risk_parity(self, portfolio_optimizer):
"""Test optimize_portfolio with risk_parity method."""
result = portfolio_optimizer.optimize_portfolio(method="risk_parity")
assert result is not None
assert result["method"] == "risk_parity"
def test_optimize_ic_weighted(self, portfolio_optimizer):
"""Test optimize_portfolio with ic_weighted method."""
result = portfolio_optimizer.optimize_portfolio(method="ic_weighted")
assert result is not None
assert result["method"] == "ic_weighted"
def test_optimize_unknown_method(self, portfolio_optimizer):
"""Test optimize_portfolio with unknown method."""
result = portfolio_optimizer.optimize_portfolio(method="unknown_method")
assert result is None
class TestReportGeneration:
"""Test report generation."""
def test_generate_report(self, portfolio_optimizer):
"""Test generating optimization report."""
portfolio_optimizer._load_strategy_data()
report = portfolio_optimizer.generate_report()
assert isinstance(report, str)
assert "PORTFOLIO OPTIMIZATION REPORT" in report
assert "Configuration" in report
class TestSaveResults:
"""Test saving optimization results."""
def test_save_result_creates_file(self, portfolio_optimizer, tmp_path):
"""Test that saving creates a JSON file."""
portfolio_optimizer.project_root = tmp_path
result = {
"weights": {"A": 0.5, "B": 0.5},
"method": "test",
"sharpe": 1.5,
}
portfolio_optimizer._save_optimization_result(result)
# Check file was created
results_dir = tmp_path / "results" / "portfolios"
assert results_dir.exists()
json_files = list(results_dir.glob("*.json"))
assert len(json_files) == 1