blackopsrepl's picture
.
d9f5c15
"""
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)}"