8-K Ingestion Pipeline

바이오 universe 의 모든 SEC 8-K filing 을 전수 수집 + 구조화 해서 DuckDB 에 저장한다. 이 파이프라인이 두 상위 소비자의 기반이 됨:

  1. Universe feature 생성 (backtest) — pipeline breadth / momentum / phase upgrade signal
  2. 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_datetime TIMESTAMP 는 보존 (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 확보 가능
  • 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-indexundocumented 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 용량 추정: 평균 3050KB 본문 × 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.gov conditions via nct_id join 으로 대체 가능
  • outcome 은 post-announcement 주가 반응이 더 정확한 ground truth (post-hoc 라벨 불필요)
  • event_date forward-looking 은 CT.gov primary_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 누락)
  • 실패 시 prompt / rule 튜닝 반복

Deliverables

Week 1: Infrastructure (2~3일)

  • filings_8k, filings_8k_body, filings_8k_events DDL + 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 테이블로 정제

실무 순서:

  1. 이 파이프라인 Week 1-2 완료 (raw ingestion)
  2. catalyst-data-collection 은 이 위에 view 로 구축 — 기존 catalyst plan 의 EDGAR Source 1 섹션이 자동 완성됨
  3. 두 파이프라인 병렬 진화

연결 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 이 뉴스 본 시점에 쿼리 가능.

리스크

  1. EDGAR schema drift — filing 포맷 간간이 변화. Parser regression test 필요.
  2. NLP false positive/negative — 특히 작은 바이오의 “pipeline expansion” vs “generic investor update” 경계. Confidence 0.5 미만은 manual review queue 로.
  3. CIK 매핑 누락 — 신규 상장, symbol 변경 (e.g. reverse stock split, ticker change) 케이스. profile + historical 매핑 함께 관리.
  4. (V2 도입 시) LLM rate limit / cost — V1 은 해당 없음.
  5. Privacy / robots.txt — EDGAR 는 공개 자료이지만 User-Agent 미설정 시 차단. 자동화 계정 명확히.
  6. 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_name raw 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 채널에서:

  1. Schema DDL 확정 + migration (0.5일)
  2. profile.cik 존재 확인, 없으면 EDGAR company_tickers.json 에서 bulk 매핑 (0.5일)
  3. edgar_client.py + rate-limit test (1일)
  4. 10-symbol smoke test — 기존 filings_8k 테이블에 metadata 채우기 (1일)
  5. 이후 Week 1~4 순서대로 진행

질문/이슈 발생 시: backtest 세션 (bioreport 채널) 과 스키마 or event_type enum 협의. 변경은 이 기획서에 반영.