@ -23,7 +23,7 @@ from app.schemas.financial import (
TodayOHLCResponse ,
)
from app . services . price_data_service import PriceDataService
from app . services . alpaca_ client import AlpacaClient , normalize_ticker
from app . services . alpaca_ price_service import AlpacaPriceService
from app . schemas . financial import AlpacaMultiBarsResponse
from app . core . config import settings
from app . utils . date_utils import quarters_to_date_range
@ -291,13 +291,14 @@ async def get_price_data(
@router.get (
" /data " ,
response_model = AlpacaMultiBarsResponse ,
summary = " Get daily bars for multiple tickers via Alpaca " ,
summary = " Get daily bars for multiple tickers via Alpaca (DB-backed) " ,
description = (
" Fetch OHLCV daily bars for up to ~500 tickers in one call using Alpaca' s "
" multi-bar endpoint. Data is **not** cached (live trading use-case) .\n \n "
" Fetch OHLCV daily bars for up to ~500 tickers . Results are stored in DB so "
" subsequent calls only fetch new/missing dates from Alpaca .\n \n "
" - `tickers`: comma-separated list, e.g. `AAPL,MSFT,BF-B` \n "
" - Ticker normalization: `BF-B` → `BF.B` is handled automatically; "
" response keys use the *original* symbol names you passed in. \n "
" - Ticker normalization: `BF-B` → `BF.B` handled automatically; "
" response keys use the original symbol names. \n "
" - `force_refresh=true`: re-fetch all from Alpaca regardless of DB state. \n "
" - Requires `ALPACA_API_KEY` / `ALPACA_SECRET_KEY`. "
) ,
tags = [ " price " , " alpaca " ] ,
@ -307,56 +308,46 @@ async def get_multi_ticker_daily_bars(
start_date : date = Query ( . . . , description = " Start date (YYYY-MM-DD) " ) ,
end_date : date = Query ( . . . , description = " End date (YYYY-MM-DD) " ) ,
interval : str = Query ( " 1d " , description = " Bar interval: 1d, 1w, 1mo " ) ,
force_refresh : bool = Query ( False , description = " Ignored — no caching for this endpoint " ) ,
force_refresh : bool = Query ( False , description = " Re-fetch from Alpaca even if DB has data " ) ,
db : AsyncSession = Depends ( get_db ) ,
) :
""" Multi-ticker daily bars via Alpaca (ORB engine interface)."""
""" Multi-ticker daily bars via Alpaca with DB storage (ORB engine interface)."""
symbols = [ s . strip ( ) . upper ( ) for s in tickers . split ( " , " ) if s . strip ( ) ]
if not symbols :
raise HTTPException ( status_code = 400 , detail = " No tickers provided. " )
if len ( symbols ) > 1000 :
raise HTTPException ( status_code = 400 , detail = " Maximum 1000 tickers per request. " )
client = AlpacaClient ( )
if not client. is_configured ( ) :
svc = AlpacaPriceService ( )
if not svc. is_available ( ) :
raise HTTPException ( status_code = 503 , detail = " Alpaca API keys not configured. " )
# Build reverse-map: normalized_alpaca_symbol → original_input_symbol
reverse_map = { normalize_ticker ( s ) : s for s in symbols }
start_str = start_date . isoformat ( )
end_str = end_date . isoformat ( )
start_dt = datetime . combine ( start_date , datetime . min . time ( ) ) . replace ( tzinfo = timezone . utc )
end_dt = datetime . combine ( end_date , datetime . min . time ( ) ) . replace ( tzinfo = timezone . utc )
try :
raw = await client . get_multi_bars (
symbols = symbols ,
timeframe = interval ,
start = start_str ,
end = end_str ,
data = await svc . get_or_fetch_multi_bars (
db , symbols , start_dt , end_dt , interval , force_refresh
)
except Exception as e :
raise HTTPException ( status_code = 502 , detail = f " Alpaca API error: { e } " )
finally :
await client. close ( )
await svc. client. close ( )
# Re-key results back to original input symbols and reshape bar dicts
bars : dict = { }
for alpaca_sym , bar_list in raw . items ( ) :
original_sym = reverse_map . get ( alpaca_sym , alpaca_sym )
bars [ original_sym ] = [
bars = {
ticker : [
{
" date " : b[ " t " ] [ : 10 ] , # "2026-02-07T05:00:00Z" → "2026-02-07"
" open " : b. get ( " o " ) ,
" high " : b. get ( " h " ) ,
" low " : b. get ( " l " ) ,
" close " : b. get ( " c " ) ,
" volume " : b. get ( " v " ) ,
" date " : row . date . date ( ) . isoformat ( ) ,
" open " : row . open ,
" high " : row. high ,
" low " : row. low ,
" close " : row. close ,
" volume " : row. volume ,
}
for b in bar_list
for row in rows
]
# Include symbols with no data as empty lists
for sym in symbols :
bars . setdefault ( sym , [ ] )
for ticker , rows in data . items ( )
}
return AlpacaMultiBarsResponse (
interval = interval ,