@ -3,10 +3,11 @@ FINRA Short Sale Volume endpoints
"""
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 . responses import Response
from sqlalchemy import text
from sqlalchemy . ext . asyncio import AsyncSession
from app . core . config import settings
@ -104,6 +105,85 @@ async def get_short_ratio(
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 (
" /admin/ingest " ,
response_model = IngestResponse ,