File size: 5,987 Bytes
d9f5c15 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
"""
Feasibility tests for the Portfolio Optimization quickstart.
These tests verify that the solver can find valid solutions
for the demo datasets.
"""
from solverforge_legacy.solver import SolverFactory
from solverforge_legacy.solver.config import (
SolverConfig,
ScoreDirectorFactoryConfig,
TerminationConfig,
Duration,
)
from portfolio_optimization.domain import PortfolioOptimizationPlan, StockSelection
from portfolio_optimization.constraints import define_constraints
from portfolio_optimization.demo_data import generate_demo_data, DemoData
import pytest
def solve_portfolio(plan: PortfolioOptimizationPlan, seconds: int = 5) -> PortfolioOptimizationPlan:
"""Run the solver on a portfolio for a given number of seconds."""
solver_config = SolverConfig(
solution_class=PortfolioOptimizationPlan,
entity_class_list=[StockSelection],
score_director_factory_config=ScoreDirectorFactoryConfig(
constraint_provider_function=define_constraints
),
termination_config=TerminationConfig(spent_limit=Duration(seconds=seconds)),
)
solver = SolverFactory.create(solver_config).build_solver()
return solver.solve(plan)
class TestFeasibility:
"""Test that the solver can find feasible solutions."""
def test_small_dataset_feasible(self):
"""The SMALL dataset should be solvable to a feasible solution."""
plan = generate_demo_data(DemoData.SMALL)
solution = solve_portfolio(plan, seconds=10)
# Check that we got a solution
assert solution is not None
assert solution.score is not None
# Check feasibility (hard score = 0)
assert solution.score.hard_score == 0, \
f"Solution should be feasible, got hard score: {solution.score.hard_score}"
# Check we selected exactly 20 stocks
selected_count = solution.get_selected_count()
assert selected_count == 20, \
f"Should select 20 stocks, got {selected_count}"
def test_large_dataset_feasible(self):
"""The LARGE dataset should be solvable to a feasible solution."""
plan = generate_demo_data(DemoData.LARGE)
solution = solve_portfolio(plan, seconds=15)
# Check that we got a solution
assert solution is not None
assert solution.score is not None
# Check feasibility (hard score = 0)
assert solution.score.hard_score == 0, \
f"Solution should be feasible, got hard score: {solution.score.hard_score}"
# Check we selected exactly 20 stocks
selected_count = solution.get_selected_count()
assert selected_count == 20, \
f"Should select 20 stocks, got {selected_count}"
def test_sector_limits_respected(self):
"""The solver should respect sector exposure limits."""
plan = generate_demo_data(DemoData.SMALL)
solution = solve_portfolio(plan, seconds=10)
# Check sector weights
sector_weights = solution.get_sector_weights()
for sector, weight in sector_weights.items():
assert weight <= 0.26, \
f"Sector {sector} has {weight*100:.1f}% weight, exceeds 25% limit"
def test_positive_expected_return(self):
"""The solver should find a portfolio with positive expected return."""
plan = generate_demo_data(DemoData.SMALL)
solution = solve_portfolio(plan, seconds=10)
expected_return = solution.get_expected_return()
# With our demo data, we should get at least 5% expected return
assert expected_return > 0.05, \
f"Expected return should be > 5%, got {expected_return*100:.2f}%"
def test_expected_return_reasonable(self):
"""The expected return should be reasonable for valid solutions."""
plan = generate_demo_data(DemoData.SMALL)
solution = solve_portfolio(plan, seconds=10)
# Check expected return is positive
expected_return = solution.get_expected_return()
assert expected_return > 0, \
f"Expected return should be positive, got {expected_return}"
class TestDemoData:
"""Test demo data generation."""
def test_small_dataset_has_25_stocks(self):
"""SMALL dataset should have 25 stocks (5+ per sector for feasibility)."""
plan = generate_demo_data(DemoData.SMALL)
assert len(plan.stocks) == 25
def test_large_dataset_has_51_stocks(self):
"""LARGE dataset should have 51 stocks."""
plan = generate_demo_data(DemoData.LARGE)
assert len(plan.stocks) == 51
def test_stocks_have_sectors(self):
"""All stocks should have a sector assigned."""
plan = generate_demo_data(DemoData.SMALL)
for stock in plan.stocks:
assert stock.sector is not None
assert len(stock.sector) > 0
def test_stocks_have_predictions(self):
"""All stocks should have predicted returns."""
plan = generate_demo_data(DemoData.SMALL)
for stock in plan.stocks:
assert stock.predicted_return is not None
# Predictions should be reasonable (-10% to +25%)
assert -0.10 <= stock.predicted_return <= 0.25
def test_stocks_initially_unselected(self):
"""All stocks should start with selected=None."""
plan = generate_demo_data(DemoData.SMALL)
for stock in plan.stocks:
assert stock.selected is None
def test_has_multiple_sectors(self):
"""Demo data should have multiple sectors for diversification testing."""
plan = generate_demo_data(DemoData.SMALL)
sectors = {stock.sector for stock in plan.stocks}
assert len(sectors) >= 4, \
f"Should have at least 4 sectors for diversification, got {len(sectors)}"
|