|
|
|
|
@ -413,36 +413,47 @@ async def get_alpaca_intraday_today(
|
|
|
|
|
@router.get(
|
|
|
|
|
"/intraday/{ticker}",
|
|
|
|
|
response_model=AlpacaIntradayResponse,
|
|
|
|
|
summary="Get intraday candles from Alpaca (single ticker, IEX feed)",
|
|
|
|
|
summary="Get historical intraday candles from Alpaca (single ticker, SIP feed)",
|
|
|
|
|
description=(
|
|
|
|
|
"단일 종목의 분봉 데이터를 Alpaca **IEX 피드**로 직접 가져옵니다.\n\n"
|
|
|
|
|
"**데이터 소스: IEX 피드**\n\n"
|
|
|
|
|
"단일 종목의 과거 분봉 데이터를 Alpaca **SIP 피드**로 가져옵니다.\n\n"
|
|
|
|
|
"**데이터 소스: SIP 피드**\n\n"
|
|
|
|
|
"| 항목 | 내용 |\n"
|
|
|
|
|
"|------|------|\n"
|
|
|
|
|
"| 피드 | **IEX** (IEX 거래소 단일) |\n"
|
|
|
|
|
"| 지연 | **실시간** (지연 없음) |\n"
|
|
|
|
|
"| 거래량 커버리지 | 미국 전체 시장의 약 **2~5%** |\n"
|
|
|
|
|
"| 피드 | **SIP** (전체 미국 거래소 통합) |\n"
|
|
|
|
|
"| 거래량 커버리지 | **100%** |\n"
|
|
|
|
|
"| OHLCV 정확도 | **정확** |\n"
|
|
|
|
|
"| 조회 가능 범위 | **2016년~어제** (당일 조회 시 400 에러) |\n"
|
|
|
|
|
"| DB 저장 | **없음** — 매 요청 Alpaca 직접 호출 |\n"
|
|
|
|
|
"| 캐시 | Redis **5분 TTL** |\n\n"
|
|
|
|
|
"멀티 종목이 필요하면 → `GET /api/v1/alpaca/intraday/today` (오늘) 또는 "
|
|
|
|
|
"`GET /api/v1/alpaca/intraday` (과거, SIP)\n\n"
|
|
|
|
|
"| 캐시 | Redis **24시간 TTL** |\n\n"
|
|
|
|
|
"당일 실시간 데이터가 필요하면 → `GET /api/v1/alpaca/intraday/today`\n\n"
|
|
|
|
|
"- Requires `ALPACA_API_KEY` / `ALPACA_SECRET_KEY`"
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
@with_cache(namespace="alpaca:intraday", ttl=300, key_params=["ticker", "interval", "start_date", "end_date"])
|
|
|
|
|
@with_cache(namespace="alpaca:intraday", ttl=86400, key_params=["ticker", "interval", "start_date", "end_date"])
|
|
|
|
|
async def get_alpaca_intraday(
|
|
|
|
|
ticker: str,
|
|
|
|
|
response: Response,
|
|
|
|
|
interval: str = Query("1m", description="Interval: 1m, 5m, 15m, 1h"),
|
|
|
|
|
start_date: Optional[date] = Query(None, description="Start date"),
|
|
|
|
|
end_date: Optional[date] = Query(None, description="End date"),
|
|
|
|
|
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"),
|
|
|
|
|
limit: int = Query(1000, ge=1, le=10000, description="Max candles"),
|
|
|
|
|
force_refresh: bool = Query(False, description="Bypass cache"),
|
|
|
|
|
):
|
|
|
|
|
yesterday = date.today() - timedelta(days=1)
|
|
|
|
|
_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()
|
|
|
|
|
|
|
|
|
|
start_dt = datetime.combine(start_date, datetime.min.time()).replace(tzinfo=timezone.utc) if start_date else None
|
|
|
|
|
end_dt = datetime.combine(end_date, datetime.min.time()).replace(tzinfo=timezone.utc) if end_date else None
|
|
|
|
|
_start = start_date or yesterday
|
|
|
|
|
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:
|
|
|
|
|
bars = await svc.fetch_bars_raw(
|
|
|
|
|
@ -450,6 +461,7 @@ async def get_alpaca_intraday(
|
|
|
|
|
interval=interval,
|
|
|
|
|
start_date=start_dt,
|
|
|
|
|
end_date=end_dt,
|
|
|
|
|
feed="sip",
|
|
|
|
|
)
|
|
|
|
|
bars = bars[:limit]
|
|
|
|
|
body_dict = {
|
|
|
|
|
|