""" FINRA Short Sale Volume endpoints """ from datetime import date, datetime, timedelta, timezone 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.config import settings from app.core.database import get_db from app.schemas.finra import ( ShortVolumeEntry, ShortVolumeResponse, ShortRatioHistoryResponse, ShortRatioPoint, IngestResponse, ) from app.services.finra_short_volume_service import FinraShortVolumeService from app.utils.cache import build_cache_key, get_cached_response, set_cached_response, with_cache router = APIRouter() @router.get( "/short-volume/{symbol}", response_model=ShortVolumeResponse, summary="Get short volume data for a symbol", description=( "Query FINRA RegSHO short sale volume. Auto-ingests if data is missing.\n\n" "**DB 보유**: 2021년 ~ 현재 (5년치 백필 완료). " "추가 백필: `POST /finra/admin/ingest?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD`\n\n" "**데이터 소스**: FINRA RegSHO CDN (공개, API 키 불필요). 주말/공휴일 데이터 없음." ), ) @with_cache(namespace="finra:short-volume", ttl=None, key_params=["symbol", "days", "limit"]) async def get_short_volume( symbol: str, response: Response, days: int = Query(30, ge=1, le=3650, description="Number of days to look back (max ~10 years)"), limit: int = Query(100, ge=1, le=10000, description="Max entries to return"), force_refresh: bool = Query(False, description="Bypass cache"), db: AsyncSession = Depends(get_db), ): svc = FinraShortVolumeService() end = datetime.now(timezone.utc).date() start = end - timedelta(days=days) rows, total = await svc.get_short_volume( db, symbol=symbol, start_date=start, end_date=end, limit=limit ) entries = [ShortVolumeEntry.from_orm_obj(r) for r in rows] body = ShortVolumeResponse( symbol=symbol.upper(), entries=entries, total_count=total, metadata={ "days_requested": days, "start_date": start.isoformat(), "end_date": end.isoformat(), }, ) return body @router.get( "/short-ratio/{symbol}", response_model=ShortRatioHistoryResponse, summary="Get short ratio history for a symbol", description=( "Return daily short_ratio (aggregated across markets) for the last N days.\n\n" "**DB 보유**: 2021년 ~ 현재 (5년치). days 최대 3650 (10년).\n\n" "추가 백필: `POST /finra/admin/ingest?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD`" ), ) @with_cache(namespace="finra:short-ratio", ttl=None, key_params=["symbol", "days"]) async def get_short_ratio( symbol: str, response: Response, days: int = Query(60, ge=1, le=3650, description="Number of days (max ~10 years)"), force_refresh: bool = Query(False, description="Bypass cache"), db: AsyncSession = Depends(get_db), ): svc = FinraShortVolumeService() history = await svc.get_short_ratio_history(db, symbol=symbol, days=days) points = [ShortRatioPoint(**h) for h in history] avg = ( sum(p.short_ratio for p in points) / len(points) if points else None ) body = ShortRatioHistoryResponse( symbol=symbol.upper(), history=points, avg_short_ratio=round(avg, 6) if avg is not None else None, metadata={"days_requested": days, "data_points": len(points)}, ) return body @router.post( "/admin/ingest", response_model=IngestResponse, summary="Manually ingest FINRA short volume data", description=( "Download and ingest FINRA short volume file(s) for a specific date or date range.\n\n" "**백필 예시**:\n" "- 단일 날짜: `?date=2025-01-15`\n" "- 날짜 범위: `?start_date=2025-01-01&end_date=2025-12-31`\n" "- 이미 있는 데이터 재인제스트: `?start_date=...&end_date=...&force=true`\n\n" "주말/공휴일은 자동으로 건너뜀. 1년치 기준 약 20-40분 소요." ), ) async def ingest_short_volume( date_str: Optional[str] = Query(None, alias="date", description="Single date (YYYY-MM-DD)"), start_date: Optional[date] = Query(None, description="Range start (YYYY-MM-DD)"), end_date: Optional[date] = Query(None, description="Range end (YYYY-MM-DD)"), force: bool = Query(False, description="Re-ingest even if data exists"), db: AsyncSession = Depends(get_db), ): svc = FinraShortVolumeService() # Single date if date_str: try: target = datetime.strptime(date_str, "%Y-%m-%d").date() except ValueError: raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD.") count = await svc.ingest_date(db, target, force_refresh=force) return IngestResponse( date=target.isoformat(), records_ingested=count, status="completed", ) # Date range if start_date and end_date: if start_date > end_date: raise HTTPException(status_code=400, detail="start_date must be <= end_date") count = await svc.ingest_date_range(db, start_date, end_date, force_refresh=force) return IngestResponse( date_range={"start": start_date.isoformat(), "end": end_date.isoformat()}, records_ingested=count, status="completed", ) raise HTTPException( status_code=400, detail="Provide either 'date' (single date) or both 'start_date' and 'end_date'.", )