@ -19,6 +19,8 @@ from app.schemas.financial import (
AlpacaPriceDataPoint ,
AlpacaBarsResponse ,
AlpacaIntradayResponse ,
AlpacaSnapshotResponse ,
AlpacaMultiSnapshotResponse ,
ErrorType ,
)
from app . services . alpaca_client import AlpacaClient
@ -259,3 +261,97 @@ async def get_alpaca_intraday(
raise HTTPException ( status_code = 502 , detail = f " Alpaca API error: { e } " )
finally :
await svc . client . close ( )
# ------------------------------------------------------------------
# Real-time snapshot (no cache)
# ------------------------------------------------------------------
def _parse_snapshot ( ticker : str , raw : dict ) - > AlpacaSnapshotResponse :
""" Convert raw Alpaca snapshot dict → AlpacaSnapshotResponse. """
trade = raw . get ( " latestTrade " ) or { }
quote = raw . get ( " latestQuote " ) or { }
daily = raw . get ( " dailyBar " ) or { }
prev = raw . get ( " prevDailyBar " ) or { }
price = trade . get ( " p " )
prev_close = prev . get ( " c " )
change = round ( price - prev_close , 4 ) if price is not None and prev_close else None
change_pct = round ( change / prev_close * 100 , 4 ) if change is not None and prev_close else None
return AlpacaSnapshotResponse (
ticker = ticker . upper ( ) ,
timestamp = trade . get ( " t " ) ,
price = price ,
trade_size = trade . get ( " s " ) ,
bid = quote . get ( " bp " ) ,
ask = quote . get ( " ap " ) ,
bid_size = quote . get ( " bs " ) ,
ask_size = quote . get ( " as " ) ,
open = daily . get ( " o " ) ,
high = daily . get ( " h " ) ,
low = daily . get ( " l " ) ,
volume = daily . get ( " v " ) ,
vwap = daily . get ( " vw " ) ,
prev_close = prev_close ,
change = change ,
change_pct = change_pct ,
)
@router.get (
" /snapshot/ {ticker} " ,
response_model = AlpacaSnapshotResponse ,
summary = " Real-time snapshot for a single ticker " ,
description = (
" Returns the latest trade price, bid/ask, today ' s OHLCV, and change vs previous close "
" using Alpaca ' s `/v2/stocks/ {symbol} /snapshot` endpoint. \n \n "
" **캐시 없음** — 매 요청마다 Alpaca API를 직접 호출. "
) ,
)
async def get_snapshot ( ticker : str ) :
client = AlpacaClient ( )
if not client . is_configured ( ) :
raise HTTPException ( status_code = 503 , detail = " Alpaca API keys not configured. " )
try :
raw = await client . get_snapshot ( ticker )
return _parse_snapshot ( ticker , raw )
except Exception as e :
raise HTTPException ( status_code = 502 , detail = f " Alpaca API error: { e } " )
finally :
await client . close ( )
@router.get (
" /snapshot " ,
response_model = AlpacaMultiSnapshotResponse ,
summary = " Real-time snapshots for multiple tickers " ,
description = (
" Returns snapshots for up to 100 tickers in a single request. \n \n "
" **Usage**: `?tickers=AAPL,MSFT,NVDA` \n \n "
" **캐시 없음** — 매 요청마다 Alpaca API를 직접 호출. "
) ,
)
async def get_snapshots (
tickers : str = Query ( . . . , description = " Comma-separated ticker symbols, e.g. AAPL,MSFT,NVDA " ) ,
) :
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 ) > 100 :
raise HTTPException ( status_code = 400 , detail = " Maximum 100 tickers per request. " )
client = AlpacaClient ( )
if not client . is_configured ( ) :
raise HTTPException ( status_code = 503 , detail = " Alpaca API keys not configured. " )
try :
raw_map = await client . get_snapshots ( symbols )
results = [
_parse_snapshot ( sym , raw_map . get ( sym , { } ) )
for sym in symbols
]
return AlpacaMultiSnapshotResponse ( count = len ( results ) , snapshots = results )
except Exception as e :
raise HTTPException ( status_code = 502 , detail = f " Alpaca API error: { e } " )
finally :
await client . close ( )