"""Market-level endpoints (benchmark regime + AI/Tech regime-change monitor).""" from fastapi import APIRouter, Depends, Query from pydantic import BaseModel, Field, field_validator from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_db, require_access, require_admin from app.models.user import User from app.schemas.common import APIEnvelope from app.services import event_study_service, regime_monitor_service from app.services.backtest_service import get_backtest_report from app.services.market_regime_service import get_market_regime router = APIRouter(tags=["market"]) @router.get("/market/regime", response_model=APIEnvelope) async def market_regime( _user: User = Depends(require_access), db: AsyncSession = Depends(get_db), ) -> APIEnvelope: """Current benchmark (SPY) trend regime: bullish / bearish / neutral.""" data = await get_market_regime(db) return APIEnvelope(status="success", data=data) @router.get("/backtest/report", response_model=APIEnvelope) async def backtest_report( _user: User = Depends(require_access), db: AsyncSession = Depends(get_db), ) -> APIEnvelope: """Latest cached historical backtest report (None until the job runs).""" data = await get_backtest_report(db) return APIEnvelope(status="success", data=data) # --------------------------------------------------------------------------- # AI/Tech Regime-Change Monitor (standalone, observational) # --------------------------------------------------------------------------- class RegimeConfigUpdate(BaseModel): breadth_basket: list[str] | None = Field(default=None, min_length=20, max_length=100) fundamental_staleness_days: int | None = Field(default=None, ge=30, le=180) @field_validator("breadth_basket") @classmethod def normalise_basket(cls, value: list[str] | None) -> list[str] | None: if value is None: return None cleaned = [symbol.strip().upper().replace(".", "-") for symbol in value if symbol.strip()] if len(cleaned) != len(set(cleaned)): raise ValueError("breadth basket symbols must be unique") return cleaned class RegimeFundamentalsUpdate(BaseModel): f1_score: float | None = None f3_score: float | None = None locked: bool | None = None @router.get("/regime/monitor", response_model=APIEnvelope) async def regime_monitor( _user: User = Depends(require_access), db: AsyncSession = Depends(get_db), ) -> APIEnvelope: """Latest v2 State and Warning risk-thermometer readings.""" data = await regime_monitor_service.get_regime_monitor(db) return APIEnvelope(status="success", data=data) @router.get("/regime/config", response_model=APIEnvelope) async def regime_config( _admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ) -> APIEnvelope: """Editable fixed breadth basket and fundamental freshness window.""" data = await regime_monitor_service.get_regime_config(db) return APIEnvelope(status="success", data=data) @router.put("/regime/config", response_model=APIEnvelope) async def update_regime_config( body: RegimeConfigUpdate, _admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ) -> APIEnvelope: """Update the deliberately small v2 operator configuration.""" updates = body.model_dump(exclude_none=True) data = await regime_monitor_service.update_regime_config(db, updates) return APIEnvelope(status="success", data=data) @router.get("/regime/fundamentals", response_model=APIEnvelope) async def regime_fundamentals( _admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ) -> APIEnvelope: """Current F1 (capex) / F3 (earnings reaction) override (LLM-proposed or manual).""" data = await regime_monitor_service.get_fundamental_overrides(db) return APIEnvelope(status="success", data=data) @router.put("/regime/fundamentals", response_model=APIEnvelope) async def update_regime_fundamentals( body: RegimeFundamentalsUpdate, _admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ) -> APIEnvelope: """Manually override F1/F3 (locks out the LLM refresh until unlocked).""" data = await regime_monitor_service.set_fundamental_overrides( db, f1_score=body.f1_score, f3_score=body.f3_score, locked=body.locked ) return APIEnvelope(status="success", data=data) @router.post("/regime/fundamentals/refresh", response_model=APIEnvelope) async def refresh_regime_fundamentals( _admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ) -> APIEnvelope: """Ask the configured LLM to re-estimate F1/F3 now (forces past a lock).""" data = await regime_monitor_service.refresh_fundamental_overrides(db, force=True) return APIEnvelope(status="success", data=data) @router.get("/regime/event-study", response_model=APIEnvelope) async def regime_event_study( _user: User = Depends(require_access), db: AsyncSession = Depends(get_db), ) -> APIEnvelope: """Cached early-warning event study (lead time vs. historical drawdowns). None until the manual "Event Study" job has run (Admin → Jobs).""" data = await event_study_service.get_event_study_report(db) return APIEnvelope(status="success", data=data) @router.get("/regime/history", response_model=APIEnvelope) async def regime_history( days: int = Query(default=800, ge=7, le=2000), _user: User = Depends(require_access), db: AsyncSession = Depends(get_db), ) -> APIEnvelope: """Point-in-time v2 State/Warning history. Legacy rows are excluded.""" data = await regime_monitor_service.get_regime_history(db, days=days) return APIEnvelope(status="success", data=data)