@ -23,7 +23,6 @@ from app.schemas.financial import (
TodayOHLCResponse ,
TodayOHLCResponse ,
)
)
from app . services . price_data_service import PriceDataService
from app . services . price_data_service import PriceDataService
from app . services . alpaca_price_service import AlpacaPriceService
from app . schemas . financial import AlpacaMultiBarsResponse
from app . schemas . financial import AlpacaMultiBarsResponse
from app . core . config import settings
from app . core . config import settings
from app . utils . date_utils import quarters_to_date_range
from app . utils . date_utils import quarters_to_date_range
@ -289,78 +288,69 @@ async def get_price_data(
@router.get (
@router.get (
" /data " ,
" /data " ,
response_model = AlpacaMultiBarsResponse ,
response_model = AlpacaMultiBarsResponse ,
summary = " Get daily bars for multiple tickers via Alpaca (DB-backed)" ,
summary = " Get daily bars for multiple tickers via yfinance (DB-backed)" ,
description = (
description = (
" Fetch OHLCV daily bars for up to ~500 tickers. Results are stored in DB so "
" Fetch OHLCV daily bars for one or more tickers via Yahoo Finance (yfinance-plus). "
" subsequent calls only fetch new/missing dates from Alpaca. \n \n "
" Results are stored in DB; subsequent calls for the same range skip the external API. \n \n "
" - `tickers`: comma-separated list, e.g. `AAPL,MSFT,BF-B` \n "
" - `tickers` or `ticker`: comma-separated list, e.g. `AAPL,MSFT` or single `QQQ` \n "
" - Ticker normalization: `BF-B` → `BF.B` handled automatically; "
" - `force_refresh=true`: re-fetch from Yahoo Finance even if DB has data \n "
" response keys use the original symbol names. \n "
" - Up to 1000 tickers per request (auto-chunked internally) \n \n "
" - `force_refresh=true`: re-fetch all from Alpaca regardless of DB state. \n "
" **경로 파라미터 대안**: 단일 종목은 `/data/ {ticker} ?start_date=...&end_date=...` 도 동일하게 동작합니다. "
" - Requires `ALPACA_API_KEY` / `ALPACA_SECRET_KEY`. \n \n "
" **⚠️ Alpaca 배치 제한** \n \n "
" Alpaca multi-bar 엔드포인트는 요청당 **~100개 심볼**이 실질적 상한입니다 "
" (공식 문서 미명시, 커뮤니티 보고 및 실제 운용 기준). "
" 내부적으로 **100개 단위로 자동 분할**하여 요청하므로 클라이언트는 신경 쓸 필요 없음. "
" 단, 배치 수가 늘어날수록 응답 시간이 선형적으로 증가함 (500종목 → Alpaca 5회 호출). "
) ,
) ,
tags = [ " price " , " alpaca " ],
tags = [ " price " ] ,
)
)
async def get_multi_ticker_daily_bars (
async def get_multi_ticker_daily_bars (
tickers : str = Query ( . . . , description = " Comma-separated tickers, e.g. AAPL,MSFT,BF-B " ) ,
tickers : Optional [ str ] = Query ( None , description = " Comma-separated tickers, e.g. AAPL,MSFT,QQQ " ) ,
ticker : Optional [ str ] = Query ( None , description = " Alias for tickers (single ticker shorthand) " ) ,
start_date : date = Query ( . . . , description = " Start date (YYYY-MM-DD) " ) ,
start_date : date = Query ( . . . , description = " Start date (YYYY-MM-DD) " ) ,
end_date : date = Query ( . . . , description = " End date (YYYY-MM-DD) " ) ,
end_date : date = Query ( . . . , description = " End date (YYYY-MM-DD) " ) ,
interval : str = Query ( " 1d " , description = " Bar interval: 1d, 1w, 1m o " ) ,
interval : str = Query ( " 1d " , description = " Bar interval: 1d, 1w, 1m " ) ,
force_refresh : bool = Query ( False , description = " Re-fetch from Alpaca even if DB has data" ) ,
force_refresh : bool = Query ( False , description = " Re-fetch from Yahoo Finance even if DB has data" ) ,
) :
) :
""" Multi-ticker daily bars via Alpaca with DB storage (ORB engine interface). """
""" Multi-ticker daily bars via yfinance with DB storage. """
symbols = [ s . strip ( ) . upper ( ) for s in tickers . split ( " , " ) if s . strip ( ) ]
raw = tickers or ticker
if not raw :
raise HTTPException ( status_code = 400 , detail = " No tickers provided. Use ?tickers=AAPL,MSFT or ?ticker=QQQ. " )
symbols = [ s . strip ( ) . upper ( ) for s in raw . split ( " , " ) if s . strip ( ) ]
if not symbols :
if not symbols :
raise HTTPException ( status_code = 400 , detail = " No tickers provided. " )
raise HTTPException ( status_code = 400 , detail = " No tickers provided. " )
if len ( symbols ) > 1000 :
if len ( symbols ) > 1000 :
raise HTTPException ( status_code = 400 , detail = " Maximum 1000 tickers per request. " )
raise HTTPException ( status_code = 400 , detail = " Maximum 1000 tickers per request. " )
svc = AlpacaPriceService ( )
if not svc . is_available ( ) :
raise HTTPException ( status_code = 503 , detail = " Alpaca API keys not configured. " )
start_dt = datetime . combine ( start_date , datetime . min . time ( ) ) . replace ( tzinfo = timezone . utc )
start_dt = datetime . combine ( start_date , datetime . min . time ( ) ) . replace ( tzinfo = timezone . utc )
end_dt = datetime . combine ( end_date , datetime . m in . time ( ) ) . replace ( tzinfo = timezone . utc )
end_dt = datetime . combine ( end_date , datetime . max . time ( ) ) . replace ( tzinfo = timezone . utc )
price_service = PriceDataService ( )
try :
try :
data = await svc . get_or_fetch_multi_bars (
results , _ , _ = await price_service . get_multiple_tickers_data_optimized (
symbols , start_dt , end_dt , interval , force_refresh
tickers = symbols ,
start_date = start_dt ,
end_date = end_dt ,
interval = interval ,
force_refresh = force_refresh ,
)
)
except Exception as e :
except Exception as e :
err = str ( e )
raise HTTPException ( status_code = 500 , detail = f " yfinance error: { e } " )
detail = f " Alpaca API error: { err } "
if " 502 " in err or " Bad Gateway " in err :
detail = (
f " Alpaca 502 Bad Gateway — 요청당 심볼 수 초과 가능성. "
f " 내부 배치 크기: 100개/요청. 원인: { err } "
)
raise HTTPException ( status_code = 502 , detail = detail )
finally :
await svc . client . close ( )
bars = {
bars = { }
ticker : [
for item in results :
if item . success and item . data and item . data . data :
bars [ item . ticker ] = [
{
{
" date " : row . date . date ( ) . isoformat ( ) ,
" date " : point . date . isoformat ( ) ,
" open " : row . open ,
" open " : point . open ,
" high " : row . high ,
" high " : point . high ,
" low " : row . low ,
" low " : point . low ,
" close " : row . close ,
" close " : point . close ,
" volume " : row . volume ,
" volume " : point . volume ,
}
}
for row in rows
for point in item . data . data
]
]
for ticker , rows in data . items ( )
}
return AlpacaMultiBarsResponse (
return AlpacaMultiBarsResponse (
source = " YAHOO_FINANCE " ,
interval = interval ,
interval = interval ,
count = len ( symbol s) ,
count = len ( bar s) ,
bars = bars ,
bars = bars ,
)
)