8-K Ingestion Pipeline
바이오 universe 의 모든 SEC 8-K filing 을 전수 수집 + 구조화 해서 DuckDB 에 저장한다. 이 파이프라인이 두 상위 소비자의 기반이 됨:
- Universe feature 생성 (backtest) — pipeline breadth / momentum / phase upgrade signal
- Catalyst 추출 — catalyst-data-collection 의 Primary source
Background — 왜 8-K 인가
바이오텍 리서치 세션 (bioreport) 에서 관찰:
- 뉴스 → ClinicalTrials.gov 등록까지 6~9개월 lag — Phase 1 승인 시점과 실제 trial 등록 사이 시차
- 이 lag 구간이 alpha — Dan 의 TCRX 투자 근거가 이 패턴 (뉴스는 봤지만 CT.gov 엔 아직 없음)
- SEC 8-K 는 이 lag 을 해소 — material event 는 4 business day 내 공시. ~85% 의 주요 pipeline 뉴스 커버
즉 8-K 를 구조화된 DB 에 넣으면 ClinicalTrials.gov 가 못 잡는 선행 signal 이 확보됨.
Scope
- 기간: 2015-01-01 ~ 현재 (+ daily incremental)
- 대상 기업: biotech universe (아래 SQL 로 고정). 2026-04-23 기준 실측 1,109 distinct symbols.
CREATE OR REPLACE VIEW biotech_universe AS SELECT DISTINCT symbol FROM profile WHERE industry IN ('Biotechnology', 'Drug Manufacturers - Specialty & Generic', 'Drug Manufacturers - General') UNION SELECT DISTINCT symbol FROM universe -- 과거 universe 포함 종목 UNION SELECT symbol FROM excluded_symbols; -- historical 분석용 - Item 범위: 전체 8-K item (제한 없음) — 나중에 분류할 때 필터링
- Priority items: 1.01 (Material Agreement), 2.02 (Earnings), 7.01 (Reg FD), 8.01 (Other Events)
- 예상 volume: biotech universe 1,109 tickers ×
10 filing/year × 11년 ≈ ~100k filings (large cap 은 연 50+, small cap 은 58) - 기존 데이터 상태 (2026-04-23):
sec_filings테이블에 type=‘8-K’ 3,400 + ‘8-K/A’ 55 rows 이미 존재 (688 symbols 커버, 대부분 2021+). 2015~2020 구간은 15 rows 뿐. OpenClaw Weekly Heavy (월/목) 가 FMP API 로 증분 수집 중 — metadata only (본문/items/datetime 없음). 이 파이프라인은 독립 신규 테이블 (filings_8k*) 로 구축하되, 기존 3,400 행의link필드를 seed 삼아 EDGAR body fetch 가능한지 cross-check (§Deliverables Week 1 참조).
Target Schema
DuckDB biotech.duckdb 에 3개 테이블 추가.
filings_8k — raw filing metadata
CREATE TABLE filings_8k (
accession_number VARCHAR PRIMARY KEY, -- e.g. "0001213900-26-012345"
cik INTEGER NOT NULL,
symbol VARCHAR, -- null-able (CIK → symbol 매핑 실패 시)
filing_date DATE NOT NULL, -- DATE(filing_datetime at EST)
filing_datetime TIMESTAMP NOT NULL, -- SEC acceptance timestamp (EST)
-- 장 마감 후 (≥16:00 EST) vs 장중 공시는
-- D+0 vs D+1 진입 가능성이 달라져 백테스트 look-ahead
-- 방어에 필수. TIMESTAMP 로 반드시 보존.
period_of_report DATE, -- 8-K 는 event date
items VARCHAR[], -- ['7.01', '9.01'] 등
primary_doc_url VARCHAR NOT NULL, -- 8-K index 페이지 URL
exhibit_count INTEGER,
fetched_at TIMESTAMP NOT NULL
);
CREATE INDEX filings_8k_symbol_date ON filings_8k(symbol, filing_date);
CREATE INDEX filings_8k_cik ON filings_8k(cik);filings_8k_body — full text (분리, 크기 커서)
CREATE TABLE filings_8k_body (
accession_number VARCHAR REFERENCES filings_8k(accession_number),
doc_type VARCHAR NOT NULL, -- 'body' | 'exhibit_99_1' | 'exhibit_99_2' ...
content TEXT NOT NULL, -- HTML-stripped plain text
char_count INTEGER
);
CREATE INDEX filings_8k_body_accession ON filings_8k_body(accession_number);filings_8k_events — NLP-classified events
CREATE TABLE filings_8k_events (
id BIGINT PRIMARY KEY,
accession_number VARCHAR REFERENCES filings_8k(accession_number),
symbol VARCHAR NOT NULL,
event_type VARCHAR NOT NULL, -- 아래 enum
drug_name VARCHAR,
phase VARCHAR, -- 'Phase 1' | 'Phase 2' | 'Phase 3' | 'Pivotal'
indication VARCHAR,
nct_id VARCHAR,
announced_datetime TIMESTAMP NOT NULL, -- = filings_8k.filing_datetime (EST). 시각 보존.
event_date DATE, -- 본문에서 추출한 forward-looking 이벤트 날짜
-- (e.g. "Q2 2026", "primary completion 2027-01")
outcome VARCHAR, -- 'positive' | 'negative' | 'mixed' | null
confidence FLOAT, -- 0~1 (분류기 score)
excerpt TEXT, -- 관련 문단
classifier_version VARCHAR, -- 재처리 tracking
classified_at TIMESTAMP NOT NULL
);
-- announced_date (DATE) 는 파생 view 로 노출 — 쿼리 편의용
CREATE OR REPLACE VIEW filings_8k_events_v AS
SELECT *,
DATE(announced_datetime AT TIME ZONE 'America/New_York') AS announced_date,
CASE WHEN EXTRACT(hour FROM announced_datetime AT TIME ZONE 'America/New_York') >= 16
THEN 'after_close' ELSE 'intraday' END AS announced_session
FROM filings_8k_events;
CREATE INDEX filings_8k_events_symbol_date ON filings_8k_events(symbol, announced_datetime);
CREATE INDEX filings_8k_events_type ON filings_8k_events(event_type);백테스트의 look-ahead 방어 기본 원칙 (Dan 의 현재 전략):
- 모든 의사결정은 장 마감 후 이루어지고 D+1 오픈 진입
- 즉 T 일 진입에 사용 가능한 이벤트 =
DATE(announced_datetime AT TZ 'America/New_York') ≤ T - 1 day announced_datetimeTIMESTAMP 는 보존 (intraday 전략 등 향후 확장 대비) 하되, 현재 전략에선 date 단위로 충분
Event type enum
pipeline_addition -- 새 drug candidate / program 추가
ind_clearance -- FDA IND 승인
trial_initiation -- 새 trial 시작
trial_milestone -- enrollment complete, cohort complete 등
trial_readout -- topline data
phase_transition -- Phase 1 → Phase 2, 또는 pivotal 승인
pdufa_update -- PDUFA 일자 지정/연기
adcom -- FDA AdCom 관련
partnership -- licensing, collaboration
financing -- offering, convertible, loan
earnings -- 분기 실적
other -- 위 분류에 속하지 않는 모든 것 (Item 5.02 personnel 포함)
Note: Item 5.02 (임원 변경) 는 현재 backtest 가 쓸 구체 use case 가 없어 other 에 포함. CMO/CSO 교체가 pipeline signal 로 검증되면 별도 enum 으로 분리 (backlog).
Architecture
biotech/data/ingest/filings/
├── common/
│ ├── schema.py # DDL + migration
│ ├── cik_mapper.py # symbol ↔ CIK 양방향 (재사용 with catalyst plan)
│ └── edgar_client.py # rate-limited EDGAR HTTP client
├── ingest/
│ ├── edgar_listing.py # 특정 기간 동안 filed 된 8-K 리스트 수집
│ ├── fetcher.py # 각 filing 의 index/body/exhibits download
│ ├── parser.py # HTML → plain text, item 추출, exhibit 분리
│ └── backfill.py # 2015~현재 전체 backfill orchestrator
├── classify/
│ ├── rules.py # 키워드/regex 기반 1차 분류
│ ├── llm.py # LLM 기반 상세 분류 (drug name, outcome 등)
│ └── orchestrator.py # hybrid rule+LLM pipeline
├── quality/
│ ├── spotcheck.py # 샘플 50~100건 자동 샘플링 + 수동 검증
│ └── metrics.py # coverage, classification confidence 분포
└── cli.py
EDGAR 수집 전략
API
Primary (공식·안정):
- Company filings list:
https://data.sec.gov/submissions/CIK{10-digit}.json- 최근 1,000 filings + older via
files/CIK...-submissions-NNN.json페이징 recent.items에 8-K item 리스트 이미 포함 → items parsing 불필요recent.acceptanceDateTime으로 TIMESTAMP 확보 가능
- 최근 1,000 filings + older via
- Filing index:
https://www.sec.gov/Archives/edgar/data/{cik}/{accession-no-dashes}/index.json- primary doc + 모든 exhibit 리스트 구조화로 제공 (HTML 파싱 불필요)
Fallback (1번이 10년 전까지 안 줄 때만):
https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=...&type=8-K
Avoid:
-
https://efts.sec.gov/LATEST/search-index— undocumented internal Elasticsearch API (CT.gov/api/int/와 동일한 성격). 안정성 보장 없음. raw payload 보존 패턴으로 옵션 사용은 가능하지만 primary 로 의존하지 말 것. -
Rate limit: 10 req/sec. User-Agent required (
Lucky Bio Research <email>).
Two pass 전략
Pass 1 — Listing (빠름):
data.sec.gov/submissions/CIK{cik}.json호출해 각 CIK 의 모든 8-K accession numbers + filing_date + items 획득- 결과를
filings_8k에 upsert (본문 없이)
Pass 2 — Body fetch (느림):
- 우선순위: 최근 filing 먼저, 그 다음 historical backfill
- 각 accession 의 index 페이지 → primary doc + exhibit 99.1~99.N
- HTML → plain text (beautifulsoup or lxml),
filings_8k_body에 저장 - Item 7.01 / 8.01 / 1.01 / 2.02 우선 처리, 나머지 후순위
Incremental (일일)
- 운영 인프라:
docs/cron-migration-status.md의 launchd + bash wrapper + Lucky inbox 구조 재사용. 별도 OpenClaw / 자체 데몬 추가 없음.scripts/cron/daily_8k_ingestion.sh+launchd/com.lucky.daily-8k-ingestion.plist- SUMMARY 에
filings_fetched,bodies_parsed,events_classified,errors,error_samples,errors_by_step포함 - 에러는 Layer 2 (webhook) + Layer 3 (Lucky inbox → data session 분석) 자동 분기
- Week 2 에 구축될 watchdog 가 silent miss 자동 커버
- 스케줄: KST 23:30 daily (= EST 09:30, 전일 공시 안정 후). 정확한 시각은 EDGAR update 패턴 봐가며 조정.
- 전날 filed 된 8-K 만 (
data.sec.gov/submissions의 acceptanceDateTime 기반 증분) - 일일 filing 수 ~2,000 건 (전체 U.S. 공시) → biotech 필터 후 ~50건 예상
Backfill (1회)
- 2015-01-01 ~ 현재 = ~11년 × 1,109 tickers. 연간 평균 10 filings/ticker 라 가정 시 ~120,000 filings (기획 수치
55k 는 ticker 수 저평가). 대형주 (PFE, LLY, MRK) 는 연 50+, 소형 바이오는 58 로 편차 큼. - 기존
sec_filings의 3,455 행 (8-K + 8-K/A) 은 seed:- 2021+ 최근 공시는 상당 부분 already indexed → listing phase 단축
- 2015~2020 구간은 본 파이프라인이 처음부터 EDGAR 에서 끌어옴
- Rate limit 10 req/sec → filing 당 평균 3~5 req (submissions index 는 CIK 당 1회로 amortize + filing index.json + primary doc + exhibits)
- 예상 runtime: 3~7일 (재시도 + 파싱 + Pass 1/2 분리 포함). 시간 예산 초과는
daily_ct_history처럼 budget-capped incremental 로 분산. - DB 용량 추정: 평균 30
50KB 본문 × 120k = **36 GB** (DuckDB compressed). biotech.duckdb 현재 크기 대비 유의미한 증가 — 필요 시filings_8k_body만 별도.duckdb파일로 분리 ATTACH 고려.
NLP 분류
1단계: Rule-based 1차 분류 (빠름, 무료)
키워드 매칭 + regex 로 event_type 후보 할당.
EVENT_PATTERNS = {
"ind_clearance": [
r"\bIND\b.*\b(clear|accept|approv)",
r"\binvestigational new drug application\b.*\b(clear|accept)",
],
"trial_readout": [
r"\btopline\b", r"\bprimary endpoint\b.*\b(met|missed|did not meet)\b",
r"\bPhase (1|2|3).*results\b",
],
"pipeline_addition": [
r"\bnew (product|drug) candidate\b", r"\bpipeline expansion\b",
r"\badding\b.*\bto our pipeline\b",
],
"phase_transition": [
r"\bpivotal\b.*\btrial\b", r"\bPhase 3\b.*\b(launch|initiate)\b",
r"\bend-of-Phase\b",
],
# ...
}결과: event_type (high-recall, low-precision) + confidence (rule 개수에 비례).
2단계: LLM-based 정교화 — V2 backlog (scope out)
LLM 추출 (drug_name, indication, outcome, event_date 등 고수준 필드) 은 V2 로 분리, 현재 V1 에서는 제외. 이유:
- Dan 이 2026-04-23 에 “raw data 쌓기” 를 V1 목표로 확정
indication은 CT.govconditionsvianct_idjoin 으로 대체 가능outcome은 post-announcement 주가 반응이 더 정확한 ground truth (post-hoc 라벨 불필요)event_dateforward-looking 은 CT.govprimary_completion_date가 이미 커버
V2 에서 재검토 트리거:
- V1 운영 후 특정 컬럼 (drug_name gap 등) 이 backtest 신호 품질을 좌우한다고 증명됐을 때
- 당시 LLM API 가격 재평가
V2 로 deferred 된 구성 요소:
classify/llm.py, hybrid orchestrator- 예상 비용 재추산 필요 (Claude Haiku 기준 55k filings × 2500 tokens ≈ $34/backfill 이었던 참고치)
Hybrid 결정 → V1 은 Rule-only
1차 rule classification 만으로 V1 완결. 나머지 (earnings, financing, other) 는 rule 로 충분. Pipeline/clinical 관련도 rule (regex + drug catalog) + NCT ID 추출로 기본 필드 확보.
Re-classification 정책
classifier_version 필드로 모든 event row 가 어떤 버전으로 분류됐는지 추적. 변경 발생 시:
- Rule 수정 (grep/regex 만 바꿈): 전체 재처리 비용 0 → 즉시 전체 재처리 후 version 업데이트
- (V2) LLM prompt 수정: random sample 1,000건 A/B → 개선 확인 시 전체 재처리. 월 예산 cap $100.
Validation
- 샘플 100건 수동 검증 → event_type 별 precision/recall 측정 (random 샘플링 시 rare category 놓치므로 type 당 최소 10건 stratified)
- Target (type 별 조정):
- rule-only 충분 (
earnings,financing,other): precision 95%+, recall 95%+ - hybrid 필요 (
pipeline_addition,phase_transition,trial_readout,partnership): precision 85%+, recall 80%+ - 정의상 드문 (
pdufa_update,adcom,ind_clearance): precision 90%+, recall 가능한 한 높게 (놓치면 catalyst 누락)
- rule-only 충분 (
- 실패 시 prompt / rule 튜닝 반복
Deliverables
Week 1: Infrastructure (2~3일)
-
filings_8k,filings_8k_body,filings_8k_eventsDDL + migration -
cik_mapper.py(symbol ↔ CIK 양방향, catalyst-data-collection 과 공유) -
edgar_client.py(rate-limited + User-Agent + retry) -
edgar_listing.py+ 100 symbol 대상 smoke test (pass 1) - Cross-check against existing
sec_filings: EDGAR listing 결과를 기존 3,455 (8-K + 8-K/A) rows 와 symbol×filing_date 매칭. mismatch 샘플 수동 검증해서 신규 EDGAR 파이프라인이 기존 FMP-based 수집을 superset 으로 덮는지 확인. 발견된 gap 은 2주차 backfill 범위 정의에 반영.
Week 2: Content fetch + parse (3~4일)
-
fetcher.py+ HTML parser (primary doc + exhibit 99.1) - Body 저장 pipeline + disk caching
- 10 symbol 대상 전체 filing body backfill (2015~현재) — 정상 작동 확인
Week 3: Rule-based Classification (2~3일, V1 스코프)
- Rule-based classifier (10~12 event types, regex + drug catalog 매칭)
- NCT ID 자동 추출 (
NCT\d{8}regex) - drug catalog seed (universe 의 sponsor 별 약물명 테이블)
- 100건 spot-check (event_type 별 stratified 10건 이상)
- V2 LLM 은 backlog — 이번 주차 스코프 아님
Week 4: Backfill + ops (3~5일)
- 전체 biotech universe 2015
현재 backfill (35일 runtime) - Daily cron 구성
- Quality dashboard (coverage, confidence, event type 분포)
- Backtest 세션과 인터페이스 documentation (어떻게 쿼리하는지)
총 3~4주 풀타임 기준. 부분 작업 (infra 만, 또는 분류기 없이) 으로 빠르게 MVP 도 가능.
catalyst-data-collection 과의 관계
기존 catalyst-data-collection 은 PDUFA + Phase readout 에 focused. 이 파이프라인은 그것의 하위 layer:
- 이 파이프라인: 모든 biotech 8-K 수집 + 분류
- catalyst 파이프라인: 이 결과에서
event_type IN ('pdufa_update', 'trial_readout', 'adcom')만 뽑아 별도catalysts테이블로 정제
실무 순서:
- 이 파이프라인 Week 1-2 완료 (raw ingestion)
- catalyst-data-collection 은 이 위에 view 로 구축 — 기존 catalyst plan 의 EDGAR Source 1 섹션이 자동 완성됨
- 두 파이프라인 병렬 진화
연결 view
CREATE OR REPLACE VIEW catalysts_from_8k AS
SELECT
e.accession_number,
e.symbol,
e.drug_name, e.phase, e.indication, e.nct_id,
CASE e.event_type
WHEN 'pdufa_update' THEN 'pdufa'
WHEN 'trial_readout' THEN 'phase_readout'
WHEN 'adcom' THEN 'adcom'
END AS catalyst_type,
e.announced_datetime,
DATE(e.announced_datetime AT TIME ZONE 'America/New_York') AS announced_date,
e.event_date,
e.outcome,
e.confidence,
'edgar_8k' AS source
FROM filings_8k_events e
WHERE e.event_type IN ('pdufa_update', 'trial_readout', 'adcom')
AND e.confidence >= 0.6;이 view 가 catalyst-data-collection 의 EDGAR 소스를 대체. 나머지 3개 소스 (BioPharma Catalyst, PR Newswire, CT.gov fallback) 는 catalyst 파이프라인이 별도 테이블로 유지하다가 merge.
사용 예시 (backtest 관점)
1. Pipeline breadth feature
-- 특정 rebalance 시점에 active pipeline 수
SELECT symbol, COUNT(DISTINCT drug_name) AS n_programs
FROM filings_8k_events
WHERE event_type IN ('ind_clearance', 'trial_initiation', 'pipeline_addition')
AND announced_date <= '2026-01-01'
AND announced_date >= '2024-01-01' -- 지난 2년 내 active
GROUP BY symbol;2. Pipeline momentum signal
-- 최근 180일 내 새 event 있는 종목
SELECT DISTINCT symbol
FROM filings_8k_events
WHERE event_type IN ('ind_clearance', 'phase_transition', 'pipeline_addition')
AND announced_date BETWEEN '2025-07-01' AND '2026-01-01';3. TCRX case 재현 가능성 확인
TSC-102-A01/A03 IND 승인 (2026-02-26) 이 event_type='ind_clearance' 로 분류되어야 함. 이게 announced_date=2026-02-26 으로 저장되면, Dan 이 뉴스 본 시점에 쿼리 가능.
리스크
- EDGAR schema drift — filing 포맷 간간이 변화. Parser regression test 필요.
- NLP false positive/negative — 특히 작은 바이오의 “pipeline expansion” vs “generic investor update” 경계. Confidence 0.5 미만은 manual review queue 로.
- CIK 매핑 누락 — 신규 상장, symbol 변경 (e.g. reverse stock split, ticker change) 케이스. profile + historical 매핑 함께 관리.
- (V2 도입 시) LLM rate limit / cost — V1 은 해당 없음.
- Privacy / robots.txt — EDGAR 는 공개 자료이지만 User-Agent 미설정 시 차단. 자동화 계정 명확히.
- DB 크기 증가 —
filings_8k_body가 수 GB 로 예상. biotech.duckdb 비대화 시 분리된filings_8k.duckdb로 ATTACH 고려 (ct_history.duckdb 와 동일 패턴).
Future work (backlog, 본 파이프라인 Phase 외)
- V2: LLM enrichment — drug_name (catalog miss 구간), indication, outcome 의미 추출, event_date forward-looking 파싱. V1 rule-based 결과로 특정 컬럼이 backtest 신호 품질을 결정한다고 증명된 후 도입. 샘플 1,000건 A/B 로 ROI 측정 선행. 월 LLM 예산 cap $100.
- Drug name normalization:
drug_nameraw string 으로 저장되어 “TSC-100” / “TSC100” / “tscan-100” 분리됨. 별도drugs테이블 (drug_name, aliases[], target, moa, sponsor_symbol) 도입 — 5월 이후. - DuckDB FTS:
INSTALL fts; LOAD fts;로filings_8k_body.content전문 검색 인덱스. “뉴스 문장으로 관련 8-K 찾기” 류 쿼리에 유용. - XBRL metadata: 최근 8-K 는 inline XBRL 포함. 필요 시 재무 스냅샷 자동 추출 가능.
- Item 5.02 personnel 별도 enum: CMO/CSO 교체 signal 검증 후 분리.
다음 액션 (bio-v2-data 세션으로 handoff)
승인 후 bio-v2-data 채널에서:
- Schema DDL 확정 + migration (0.5일)
profile.cik존재 확인, 없으면 EDGAR company_tickers.json 에서 bulk 매핑 (0.5일)edgar_client.py+ rate-limit test (1일)- 10-symbol smoke test — 기존
filings_8k테이블에 metadata 채우기 (1일) - 이후 Week 1~4 순서대로 진행
질문/이슈 발생 시: backtest 세션 (bioreport 채널) 과 스키마 or event_type enum 협의. 변경은 이 기획서에 반영.
Related
- catalyst-data-collection — 상위 소비자 (PDUFA / Phase readout 추출)
- research-refactor — universe thread 의 pipeline breadth/momentum 실험 (001~003) 이 이 데이터 소비
- bioreport-site — 운영 가이드