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
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}")
|