You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

713 lines
30 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Data Coverage & Backfill Guide
각 API 엔드포인트의 **실제 DB 보유 데이터 범위**와 **과거 데이터 백필 방법**을 정리한 문서입니다.
> 마지막 업데이트: 2026-04-26
> DB 실측 기준
---
## 요약 테이블
| 엔드포인트 | 데이터 소스 | DB 저장 | 현재 보유 범위 | 이론적 최대 범위 | 백필 필요 |
|---|---|---|---|---|---|
| `/price` | Yahoo Finance | ✅ | 2018-04-24 ~ 현재 (1682 티커) | 20년+ | 요청 기반 자동 누적 |
| `/alpaca/intraday` (SIP, 과거) | Alpaca SIP | ✅ | 요청 기반 자동 누적 | 2016년~어제 | 요청 기반 자동 누적 |
| `/alpaca/intraday/today` (IEX, 당일) | Alpaca IEX | ✅ | 당일만 | 오늘 장 중 | 해당 없음 |
| `/alpaca/snapshot` | Alpaca IEX | ❌ | 실시간만 | 없음 | 해당 없음 |
| `/finra/short-volume` | FINRA CDN | ✅ | **2026-02-10 ~ 현재 (28거래일)** | 수년치 | **⚠️ 백필 권장** |
| `/etf/holdings` | SEC EDGAR (NPORT) | ✅ (스냅샷) | 요청 기반 자동 누적 | 2019년~ | 요청 기반 자동 누적 |
| `/filings/search` | SEC EDGAR | ✅ | 1994-01-05 ~ 현재 (1598 티커) | 1994년~ | 요청 기반 자동 누적 |
| `/stocks/most-active` | Yahoo 실시간 스크래핑 | ❌ | 실시간만 | 없음 | 해당 없음 |
| `/stocks/52-week-gainers` | Yahoo 실시간 스크래핑 | ❌ | 실시간만 | 없음 | 해당 없음 |
| `/stocks/trending` | Yahoo 실시간 스크래핑 | ❌ | 실시간만 | 없음 | 해당 없음 |
| `/overlay/{symbol}` | Yahoo/YouTube/Wikipedia/FINRA | ✅ | 2026-03-17 ~ 현재 (50 심볼) | 서비스 시작 이후 | 과거 백필 불가 |
| `/overlay/{symbol}/headlines` | Yahoo Finance RSS | ✅ | 2026-02-24 ~ 현재 | 서비스 시작 이후 | 과거 백필 불가 |
| `/overlay/{symbol}/wiki` | Wikipedia Pageviews API | ✅ | 2015-12-26 ~ 현재 | 2015년~ | 자동 수집됨 |
| `/insider/transactions` | SEC EDGAR Form 4 | ✅ | 요청 기반 자동 누적 | 2004년~ | 요청 기반 자동 누적 |
| `/insider/form4/{ticker}` | SEC EDGAR Form 4 | ✅ | **최근 2년 (8개 quarter) 사전 bootstrap** | 2024년 Q3 ~ 현재, 676,858건 / 4,814 티커 | `bootstrap_form4_by_ticker.py` 완료 |
| `/insider/form4/by-date/{date}` | SEC EDGAR Form 4 | ✅ | 동상 | 동상 | 동상 |
| `/insider/form4/aggregate/{ticker}` | SEC EDGAR Form 4 | ✅ | 동상 | 동상 | 동상 |
| `/ownership/13dg/{ticker}` | SEC EDGAR SC 13D/G | ✅ | **최근 2년 (8개 quarter) 사전 bootstrap** | 2024년 Q3 ~ 현재, 42,329행 | `bootstrap_13dg.py` 완료 |
| `/ownership/13dg/active` | SEC EDGAR SC 13D/G | ✅ | 동상 | 동상 | 동상 |
| `/earnings/surprise` | yfinance-plus earnings_dates | ✅ | 요청 기반 자동 누적 | ~25분기 (6년+) | 요청 기반 자동 누적 |
| `/universe/screen` | SEC EDGAR + yfinance 월별 스냅샷 | ✅ (사전 빌드 필요) | admin 빌드 후 사용 가능 | 2010년~ | **⚠️ 사전 빌드 필요** |
| `/company/{ticker}` | yfinance-plus + universe_ticker_registry | ✅ (Redis 24h + DB 영구) | 모든 yfinance 지원 티커 | 즉시 | 요청 기반 자동 누적 |
| `/company/bulk` | yfinance-plus + universe_ticker_registry | ✅ (Redis 24h + DB 영구) | 최대 100 티커/요청 | 즉시 | 요청 기반 자동 누적 |
| `/news/v2/headlines` | Alpaca News (Benzinga) + StockTwits + Finnhub | ✅ (`news_headline`) | **ingest 시작 시점 이후만** (vendor 한계) | Alpaca: ingest 시작 누적 / Finnhub: 12개월 / StockTwits: 적재 시작 이후 | **⚠️ `NEWS_INGEST_ENABLED=true` opt-in 필요** |
| `/news/v2/session_aggregate*` | `news_headline` 즉시 SQL 집계 | ✅ (raw 기반) | headlines와 동일 | 동상 | 동상 |
| `/news/v2/coverage` | `news_headline` MIN/MAX/COUNT | — | — | — | 운영 도구 |
---
## 엔드포인트별 상세
---
### `/api/v1/price` — 주가 (Yahoo Finance)
**현재 DB 보유**: 2018-04-24 ~ 현재, 1682 티커, 약 190만 행
**조회 파라미터**:
- `period`: `1d` `7d` `30d` `1m` `3m` `6m` `1y` `2y` `5y` `max`
- `start_date` + `end_date`: 특정 날짜 범위 (YYYY-MM-DD)
- `quarters`: `["2024Q1", "2024Q2"]` 형식
**동작 방식**: DB 캐시 우선 → 누락 구간만 Yahoo Finance에서 실시간 페치 → 자동 저장
**백필**: 별도 작업 불필요. 처음 조회 시 자동으로 인제스트됨.
```bash
# 특정 티커 과거 데이터 미리 채우기 (선택 사항)
curl -X POST "http://localhost:18001/api/v1/price/data" \
-H "Content-Type: application/json" \
-d '{"ticker": "AAPL", "start_date": "2020-01-01", "end_date": "2024-12-31"}'
```
---
### `/api/v1/alpaca` — 분봉 (Alpaca API)
**엔드포인트 구성**:
| 엔드포인트 | 피드 | 용도 | DB 저장 |
|---|---|---|---|
| `GET /alpaca/intraday` | **SIP** | 과거 분봉 (2016년~어제) | ✅ |
| `GET /alpaca/intraday/today` | **IEX** | 당일 실시간 분봉 | ✅ |
| `GET /alpaca/snapshot` | **IEX** | 현재가 스냅샷 | ❌ |
| `GET /alpaca/status` | — | API 키 유효성 확인 | ❌ |
**피드 차이**:
- **SIP**: 전체 미국 거래소 통합 데이터. 거래량 100% 정확. 과거 데이터(어제까지) 무료 접근.
- **IEX**: IEX 거래소 단일. 실시간이지만 거래량은 실제의 2~5%. 당일 데이터 전용.
**필수 조건**: `ALPACA_API_KEY`, `ALPACA_SECRET_KEY` 환경 변수 설정 필요.
**이론적 범위**: 2016년~ (Alpaca 무료 플랜 기준)
```bash
# 과거 5분봉 조회 (SIP, DB 저장)
curl "http://localhost:18001/api/v1/alpaca/intraday?tickers=AAPL,MSFT&interval=5m&start_date=2025-01-01&end_date=2025-01-31"
# 당일 실시간 분봉 (IEX)
curl "http://localhost:18001/api/v1/alpaca/intraday/today?tickers=AAPL,MSFT&interval=5m"
# 현재가 스냅샷
curl "http://localhost:18001/api/v1/alpaca/snapshot?tickers=AAPL,MSFT,SPY"
```
---
### `/api/v1/finra` — FINRA 공매도 (RegSHO)
**현재 DB 보유**: 2026-02-10 ~ 2026-03-20, 28거래일 (서비스 가동 시점부터)
> 데이터 소스인 FINRA CDN은 수년치 과거 파일을 보유하고 있으나, 현재 DB에는 최근 28일치만 있음.
**조회 파라미터**:
- `days`: 최근 N일 (기본 30, 최대 365)
- `limit`: 반환 최대 건수 (기본 100, 최대 1000)
**⚠️ 백필 방법**:
```bash
# 단일 날짜 백필
POST /api/v1/finra/admin/ingest?date=2025-01-02&force=false
# 날짜 범위 백필 (권장)
POST /api/v1/finra/admin/ingest?start_date=2024-01-01&end_date=2026-02-09&force=false
```
curl 예시:
```bash
# 2024년 전체 백필 (~252 거래일 × ~11,000 심볼 = ~280만 행)
curl -X POST "http://localhost:18001/api/v1/finra/admin/ingest?start_date=2024-01-01&end_date=2024-12-31"
# 2025년 전체 백필
curl -X POST "http://localhost:18001/api/v1/finra/admin/ingest?start_date=2025-01-01&end_date=2025-12-31"
# 운영 공백 구간 채우기 (2026-01-01 ~ 2026-02-09)
curl -X POST "http://localhost:18001/api/v1/finra/admin/ingest?start_date=2026-01-01&end_date=2026-02-09"
```
> 주의: 1년치 백필 시 ~3000 HTTP 요청 + DB write. 수십 분 소요될 수 있음. FINRA CDN은 API 키 없이 사용 가능하나 과부하를 피하기 위해 범위를 분할해서 실행 권장.
---
### `/api/v1/etf` — ETF 보유 종목 (SEC EDGAR)
**현재 DB 보유**: 요청된 ETF 스냅샷만 (요청 기반 자동 누적)
**이론적 범위**: 2019년~ (NPORT-P 도입 이후). 일부 ETF는 더 이전 N-Q 파일링 존재.
**조회 파라미터**:
- `as_of_date`: 기준 날짜 (YYYY-MM-DD). 가장 가까운 파일링 자동 선택.
- 생략 시: 가장 최신 파일링 반환.
**동작 방식**: 첫 조회 시 SEC EDGAR에서 자동 페치 → 스냅샷 DB 저장. 재조회 시 캐시.
```bash
# 특정 날짜 기준 ETF 보유 종목 조회 (자동 캐시)
curl "http://localhost:18001/api/v1/etf/holdings/QQQ?as_of_date=2023-12-31"
curl "http://localhost:18001/api/v1/etf/holdings/SPY?as_of_date=2022-06-30"
```
> ETF 출시 이전 날짜 요청 시 `availability` 필드에 가능한 날짜 범위 반환됨.
---
### `/api/v1/filings` — SEC 공시 (EDGAR)
**현재 DB 보유**: 1994-01-05 ~ 현재, 1598 티커, 약 8000일치
**지원 양식**: `8-K`, `6-K`, `20-F`, `40-F`
**조회 파라미터**:
- `form_type`: 쉼표 구분 (예: `8-K,6-K`)
- `start_date` + `end_date`: 공시 날짜 범위
- `limit` / `offset`: 페이지네이션 (최대 100)
**동작 방식**: 첫 조회 시 SEC EDGAR 자동 인덱싱 → DB 저장. 이후 DB 캐시. 1시간 Redis 캐시.
**백필**: 별도 작업 불필요. `GET /filings/search/{ticker}` 최초 호출 시 자동 인덱싱됨.
```bash
# 특정 티커 전체 8-K 조회 (자동 인덱싱)
curl "http://localhost:18001/api/v1/filings/search/AAPL?form_type=8-K&start_date=2020-01-01"
# 여러 티커 일괄 인덱싱
curl -X POST "http://localhost:18001/api/v1/filings/search/bulk" \
-H "Content-Type: application/json" \
-d '{"tickers": ["AAPL","MSFT","NVDA","TSLA"], "form_type": "8-K", "limit_per_ticker": 100}'
```
---
### `/api/v1/stocks` — 시장 현황 (Yahoo Finance 실시간)
**현재 DB 보유**: **없음**. 실시간 스크래핑 전용.
| 엔드포인트 | 데이터 | 캐시 TTL |
|---|---|---|
| `/stocks/most-active` | 실시간 상위 ~170 종목 | 1시간 |
| `/stocks/52-week-gainers` | 실시간 상위 ~1350 종목 | 1시간 |
| `/stocks/trending` | most-active + gainers 결합 | 30분 |
> 과거 데이터 조회 불가. 시계열 추적이 필요하면 주기적으로 `/price` 엔드포인트를 통해 개별 종목 가격을 저장하는 별도 배치 작업 필요.
---
### `/api/v1/overlay` — Attention Overlay
**현재 DB 보유**:
| 데이터 | 보유 범위 |
|---|---|
| Overlay score (feature records) | 2026-03-17 ~ 현재, 50 심볼 |
| 뉴스 헤드라인 | 2026-02-24 ~ 현재 |
| Wikipedia 페이지뷰 | 2015-12-26 ~ 현재 (풍부) |
| YouTube 멘션 | 서비스 시작 이후 |
| Google Trends | 서비스 시작 이후 |
**조회 파라미터**:
- `/{symbol}`: 최신 composite score
- `/{symbol}/history?days=N`: 스코어 시계열 (최대 365일)
- `/{symbol}/headlines?hours=N`: 뉴스 (최대 168시간)
- `/{symbol}/wiki?days=N`: Wikipedia 페이지뷰 (최대 90일)
- `/{symbol}/crowding`: FINRA 기반 crowding 지표
**지원 심볼**: 기본 50개 (`TOP_50_SYMBOLS`). 그 외 심볼은 파이프라인 트리거 필요.
**과거 데이터 백필**: Overlay score는 실시간 수집 기반으로 **과거 소급 생성 불가**.
Wikipedia 페이지뷰 (`/wiki`)는 2015년부터 조회 가능.
```bash
# 파이프라인 수동 트리거 (신규 심볼 추가 시)
POST /api/v1/overlay/admin/trigger-pipeline
# 특정 심볼의 헬스 상태 확인
GET /api/v1/overlay/admin/health
```
---
### `/api/v1/insider` — 내부자 거래 (SEC Form 4)
**현재 DB 보유**:
- **기존 `/transactions`, `/summary`**: 요청 기반 자동 누적 (첫 조회 시 자동 인덱싱)
- **신규 `/form4/*`**: 최근 2년 사전 bootstrap 완료 — 676,858건 / 4,814 티커 / 2024 Q3 ~ 2026-04-23
**자동 갱신**: 매 영업일 09:00 ET — 직전 영업일 daily full-index → Form 4 upsert (scheduler)
**⚠️ PIT 주의**: 신규 `/form4/*` 엔드포인트는 `as_of` 파라미터 **필수**. 누락 시 422.
---
#### 기존 엔드포인트 (Lazy on-demand)
```bash
# 최근 90일 내부자 거래 조회 (자동 인덱싱)
curl "http://localhost:18001/api/v1/insider/transactions/NVDA?days=90"
# 내부자 매매 요약 (순매수/매도 금액)
curl "http://localhost:18001/api/v1/insider/summary/AAPL?period=90d"
```
**지원 거래 유형**: `P-Purchase`, `S-Sale`, `A-Award`, `D-Return`, `F-TaxWithholding`, `G-Gift`, `M-OptionExercise`
---
#### 신규 PIT-safe 엔드포인트 (2026-04-23)
**`GET /insider/form4/{ticker}`** — 티커별 Form 4 거래 목록
| 파라미터 | 필수 | 설명 |
|---|---|---|
| `as_of` | ✅ | 기준 날짜 (YYYY-MM-DD). `filing_date <= as_of` 필터. |
| `start` | - | 시작 날짜 (filing_date 기준) |
| `end` | - | 종료 날짜 (filing_date 기준) |
| `buy_only` | - | `true` → 매수(`P`,`A`) 거래만 |
| `csuite_only` | - | `true` → CEO·CFO·COO·CTO 등 C-suite만 |
반환 필드: `symbol`, `filing_date`, `transaction_date`, `owner_cik`, `owner_name`, `owner_relationship`, `is_officer`, `is_director`, `is_ten_percent_owner`, `is_ceo`, `is_cfo`, `is_c_suite`, `shares`, `price`, `total_value`, `shares_owned_following`, `purchase_pct_of_holding`, `transaction_code`, `accession_number`
```bash
# TSLA CEO·CFO 매수 거래 조회
curl "http://localhost:18001/api/v1/insider/form4/TSLA?as_of=2026-04-20&start=2026-01-01&csuite_only=true&buy_only=true"
# NVDA 최근 30일 내부자 거래 전체
curl "http://localhost:18001/api/v1/insider/form4/NVDA?as_of=2026-04-20&start=2026-03-20"
```
**`GET /insider/form4/by-date/{date}`** — 특정 공시일 전체 거래 (cross-ticker)
| 파라미터 | 필수 | 설명 |
|---|---|---|
| `buy_only` | - | 매수 거래만 반환 |
```bash
# 2026-04-17 공시 전체 매수 거래
curl "http://localhost:18001/api/v1/insider/form4/by-date/2026-04-17?buy_only=true"
```
**`GET /insider/form4/aggregate/{ticker}`** — 집계 요약 (PIT-safe)
| 파라미터 | 필수 | 설명 |
|---|---|---|
| `as_of` | ✅ | 기준 날짜 |
| `window_days` | - | 집계 윈도우 (기본 30일). `filing_date ∈ (as_of - window_days, as_of]` |
반환 필드: `buy_count`, `buy_dollar_total`, `cluster_size`, `csuite_count`, `avg_pct_of_holding`, `recency_days`
```bash
# AAPL 최근 30일 내부자 매수 집계
curl "http://localhost:18001/api/v1/insider/form4/aggregate/AAPL?as_of=2026-04-20&window_days=30"
```
---
#### C-suite 판별 기준
`officer_title` 에 대해 case-insensitive 정규식 적용:
- `is_ceo`: `CEO`, `Chief Executive Officer`
- `is_cfo`: `CFO`, `Chief Financial Officer`, `Principal Financial Officer`
- `is_c_suite`: 위 둘 + `COO`, `CTO`, `CIO`, `CLO`, `CMO`, `President`, `Chairman/person/woman`, `Chief * Officer`
---
---
### `/api/v1/ownership` — Activist Ownership (SEC SC 13D/G) (신규, 2026-04-23)
**현재 DB 보유**: 42,329행, 최근 2년 (2024 Q3 ~ 2026-04-23) 사전 bootstrap 완료
**자동 갱신**:
- 매 영업일 09:00 ET — daily full-index → SC 13D/G index-only upsert
- 30분 주기 background enrich — `parse_status='index_only'` 200행씩 cover-page XML/HTML 파싱 → `ownership_pct`, `shares_owned` 보강
**parse_status 의미**:
- `index_only`: EDGAR 인덱스에서 가져온 기본 메타만 있음. `ownership_pct=NULL`
- `parsed`: cover-page XML/HTML 파싱 완료. `ownership_pct` 채워짐
- `parse_failed`: 파싱 시도했으나 문서 구조 불명확
**⚠️ PIT 주의**: `as_of` 파라미터 **필수**. 누락 시 422. 모든 날짜 필터는 `filing_date` 기준.
---
**`GET /ownership/13dg/{ticker}`** — 티커별 activist 이벤트 목록
| 파라미터 | 필수 | 설명 |
|---|---|---|
| `as_of` | ✅ | 기준 날짜. `filing_date <= as_of` 필터. |
| `start` | - | 시작 날짜 (filing_date 기준) |
| `end` | - | 종료 날짜 (filing_date 기준) |
반환 필드: `symbol`, `filing_date`, `filer_name`, `filer_cik`, `form_type`, `ownership_pct`, `shares_owned`, `is_amendment`, `change_pct`, `accession_number`, `parse_status`
`form_type` 값: `SC 13D`, `SC 13G`, `SC 13D/A`, `SC 13G/A` (또는 `SCHEDULE 13D/G` 등 변형 포함)
```bash
# AAPL activist filing 전체 (as_of 기준 이전)
curl "http://localhost:18001/api/v1/ownership/13dg/AAPL?as_of=2026-04-23"
# RLGT 2025년 이후 activist 이벤트
curl "http://localhost:18001/api/v1/ownership/13dg/RLGT?as_of=2026-04-23&start=2025-01-01"
```
---
**`GET /ownership/13dg/active`** — 현재 활성 activist 포지션 목록
| 파라미터 | 필수 | 설명 |
|---|---|---|
| `as_of` | ✅ | 기준 날짜 |
| `min_ownership_pct` | - | 최소 지분율 (기본 5.0) |
**쿼리 로직**: `(filer_cik, issuer_cik)` 쌍별 최신 filing (`filing_date DESC`) 한 행씩, `ownership_pct >= min_ownership_pct` 필터.
```bash
# 현재 5% 이상 activist 포지션 전체 (파싱된 행만)
curl "http://localhost:18001/api/v1/ownership/13dg/active?as_of=2026-04-23&min_ownership_pct=5.0"
# 10% 이상 대형 activist
curl "http://localhost:18001/api/v1/ownership/13dg/active?as_of=2026-04-23&min_ownership_pct=10.0"
```
---
#### Bootstrap (1회성, 이미 완료)
```bash
# 최근 8개 quarter SC 13D/G index-only 수집
docker exec stock_oracle_api python scripts/bootstrap_13dg.py --quarters 8
# Form 4 bootstrap (최근 2년)
docker exec stock_oracle_api python scripts/bootstrap_form4_by_ticker.py
```
---
### `/api/v1/earnings` — 어닝 서프라이즈 (yfinance-plus)
**현재 DB 보유**: 요청 기반 자동 누적 (첫 조회 시 자동 인덱싱)
**이론적 범위**: 약 25분기 (6년+). yfinance `earnings_dates` 데이터 기준.
**조회 파라미터**:
- `GET /earnings/surprise/{symbol}?limit=20`: 분기별 EPS surprise 이력
**반환 필드**: `reported_eps`, `estimated_eps`, `surprise`, `surprise_percentage`, `streak`
- `surprise` = reported_eps estimated_eps
- `surprise_percentage` = (surprise / estimated) × 100
- `streak`: 연속 beat(+) 또는 miss() 횟수
**동작 방식**: 첫 조회 시 yfinance `earnings_dates` 자동 인제스트 → DB 저장. 이후 1시간 Redis 캐시.
```bash
# 어닝 서프라이즈 이력 조회 (자동 인덱싱)
curl "http://localhost:18001/api/v1/earnings/surprise/AAPL?limit=20"
```
---
### `/api/v1/universe` — 과거 주식 유니버스 (백테스팅)
**현재 DB 보유**: admin 엔드포인트로 사전 빌드 필요 (초기에는 빈 상태)
**이론적 범위**: 2010년~ (SEC EDGAR 데이터 + yfinance 가격 데이터 가용 범위)
**데이터 소스**: SEC EDGAR `companyfacts` (shares_outstanding) × yfinance 월별 종가 → 월별 시총 계산
> **Survivorship bias 주의**: 현재 상장된 종목만 포함. 상폐 종목 미포함.
> **정확도**: 시총 오차 ±10~20% (buyback 반영 지연, SEC 분기별 업데이트 때문).
#### 워크플로우
```
1단계: 유니버스 등록 (1~5분)
POST /universe/admin/discover?market_cap_min=100000000
→ yfinance screener로 ~3000~5000 종목 발견
→ universe_ticker_registry 테이블에 저장
2단계: 스냅샷 빌드 (30~60분, 백그라운드)
POST /universe/admin/build-snapshots
→ SEC EDGAR shares_outstanding × yfinance 월별 종가 = 월별 시총
→ universe_snapshot 테이블에 ~480K 행 저장 (4000 × 120개월)
3단계: 과거 스크리닝
GET /universe/screen?date=2018-01-01&market_cap_min=2e9&market_cap_max=20e9
→ 2018년 초 기준 시총 $2B~$20B 종목 목록 반환
```
#### 엔드포인트 상세
**`GET /universe/screen`** — 과거 시점 기준 종목 스크리닝
| 파라미터 | 필수 | 설명 |
|---|---|---|
| `date` | ✅ | 기준 날짜 YYYY-MM-DD (월초로 자동 반올림) |
| `market_cap_min` | - | 최소 시총 (USD), 예: `2e9` = $2B |
| `market_cap_max` | - | 최대 시총 (USD), 예: `20e9` = $20B |
| `sector` | - | 섹터 필터 (예: `Technology`, `Healthcare`) |
| `exchange` | - | 거래소 필터 (`NYSE`, `NASDAQ`, `AMEX`) |
| `page` / `page_size` | - | 페이지네이션 (기본 100, 최대 500) |
| `sort_by` | - | 정렬 기준 (`market_cap` 또는 `ticker`) |
```bash
# 2018년 초 시총 $2B~$20B 종목 (Small/Mid Cap)
curl "http://localhost:18001/api/v1/universe/screen?date=2018-01-01&market_cap_min=2000000000&market_cap_max=20000000000"
# 2020년 기준 Technology 섹터 Large Cap ($10B+)
curl "http://localhost:18001/api/v1/universe/screen?date=2020-01-01&market_cap_min=10000000000&sector=Technology"
# 2023년 기준 Top 100 시총 순위
curl "http://localhost:18001/api/v1/universe/screen?date=2023-01-01&sort_by=market_cap&sort_ascending=false&page_size=100"
```
**`GET /universe/registry`** — 등록된 종목 목록 조회
```bash
# 등록된 전체 종목 조회
curl "http://localhost:18001/api/v1/universe/registry?page_size=200"
# NASDAQ 기술주 필터
curl "http://localhost:18001/api/v1/universe/registry?exchange=NASDAQ&sector=Technology"
```
**`POST /universe/admin/discover`** — 종목 발견 및 등록
```bash
# $100M 이상 ~3000~5000 종목 등록 (1~5분 소요)
curl -X POST "http://localhost:18001/api/v1/universe/admin/discover?market_cap_min=100000000"
# 소규모 테스트 ($1T 이상, ~50 종목)
curl -X POST "http://localhost:18001/api/v1/universe/admin/discover?market_cap_min=1000000000000"
```
**`POST /universe/admin/build-snapshots`** — 월별 시총 스냅샷 빌드
```bash
# 소규모 테스트 (3 종목 × 2년, 즉시 반환)
curl -X POST "http://localhost:18001/api/v1/universe/admin/build-snapshots" \
-H "Content-Type: application/json" \
-d '{"tickers":["AAPL","MSFT","NVDA"],"start_date":"2023-01-01","end_date":"2024-12-01"}'
# 전체 유니버스 × 10년 빌드 (백그라운드, 30~60분)
curl -X POST "http://localhost:18001/api/v1/universe/admin/build-snapshots" \
-H "Content-Type: application/json" \
-d '{"start_date":"2015-01-01","end_date":"2025-12-01","force_rebuild":false}'
```
> `tickers` 생략 시 registry 전체 대상. 20개 이하면 동기 실행(즉시 결과), 21개 이상이면 백그라운드 실행.
#### 초기 세팅 권장 순서
```bash
# 1. 유니버스 등록 ($100M+ → ~4000 종목)
curl -X POST "http://localhost:18001/api/v1/universe/admin/discover?market_cap_min=100000000"
# 2. 전체 스냅샷 빌드 (백그라운드 시작)
curl -X POST "http://localhost:18001/api/v1/universe/admin/build-snapshots" \
-H "Content-Type: application/json" \
-d '{"start_date":"2015-01-01","end_date":"2025-12-01"}'
# 3. 빌드 완료 후 스크리닝 테스트
curl "http://localhost:18001/api/v1/universe/screen?date=2020-01-01&market_cap_min=10000000000"
```
---
### `/api/v1/company` — 종목 메타데이터 (신규, 2026-04-20)
**현재 DB 보유**: 조회 시 자동 저장. 두 번째 조회부터 DB 히트 (Redis 24h → DB 영구).
**데이터 소스**: yfinance-plus `.info``universe_ticker_registry` + `companies` 테이블 UPSERT
**반환 필드**: `ticker`, `name`, `cik`, `exchange`, `sector`, `industry`, `country`, `market_cap`, `business_description`
| 엔드포인트 | 메서드 | 설명 |
|---|---|---|
| `/company/{ticker}` | `GET` | 단일 종목 메타데이터. 미지원 티커 → 404 |
| `/company/bulk` | `POST` | 최대 100개 일괄 조회. 부분 실패 허용 (per-ticker `error` 필드) |
**캐시 전략**:
- Redis 24h TTL (key: `company:meta:{TICKER}`)
- yfinance 실패 시 15분 TTL (재시도 빈도 조절)
- DB에 영구 저장 (sector 있는 경우 fast-path: DB만 조회, yfinance 미호출)
**조회 예시**:
```bash
# 단일 종목
curl "http://localhost:18001/api/v1/company/AU"
# → {"ticker":"AU","sector":"Basic Materials","industry":"Gold","exchange":"NYSE","country":"United States",...}
# 유효하지 않은 티커 → 404
curl "http://localhost:18001/api/v1/company/ZZZZZZ"
# → {"detail":"Unknown ticker: ZZZZZZ"}
# 최대 100개 일괄 조회
curl -X POST "http://localhost:18001/api/v1/company/bulk" \
-H "Content-Type: application/json" \
-d '{"tickers":["AU","USAS","CPRX","HE","ACHR"]}'
# → {"results":[...],"total":5,"success_count":5,"error_count":0}
```
**성능 특성**:
- 콜드 캐시 (첫 조회): yfinance 호출 ~2-5초 / 티커. 동시 최대 5개 (semaphore)
- 웜 캐시 (재조회): Redis <10ms / DB <50ms
- bulk 100 콜드 캐시: ~30-50 (semaphore=5로 직렬화)
**sector 대량 사전 보강** (선택):
```bash
# universe_ticker_registry의 NULL sector 전체 보강 (~30분, 9376 티커)
python scripts/backfill_registry_sector.py --batch 50
# Dry-run
python scripts/backfill_registry_sector.py --dry-run
```
**`/financial/data/{ticker}` 변경사항** (2026-04-20):
- `company` 블록에 `exchange`, `country`, `market_cap` 필드 추가
- placeholder sector(`Technology/Software/XXX Corporation`) 감지 자동 재보강
- financials/price 조회 실패 시에도 `company` 블록은 200 OK 유지 (이전: 500 가능)
---
### `/api/v1/news/v2` — 멀티소스 헤드라인 + 세션 집계 (신규, 2026-04-26)
**현재 DB 보유**: `news_headline` 테이블. ingest 시작 누적. DB 상태에서도 엔드포인트는 200 + 결과 반환.
**데이터 소스** (모두 `NEWS_INGEST_ENABLED=true` 활성화):
| Source | History | Rate limit | Sentiment |
|---|---|---|---|
| `alpaca_benzinga` | ~30 vendor cap ingest 시작 누적 | 200 req/min | 없음 (free tier) |
| `stocktwits` | rolling | 200 req/hr/IP | Bullish/Bearish ±1 |
| `finnhub` | ~12개월 vendor cap | 60 calls/min free | 없음 |
**카테고리 정규화**: vendor 라벨 22-term 통합 vocab (`analyst_rating_*`, `earnings_release`, `m_and_a`, `fda_approval/rejection`, `buyback`, `litigation`, ... 등). 원본은 `vendor_categories` 보존.
**세션 윈도우** (NYSE / `pandas_market_calendars` XNYS 휴장일·short day 처리):
- `premarket` = 전일 close 당일 09:30 ET
- `intraday` = 09:30 16:00 ET
- `post` = 16:00 다음 거래일 04:00 ET (다음 premarket 중복 없음)
- `full_session` = 전일 close 다음 거래일 04:00 ET
**PIT 안전성**: 집계 `ingested_at <= window_end_utc` 필터 적용 backtest lookahead 데이터를 봄.
| 엔드포인트 | 메서드 | 설명 |
|---|---|---|
| `/news/v2/headlines` | `GET` | raw 헤드라인 (symbols, start, end, sources, limit, cursor 필터) |
| `/news/v2/session_aggregate` | `GET` | 단일 ticker × session × window 집계 (Redis 캐시) |
| `/news/v2/session_aggregate/batch` | `POST` | 다수 ticker 일괄 (V49 핫패스, 캐싱 없음 클라이언트 disk-cache 가정) |
| `/news/v2/coverage` | `GET` | source × symbol 적재 깊이 probe |
**Ingest 활성화 절차**:
```bash
# 1. .env에 키 설정 (Alpaca는 기존 키 재사용, Finnhub은 신규)
echo "FINNHUB_API_KEY=<your-key>" >> .env
echo "NEWS_INGEST_ENABLED=true" >> .env
# 2. 컨테이너 재시작
docker restart stock_oracle_api
# 3. 로그에서 시작 메시지 확인
docker logs stock_oracle_api 2>&1 | grep "\[News\]"
# → "[News] ingest scheduler started — sources: ['alpaca_benzinga', 'finnhub', 'stocktwits']"
# 4. 5분 후 첫 Alpaca News poll 결과 확인
curl "http://localhost:18001/api/v1/news/v2/coverage?source=alpaca_benzinga"
```
**Fail-fast**: `NEWS_INGEST_ENABLED=true`인데 `ALPACA_API_KEY/SECRET` & `FINNHUB_API_KEY` 모두 없으면 scheduler 시작 거부 (StockTwits만으로는 신호 부족).
**Finnhub 12개월 백필** (수동 1회):
```bash
docker exec stock_oracle_api python scripts/news_backfill.py \
--source finnhub \
--tickers AAPL,MSFT,NVDA,TSLA,GOOGL \
--start 2025-04-26 --end 2026-04-26 \
--chunk monthly
# → 100 ticker × 12 month ≈ 1,200 calls @ 60 cpm ≈ 20분
```
**StockTwits 동적 universe** (자동, 평일 09:00 ET):
- `(최근 14일 V49 universe 합집합) (당일 09:00 ET premarket gap > threshold movers)`
- 기본 cap 300 ticker, 5 polling, 200 req/hr 안정
- Config: `STOCKTWITS_UNIVERSE_LOOKBACK_DAYS` (14), `STOCKTWITS_PREMARKET_GAP_THRESHOLD` (0.02), `STOCKTWITS_UNIVERSE_MAX_SIZE` (300)
- Redis key: `news_v2:stocktwits:universe`
**제외**:
- WebSocket 실시간 push (P2)
- vendor 통합 sentiment NLP (현재는 vendor passthrough)
- GDELT raw feed 노출 (기존 attention 서브시스템 활용)
- Reddit/Twitter
---
## 백필 우선순위 권장 사항
| 우선순위 | 대상 | 이유 | 예상 소요 시간 |
|---|---|---|---|
| 🔴 높음 | FINRA 1년치 (2025년) | z-score 계산 윈도우(30일)가 너무 짧아 신호 품질 저하 | 20-40 |
| 🔴 높음 | Universe 스냅샷 빌드 | 백테스팅 유니버스 기능 사용 필수 1 실행 | 30-60 (4000 종목 × 10년) |
| 🟡 중간 | FINRA 2년치 (2024년) | 추세 분석 가능 | 1-2시간 |
| 🟢 낮음 | Alpaca 데이터 | Yahoo Finance 중복, API 필요 | 필요시 |
### FINRA 권장 백필 스크립트
```bash
# 1단계: 2025년 (가장 중요)
curl -X POST "http://localhost:18001/api/v1/finra/admin/ingest?start_date=2025-01-02&end_date=2025-12-31"
# 2단계: 2026년 공백 구간
curl -X POST "http://localhost:18001/api/v1/finra/admin/ingest?start_date=2026-01-02&end_date=2026-02-09"
# 3단계 (선택): 2024년
curl -X POST "http://localhost:18001/api/v1/finra/admin/ingest?start_date=2024-01-02&end_date=2024-12-31"
```
---
## 현재 DB 상태 확인 쿼리
```sql
-- 각 테이블 데이터 범위 확인
SELECT 'price_data' AS tbl, MIN(date)::date, MAX(date)::date, COUNT(DISTINCT date::date) AS days, COUNT(DISTINCT ticker) AS tickers FROM price_data
UNION ALL
SELECT 'finra_short_volume', MIN(date)::date, MAX(date)::date, COUNT(DISTINCT date::date), NULL FROM finra_short_volume
UNION ALL
SELECT 'sec_filings', MIN(filing_date)::date, MAX(filing_date)::date, COUNT(DISTINCT filing_date::date), COUNT(DISTINCT ticker) FROM sec_filings
UNION ALL
SELECT 'overlay_feature_records', MIN(as_of_ts)::date, MAX(as_of_ts)::date, COUNT(DISTINCT as_of_ts::date), COUNT(DISTINCT symbol) FROM overlay_feature_records
UNION ALL
SELECT 'insider_transactions', MIN(transaction_date)::date, MAX(transaction_date)::date, COUNT(DISTINCT transaction_date::date), COUNT(DISTINCT ticker) FROM insider_transactions
UNION ALL
SELECT 'earnings_surprise', MIN(earnings_date)::date, MAX(earnings_date)::date, COUNT(DISTINCT earnings_date::date), COUNT(DISTINCT ticker) FROM earnings_surprise
ORDER BY tbl;
-- Form 4 PIT 데이터 상태
SELECT MIN(filing_date)::date, MAX(filing_date)::date, COUNT(*) AS txns, COUNT(DISTINCT ticker) AS tickers
FROM insider_transactions;
-- SC 13D/G activist 데이터 상태
SELECT parse_status, COUNT(*),
COUNT(*) FILTER (WHERE ownership_pct IS NOT NULL) AS has_pct,
MIN(filing_date)::date, MAX(filing_date)::date
FROM activist_ownership_events GROUP BY parse_status ORDER BY parse_status;
-- Universe 스냅샷 상태 확인
SELECT
COUNT(DISTINCT ticker) AS tickers,
COUNT(*) AS snapshots,
MIN(snapshot_date)::date AS earliest,
MAX(snapshot_date)::date AS latest
FROM universe_snapshot;
-- Universe 등록 종목 수
SELECT COUNT(*) AS registered, COUNT(*) FILTER (WHERE is_active) AS active
FROM universe_ticker_registry;
```