|
|
|
@ -14,10 +14,12 @@ _ET = ZoneInfo("America/New_York")
|
|
|
|
_MARKET_CLOSE_HOUR = 16 # 4:00 PM ET
|
|
|
|
_MARKET_CLOSE_HOUR = 16 # 4:00 PM ET
|
|
|
|
|
|
|
|
|
|
|
|
# Limit concurrent Alpaca intraday processing to prevent event-loop saturation
|
|
|
|
# Limit concurrent Alpaca intraday processing to prevent event-loop saturation
|
|
|
|
# under bulk backfill workloads. Callers beyond this limit wait on the semaphore
|
|
|
|
# under bulk backfill workloads.
|
|
|
|
# (cheap asyncio wait) rather than flooding httpx connections and DB sessions.
|
|
|
|
|
|
|
|
# Semaphore(5): BaseHTTPMiddleware removed → task count halved → safe to allow 5.
|
|
|
|
|
|
|
|
_INTRADAY_SEMAPHORE = asyncio.Semaphore(5)
|
|
|
|
_INTRADAY_SEMAPHORE = asyncio.Semaphore(5)
|
|
|
|
|
|
|
|
# Max seconds a request waits for a semaphore slot before returning 429.
|
|
|
|
|
|
|
|
# Prevents hundreds of requests piling up (each holding a connection slot)
|
|
|
|
|
|
|
|
# during A-Z full-ticker scans.
|
|
|
|
|
|
|
|
_SEMAPHORE_WAIT_TIMEOUT = 10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _market_closed_for(d: date) -> bool:
|
|
|
|
def _market_closed_for(d: date) -> bool:
|
|
|
|
@ -129,12 +131,21 @@ async def get_alpaca_intraday_multi(
|
|
|
|
end_dt = datetime.combine(_end, datetime.max.time()).replace(tzinfo=timezone.utc)
|
|
|
|
end_dt = datetime.combine(_end, datetime.max.time()).replace(tzinfo=timezone.utc)
|
|
|
|
|
|
|
|
|
|
|
|
async def _do_fetch():
|
|
|
|
async def _do_fetch():
|
|
|
|
async with _INTRADAY_SEMAPHORE:
|
|
|
|
try:
|
|
|
|
|
|
|
|
await asyncio.wait_for(
|
|
|
|
|
|
|
|
_INTRADAY_SEMAPHORE.acquire(), timeout=_SEMAPHORE_WAIT_TIMEOUT
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
|
|
status_code=429,
|
|
|
|
|
|
|
|
detail="서버가 바빠서 요청을 처리할 수 없습니다. 잠시 후 다시 시도하세요.",
|
|
|
|
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
return await svc.get_or_fetch_multi_bars(
|
|
|
|
return await svc.get_or_fetch_multi_bars(
|
|
|
|
symbols, start_dt, end_dt, interval, force_refresh, feed="sip"
|
|
|
|
symbols, start_dt, end_dt, interval, force_refresh, feed="sip"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
finally:
|
|
|
|
finally:
|
|
|
|
|
|
|
|
_INTRADAY_SEMAPHORE.release()
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
await svc.client.close()
|
|
|
|
await svc.client.close()
|
|
|
|
except Exception:
|
|
|
|
except Exception:
|
|
|
|
@ -219,12 +230,21 @@ async def get_alpaca_intraday_today(
|
|
|
|
end_dt = datetime.combine(today, datetime.max.time()).replace(tzinfo=timezone.utc)
|
|
|
|
end_dt = datetime.combine(today, datetime.max.time()).replace(tzinfo=timezone.utc)
|
|
|
|
|
|
|
|
|
|
|
|
async def _do_fetch():
|
|
|
|
async def _do_fetch():
|
|
|
|
async with _INTRADAY_SEMAPHORE:
|
|
|
|
try:
|
|
|
|
|
|
|
|
await asyncio.wait_for(
|
|
|
|
|
|
|
|
_INTRADAY_SEMAPHORE.acquire(), timeout=_SEMAPHORE_WAIT_TIMEOUT
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
|
|
status_code=429,
|
|
|
|
|
|
|
|
detail="서버가 바빠서 요청을 처리할 수 없습니다. 잠시 후 다시 시도하세요.",
|
|
|
|
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
return await svc.get_or_fetch_multi_bars(
|
|
|
|
return await svc.get_or_fetch_multi_bars(
|
|
|
|
symbols, start_dt, end_dt, interval, force_refresh=True, feed="iex"
|
|
|
|
symbols, start_dt, end_dt, interval, force_refresh=True, feed="iex"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
finally:
|
|
|
|
finally:
|
|
|
|
|
|
|
|
_INTRADAY_SEMAPHORE.release()
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
await svc.client.close()
|
|
|
|
await svc.client.close()
|
|
|
|
except Exception:
|
|
|
|
except Exception:
|
|
|
|
|