ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-3 Pipeline was successful
ci/woodpecker/push/build-1 Pipeline was successful
ci/woodpecker/push/build-2 Pipeline was successful
ci/woodpecker/push/finalize Pipeline is running
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.adapters.broker_adapter name:broker-adapter]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.aggregation.worker name:aggregation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.extractor.worker name:extractor]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.ingestion.worker name:ingestion]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.lake_publisher.worker name:lake-publisher]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.parser.worker name:parser]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.recommendation.worker name:recommendation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.scheduler.app name:scheduler]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.api.app:app --host 0.0.0.0 --port 8000 name:query-api]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.risk.app:app --host 0.0.0.0 --port 8000 name:risk]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.symbol_registry.app:app --host 0.0.0.0 --port 8000 name:symbol-registry]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.trading.app:app --host 0.0.0.0 --port 8000 name:trading-engine]) (push) Has been cancelled
Build and Push / build-dashboard (push) Has been cancelled
Build and Push / build-superset (push) Has been cancelled
Build and Push / integration-test (push) Has been cancelled
1. patterns endpoint: fix query referencing non-existent column di.catalyst_type → dir.catalyst_type (column is on document_impact_records) 2. lockouts seed: use relative timestamps (now + 7d) so active lockout is always in the future regardless of when tests run 3. create_agent: make slug optional with auto-generation from name 4. create_source: json.dumps(config) + ::jsonb cast for asyncpg JSONB compat 5. approval_expiry: return count as int (len(expired)) not the list itself 6. metrics_consistency: fix test assertion to match API contract (total >= active + reserve, not total == active + reserve + unrealized)
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": len(expired), "items": expired}
|