feat: GET /finra/pit-panel — PIT 횡단면 패널 엔드포인트

날짜별 전체 심볼의 공매도량 + 조정종가 조인 결과 반환.
pit_universe_membership ⋈ alpaca_price_data(interval='1d') 조인.
생존편향-0: 상폐 종목도 그날 거래됐으면 포함.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
main
I Luk Kim 3 months ago
parent ab693d332c
commit 843a188b76

@ -3,10 +3,11 @@ FINRA Short Sale Volume endpoints
""" """
from datetime import date, datetime, timedelta, timezone from datetime import date, datetime, timedelta, timezone
from typing import Optional from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import Response from fastapi.responses import Response
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings from app.core.config import settings
@ -104,6 +105,85 @@ async def get_short_ratio(
return body return body
@router.get(
"/pit-panel",
summary="PIT 횡단면 패널 — 특정 날짜 전체 심볼 (생존편향-0)",
description=(
"특정 날짜(또는 날짜 범위)에 실제 거래되던 모든 종목의 공매도량 + 종가를 반환.\n\n"
"`pit_universe_membership` 뷰 ⋈ `alpaca_price_data(interval='1d', adjustment='all')` 조인.\n\n"
"**생존편향-0**: 상폐/합병 종목(SIVB, FRC, TWTR 등)도 그날 거래됐으면 포함됨.\n\n"
"**날짜 범위**: `date_from`/`date_to` 둘 다 지정 시 최대 `limit`일치 반환 (기본 1일).\n\n"
"**주의**: 전체 패널(22k×8yr)은 날짜별 반복 호출로 조합. "
"단일 날짜 응답은 ~11k rows."
),
)
async def get_pit_panel(
date_from: date = Query(..., description="조회 시작일 (YYYY-MM-DD)"),
date_to: Optional[date] = Query(None, description="조회 종료일 — 생략 시 date_from 단일 날짜"),
limit: int = Query(50000, ge=1, le=200000, description="최대 반환 행 수"),
db: AsyncSession = Depends(get_db),
):
if date_to is None:
date_to = date_from
if date_from > date_to:
raise HTTPException(status_code=400, detail="date_from must be <= date_to")
rows = (await db.execute(
text("""
SELECT
p.d AS date,
p.symbol,
p.short_volume,
p.short_exempt_volume,
p.total_volume,
p.short_ratio,
a.open,
a.high,
a.low,
a.close,
a.volume AS price_volume,
a.vwap
FROM pit_universe_membership p
LEFT JOIN alpaca_price_data a
ON a.ticker = p.symbol
AND a.date::date = p.d
AND a.interval = '1d'
WHERE p.d BETWEEN :d_from AND :d_to
ORDER BY p.d, p.symbol
LIMIT :lim
"""),
{"d_from": date_from, "d_to": date_to, "lim": limit},
)).fetchall()
data = [
{
"date": str(r.date),
"symbol": r.symbol,
"short_volume": r.short_volume,
"short_exempt_volume":r.short_exempt_volume,
"total_volume": r.total_volume,
"short_ratio": r.short_ratio,
"open": r.open,
"high": r.high,
"low": r.low,
"close": r.close,
"price_volume": r.price_volume,
"vwap": r.vwap,
}
for r in rows
]
price_matched = sum(1 for d in data if d["close"] is not None)
return {
"date_from": date_from.isoformat(),
"date_to": date_to.isoformat(),
"count": len(data),
"price_matched": price_matched,
"price_coverage_pct": round(100 * price_matched / len(data), 1) if data else 0,
"data": data,
}
@router.post( @router.post(
"/admin/ingest", "/admin/ingest",
response_model=IngestResponse, response_model=IngestResponse,

Loading…
Cancel
Save