# Phase 2 저장 구조 및 데이터 계약

## 1. 목적

이 문서는 Phase 2 수집 파이프라인이 쓰는 **파일 경로 규칙**, **raw sidecar 형식**, **staging canonical schema**, **structured write 규칙**을 정의합니다.

핵심 목표는 아래와 같습니다.

1. 모든 raw 파일의 출처와 실행(run)을 추적할 수 있어야 한다.
2. 모든 staging/structured 레코드가 원문(raw)로 역추적 가능해야 한다.
3. 파일명/경로만 보고 source, date, object type을 알 수 있어야 한다.
4. 재실행 시 같은 경로/키 체계를 사용해야 한다.

## 2. Raw 저장 규칙

### 2.1 공통 경로 형식

```text
data/raw/{source}/{ingestion_date}/{object_scope}/...
```

예시:

```text
data/raw/sec/2026-03-12/cik_0000789019/0001193125-26-027198/submissions.json
data/raw/sec/2026-03-12/cik_0000789019/0001193125-26-027198/filing.txt
data/raw/sec/2026-03-12/cik_0000789019/0001193125-26-027198/exhibit_99_1.html

data/raw/alpaca/2026-03-12/daily/AAPL.json

data/raw/fred/2026-03-12/series/DGS10.json

data/raw/finra/2026-03-12/short_volume/2026-03-11.txt
```

### 2.2 Sidecar 메타데이터

각 raw object마다 `.meta.json` sidecar를 생성합니다.

예:

```text
submissions.json
submissions.json.meta.json
```

### 2.3 Sidecar 필수 필드

```json
{
  "run_id": "2026-03-12T21:05:14Z_sec_submissions_poll_001",
  "job_name": "sec_submissions_poll",
  "source": "sec",
  "source_url": "https://...",
  "http_status": 200,
  "fetched_at": "2026-03-12T21:05:14Z",
  "content_type": "application/json",
  "payload_bytes": 18293,
  "sha256": "...",
  "mode": "poll",
  "source_identifier": {
    "cik": "0000789019",
    "accession": "0001193125-26-027198"
  },
  "parser_hint": {
    "object_type": "sec_submissions"
  }
}
```

### 2.4 Raw 쓰기 원칙

- 임시 파일에 먼저 씁니다.
- checksum 계산 후 원자적 rename을 합니다.
- sidecar까지 성공해야 raw 저장 성공으로 간주합니다.
- sidecar 저장 전에는 `job_runs.status = raw_saved`로 올리지 않습니다.

## 3. Staging 저장 규칙

staging은 파싱/정규화의 중간 결과입니다.

경로 형식:

```text
data/staging/{source}/{logical_date}/{object_type}/{entity_key}.jsonl
```

예시:

```text
data/staging/sec/2026-03-12/filing_headers/cik_0000789019.jsonl
data/staging/sec/2026-03-12/exhibit_inventory/0001193125-26-027198.jsonl

data/staging/alpaca/2026-03-12/daily_bars/AAPL.jsonl

data/staging/fred/2026-03-12/series/DGS10.jsonl

data/staging/finra/2026-03-12/short_volume/2026-03-11.jsonl
```

원칙:
- staging은 재생성 가능 데이터입니다.
- raw가 진실의 원천이고, staging은 파생 데이터입니다.
- staging은 overwrite 가능하지만 lineage가 남아야 합니다.

## 4. Canonical Record 계약

### 4.1 SEC Document Header Record

```json
{
  "document_id": "sec:0001193125-26-027198",
  "source": "sec",
  "cik": "0000789019",
  "accession": "0001193125-26-027198",
  "form_type": "8-K",
  "filing_date": "2026-01-28",
  "acceptance_datetime": "2026-01-28T16:13:02Z",
  "primary_document": "msft-8k.htm",
  "has_xbrl": true,
  "raw_path": "data/raw/.../filing.txt",
  "raw_sha256": "...",
  "run_id": "..."
}
```

### 4.2 SEC Exhibit Record

```json
{
  "artifact_id": "sec_artifact:0001193125-26-027198:ex99_1",
  "document_id": "sec:0001193125-26-027198",
  "artifact_name": "ex99_1.htm",
  "artifact_type": "exhibit_99_1",
  "mime_type": "text/html",
  "raw_path": "data/raw/.../exhibit_99_1.html",
  "raw_sha256": "...",
  "run_id": "..."
}
```

