"""Risk Engine API - FastAPI application for order risk evaluation and approval workflow.""" from __future__ import annotations from contextlib import asynccontextmanager import asyncpg from fastapi import FastAPI, HTTPException from pydantic import BaseModel from services.risk.approval import ( expire_stale_approvals, get_approval_by_id, get_pending_approvals, review_approval, ) from services.risk.engine import ( AccountRiskState, PortfolioRiskConfig, ProposedOrder, RiskEvaluation, evaluate_order, ) from services.shared.config import load_config from services.shared.logging import setup_logging config = load_config() pool: asyncpg.Pool | None = None @asynccontextmanager async def lifespan(app: FastAPI): global pool setup_logging("risk_engine", level=config.log_level, json_output=config.json_logs) pool = await asyncpg.create_pool(dsn=config.postgres.dsn, min_size=2, max_size=8) yield if pool: await pool.close() app = FastAPI(title="Stonks Oracle - Risk Engine", lifespan=lifespan) class EvaluateRequest(BaseModel): order: ProposedOrder config: PortfolioRiskConfig | None = None state: AccountRiskState | None = None @app.post("/evaluate", response_model=RiskEvaluation) async def evaluate(req: EvaluateRequest) -> RiskEvaluation: risk_config = req.config or PortfolioRiskConfig() return evaluate_order(req.order, risk_config, req.state) @app.get("/health") async def health(): return {"status": "ok"} class ReviewRequest(BaseModel): approved: bool reviewed_by: str = "operator" review_note: str = "" @app.get("/approvals/pending") async def list_pending(): if not pool: raise HTTPException(503, "Database not ready") requests = await get_pending_approvals(pool) return [r.to_dict() for r in requests] @app.get("/approvals/{approval_id}") async def get_approval(approval_id: str): if not pool: raise HTTPException(503, "Database not ready") req = await get_approval_by_id(pool, approval_id) if not req: raise HTTPException(404, "Approval not found") return req.to_dict() @app.post("/approvals/{approval_id}/review") async def review(approval_id: str, body: ReviewRequest): if not pool: raise HTTPException(503, "Database not ready") status = await review_approval( pool, approval_id, body.approved, body.reviewed_by, body.review_note, ) if status is None: raise HTTPException(404, "Approval not found or no longer pending") return {"approval_id": approval_id, "status": status.value} @app.post("/approvals/expire") async def expire(): if not pool: raise HTTPException(503, "Database not ready") expired = await expire_stale_approvals(pool) return {"expired": expired}