Team Ai
Apppublic

blackopsrepl/portfolio-optimization-python

sourceHugging Faceapache-2.0updated 10mo agoView on Hugging Face
1likes
converters.py115 linesDownload Raw Back to portfolio_optimization
1"""
2Converters between domain objects and REST API models.
3
4These functions handle the transformation between:
5- Domain objects (dataclasses used by the solver)
6- REST models (Pydantic models used by the API)
7"""
8from . import domain
9from .domain import SELECTED, NOT_SELECTED, PortfolioConfig
10
11
12def stock_to_model(stock: domain.StockSelection) -> domain.StockSelectionModel:
13    """Convert a StockSelection domain object to REST model."""
14    # Note: Pydantic model has populate_by_name=True, allowing snake_case field names
15    return domain.StockSelectionModel(  # type: ignore[call-arg]
16        stock_id=stock.stock_id,
17        stock_name=stock.stock_name,
18        sector=stock.sector,
19        predicted_return=stock.predicted_return,
20        selected=stock.selected,  # Uses the @property that returns bool
21    )
22
23
24def plan_to_metrics(plan: domain.PortfolioOptimizationPlan) -> domain.PortfolioMetricsModel | None:
25    """Calculate business metrics from a plan."""
26    if plan.get_selected_count() == 0:
27        return None
28
29    return domain.PortfolioMetricsModel(  # type: ignore[call-arg]
30        expected_return=plan.get_expected_return(),
31        sector_count=plan.get_sector_count(),
32        max_sector_exposure=plan.get_max_sector_exposure(),
33        herfindahl_index=plan.get_herfindahl_index(),
34        diversification_score=plan.get_diversification_score(),
35        return_volatility=plan.get_return_volatility(),
36        sharpe_proxy=plan.get_sharpe_proxy(),
37    )
38
39
40def plan_to_model(plan: domain.PortfolioOptimizationPlan) -> domain.PortfolioOptimizationPlanModel:
41    """Convert a PortfolioOptimizationPlan domain object to REST model."""
42    # Note: Pydantic model has populate_by_name=True, allowing snake_case field names
43    return domain.PortfolioOptimizationPlanModel(  # type: ignore[call-arg]
44        stocks=[stock_to_model(s) for s in plan.stocks],
45        target_position_count=plan.target_position_count,
46        max_sector_percentage=plan.max_sector_percentage,
47        score=str(plan.score) if plan.score else None,
48        solver_status=plan.solver_status.name if plan.solver_status else None,
49        metrics=plan_to_metrics(plan),
50    )
51
52
53def model_to_stock(model: domain.StockSelectionModel) -> domain.StockSelection:
54    """Convert a StockSelectionModel REST model to domain object.
55
56    Note: The REST model uses `selected: bool` but the domain uses
57    `selection: SelectionValue`. We convert here.
58    """
59    # Convert bool to SelectionValue (or None if not set)
60    selection = None
61    if model.selected is True:
62        selection = SELECTED
63    elif model.selected is False:
64        selection = NOT_SELECTED
65    # If model.selected is None, leave selection as None
66
67    return domain.StockSelection(
68        stock_id=model.stock_id,
69        stock_name=model.stock_name,
70        sector=model.sector,
71        predicted_return=model.predicted_return,
72        selection=selection,
73    )
74
75
76def model_to_plan(model: domain.PortfolioOptimizationPlanModel) -> domain.PortfolioOptimizationPlan:
77    """Convert a PortfolioOptimizationPlanModel REST model to domain object.
78
79    Creates a PortfolioConfig from the model's target_position_count and
80    max_sector_percentage so that constraints can access these values.
81    """
82    stocks = [model_to_stock(s) for s in model.stocks]
83
84    # Parse score if provided
85    score = None
86    if model.score:
87        from solverforge_legacy.solver.score import HardSoftScore
88        score = HardSoftScore.parse(model.score)
89
90    # Parse solver status if provided
91    solver_status = domain.SolverStatus.NOT_SOLVING
92    if model.solver_status:
93        solver_status = domain.SolverStatus[model.solver_status]
94
95    # Calculate max_per_sector from max_sector_percentage and target_position_count
96    # Example: 25% of 20 stocks = 5 stocks max per sector
97    target_count = model.target_position_count
98    max_per_sector = max(1, int(model.max_sector_percentage * target_count))
99
100    # Create PortfolioConfig for constraints to access
101    portfolio_config = PortfolioConfig(
102        target_count=target_count,
103        max_per_sector=max_per_sector,
104        unselected_penalty=10000,  # Default penalty
105    )
106
107    return domain.PortfolioOptimizationPlan(
108        stocks=stocks,
109        target_position_count=model.target_position_count,
110        max_sector_percentage=model.max_sector_percentage,
111        portfolio_config=portfolio_config,
112        score=score,
113        solver_status=solver_status,
114    )
115