Team Ai
Apppublic

blackopsrepl/portfolio-optimization-python

sourceHugging Faceapache-2.0updated 10mo agoView on Hugging Face
1likes
rest_api.py190 linesDownload Raw Back to portfolio_optimization
1"""
2REST API for Portfolio Optimization
3
4This module provides HTTP endpoints for the portfolio optimization quickstart:
5
6Endpoints:
7- GET  /demo-data           - List available demo datasets
8- GET  /demo-data/{id}      - Load a specific demo dataset
9- POST /portfolios          - Submit a portfolio for optimization
10- GET  /portfolios/{id}     - Get current solution for a job
11- GET  /portfolios/{id}/status - Get solving status
12- DELETE /portfolios/{id}   - Stop solving
13- PUT  /portfolios/analyze  - Analyze a submitted portfolio's score
14
15The API follows the same patterns as other SolverForge quickstarts.
16"""
17from fastapi import FastAPI, Request
18from fastapi.staticfiles import StaticFiles
19from uuid import uuid4
20from dataclasses import replace
21from typing import Any
22
23from solverforge_legacy.solver import SolverManager, SolverFactory
24
25from .domain import PortfolioOptimizationPlan, PortfolioOptimizationPlanModel
26from .converters import plan_to_model, model_to_plan
27from .demo_data import DemoData, generate_demo_data
28from .solver import solver_manager, solution_manager, create_solver_config
29from .score_analysis import ConstraintAnalysisDTO, MatchAnalysisDTO
30
31
32app = FastAPI(
33    title="Portfolio Optimization Quickstart",
34    description="SolverForge quickstart for stock portfolio optimization",
35    docs_url='/q/swagger-ui'
36)
37
38# In-memory storage for submitted portfolios and their solver managers
39data_sets: dict[str, PortfolioOptimizationPlan] = {}
40solver_managers: dict[str, SolverManager] = {}
41
42
43@app.get("/demo-data")
44async def demo_data_list() -> list[DemoData]:
45    """List available demo datasets."""
46    return [e for e in DemoData]
47
48
49@app.get("/demo-data/{dataset_id}", response_model_exclude_none=True)
50async def get_demo_data(dataset_id: str) -> PortfolioOptimizationPlanModel:
51    """Load a specific demo dataset."""
52    demo_data = getattr(DemoData, dataset_id)
53    domain_plan = generate_demo_data(demo_data)
54    return plan_to_model(domain_plan)
55
56
57@app.get("/portfolios/{problem_id}", response_model_exclude_none=True)
58async def get_portfolio(problem_id: str) -> PortfolioOptimizationPlanModel:
59    """Get current solution for a portfolio optimization job."""
60    plan = data_sets[problem_id]
61    # Use per-job solver manager if available, otherwise use default
62    manager = solver_managers.get(problem_id, solver_manager)
63    updated_plan = replace(plan, solver_status=manager.get_solver_status(problem_id))
64    return plan_to_model(updated_plan)
65
66
67def update_portfolio(problem_id: str, plan: PortfolioOptimizationPlan) -> None:
68    """Callback to update the stored solution as solver improves it."""
69    global data_sets
70    data_sets[problem_id] = plan
71
72
73@app.post("/portfolios")
74async def solve_portfolio(plan_model: PortfolioOptimizationPlanModel) -> str:
75    """
76    Submit a portfolio for optimization.
77
78    Returns a job ID that can be used to retrieve the solution.
79    Supports custom solver configuration via solverConfig field.
80    """
81    job_id = str(uuid4())
82    plan = model_to_plan(plan_model)
83    data_sets[job_id] = plan
84
85    # Get termination time from config or use default
86    termination_seconds = 30
87    if plan_model.solver_config and plan_model.solver_config.termination_seconds:
88        termination_seconds = plan_model.solver_config.termination_seconds
89
90    # Create solver with dynamic config
91    config = create_solver_config(termination_seconds)
92    manager: SolverManager = SolverManager.create(SolverFactory.create(config))
93    solver_managers[job_id] = manager
94
95    manager.solve_and_listen(
96        job_id,
97        plan,
98        lambda solution: update_portfolio(job_id, solution)
99    )
100    return job_id
101
102
103@app.get("/portfolios")
104async def list_portfolios() -> list[str]:
105    """List all job IDs of submitted portfolios."""
106    return list(data_sets.keys())
107
108
109@app.get("/portfolios/{problem_id}/status")
110async def get_status(problem_id: str) -> dict[str, Any]:
111    """Get the portfolio status and score for a given job ID."""
112    if problem_id not in data_sets:
113        raise ValueError(f"No portfolio found with ID {problem_id}")
114
115    plan = data_sets[problem_id]
116    # Use per-job solver manager if available, otherwise use default
117    manager = solver_managers.get(problem_id, solver_manager)
118    solver_status = manager.get_solver_status(problem_id)
119
120    # Calculate additional metrics
121    selected_count = plan.get_selected_count()
122    expected_return = plan.get_expected_return() if selected_count > 0 else 0
123
124    return {
125        "score": {
126            "hardScore": plan.score.hard_score if plan.score else 0,
127            "softScore": plan.score.soft_score if plan.score else 0,
128        },
129        "solverStatus": solver_status.name,
130        "selectedCount": selected_count,
131        "expectedReturn": expected_return,
132        "sectorWeights": plan.get_sector_weights() if selected_count > 0 else {},
133    }
134
135
136@app.delete("/portfolios/{problem_id}")
137async def stop_solving(problem_id: str) -> PortfolioOptimizationPlanModel:
138    """Terminate solving for a given job ID."""
139    if problem_id not in data_sets:
140        raise ValueError(f"No portfolio found with ID {problem_id}")
141
142    # Use per-job solver manager if available, otherwise use default
143    manager = solver_managers.get(problem_id, solver_manager)
144    try:
145        manager.terminate_early(problem_id)
146    except Exception as e:
147        print(f"Warning: terminate_early failed for {problem_id}: {e}")
148
149    return await get_portfolio(problem_id)
150
151
152@app.put("/portfolios/analyze")
153async def analyze_portfolio(request: Request) -> dict[str, Any]:
154    """Submit a portfolio to analyze its score in detail."""
155    json_data = await request.json()
156
157    # Parse the incoming JSON using Pydantic models
158    plan_model = PortfolioOptimizationPlanModel.model_validate(json_data)
159
160    # Convert to domain model for analysis
161    domain_plan = model_to_plan(plan_model)
162
163    analysis = solution_manager.analyze(domain_plan)
164
165    # Convert to DTOs for proper serialization
166    constraints = []
167    for constraint in getattr(analysis, 'constraint_analyses', []) or []:
168        matches = [
169            MatchAnalysisDTO(
170                name=str(getattr(getattr(match, 'constraint_ref', None), 'constraint_name', "")),
171                score=str(getattr(match, 'score', "0hard/0soft")),
172                justification=str(getattr(match, 'justification', "")),
173            )
174            for match in getattr(constraint, 'matches', []) or []
175        ]
176
177        constraint_dto = ConstraintAnalysisDTO(
178            name=str(getattr(constraint, 'constraint_name', "")),
179            weight=str(getattr(constraint, 'weight', "0hard/0soft")),
180            score=str(getattr(constraint, 'score', "0hard/0soft")),
181            matches=matches,
182        )
183        constraints.append(constraint_dto)
184
185    return {"constraints": [constraint.model_dump() for constraint in constraints]}
186
187
188# Mount static files for the web UI
189app.mount("/", StaticFiles(directory="static", html=True), name="static")
190