@ -2,14 +2,20 @@
FINRA Short Sale Volume endpoints
"""
import logging
import os
from datetime import date , datetime , timedelta , timezone
from typing import List , Optional
from fastapi import APIRouter , Depends, HTTPException , Query
from fastapi . responses import Response
from fastapi import APIRouter , BackgroundTasks, Depends, HTTPException , Query
from fastapi . responses import FileResponse, Response
from sqlalchemy import text
from sqlalchemy . ext . asyncio import AsyncSession
from app . core . database import AsyncSessionLocal
logger = logging . getLogger ( __name__ )
from app . core . config import settings
from app . core . database import get_db
from app . schemas . finra import (
@ -184,6 +190,100 @@ async def get_pit_panel(
}
_EXPORT_PATH = " /app/data/pit_panel.parquet "
async def _run_pit_export ( start_str : str , end_str : str ) - > None :
""" Background task: export PIT panel to /app/data/pit_panel.parquet. """
try :
import pandas as pd
except ImportError :
logger . error ( " pandas not installed " ) ; return
try :
import pyarrow # noqa: F401
except ImportError :
logger . error ( " pyarrow not installed " ) ; return
logger . info ( f " PIT export: loading { start_str } → { end_str } ... " )
async with AsyncSessionLocal ( ) as db :
rows = ( await db . execute ( text ( """
SELECT
p . d : : date 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 : s AND : e
ORDER BY p . d , p . symbol
""" ), { " s " : start_str, " e " : end_str})).fetchall()
logger . info ( f " PIT export: { len ( rows ) : , } rows fetched, writing parquet ... " )
df = pd . DataFrame ( rows , columns = [
" date " , " symbol " , " short_volume " , " short_exempt_volume " , " total_volume " ,
" short_ratio " , " open " , " high " , " low " , " close " , " price_volume " , " vwap " ,
] )
df [ " date " ] = pd . to_datetime ( df [ " date " ] )
os . makedirs ( os . path . dirname ( _EXPORT_PATH ) , exist_ok = True )
df . to_parquet ( _EXPORT_PATH , index = False , engine = " pyarrow " )
mb = os . path . getsize ( _EXPORT_PATH ) / 1_048_576
logger . info (
f " PIT export complete: { _EXPORT_PATH } | { mb : .0f } MB | shape= { df . shape } | "
f " price_coverage= { df [ ' close ' ] . notna ( ) . mean ( ) * 100 : .1f } % "
)
@router.post (
" /admin/export-pit-panel " ,
summary = " PIT 패널 parquet export → /app/data/pit_panel.parquet " ,
description = (
" FINRA × Alpaca(adjustment= ' all ' ) 조인 패널을 parquet으로 저장. \n \n "
" `GET /finra/admin/export-pit-panel/download` 로 다운로드. \n \n "
" 백그라운드 실행 — 전체 기간(2018-08~현재) 기준 수 분 ~ 10여 분 소요. "
) ,
)
async def export_pit_panel (
background_tasks : BackgroundTasks ,
start_date : date = Query ( date ( 2018 , 8 , 1 ) , description = " 시작일 " ) ,
end_date : date = Query ( None , description = " 종료일 (기본: 오늘) " ) ,
) :
_end = end_date or date . today ( )
background_tasks . add_task ( _run_pit_export , start_date . isoformat ( ) , _end . isoformat ( ) )
return {
" status " : " started " ,
" start_date " : start_date . isoformat ( ) ,
" end_date " : _end . isoformat ( ) ,
" output " : _EXPORT_PATH ,
" note " : " 완료 후 GET /finra/admin/export-pit-panel/download 로 다운로드. " ,
}
@router.get (
" /admin/export-pit-panel/download " ,
summary = " pit_panel.parquet 다운로드 " ,
)
async def download_pit_panel ( ) :
if not os . path . exists ( _EXPORT_PATH ) :
raise HTTPException (
status_code = 404 ,
detail = " parquet 파일 없음. POST /finra/admin/export-pit-panel 먼저 실행. " ,
)
return FileResponse (
_EXPORT_PATH ,
media_type = " application/octet-stream " ,
filename = " pit_panel.parquet " ,
)
@router.post (
" /admin/ingest " ,
response_model = IngestResponse ,