88 Commits (253086eaad719ef99927c7fdb677b5c5c1ef4767)
 

Author SHA1 Message Date
I Luk Kim 253086eaad fix: Python heap 누적 OOM 방지 — max-requests 500 + 컨테이너 메모리 2GB 제한
Python CPython 메모리 할당자는 freed 블록을 OS에 반환하지 않음.
수백 건의 백필 요청 처리 후 heap이 계속 성장 → 시스템 전체 OOM.

변경:
- --limit-max-requests 500: 500요청 후 uvicorn worker 자동 재시작
  → heap 초기화, 메모리 누수 방지
- mem_limit/memswap_limit 2g: 컨테이너 메모리 2GB 하드 캡
  → OS 전체가 아닌 컨테이너 안에서 OOM 제어
  → Docker restart: unless-stopped로 자동 복구

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 7e1a4699e9 fix: request_logs 18GB 블로트 방지 — 7일 retention + autovacuum 강화
원인: 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>
4 months ago
I Luk Kim abfab0fa57 fix: OOM kill 방지 — Phase 4 경량화 + Semaphore 3으로 제한
Worker가 OOM kill로 죽는 것을 확인 (docker inspect: OOMKilled=true).
호스트 메모리: 물리 RAM ~68MB 여유, 스왑 97% 사용 상태.

원인: Phase 4에서 SELECT AlpacaPriceData (전체 ORM 객체) 로드.
- 75 ticker × 78 5분봉 = 5,850개 ORM 객체/요청
- SQLAlchemy ORM instrumentation으로 dict 대비 ~10x 메모리 소비
- Semaphore(10) → 최대 10개 요청 동시 처리 = 피크 수백 MB ~ 1GB

수정:
- Phase 4: SELECT * → SELECT 7 columns only (ticker, date, OHLCV)
  → result.all()로 경량 Row namedtuple 반환, ORM 객체 생성 없음
  → SQLAlchemy Row는 속성 접근(row.date, row.open 등) 지원 — 엔드포인트 호환
- Semaphore(10) → Semaphore(3): 동시 처리 3개로 제한
  → 피크 메모리 ~70% 감소

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim ffaed4fc9d fix: 서버 응답불능 — limit-concurrency 20 + asyncio yield points
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>
4 months ago
I Luk Kim e25f7295a6 fix: 대규모 Alpaca 백필 시 uvicorn 이벤트 루프 포화 방지
동시 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>
4 months ago
I Luk Kim dd3e38fbf5 fix: GET /price/data — Alpaca → yfinance 교체 + bulk end_date 버그 2건 수정
- 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>
4 months ago
I Luk Kim c203f3920b refactor: session-per-phase — DB 커넥션을 외부 API 호출 중 해제
서비스 메서드가 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>
4 months ago
I Luk Kim 7efe6704b3 fix: DB 커넥션 풀 고갈 — pool_timeout 단축 + idle_in_transaction 자동 종료
외부 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>
4 months ago
I Luk Kim 52e2e4ed7d feat: /alpaca/intraday — 장 마감(16:00 ET) 후 당일 SIP 조회 허용
기존: 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>
4 months ago
I Luk Kim 409966a478 fix: error_logs.path VARCHAR(500) → TEXT — 서버 응답 불가 해결
멀티 티커 요청 URL이 500자를 초과하면서 error_logs INSERT가
StringDataRightTruncationError로 실패, 미들웨어에서 매 에러 응답마다
DB rollback 대기가 발생해 서버가 응답 불가 수준으로 느려졌음 (2562회 반복).

- error_logs.path: String(500) → Text (model + migration j1b2c3d4e5f6)
- request_logs.path: String(500) → Text (model + migration i0a1b2c3d4e5)
- docker-compose: alembic/ + alembic.ini 볼륨 마운트 추가
  (이전에는 컨테이너 내부에만 있어 마이그레이션 적용 불가)
