| | """
|
| | Converters between domain objects and REST API models.
|
| |
|
| | These functions handle the transformation between:
|
| | - Domain objects (dataclasses used by the solver)
|
| | - REST models (Pydantic models used by the API)
|
| | """
|
| | from . import domain
|
| | from .domain import SELECTED, NOT_SELECTED, PortfolioConfig
|
| |
|
| |
|
| | def stock_to_model(stock: domain.StockSelection) -> domain.StockSelectionModel:
|
| | """Convert a StockSelection domain object to REST model."""
|
| |
|
| | return domain.StockSelectionModel(
|
| | stock_id=stock.stock_id,
|
| | stock_name=stock.stock_name,
|
| | sector=stock.sector,
|
| | predicted_return=stock.predicted_return,
|
| | selected=stock.selected,
|
| | )
|
| |
|
| |
|
| | def plan_to_metrics(plan: domain.PortfolioOptimizationPlan) -> domain.PortfolioMetricsModel | None:
|
| | """Calculate business metrics from a plan."""
|
| | if plan.get_selected_count() == 0:
|
| | return None
|
| |
|
| | return domain.PortfolioMetricsModel(
|
| | expected_return=plan.get_expected_return(),
|
| | sector_count=plan.get_sector_count(),
|
| | max_sector_exposure=plan.get_max_sector_exposure(),
|
| | herfindahl_index=plan.get_herfindahl_index(),
|
| | diversification_score=plan.get_diversification_score(),
|
| | return_volatility=plan.get_return_volatility(),
|
| | sharpe_proxy=plan.get_sharpe_proxy(),
|
| | )
|
| |
|
| |
|
| | def plan_to_model(plan: domain.PortfolioOptimizationPlan) -> domain.PortfolioOptimizationPlanModel:
|
| | """Convert a PortfolioOptimizationPlan domain object to REST model."""
|
| |
|
| | return domain.PortfolioOptimizationPlanModel(
|
| | stocks=[stock_to_model(s) for s in plan.stocks],
|
| | target_position_count=plan.target_position_count,
|
| | max_sector_percentage=plan.max_sector_percentage,
|
| | score=str(plan.score) if plan.score else None,
|
| | solver_status=plan.solver_status.name if plan.solver_status else None,
|
| | metrics=plan_to_metrics(plan),
|
| | )
|
| |
|
| |
|
| | def model_to_stock(model: domain.StockSelectionModel) -> domain.StockSelection:
|
| | """Convert a StockSelectionModel REST model to domain object.
|
| |
|
| | Note: The REST model uses `selected: bool` but the domain uses
|
| | `selection: SelectionValue`. We convert here.
|
| | """
|
| |
|
| | selection = None
|
| | if model.selected is True:
|
| | selection = SELECTED
|
| | elif model.selected is False:
|
| | selection = NOT_SELECTED
|
| |
|
| |
|
| | return domain.StockSelection(
|
| | stock_id=model.stock_id,
|
| | stock_name=model.stock_name,
|
| | sector=model.sector,
|
| | predicted_return=model.predicted_return,
|
| | selection=selection,
|
| | )
|
| |
|
| |
|
| | def model_to_plan(model: domain.PortfolioOptimizationPlanModel) -> domain.PortfolioOptimizationPlan:
|
| | """Convert a PortfolioOptimizationPlanModel REST model to domain object.
|
| |
|
| | Creates a PortfolioConfig from the model's target_position_count and
|
| | max_sector_percentage so that constraints can access these values.
|
| | """
|
| | stocks = [model_to_stock(s) for s in model.stocks]
|
| |
|
| |
|
| | score = None
|
| | if model.score:
|
| | from solverforge_legacy.solver.score import HardSoftScore
|
| | score = HardSoftScore.parse(model.score)
|
| |
|
| |
|
| | solver_status = domain.SolverStatus.NOT_SOLVING
|
| | if model.solver_status:
|
| | solver_status = domain.SolverStatus[model.solver_status]
|
| |
|
| |
|
| |
|
| | target_count = model.target_position_count
|
| | max_per_sector = max(1, int(model.max_sector_percentage * target_count))
|
| |
|
| |
|
| | portfolio_config = PortfolioConfig(
|
| | target_count=target_count,
|
| | max_per_sector=max_per_sector,
|
| | unselected_penalty=10000,
|
| | )
|
| |
|
| | return domain.PortfolioOptimizationPlan(
|
| | stocks=stocks,
|
| | target_position_count=model.target_position_count,
|
| | max_sector_percentage=model.max_sector_percentage,
|
| | portfolio_config=portfolio_config,
|
| | score=score,
|
| | solver_status=solver_status,
|
| | )
|
| |
|