blackopsrepl/portfolio-optimization-python
1
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 