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