102 lines
2.8 KiB
Python
102 lines
2.8 KiB
Python
"""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}
|