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.

756 lines
32 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 | ✅ | **2018-08-01 ~ 현재 (1,829일, 22,722심볼)** | 2016년~ | 백필 완료 (2020-04~10 갭 제외) |
| `/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 보유**: 2018-08-01 ~ 2026-05-29, **1,829 거래일, 22,722 심볼**
- FINRA CDN(무료, API 키 없음): `cdn.finra.org/equity/regsho/daily/CNMSshvol{YYYYMMDD}.txt`
- 롤링 ~7년 보유 (2018-08 이전 403)
- **2020-04-01 ~ 2020-10-31 결손** (~138 평일 — COVID 갭): `backfill_finra_2020_gap.py`로 메울 수 있음
#### PIT(Point-in-Time) 유니버스 멤버십
`finra_short_volume`에 등장한 22,722 심볼 중 **16,143개는 현 활성 유니버스(9,635)에 없는 상폐/합병 과거 종목**. DB의 `pit_universe_membership` 뷰로 노출.
```sql
-- 특정 날짜에 실제 거래되던 종목 (PIT 유니버스, 생존편향 0)
SELECT DISTINCT symbol FROM pit_universe_membership WHERE d = '2023-03-09';
-- SIVB, SBNY 등 그날 마지막으로 거래된 종목 포함됨
-- 공매도비율 횡단면 (상폐 종목 포함)
SELECT p.d, p.symbol, p.short_ratio, a.close
FROM pit_universe_membership p
LEFT JOIN alpaca_price_data a
ON a.ticker = p.symbol AND a.date::date = p.d AND a.interval = '1d'
WHERE p.d = '2023-03-09' ORDER BY p.short_ratio;
```
**⚠️ 한계**:
- **티커 재활용**: BBBY(2023-05 파산 → 2년 공백 → 2025-08 다른 엔티티)처럼 동일 티커가 재사용될 수 있음. 심볼 기준 PIT에서 경계 날짜 부근 ±2주 윈도우 제외 권장.
- **NMS 슈퍼셋**: ETF, ADR, 워런트(/U, /WS), 우선주 포함. 분석단에서 필터.
- 엄밀한 티커 재활용 해결 = CUSIP/PERMNO 매핑 (유료 데이터, 현재 범위 외).
#### API 조회 파라미터
- `days`: 최근 N일 (기본 30, **최대 3650 ≈ 10년**)
- `limit`: 반환 최대 건수 (기본 100, **최대 10000**)
- **주의**: `limit=100` 기본값은 "100 거래일"이 아니라 "100행" 제한. 전체 히스토리 조회 시 반드시 지정:
```bash
# SIVB 전체 히스토리 (상폐 전까지)
curl "http://localhost:18001/api/v1/finra/short-volume/SIVB?days=3650&limit=10000"
# 정규화된 일봉 공매도비율 (시장 통합)
curl "http://localhost:18001/api/v1/finra/short-ratio/AAPL?days=3650"
```
#### 2020 갭 메우기
```bash
docker exec stock_oracle_api python scripts/backfill_finra_2020_gap.py
# 예상 소요: ~10-20분, idempotent (이미 있는 날짜 자동 스킵)
```
> FINRA CDN은 API 키 없이 사용 가능. 1년치 백필 시 ~3000 HTTP 요청 + DB write, 20-40분 소요.
---
### `/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
---
## 백필 우선순위 권장 사항
| 우선순위 | 대상 | 이유 | 예상 소요 시간 |
|---|---|---|---|
| 🔴 높음 | **상폐 가격 백필** (PIT 생존편향-0) | 숏볼륨 신호 검정 수익 생존편향 제거 필수 | 1-3시간 |
| 🔴 높음 | **FINRA 2020 갭** (2020-04~10) | COVID 약세장/회복 레짐 없으면 멀티레짐 검정 불가 | 10-20 |
| 🔴 높음 | Universe 스냅샷 빌드 | 백테스팅 유니버스 기능 사용 필수 1 실행 | 30-60 (4000 종목 × 10년) |
| 🟢 낮음 | 추가 FINRA 구간 | 이미 2018-08 ~ 현재 수집 완료 | |
### 생존편향-0 데이터셋 구축 (권장 실행 순서)
```bash
# Step 1: FINRA 2020 갭 메우기 (10-20분, idempotent)
docker exec stock_oracle_api python scripts/backfill_finra_2020_gap.py
# Step 2: 상폐 가격 백필 — PIT 생존편향-0 핵심 (1-3시간, idempotent)
# FINRA 22,722 심볼 전체에 대해 Alpaca SIP 일봉 수집 (무료 플랜 포함)
# adjustment='all' (분할+배당 조정), 2018-08-01부터
docker exec stock_oracle_api python scripts/backfill_alpaca_daily_pit.py
# Step 3 (옵션): 리서치 레이어용 parquet export
# 외부 환경에서 직접 DB 연결 (port 15433) 후 pandas로 export:
# python -c "
# import pandas as pd
# from sqlalchemy import create_engine
# eng = create_engine('postgresql+psycopg2://stockoracle:stockoracle2024@localhost:15433/stock_oracle')
# df = pd.read_sql('SELECT p.d, p.symbol, p.short_ratio, a.close FROM pit_universe_membership p LEFT JOIN alpaca_price_data a ON a.ticker=p.symbol AND a.date::date=p.d AND a.interval=\'1d\' WHERE p.d BETWEEN \'2018-08-01\' AND NOW()', eng)
# df.to_parquet('pit_panel.parquet', index=False)
# print(df.shape)
# "
```
### PIT 뷰 (DB 직접 쿼리 시)
```sql
-- 날짜별 PIT 유니버스 (상폐 종목 포함)
SELECT DISTINCT symbol FROM pit_universe_membership WHERE d = '2023-03-09';
-- 공매도비율 × 가격 패널 (생존편향-0, backfill_alpaca_daily_pit.py 실행 후)
SELECT p.d, p.symbol, p.short_ratio, a.close
FROM pit_universe_membership p
LEFT JOIN alpaca_price_data a
ON a.ticker = p.symbol AND a.date::date = p.d AND a.interval = '1d'
WHERE p.d BETWEEN '2022-01-01' AND '2023-12-31'
ORDER BY p.d, p.short_ratio;
```
---
## 현재 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;
```