Files
stonks-oracle/services/risk/app.py
T

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}