|
|
|
|
@ -2,7 +2,7 @@
|
|
|
|
|
Alpaca Market Data endpoints — standalone price data via Alpaca API
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from datetime import date, datetime, timezone
|
|
|
|
|
from datetime import date, datetime, timezone, timedelta
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
|
|
|
@ -223,24 +223,24 @@ async def get_alpaca_price_data(
|
|
|
|
|
@router.get(
|
|
|
|
|
"/intraday",
|
|
|
|
|
response_model=AlpacaMultiBarsResponse,
|
|
|
|
|
summary="Get intraday bars for multiple tickers via Alpaca (DB-backed)",
|
|
|
|
|
summary="Get historical intraday bars for multiple tickers via Alpaca SIP (DB-backed)",
|
|
|
|
|
description=(
|
|
|
|
|
"Fetch intraday OHLCV bars for up to ~500 tickers via Alpaca IEX feed. "
|
|
|
|
|
"Fetch historical intraday OHLCV bars for up to ~500 tickers via Alpaca **SIP 피드**. "
|
|
|
|
|
"Results are stored in DB so subsequent calls for the same period skip Alpaca.\n\n"
|
|
|
|
|
"**⚠️ Alpaca IEX 피드 분봉 데이터 한계**\n\n"
|
|
|
|
|
"**⚠️ 날짜 제한: 어제(yesterday)까지만 조회 가능**\n\n"
|
|
|
|
|
"Alpaca 무료 플랜에서 SIP 피드는 15분 이상 지난 데이터만 접근 가능합니다. "
|
|
|
|
|
"당일(오늘) 데이터가 필요하면 → **`GET /api/v1/alpaca/intraday/now`** 사용\n\n"
|
|
|
|
|
"**SIP 피드 특성**\n\n"
|
|
|
|
|
"| 항목 | 내용 |\n"
|
|
|
|
|
"|------|------|\n"
|
|
|
|
|
"| 피드 | **IEX 피드** (무료 플랜 기본값) |\n"
|
|
|
|
|
"| 지연 | **실시간** (지연 없음) |\n"
|
|
|
|
|
"| 거래량 커버리지 | 미국 전체 시장의 약 **2~5%** (IEX 거래소 거래만 집계) |\n"
|
|
|
|
|
"| OHLCV 정확도 | SIP 대비 **거래량 대폭 낮게** 표시됨 (가격 레벨은 유사) |\n"
|
|
|
|
|
"| 당일 장 중 재요청 | max_date < end_dt 조건으로 **항상 Alpaca 재호출** |\n"
|
|
|
|
|
"| SIP 피드 사용 시 | 유료 구독 + `feed=sip` 파라미터 추가 필요 |\n"
|
|
|
|
|
"| 히스토리 한계 | 과거 데이터는 SIP 기준이므로 무료 플랜 접근 범위 제한 없음 |\n\n"
|
|
|
|
|
"**권장 용도**: 당일 실시간 분봉 조회, ORB 전략 (가격 레벨 기준), 실시간 장 중 모니터링\n\n"
|
|
|
|
|
"**거래량 정확도가 중요하면** → 유료 Alpaca 플랜 + `feed=sip` 사용\n\n"
|
|
|
|
|
"| 피드 | **SIP 피드** (전체 미국 거래소 통합) |\n"
|
|
|
|
|
"| 거래량 커버리지 | **100%** (NYSE, NASDAQ, BATS 등 전체) |\n"
|
|
|
|
|
"| OHLCV 정확도 | **정확** — 백테스트에 적합 |\n"
|
|
|
|
|
"| 조회 가능 범위 | **2016년~어제** (당일 조회 시 400 에러) |\n"
|
|
|
|
|
"| 히스토리 | 2016년부터 제공 (무료 플랜 기준) |\n\n"
|
|
|
|
|
"**권장 용도**: 백테스트, 과거 분봉 분석, ORB 전략 히스토리컬 검증\n\n"
|
|
|
|
|
"- `tickers`: comma-separated list, e.g. `AAPL,MSFT,BF-B`\n"
|
|
|
|
|
"- `interval`: `1m`, `5m`, `15m`, `30m`, `1h` (또는 `5min`, `15min` 등)\n"
|
|
|
|
|
"- `interval`: `1m`, `5m`, `15m`, `30m`, `1h`\n"
|
|
|
|
|
"- DB 저장 후 재요청 시 Alpaca 미사용\n"
|
|
|
|
|
"- Requires `ALPACA_API_KEY` / `ALPACA_SECRET_KEY`.\n\n"
|
|
|
|
|
"**⚠️ Alpaca 배치 제한**\n\n"
|
|
|
|
|
@ -252,30 +252,118 @@ async def get_alpaca_price_data(
|
|
|
|
|
)
|
|
|
|
|
async def get_alpaca_intraday_multi(
|
|
|
|
|
tickers: str = Query(..., description="Comma-separated tickers, e.g. AAPL,MSFT,BF-B"),
|
|
|
|
|
interval: str = Query("5m", description="Interval: 1m, 5m, 15m, 30m, 1h (or 5min, 15min, etc.)"),
|
|
|
|
|
start_date: Optional[date] = Query(None, description="Start date (YYYY-MM-DD)"),
|
|
|
|
|
end_date: Optional[date] = Query(None, description="End date (YYYY-MM-DD)"),
|
|
|
|
|
interval: str = Query("5m", description="Interval: 1m, 5m, 15m, 30m, 1h"),
|
|
|
|
|
start_date: Optional[date] = Query(None, description="Start date (YYYY-MM-DD). Default: yesterday"),
|
|
|
|
|
end_date: Optional[date] = Query(None, description="End date (YYYY-MM-DD). Must be before today. Default: yesterday"),
|
|
|
|
|
force_refresh: bool = Query(False, description="Re-fetch from Alpaca even if DB has data"),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Multi-ticker intraday bars via Alpaca with DB storage (ORB engine interface)."""
|
|
|
|
|
"""Multi-ticker historical intraday bars via Alpaca SIP (up to yesterday)."""
|
|
|
|
|
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.")
|
|
|
|
|
|
|
|
|
|
yesterday = date.today() - timedelta(days=1)
|
|
|
|
|
_start = start_date or yesterday
|
|
|
|
|
_end = end_date or yesterday
|
|
|
|
|
|
|
|
|
|
if _end >= date.today():
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400,
|
|
|
|
|
detail="이 엔드포인트는 어제(yesterday)까지의 과거 데이터만 조회 가능합니다. "
|
|
|
|
|
"당일 실시간 데이터는 GET /api/v1/alpaca/intraday/today 를 사용하세요.",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
svc = _require_alpaca()
|
|
|
|
|
|
|
|
|
|
# Default date range: today
|
|
|
|
|
_start = start_date or date.today()
|
|
|
|
|
_end = end_date or date.today()
|
|
|
|
|
start_dt = datetime.combine(_start, datetime.min.time()).replace(tzinfo=timezone.utc)
|
|
|
|
|
end_dt = datetime.combine(_end, datetime.max.time()).replace(tzinfo=timezone.utc)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
data = await svc.get_or_fetch_multi_bars(
|
|
|
|
|
db, symbols, start_dt, end_dt, interval, force_refresh
|
|
|
|
|
db, symbols, start_dt, end_dt, interval, force_refresh, feed="sip"
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
err = str(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 = {
|
|
|
|
|
ticker: [
|
|
|
|
|
{
|
|
|
|
|
"timestamp": row.date.isoformat(),
|
|
|
|
|
"open": row.open,
|
|
|
|
|
"high": row.high,
|
|
|
|
|
"low": row.low,
|
|
|
|
|
"close": row.close,
|
|
|
|
|
"volume": row.volume,
|
|
|
|
|
}
|
|
|
|
|
for row in rows
|
|
|
|
|
]
|
|
|
|
|
for ticker, rows in data.items()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return AlpacaMultiBarsResponse(
|
|
|
|
|
interval=interval,
|
|
|
|
|
count=len(symbols),
|
|
|
|
|
bars=bars,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
"/intraday/today",
|
|
|
|
|
response_model=AlpacaMultiBarsResponse,
|
|
|
|
|
summary="Get today's real-time intraday bars via Alpaca IEX (DB-backed)",
|
|
|
|
|
description=(
|
|
|
|
|
"당일(오늘) 실시간 분봉 데이터를 Alpaca **IEX 피드**로 가져옵니다. "
|
|
|
|
|
"DB에 저장되며, 장 중 재요청 시 항상 Alpaca에서 최신 데이터를 가져옵니다.\n\n"
|
|
|
|
|
"**IEX 피드 특성**\n\n"
|
|
|
|
|
"| 항목 | 내용 |\n"
|
|
|
|
|
"|------|------|\n"
|
|
|
|
|
"| 피드 | **IEX 피드** (IEX 거래소 단일) |\n"
|
|
|
|
|
"| 지연 | **실시간** (지연 없음) |\n"
|
|
|
|
|
"| 거래량 커버리지 | 미국 전체 시장의 약 **2~5%** |\n"
|
|
|
|
|
"| 가격 방향성 | **신뢰 가능** (대형주 기준) |\n"
|
|
|
|
|
"| High/Low range | SIP 대비 **좁게** 표시될 수 있음 |\n"
|
|
|
|
|
"| 조회 가능 범위 | **오늘만** (어제 이전 데이터는 `/intraday` 사용) |\n\n"
|
|
|
|
|
"**권장 용도**: 당일 ORB 전략, 실시간 장 중 모니터링\n\n"
|
|
|
|
|
"- `tickers`: comma-separated list, e.g. `AAPL,MSFT,BF-B`\n"
|
|
|
|
|
"- `interval`: `1m`, `5m`, `15m`, `30m`, `1h`\n"
|
|
|
|
|
"- 과거 분봉 히스토리가 필요하면 → `GET /api/v1/alpaca/intraday` 사용\n"
|
|
|
|
|
"- Requires `ALPACA_API_KEY` / `ALPACA_SECRET_KEY`.\n\n"
|
|
|
|
|
"**⚠️ Alpaca 배치 제한**: 내부적으로 100개 단위 자동 분할 처리."
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
async def get_alpaca_intraday_today(
|
|
|
|
|
tickers: str = Query(..., description="Comma-separated tickers, e.g. AAPL,MSFT,BF-B"),
|
|
|
|
|
interval: str = Query("5m", description="Interval: 1m, 5m, 15m, 30m, 1h"),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
):
|
|
|
|
|
"""Today's real-time intraday bars via Alpaca IEX (always re-fetches latest)."""
|
|
|
|
|
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.")
|
|
|
|
|
|
|
|
|
|
svc = _require_alpaca()
|
|
|
|
|
|
|
|
|
|
today = date.today()
|
|
|
|
|
start_dt = datetime.combine(today, datetime.min.time()).replace(tzinfo=timezone.utc)
|
|
|
|
|
end_dt = datetime.combine(today, datetime.max.time()).replace(tzinfo=timezone.utc)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
data = await svc.get_or_fetch_multi_bars(
|
|
|
|
|
db, symbols, start_dt, end_dt, interval, force_refresh=True, feed="iex"
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
err = str(e)
|
|
|
|
|
|