feat: Alpaca 실시간 snapshot 엔드포인트 추가

- GET /alpaca/snapshot/{ticker} — 단일 티커 실시간 가격/bid-ask/OHLCV/등락률
- GET /alpaca/snapshot?tickers=A,B — 최대 100개 멀티 티커 일괄 조회
- AlpacaClient.get_snapshot / get_snapshots 메서드 추가
- AlpacaSnapshotResponse / AlpacaMultiSnapshotResponse 스키마 추가
- docs/PYTHON_CLIENT.md 사용 예시 업데이트
- 캐시 없음 — 매 요청마다 Alpaca API 직접 호출

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
I Luk Kim 4 months ago
parent 33b1749721
commit a9a158d804

@ -19,6 +19,8 @@ from app.schemas.financial import (
AlpacaPriceDataPoint, AlpacaPriceDataPoint,
AlpacaBarsResponse, AlpacaBarsResponse,
AlpacaIntradayResponse, AlpacaIntradayResponse,
AlpacaSnapshotResponse,
AlpacaMultiSnapshotResponse,
ErrorType, ErrorType,
) )
from app.services.alpaca_client import AlpacaClient 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}") raise HTTPException(status_code=502, detail=f"Alpaca API error: {e}")
finally: finally:
await svc.client.close() 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()

@ -537,6 +537,42 @@ class AlpacaIntradayResponse(BaseModel):
candles: List[Dict[str, Any]] candles: List[Dict[str, Any]]
class AlpacaSnapshotResponse(BaseModel):
"""Real-time snapshot for a single ticker via Alpaca."""
ticker: str
source: str = "ALPACA"
timestamp: Optional[str] = None # latestTrade.t
# Latest trade
price: Optional[float] = None
trade_size: Optional[int] = None
# Latest quote (bid/ask)
bid: Optional[float] = None
ask: Optional[float] = None
bid_size: Optional[int] = None
ask_size: Optional[int] = None
# Today's session (dailyBar)
open: Optional[float] = None
high: Optional[float] = None
low: Optional[float] = None
volume: Optional[int] = None
vwap: Optional[float] = None
# Change vs previous close (prevDailyBar.c)
prev_close: Optional[float] = None
change: Optional[float] = None
change_pct: Optional[float] = None
class AlpacaMultiSnapshotResponse(BaseModel):
"""Real-time snapshots for multiple tickers."""
source: str = "ALPACA"
count: int
snapshots: List[AlpacaSnapshotResponse]
class NewsOnlyResponse(BaseModel): class NewsOnlyResponse(BaseModel):
ticker: str ticker: str
retrieved_at: str retrieved_at: str

@ -216,6 +216,26 @@ class AlpacaClient:
logger.info(f"Alpaca: fetched {total} bars for {len(symbols)} symbols ({alpaca_tf})") logger.info(f"Alpaca: fetched {total} bars for {len(symbols)} symbols ({alpaca_tf})")
return result return result
async def get_snapshot(self, symbol: str) -> Dict:
"""
Fetch a real-time snapshot for a single symbol.
Returns Alpaca's snapshot object with keys:
latestTrade, latestQuote, minuteBar, dailyBar, prevDailyBar
"""
data = await self._request("GET", f"/v2/stocks/{symbol.upper()}/snapshot")
return data
async def get_snapshots(self, symbols: List[str]) -> Dict[str, Dict]:
"""
Fetch real-time snapshots for multiple symbols in one request.
Returns dict mapping symbol snapshot object.
"""
params = {"symbols": ",".join(s.upper() for s in symbols)}
data = await self._request("GET", "/v2/stocks/snapshots", params=params)
return data # {SYMBOL: {...snapshot...}, ...}
async def check_connection(self) -> Dict: async def check_connection(self) -> Dict:
"""Verify API key validity by requesting a small amount of data.""" """Verify API key validity by requesting a small amount of data."""
try: try:

@ -195,6 +195,16 @@ candles = requests.get(f"{BASE}/alpaca/intraday/NVDA", params={
"end_date": "2025-03-10", "end_date": "2025-03-10",
}).json() }).json()
print(f"Intraday candles: {candles['count']}") print(f"Intraday candles: {candles['count']}")
# Real-time snapshot — single ticker (no cache, direct Alpaca call)
snap = requests.get(f"{BASE}/alpaca/snapshot/SPY").json()
print(f"SPY price: {snap['price']}, change: {snap['change_pct']}%")
print(f" bid={snap['bid']} ask={snap['ask']}")
# Real-time snapshots — multiple tickers
multi = requests.get(f"{BASE}/alpaca/snapshot", params={"tickers": "SPY,QQQ,AAPL"}).json()
for s in multi["snapshots"]:
print(f" {s['ticker']}: {s['price']} ({s['change_pct']:+.2f}%)")
``` ```
### 7. FINRA Short Volume ### 7. FINRA Short Volume

Loading…
Cancel
Save