동시 요청 시 둘 다 빈 existing_accs 읽고 중복 INSERT → UniqueViolationError 발생.
pg_insert().on_conflict_do_nothing()으로 원자적 upsert 교체,
staleness 재인덱스를 background task로 이동해 요청 차단 제거.
auto-parse는 최근 20개로 제한.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
날짜만 보내면(2026-04-15) Alpaca가 ET 자정(23:59 ET = 03:59 UTC)으로 해석해
당일 장 마감 후에도 "미래 end" 로 판정 → SIP 403 에러 발생.
RFC-3339 datetime 형식(2026-04-15T23:59:59Z)으로 변경하면
Alpaca가 UTC 기준으로 정확히 해석해 15분 규칙을 통과함.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
반복적인 서버 응답불능의 근본 원인 2가지를 해결:
1. BaseHTTPMiddleware → Pure ASGI 미들웨어 (app/middleware/error_logger.py)
- BaseHTTPMiddleware.call_next()가 요청당 asyncio task 2개 생성
- 20 동시연결 × 2 = 40 tasks → event loop scheduler 포화 → health check 타임아웃
- __call__(scope, receive, send) + send_wrapper 패턴으로 교체
- 요청당 단일 task, X-Request-ID 헤더 주입, 에러 응답 body 캡처 유지
- 불필요한 의존성 제거: BaseHTTPMiddleware, Request, Callable, get_db, AsyncSession
2. gc.collect() 추가 + --limit-max-requests 제거 (alpaca.py, docker-compose.yml)
- --limit-max-requests 500: 500요청 후 단일 worker 종료 → 서비스 gap 발생
- 대신 intraday 요청 처리 후 gc.collect()로 Python heap 명시적 회수
- 장시간 백필 중 메모리 누적 방지, worker 재시작 없이 안정 운영
3. Semaphore(3 → 5): BaseHTTPMiddleware 제거로 task 수 절반 → 처리량 복원
유지: mem_limit 2g, --limit-concurrency 20, Phase 4 경량 쿼리, request_logs 7일 retention
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
원인: request_logs에 cleanup 정책이 없어 65M행이 쌓였다가 DELETE되면서
VACUUM FULL 없이 18GB dead page bloat 발생.
PostgreSQL 메모리/디스크 압박 → API worker OOM kill의 간접 원인.
수정:
- _request_log_cleanup() 백그라운드 코루틴 추가
→ 1시간마다 실행, 7일 초과 request_logs 자동 DELETE
→ start_request_log_flusher()에서 함께 스케줄
- request_logs autovacuum scale_factor 0.2 → 0.01
→ 1% 변경 시 autovacuum 즉시 실행, dead tuple 빠르게 회수
즉각 조치: TRUNCATE request_logs 실행 (25GB → 56kB)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BaseHTTPMiddleware는 요청당 2개 asyncio task를 생성한다.
--limit-concurrency 100은 여전히 200개 task가 event loop를 포화시켜
health check 포함 모든 요청이 block됐다.
변경 사항:
- docker-compose.yml: --limit-concurrency 100 → 20
(세마포어 10개 처리 + 10개 대기, 나머지 20초과 시 503으로 즉시 반환)
- alpaca_price_service.py: upsert 루프 청크 사이 + Phase 4 ORM 일괄 생성 후
await asyncio.sleep(0) 추가 — 동기 CPU 블로킹 중 event loop yield 보장
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
동시 Alpaca intraday 요청이 50개 이상 쌓이면 BaseHTTPMiddleware의
task 스케줄링 오버헤드로 이벤트 루프가 응답불능 상태가 되는 현상 수정.
- alpaca.py: _INTRADAY_SEMAPHORE(10) 추가 — /intraday, /intraday/today
양쪽 핸들러를 감쌈. 11번째 이후 요청은 세마포어 대기(비용 없음)
- docker-compose.yml: --limit-concurrency 100 추가 — 100개 초과 시
uvicorn이 503 반환, health 엔드포인트 보호
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GET /price/data를 AlpacaPriceService 대신 PriceDataService(yfinance)로 교체
- ?ticker= alias 추가 (기존 ?tickers= 유지, 인터페이스 호환)
- _bulk_fetch_price_data: yfinance end 파라미터 exclusive 미반영 (+1일 누락) 수정
- get_multi_ticker_daily_bars: end_dt를 min.time(00:00) → max.time(23:59:59)으로 수정
(DB 쿼리 date <= end_dt 에서 당일 레코드가 필터링되던 버그)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
서비스 메서드가 db: AsyncSession을 인자로 받아 외부 API 호출(yfinance 30-60s,
Alpaca HTTP) 중에도 DB 커넥션을 잡고 있던 구조를 제거.
변경 패턴 (Session-per-phase):
Before: Endpoint(db) → Service(db) → DB check → API call(30s 세션 유지) → DB store
After: Endpoint() → Service() → DB check(세션1) → API call(세션 없음) → DB store(세션2)
변경 파일:
- alpaca_price_service.py: fetch_and_store_bars, get_or_fetch_multi_bars에서 db 제거
- price_data_service.py: get_or_update_price_data, get_multiple_tickers_data_optimized에서
db 제거; _fetch_price_data(새), _bulk_fetch_price_data(새, lambda 클로저 버그 수정);
_fetch_and_store_price_data, _bulk_fetch_and_store_price_data, get_multiple_tickers_data 제거
- alpaca.py, price.py: Depends(get_db) 제거 (GET /latest 제외)
- financial_service.py, real_sec_financial_service.py: 호출 인자 정리
결과: "idle in transaction" 커넥션 0개, 풀 고갈 원인 근본 해결
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
외부 API 호출(yfinance, Alpaca) 중 DB 세션을 유지하는 구조로 인해
idle in transaction 커넥션이 쌓여 풀(50개) 고갈 → 서버 응답 불가.
- pool_timeout: 30s → 5s (풀 고갈 시 빠른 실패, 30s 행 방지)
- PostgreSQL idle_in_transaction_session_timeout: 90s 설정
(90초 이상 방치된 트랜잭션 자동 종료)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
기존: end_date >= today 이면 무조건 400
변경: 장 마감(오후 4시 ET) 후에는 당일 날짜도 SIP로 조회 가능
- zoneinfo.ZoneInfo("America/New_York") 기반 ET 시각 체크
- 장 중 당일 요청 시 /intraday/today 안내 메시지
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- interval 누락으로 uq_alpaca_price_data constraint 위반 방지
- 단일 INSERT → CHUNK=2300 분할 (14 params/row, asyncpg 한도 대비)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
제거: /bars/{ticker}, /data/{ticker}, /intraday/{ticker}, /snapshot/{ticker}
유지:
- GET /alpaca/status
- GET /alpaca/intraday (SIP, 과거, 멀티 종목)
- GET /alpaca/intraday/today (IEX, 당일, 멀티 종목)
- GET /alpaca/snapshot (IEX, 단일/멀티 통합)
openapi.json 업데이트 (95 → 91 endpoints)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_check_missing_periods는 수정됐지만 bulk 경로에서 사용하는
_batch_check_missing_periods에는 동일한 today 체크가 빠져 있었음.
POST /price/data/bulk로 당일 데이터 요청 시 DB에 mid-session으로
캐시된 partial-volume 데이터를 그대로 반환하는 버그.
end_date.date() >= today이면 모든 티커를 missing으로 처리하여
_check_missing_periods와 동일한 동작 보장.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both GET /price/data and GET /alpaca/intraday:
- description에 배치 제한 설명 추가 (100개/요청, 자동 분할, 응답시간 선형 증가)
- 502 에러 메시지에 심볼 수 초과 가능성 힌트 추가
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Community reports 502 above ~100 symbols per request. Official docs have
no stated limit but 200 appears to be a ceiling. 100 is a safe conservative
default that avoids the 502 edge case with minimal performance impact
(500 tickers = 5 batches instead of 3).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GET /price/intraday (Yahoo Finance):
- 15분 지연, 1m=7일/5m-30m=60일/1h=730일 한계
- 백테스트 용도, 실시간 전략 부적합
GET /alpaca/intraday (Alpaca IEX):
- 실시간, IEX 피드 2~5% 커버리지
- 거래량 낮게 표시, 가격 레벨은 유사
- ORB 등 당일 실시간 전략 용도
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Free plan blocks SIP intraday data (403). IEX feed is free and real-time.
- _default_feed(): returns 'iex' for intraday timeframes, None for daily+
- get_bars() / get_multi_bars(): apply default feed automatically
- Callers can override with feed='sip' if on paid plan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Alpaca free plan blocks recent SIP data (403 on same-day requests).
Replace with Yahoo Finance which has no subscription requirement.
- PriceDataService.get_multi_intraday(): yf.download() in chunks of 50,
handles both single (flat DataFrame) and multi-ticker (MultiIndex) cases
- GET /price/intraday?tickers=...&interval=5m&start_date=...&end_date=...
→ same AlpacaMultiBarsResponse format (bars: {sym → [{timestamp,ohlcv}]})
→ source="YAHOO_FINANCE", Redis 5-min TTL cache
- /alpaca/intraday still exists for historical data (works on free plan)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same get_or_fetch_multi_bars() approach as daily bars:
- stores intraday rows in AlpacaPriceData (full timestamp as PK component)
- subsequent requests for historical periods served from DB
- same-day requests during market hours always hit Alpaca (max_date < end_dt)
- force_refresh=true bypasses DB check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AlpacaPriceService.get_or_fetch_multi_bars(): checks DB max_date per
ticker, only fetches missing ranges from Alpaca, upserts with chunking
(3000 rows/chunk, asyncpg 32767-param limit) then reads back from DB
- GET /price/data endpoint: now uses service + Depends(get_db); subsequent
calls for same date range skip Alpaca entirely
- force_refresh=true bypasses DB check and re-fetches all from Alpaca
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- POST /filings/events/parse/{accession_number}: 특정 filing 강제 재파싱
(parsed_status 무관하게 pending으로 리셋 후 즉시 파싱)
- POST /filings/events/parse/bulk에 force_reparse: bool 추가
(true이면 succeeded/failed도 pending으로 리셋 후 재처리)
- BulkParseRequest에 force_reparse 필드 추가
사용법:
curl -X POST /api/v1/filings/events/parse/0001193125-26-144028
curl -X POST /api/v1/filings/events/parse/bulk -d '{"tickers":["AVGO"],"force_reparse":true}'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dividend calendar:
- DividendCalendar 모델 (PIT revisioned: as_of_date 컬럼으로 lookahead bias 방지)
- FINRA 5yr 데이터 backfill + yfinance 갱신 지원
- GET /dividends/calendar/{ticker}, POST /dividends/calendar/bulk
- alembic migration: f7a8b9c0d1e2
earnings:
- GET /earnings/calendar/{ticker}: ex-dividend 방식 earnings calendar 제공
- EarningsSurprise 모델에 fiscal_date 인덱스 추가
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fetch_json()에 skip_cache 파라미터 추가 (in-memory/disk 캐시 bypass)
- index_filings()에 force_refresh → skip_cache 자동 연동
- search_filings()에 staleness check (MAX indexed_at vs SEC_DATA_REFRESH_HOURS)
→ 24h 이상 stale한 ticker 자동 re-index (per-ticker asyncio.Lock으로 thundering herd 방지)
- search_filings_bulk()도 동일 staleness 처리
- 테스트 7개 추가 (skip_cache 동작, force_refresh 연동)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
429/rate limit 및 401/unauthorized 발생 시 최대 3회 재시도 (delay 3s, 6s).
모든 retry 소진 후 RuntimeError로 변환하여 503 응답.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes to universe snapshot build:
1. SEC EDGAR exchange case mismatch: company_tickers_exchange.json uses
"Nasdaq" (mixed case) but filter expected "NASDAQ". All NASDAQ-listed
stocks (AAPL, MSFT, GOOGL, etc.) were silently excluded from registry.
Fixed with case-insensitive _US_EXCHANGE_MAP lookup + canonical normalization.
2. OOM during large builds: SEC EDGAR companyfacts JSONs accumulate in
_json_cache without eviction, causing OOM after ~1500-1800 tickers.
Fixed by clearing _json_cache + _text_cache + gc.collect() every 20
batches. Memory remains stable throughout full 9,376-ticker build.
Result: 529,328 snapshot rows, 5,570 tickers (NASDAQ:2515, NYSE:2084, OTC:971)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
asyncpg 커넥션은 event loop에 바인딩됨. API의 AsyncSessionLocal을
다른 loop에서 사용하면 'Future attached to a different loop' 오류 발생.
→ build thread 내에서 전용 engine + session factory 생성.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BackgroundTask가 FastAPI event loop을 공유해서 deadlock 발생 →
별도 thread에서 새 asyncio event loop으로 실행하도록 변경.
- API event loop 완전 분리
- DB 커넥션 풀 독립적 사용 (per-batch factory session)
- 빌드 중 API 정상 응답 유지
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
yf.screen() 401 에러 시 SEC EDGAR company_tickers_exchange.json으로 자동 전환.
- 5,848개 NYSE/NASDAQ/AMEX/ARCA 종목 등록 가능
- response에 source 필드 추가 (yfinance / sec_edgar)
- market_cap_min 필터는 yfinance 사용 시에만 적용 (SEC Edgar fallback 시 미적용, 스크리닝 시점에 필터링)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## 새 기능
- GET /universe/screen — 과거 날짜 기준 시총/섹터/거래소 필터링
- GET /universe/registry — 추적 종목 목록 조회
- POST /universe/admin/discover — yfinance screener로 US 주식 자동 등록
- POST /universe/admin/build-snapshots — SEC EDGAR × yfinance 월별 시총 스냅샷 생성
## 데이터 모델
- universe_ticker_registry: 종목 마스터 (ticker, name, cik, sector, industry, exchange)
- universe_snapshot: 월별 스냅샷 (ticker, snapshot_date, market_cap, close_price, shares_outstanding)
- 인덱스: (snapshot_date, market_cap) — 핵심 스크리닝 쿼리 최적화
- ~4000종목 × 120개월 ≈ 480K 행 예상
## 데이터 흐름
1. SEC EDGAR companyfacts → shares_outstanding (최신, 주가분할 반영)
2. yfinance bulk download 1mo interval → 월별 종가
3. market_cap = latest_shares × close_price (yfinance 분할조정 가격과 일관성)
## 제한사항
- Survivorship bias: 현재 상장 종목만 (상폐 종목 미포함)
- 자사주 매입으로 과거 시총 ~20% 오차 가능 (분할 오차 방지가 주목적)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>