from __future__ import annotations from typing import Annotated from fastapi import APIRouter, Path, Query, Request from backend.data.gateway import MarketDataUnavailable from backend.data.providers.base import ProviderError from backend.data.quality import DataQualityError from backend.features.accounts.auth import ( AdminWritePrincipal, AuthenticatedPrincipal, SmartAccessPrincipal, SmartWritePrincipal, ) from backend.features.screener.schemas import ( CustomStrategyInput, FormulaCompileInput, FormulaCompileResponse, IdentifierResponse, MessageResponse, ScreenerCatalogResponse, ScreenerSyncResponse, ScreenerWorkspaceResponse, TrackInput, ) from backend.features.screener.service import ScreenerError from backend.http.errors import AppError from backend.llm.gateway import LLMGatewayError router = APIRouter(prefix="/screener", tags=["screener"]) @router.get("/catalog", response_model=ScreenerCatalogResponse) def catalog(request: Request, _principal: AuthenticatedPrincipal) -> dict: return request.app.state.container.screener.catalog() @router.get("", response_model=ScreenerWorkspaceResponse) def workspace( request: Request, principal: SmartAccessPrincipal, requested_date: Annotated[str, Query(alias="date")], ) -> dict: return _call(request, "workspace", requested_date, principal.user.id) @router.post("/sync", response_model=ScreenerSyncResponse) def sync( request: Request, _principal: AdminWritePrincipal, requested_date: Annotated[str, Query(alias="date")], ) -> dict: return _call(request, "sync_and_run", requested_date) @router.put("/custom", response_model=dict) def save_custom( payload: CustomStrategyInput, request: Request, principal: SmartWritePrincipal, ) -> dict: return _call(request, "save_custom", principal.user.id, payload.name, payload.formula) @router.post("/formula/compile", response_model=FormulaCompileResponse) def compile_formula( payload: FormulaCompileInput, request: Request, principal: SmartWritePrincipal, ) -> dict: return _call(request, "compile_formula", principal, payload.description) @router.delete("/custom/{strategy_id}", response_model=MessageResponse) def delete_custom( request: Request, principal: SmartWritePrincipal, strategy_id: Annotated[int, Path(gt=0)], ) -> MessageResponse: _call(request, "delete_custom", principal.user.id, strategy_id) return MessageResponse(message="自定义策略已删除。") @router.post("/custom/{strategy_id}/run", response_model=dict) def run_custom( request: Request, principal: SmartWritePrincipal, strategy_id: Annotated[int, Path(gt=0)], requested_date: Annotated[str, Query(alias="date")], ) -> dict: return _call(request, "run_custom", principal.user.id, strategy_id, requested_date) @router.get("/tracks", response_model=list[dict]) def tracks(request: Request, principal: SmartAccessPrincipal) -> list[dict]: return _call(request, "tracks", principal.user.id) @router.post("/tracks", response_model=IdentifierResponse) def add_track( payload: TrackInput, request: Request, principal: SmartWritePrincipal, ) -> IdentifierResponse: identifier = _call(request, "add_track", principal.user.id, payload.run_id, payload.identifier) return IdentifierResponse(id=identifier) @router.delete("/tracks/{track_id}", response_model=MessageResponse) def remove_track( request: Request, principal: SmartWritePrincipal, track_id: Annotated[int, Path(gt=0)], ) -> MessageResponse: _call(request, "remove_track", principal.user.id, track_id) return MessageResponse(message="已停止跟踪。") def _call(request: Request, method: str, *args): try: return getattr(request.app.state.container.screener, method)(*args) except (ScreenerError, MarketDataUnavailable, ProviderError, DataQualityError) as exc: raise AppError("screener_unavailable", str(exc), 409) from exc except LLMGatewayError as exc: status = 403 if exc.code in {"membership_required", "quota_exhausted"} else 503 raise AppError(exc.code, str(exc), status) from exc