You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

111 lines
4.1 KiB
Python

"""
Insider Transaction endpoints — SEC Form 4 data
"""
import logging
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.insider import (
InsiderTransactionEntry,
InsiderTransactionResponse,
InsiderSummaryPeriod,
InsiderSummaryResponse,
)
from app.services.insider_transaction_service import InsiderTransactionService
from app.utils.cache import with_cache
router = APIRouter()
logger = logging.getLogger("app.api.v1.insider")
@router.get(
"/transactions/{symbol}",
response_model=InsiderTransactionResponse,
summary="Get insider transactions for a symbol",
description=(
"Query SEC Form 4 insider trading data. Auto-fetches from SEC EDGAR if data is missing.\n\n"
"**데이터 소스**: SEC EDGAR (무료, API 키 불필요). 첫 조회 시 자동 인덱싱.\n\n"
"**Transaction codes**: P=Purchase, S=Sale, A=Award, M=Exercise, G=Gift, F=Tax Withholding"
),
)
@with_cache(namespace="insider:transactions", ttl=None, key_params=["symbol", "days", "transaction_type", "insider_title", "limit"])
async def get_insider_transactions(
symbol: str,
response: Response,
days: int = Query(90, ge=1, le=3650, description="Days to look back (max ~10 years)"),
transaction_type: Optional[str] = Query(None, description="Filter: P=Purchase, S=Sale, A=Award, M=Exercise"),
insider_title: Optional[str] = Query(None, description="Filter by title keyword (e.g., CEO, CFO, Director)"),
limit: int = Query(50, ge=1, le=500, description="Max entries to return"),
force_refresh: bool = Query(False, description="Bypass cache and re-fetch from SEC"),
db: AsyncSession = Depends(get_db),
):
svc = InsiderTransactionService()
try:
rows, total = await svc.get_transactions(
db,
ticker=symbol,
days=days,
transaction_type=transaction_type,
insider_title=insider_title,
limit=limit,
)
entries = [InsiderTransactionEntry.from_orm_obj(r) for r in rows]
return InsiderTransactionResponse(
symbol=symbol.upper(),
transactions=entries,
total_count=total,
metadata={
"days_requested": days,
"transaction_type_filter": transaction_type,
"insider_title_filter": insider_title,
},
)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Insider transactions error for {symbol}: {e}")
raise HTTPException(status_code=502, detail=f"Failed to fetch insider data: {e}")
@router.get(
"/summary/{symbol}",
response_model=InsiderSummaryResponse,
summary="Get insider trading summary",
description=(
"Aggregated insider buy/sell activity for 3, 6, and 12 month periods.\n\n"
"Includes net buy/sell shares and values, plus top 5 notable transactions by value."
),
)
@with_cache(namespace="insider:summary", ttl=None, key_params=["symbol"])
async def get_insider_summary(
symbol: str,
response: Response,
force_refresh: bool = Query(False, description="Bypass cache"),
db: AsyncSession = Depends(get_db),
):
svc = InsiderTransactionService()
try:
result = await svc.get_summary(db, ticker=symbol)
periods = [InsiderSummaryPeriod(**p) for p in result["periods"]]
notable = [InsiderTransactionEntry.from_orm_obj(r) for r in result["notable"]]
return InsiderSummaryResponse(
symbol=symbol.upper(),
periods=periods,
notable_transactions=notable,
metadata={"data_source": "SEC EDGAR Form 4"},
)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"Insider summary error for {symbol}: {e}")
raise HTTPException(status_code=502, detail=f"Failed to fetch insider summary: {e}")