### 4.3 Alpaca Canonical Bar Record

```json
{
  "source": "alpaca",
  "symbol": "AAPL",
  "timeframe": "1D",
  "ts": "2026-03-11T21:00:00Z",
  "trading_date": "2026-03-11",
  "open": 212.31,
  "high": 214.05,
  "low": 211.62,
  "close": 213.98,
  "volume": 48761234,
  "trade_count": 302119,
  "vwap": 213.12,
  "raw_path": "data/raw/.../AAPL.json",
  "raw_sha256": "...",
  "run_id": "..."
}
```

### 4.4 FRED Observation Record

```json
{
  "source": "fred",
  "series_id": "DGS10",
  "observation_date": "2026-03-11",
  "value": 4.13,
  "frequency": "Daily",
  "units": "Percent",
  "raw_path": "data/raw/.../DGS10.json",
  "raw_sha256": "...",
  "run_id": "..."
}
```

### 4.5 FINRA Short Volume Record

```json
{
  "source": "finra",
  "trade_date": "2026-03-11",
  "symbol": "AAPL",
  "short_volume": 14122112,
  "total_volume": 42211884,
  "short_volume_ratio": 0.3345,
  "raw_path": "data/raw/.../2026-03-11.txt",
  "raw_sha256": "...",
  "run_id": "..."
}
```

## 5. Structured Write 규칙

### 5.1 PostgreSQL

운영 상태/메타데이터는 PostgreSQL에 저장합니다.

대표 테이블:
- `job_runs`
- `source_checkpoints`
- `raw_objects`
- `documents`
- `document_artifacts`
- `xbrl_facts`
- `macro_series_observations`
- `short_sale_volume_daily`

규칙:
- 운영 테이블은 upsert 기반
- natural key 또는 idempotency key를 반드시 둠
- `created_at`, `updated_at`, `run_id` 필수

### 5.2 Parquet

대량 시계열/분석용 데이터는 Parquet로 적재합니다.

경로 예시:

```text
data/parquet/market_bars_daily/trading_date=2026-03-11/part-000.parquet
data/parquet/market_bars_intraday/trading_date=2026-03-11/symbol=AAPL/part-000.parquet
data/parquet/fred/series_id=DGS10/part-000.parquet
data/parquet/finra_short_volume/trade_date=2026-03-11/part-000.parquet
```

규칙:
- partition overwrite는 날짜/심볼 범위 단위로 제한
- late-arriving data는 해당 partition 재작성
- write manifest로 어떤 partition이 언제 갱신됐는지 기록

## 6. Lineage 계약

모든 staging/structured 레코드는 최소 아래 필드를 가져야 합니다.

- `source`
- `run_id`
- `raw_path`
- `raw_sha256`
- `ingested_at`

이 5개가 없으면 downstream에서 사용 금지입니다.

## 7. 상태 필드 규칙

`job_runs.status`는 아래 enum만 허용합니다.

- `created`
- `running`
- `raw_saved`
- `staged`
- `structured_written`
- `validated`
- `completed`
- `failed_retriable`
- `failed_terminal`
- `quarantined`

## 8. 중복 방지

중복 방지는 아래 3단계로 합니다.

1. raw checksum 중복 검사
2. staging natural key 중복 검사
3. structured upsert key 검사

어느 단계에서도 중복이 발생하면 경고를 남기고 동일성 비교 후 no-op 처리합니다.

## 9. 운영자가 빠르게 확인해야 할 경로

최소 아래 경로는 사람이 쉽게 찾을 수 있어야 합니다.

- 최근 실패 잡 로그
- quarantine raw
- 특정 accession 관련 raw/staging/structured lineage
- 특정 심볼/거래일 bars 파일
- 특정 FRED series 최근 observation

## 10. 완료 기준

- 임의의 structured row에서 raw_path를 따라가면 원문을 확인할 수 있어야 합니다.
- raw object checksum이 바뀌면 새 버전 또는 경고가 생성되어야 합니다.
- 동일 run을 다시 실행해도 동일 natural key에 대해 중복 row가 생기지 않아야 합니다.