- middleware: 요청마다 찍히던 logger.info → logger.debug 로 변경

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 5bcba8632c fix: fetch_and_store_bars() interval 컬럼 추가 + CHUNK 분할
- interval 누락으로 uq_alpaca_price_data constraint 위반 방지
- 단일 INSERT → CHUNK=2300 분할 (14 params/row, asyncpg 한도 대비)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim e6ea1abb0e fix: asyncpg param 한도 초과 — INSERT CHUNK 2900→2300
interval 컬럼 추가로 row당 파라미터 14개로 증가.
2900 × 14 = 40,600 > 32,767 → InterfaceError 발생.
2300 × 14 = 32,200으로 안전 범위 내 수정.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim af26df8895 feat: AlpacaPriceData에 interval 컬럼 추가 — 일봉/분봉 DB 충돌 해결
- alpaca_price_data 테이블에 interval 컬럼 추가 (VARCHAR, default '1d')
- unique constraint: (ticker, date) → (ticker, date, interval)
- get_or_fetch_multi_bars(): interval 필터로 coverage 체크 및 DB 조회
- insert rows에 interval 포함
- asyncpg param limit 대비 CHUNK 3000→2900 (파라미터 11개/행)
- alembic 마이그레이션: h9b0c1d2e3f4

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 3766c00b97 docs: Alpaca 엔드포인트 정리 반영 — DATA_COVERAGE, PYTHON_CLIENT 업데이트
- 요약 테이블: /bars, /data 제거 → /intraday(SIP), /intraday/today(IEX), /snapshot
- Alpaca 섹션: 피드별 차이(SIP/IEX) 설명 추가, 샘플 코드 전면 교체
- 제거된 /bars/{ticker}, /data/{ticker}, /intraday/{ticker}, /snapshot/{ticker} 참조 삭제

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim f528ac751b refactor: Alpaca 엔드포인트 정리 — 4개로 통합
제거: /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>
4 months ago
I Luk Kim 800bb9b4a6 fix: /alpaca/intraday/{ticker} SIP 피드로 변경 + 어제까지 날짜 제한
- fetch_bars_raw()에 feed 파라미터 추가
- /intraday/{ticker}: IEX → SIP, end_date >= 오늘이면 400 에러
- 캐시 TTL: 5분 → 24시간 (과거 데이터는 변하지 않음)
- openapi.json 업데이트

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 75b3a356d0 docs: Alpaca 엔드포인트 전체 IEX/SIP 피드 정보 명시
- /bars/{ticker}, /data/{ticker}: interval별 자동 피드 선택 표 추가
- /intraday/{ticker}: IEX 피드, 5분 캐시, DB 없음 명시
- /snapshot, /snapshot/{ticker}: IEX 강제 (무료 플랜 제한) 명시
- openapi.json 업데이트 (95 endpoints)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 9882592ac1 docs: openapi.json 업데이트 — /alpaca/intraday/today 엔드포인트 추가 (95 endpoints)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 8e58fdffa5 feat: Alpaca intraday 엔드포인트 분리 — /intraday (SIP, 과거) + /intraday/today (IEX, 당일)
- GET /alpaca/intraday: SIP 피드로 변경, end_date >= 오늘이면 400 에러, 백테스트용
- GET /alpaca/intraday/today: 신규, IEX 피드, 오늘 당일 실시간 전용, force_refresh=True
- AlpacaPriceService.get_or_fetch_multi_bars()에 feed 파라미터 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 73bf6115fa fix: _batch_check_missing_periods — today 포함 시 항상 re-fetch
_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>
4 months ago
I Luk Kim 7db2e28691 docs: Alpaca 100-symbol batch limit — endpoint description + 502 error msg
Both GET /price/data and GET /alpaca/intraday:
- description에 배치 제한 설명 추가 (100개/요청, 자동 분할, 응답시간 선형 증가)
- 502 에러 메시지에 심볼 수 초과 가능성 힌트 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim bb2a06ce6e fix: reduce Alpaca multi-bar batch_size 200 → 100
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>
4 months ago
I Luk Kim 22c1d975d4 docs: add intraday data limitations to endpoint descriptions
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>
4 months ago
I Luk Kim 57eb44c735 docs: update openapi.json (94 endpoints)
New endpoints added:
- GET /api/v1/price/data — multi-ticker daily bars (Alpaca, DB-backed)
- GET /api/v1/price/intraday — multi-ticker intraday (Yahoo Finance, Redis cache)
- GET /api/v1/alpaca/intraday — multi-ticker intraday (Alpaca IEX feed, DB-backed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 38207d342c fix: Alpaca intraday 503 — auto-select feed=iex for free plan
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>
4 months ago
I Luk Kim 5ef4d1790d feat: GET /price/intraday — multi-ticker intraday via Yahoo Finance
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>
4 months ago
I Luk Kim 4edb0a244a fix: multi-ticker intraday — DB storage + DB-first fetch logic
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>
4 months ago
I Luk Kim 2081611884 fix: multi-ticker daily bars — DB storage + DB-first fetch logic
- 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>
4 months ago
I Luk Kim fbb42edfc2 feat: multi-ticker bulk bars endpoints + BF-B ticker normalization
- GET /api/v1/price/data?tickers=AAPL,MSFT,BF-B&start_date=...&end_date=...
  → multi-ticker daily OHLCV via Alpaca (ORB engine daily bars interface)
- GET /api/v1/alpaca/intraday?tickers=...&interval=5min&start_date=...&end_date=...
  → multi-ticker intraday OHLCV via Alpaca (ORB engine ORB-window interface)
- AlpacaMultiBarsResponse schema: {source, interval, count, bars: {sym → [bar]}}
- normalize_ticker(): BF-B→BF.B, BRK-B→BRK.B applied in get_bars/get_multi_bars/get_snapshot(s)
- get_multi_bars: transparent batching (200 symbols/request) + INTERVAL_MAP aliases (5min, 15min, 60min, …)
- Response re-keys Alpaca normalized symbols back to original input names

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim ea36a4287a chore: yfinance_plus 서브모듈 업데이트
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 8df22538a6 docs: openapi.json 업데이트 (alpaca snapshot 엔드포인트 반영)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 846280c3f6 fix: alpaca snapshot 멀티 티커 제한 100 → 1000
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim a9a158d804 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>
4 months ago
I Luk Kim 33b1749721 fix: GET /price/data/{ticker} — start/end alias 지원 + 단일 날짜 조회 허용
- ?start=&end= 쿼리 파라미터를 start_date/end_date의 alias로 수용
- end_date <= start_date 검증을 end_date < start_date 로 완화하여 당일(start==end) OHLCV 조회 가능

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim a496789fbe feat: 8-K 재파싱 지원 — force_reparse + 단일 accession 재처리 endpoint
- 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>
4 months ago
I Luk Kim 25603c390c feat: 8-K filing parser — Item 추출 + event 생성
AVGO 8-K (accession 0001193125-26-144028, Item 8.01, Google TPU 공급계약)이
DB에 수집은 되었으나 파싱/event 생성이 불가했던 문제 해결.

구현:
- sec_8k_parser.py: 8-K primary document HTML 파싱
  - extract_items(): regex 기반 Item 헤더 추출, 목차 중복 제거 (last-wins)
  - _strip_ixbrl_viewer(): documents_json의 /ix?doc=... URL → 직접 URL 변환
  - _find_primary_doc_url(): primary_document_url 우선 사용 (iXBRL viewer 회피)
  - Item 8.01 단독 filing: exhibit(9.01) 없이 본문에서 직접 content 추출
  - Item 9.01 skip, 나머지는 ITEM_EVENT_MAP으로 event_type 분류
  - Exhibit enrichment: 2.02/7.01/8.01 + EX-99.1 있을 때 exhibit content 우선
- sec_filing_events 테이블 신설 (UniqueConstraint: accession_number + item_number)
- sec_filings 테이블에 parsed_status / items_json 컬럼 추가
- index_filings() 완료 후 신규 8-K auto-parse 트리거
- GET /filings/events/{ticker}: lazy parse + 조회
- POST /filings/events/parse/bulk: backfill용 일괄 파싱
- FilingSummary에 parsed_status / items 필드 포함
- alembic migration: g8a9b0c1d2e3
- 테스트 22개 추가 (extract_items, strip_ixbrl, find_primary_doc, parse_filing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim 3eb736445b feat: PIT dividend calendar + earnings surprise calendar endpoint
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>
4 months ago
I Luk Kim 2438c511c7 fix: SEC EDGAR stale 8-K index auto-refresh + HTTP cache bypass
- 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>
4 months ago
I Luk Kim 7a003a5360 fix: screener yf.screen() rate limit/401 retry 처리 추가
429/rate limit 및 401/unauthorized 발생 시 최대 3회 재시도 (delay 3s, 6s).
모든 retry 소진 후 RuntimeError로 변환하여 503 응답.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4 months ago
I Luk Kim fb692fe592 feat: yfinance rate limit 처리 강화
- yfinance_plus: 세션 풀 다양화 (chrome/edge/safari/firefox 핑거프린트 6개)
  + 모듈 싱글톤으로 process-wide rate limiter 공유
  + Adaptive throttling (rate limit 감지 시 0.3s→최대 5s 자동 증가)
  + EnhancedTicker별 전용 세션으로 스레드 race condition 해결
- price.py: rate limit 에러 감지 시 HTTP 500 → HTTP 429 + Retry-After: 30
- test_rate_limit.py: rate limit 발생 조건 측정 스크립트 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
I Luk Kim c4565159b1 fix: universe service - NASDAQ case bug + OOM prevention
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>
5 months ago
I Luk Kim 24736954bb fix: yfinance threads=False + 배치 30개로 OS 스레드 폭발 방지
threads=True일 때 배치당 50개 OS 스레드 생성 → 시스템 자원 고갈 → API 먹통.
threads=False로 순차 다운로드, 배치 30개로 축소.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
I Luk Kim d4008924d9 fix: isolated thread에 독립적인 DB 엔진 생성
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>
5 months ago
I Luk Kim 1b037c9256 fix: full force_rebuild 시 universe_snapshot 전체 삭제
tickers=None (전체 빌드)일 때 registry에서 제거된 stale 티커 데이터도
정리되도록 DELETE FROM universe_snapshot 실행.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
I Luk Kim e51e045169 fix: universe build을 isolated thread로 분리하여 API 먹통 방지
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>
5 months ago
I Luk Kim 0e048f82ef fix: universe build event loop 차단 방지
- _PRICE_BATCH 100 → 50 (배치당 부하 감소)
- 배치 완료 후 asyncio.sleep(1) 추가 (event loop yield)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
I Luk Kim b54ac6d5b7 fix: universe 데이터 품질 개선 — sanity check + preferred stock 필터링
- SEC EDGAR fallback에서 "-" 포함 티커 제외 (preferred stock, BAC-PL 등)
- W/R/Z suffix 티커 제외 (warrant, right, special)
- build_snapshots: close > $1M 또는 shares < 100,000 또는 market_cap > $5T 제외
- screen_historical: market_cap > $5T 쿼리 레벨 필터 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
I Luk Kim 5ab1663977 feat: universe discover에 SEC EDGAR fallback 추가
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>
5 months ago
I Luk Kim d430487882 chore: yfinance_plus 서브모듈 업데이트 + API 문서 업데이트
- yfinance_plus: period=None 방어 코드, 테스트 파일 제거
- docs: insider/earnings/universe 엔드포인트 문서 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 months ago
I Luk Kim b3092a0d5e feat: 백테스팅 유니버스 과거 시점 주식 스크리닝 (Historical Stock Universe)
## 새 기능
- 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>
5 months ago