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)}"