""" Activist Ownership endpoints — SC 13D / 13G """ import logging from datetime import date from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import Response from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.schemas.ownership import ( ActivistEventEntry, ActivistEventsResponse, ActivistActiveResponse, ) from app.services.activist_ownership_service import ActivistOwnershipService from app.utils.cache import with_cache router = APIRouter() logger = logging.getLogger("app.api.v1.ownership") @router.get( "/13dg/active", response_model=ActivistActiveResponse, summary="Active activist positions as-of a date", description=( "Returns the latest SC 13D/13G filing per (filer, issuer) pair where **filing_date ≤ as_of** " "and **ownership_pct ≥ min_ownership_pct**.\n\n" "Positions with `ownership_pct = null` (not yet enriched) are excluded.\n\n" "`as_of` is required." ), ) @with_cache(namespace="ownership:13dg_active", ttl=300, key_params=["as_of", "min_ownership_pct"]) async def get_13dg_active( response: Response, as_of: date = Query(..., description="Point-in-time cutoff. Required."), min_ownership_pct: float = Query(5.0, ge=0.0, le=100.0, description="Minimum ownership %"), db: AsyncSession = Depends(get_db), ): svc = ActivistOwnershipService() try: rows = await svc.get_active_positions(db, as_of=as_of, min_ownership_pct=min_ownership_pct) entries = [ActivistEventEntry.from_orm_obj(r) for r in rows] return ActivistActiveResponse( as_of=as_of, min_ownership_pct=min_ownership_pct, positions=entries, total_count=len(entries), ) except Exception as e: logger.error(f"13D/G active positions error: {e}") raise HTTPException(status_code=502, detail=str(e)) @router.get( "/13dg/{ticker}", response_model=ActivistEventsResponse, summary="SC 13D/13G activist ownership events for a ticker", description=( "Returns SC 13D and SC 13G filings (including amendments) where **filing_date ≤ as_of**.\n\n" "`as_of` is required for PIT safety in backtests.\n\n" "Note: `ownership_pct` / `shares_owned` will be `null` until background enrichment runs (~30 min)." ), ) @with_cache(namespace="ownership:13dg", ttl=300, key_params=["ticker", "start", "end", "as_of"]) async def get_13dg_events( ticker: str, response: Response, as_of: date = Query(..., description="Point-in-time cutoff (filing_date ≤ as_of). Required."), start: Optional[date] = Query(None, description="Window start (filing_date ≥ start)"), end: Optional[date] = Query(None, description="Window end (filing_date ≤ end)"), db: AsyncSession = Depends(get_db), ): svc = ActivistOwnershipService() try: rows = await svc.get_events(db, ticker=ticker, as_of=as_of, start=start, end=end) entries = [ActivistEventEntry.from_orm_obj(r) for r in rows] return ActivistEventsResponse( symbol=ticker.upper(), as_of=as_of, window={"start": start.isoformat() if start else None, "end": end.isoformat() if end else None}, events=entries, total_count=len(entries), ) except Exception as e: logger.error(f"13D/G events error for {ticker}: {e}") raise HTTPException(status_code=502, detail=str(e))