# Phase 1 DB 스키마

## 1. 설계 원칙

- PostgreSQL은 **운영 상태와 canonical metadata**를 담당합니다.
- 대량 시계열과 연구용 매트릭스는 **Parquet/DuckDB**로 분리합니다.
- 각 테이블은 가능한 한 **append-safe + upsert-safe** 하게 설계합니다.
- 모든 핵심 테이블은 `created_at_utc`, `updated_at_utc`를 가집니다.
- 모든 외부 문서는 source checksum을 가집니다.

## 2. PostgreSQL 핵심 테이블

### 2.1 source_registry
외부 데이터 원천 정의

| column | type | note |
|---|---|---|
| source_name | text PK | `sec`, `alpaca`, `fred`, `finra` |
| source_type | text | filing, market_data, macro, short_volume |
| enabled | boolean | 사용 여부 |
| config_json | jsonb | adapter 설정 |
| created_at_utc | timestamptz | 생성 시각 |
| updated_at_utc | timestamptz | 수정 시각 |

### 2.2 issuer_master
발행사 마스터

| column | type | note |
|---|---|---|
| issuer_id | text PK | 내부 발행사 ID |
| cik | text unique | SEC CIK |
| ticker | text | 대표 ticker |
| issuer_name | text | 회사명 |
| exchange | text | 거래소 |
| country_code | text | 국가 |
| is_active | boolean | 활성 여부 |
| created_at_utc | timestamptz | |
| updated_at_utc | timestamptz | |

### 2.3 symbol_master
종목 마스터

| column | type | note |
|---|---|---|
| symbol_id | text PK | 내부 종목 ID |
| issuer_id | text FK | issuer_master |
| ticker | text | 심볼 |
| venue | text | 거래 venue |
| asset_type | text | common_stock 등 |
| currency | text | 통화 |
| start_date | date | 유효 시작 |
| end_date | date nullable | 유효 종료 |
| is_primary | boolean | 대표 종목 여부 |
| created_at_utc | timestamptz | |
| updated_at_utc | timestamptz | |

### 2.4 job_runs
배치/수집 실행 로그

| column | type | note |
|---|---|---|
| job_run_id | uuid PK | 실행 ID |
| job_name | text | job 이름 |
| source_name | text | source |
| run_date | date | 기준 일자 |
| status | text | pending/running/succeeded/failed/partial |
| started_at_utc | timestamptz | 시작 시각 |
| finished_at_utc | timestamptz nullable | 종료 시각 |
| records_seen | integer | 관측 수 |
| records_written | integer | 적재 수 |
| records_skipped | integer | 스킵 수 |
| error_count | integer | 에러 수 |
| error_summary | text nullable | 요약 |
| metadata_json | jsonb | 기타 메타 |

### 2.5 raw_objects
원문 보관 메타데이터

| column | type | note |
|---|---|---|
| raw_object_id | text PK | 내부 원문 ID |
| source_name | text | source |
| source_object_key | text | 원천 고유 키 |
| storage_path | text | raw 파일 경로 |
| content_type | text | mime type |
| checksum_sha256 | text | 체크섬 |
| collected_at_utc | timestamptz | 수집 시각 |
| published_at_utc | timestamptz nullable | 원천 게시 시각 |
| metadata_json | jsonb | sidecar metadata |
| created_at_utc | timestamptz | |

### 2.6 documents
정규화된 문서 메타

| column | type | note |
|---|---|---|
| document_id | text PK | 내부 문서 ID |
| source_name | text | 보통 `sec` |
| issuer_id | text FK | |
| symbol_id | text nullable FK | |
| accession_no | text nullable | SEC accession |
| form_type | text | 8-K, 10-Q 등 |
| filing_date | date | filing date |
| accepted_at_utc | timestamptz nullable | SEC acceptance time |
| primary_document_name | text nullable | primary doc |
| raw_object_id | text FK | 원문 참조 |
| text_path | text nullable | 추출 텍스트 경로 |
| html_path | text nullable | html 경로 |
| parsed_status | text | pending/succeeded/failed |
| created_at_utc | timestamptz | |
| updated_at_utc | timestamptz | |

고유 제약:
- `(source_name, accession_no, primary_document_name)` unique

### 2.7 document_exhibits
부속 문서 목록

| column | type | note |
|---|---|---|
| exhibit_id | text PK | 내부 exhibit ID |
| document_id | text FK | |
| exhibit_code | text | EX-99.1 등 |
| exhibit_name | text | 파일명 |
| raw_object_id | text FK | |
| text_path | text nullable | |
| html_path | text nullable | |
| created_at_utc | timestamptz | |

### 2.8 events
문서에서 추출된 이벤트 단위

| column | type | note |
|---|---|---|
| event_id | text PK | 내부 이벤트 ID |
| issuer_id | text FK | |
| symbol_id | text nullable FK | |
| primary_document_id | text FK | |
| event_type | text | earnings_release 등 |
| event_direction | text | bullish/bearish/mixed/unknown |
| event_date | date | 거래 로직 기준 일자 |
| filed_at_utc | timestamptz nullable | filing/published time |
| parser_version | text | parser 버전 |
| parse_confidence | numeric | 0~1 |
| status | text | pending/valid/rejected |
| created_at_utc | timestamptz | |
| updated_at_utc | timestamptz | |

### 2.9 event_parses
파서 원출력 저장

| column | type | note |
|---|---|---|
| event_parse_id | bigserial PK | |
| event_id | text FK | |
| parser_kind | text | rule / llm / merged |
| parser_version | text | |
| schema_version | text | |
| output_json | jsonb | full parser output |
| validation_status | text | valid/invalid |
| validation_errors | jsonb nullable | |
| created_at_utc | timestamptz | |

### 2.10 market_bars_daily
일봉 canonical 저장

| column | type | note |
|---|---|---|
| symbol_id | text FK | |
| trade_date | date | |
| open | numeric | |
| high | numeric | |
| low | numeric | |
| close | numeric | |
| volume | bigint | |
| vwap | numeric nullable | |
| trade_count | bigint nullable | |
| source_name | text | |
| created_at_utc | timestamptz | |
| updated_at_utc | timestamptz | |

PK:
- `(symbol_id, trade_date, source_name)`

### 2.11 market_bars_intraday
분봉 canonical 저장

| column | type | note |
|---|---|---|
| symbol_id | text FK | |
| bar_start_utc | timestamptz | |
| timeframe | text | 1Min, 5Min 등 |
| open | numeric | |
| high | numeric | |
| low | numeric | |
| close | numeric | |
| volume | bigint | |
| vwap | numeric nullable | |
| trade_count | bigint nullable | |
| source_name | text | |
| created_at_utc | timestamptz | |

PK:
- `(symbol_id, bar_start_utc, timeframe, source_name)`

### 2.12 macro_series
거시 시계열 메타

| column | type | note |
|---|---|---|
| series_id | text PK | FRED series_id |
| title | text | |
| frequency | text | |
| units | text | |
| source_name | text | |
| metadata_json | jsonb | |
| created_at_utc | timestamptz | |

### 2.13 macro_observations
거시 시계열 값

| column | type | note |
|---|---|---|
| series_id | text FK | |
| observation_date | date | |
| value | numeric nullable | |
| created_at_utc | timestamptz | |

PK:
- `(series_id, observation_date)`

### 2.14 short_sale_daily
FINRA short sale volume 정규화

| column | type | note |
|---|---|---|
| symbol_id | text nullable FK | mapping 실패 가능 |
| ticker_raw | text | 원본 심볼 |
| trade_date | date | |
| short_volume | bigint | |
| short_exempt_volume | bigint nullable | |
| total_volume | bigint nullable | |
| source_name | text | |
| created_at_utc | timestamptz | |

PK:
- `(ticker_raw, trade_date, source_name)`

### 2.15 feature_snapshots
이벤트 시점 특징 저장

| column | type | note |
|---|---|---|
| feature_snapshot_id | bigserial PK | |
| event_id | text FK | |
| snapshot_name | text | market_v1, event_v1 등 |
| snapshot_version | text | |
| feature_json | jsonb | |
| created_at_utc | timestamptz | |

### 2.16 order_plans
Phase 1에서는 계획만 정의

| column | type | note |
|---|---|---|
| order_plan_id | bigserial PK | |
| event_id | text FK | |
| symbol_id | text FK | |
| side | text | buy/sell |
| planned_entry_date | date | |
| planned_order_type | text | market/limit |
| planned_price | numeric nullable | |
| stop_price | numeric nullable | |
| take_profit_price | numeric nullable | |
| quantity_plan | numeric nullable | |
| status | text | draft/ready/canceled |
| created_at_utc | timestamptz | |

## 3. 권장 인덱스

- `documents(issuer_id, filing_date desc)`
- `documents(form_type, filing_date desc)`
- `events(event_type, event_date desc)`
- `events(primary_document_id)`
- `market_bars_daily(symbol_id, trade_date desc)`
- `market_bars_intraday(symbol_id, timeframe, bar_start_utc desc)`
- `short_sale_daily(ticker_raw, trade_date desc)`
- `job_runs(job_name, run_date desc)`

## 4. DuckDB / Parquet 권장 구조

```text
data/parquet/
  market/daily/year=2026/month=03/*.parquet
  market/intraday/date=2026-03-12/*.parquet
  events/year=2026/month=03/*.parquet
  features/version=v1/date=2026-03-12/*.parquet
  labels/horizon=3d/*.parquet
```

## 5. SQL DDL 초안

```sql
create table if not exists source_registry (
  source_name text primary key,
  source_type text not null,
  enabled boolean not null default true,
  config_json jsonb not null default '{}'::jsonb,
  created_at_utc timestamptz not null default now(),
  updated_at_utc timestamptz not null default now()
);

create table if not exists issuer_master (
  issuer_id text primary key,
  cik text unique,
  ticker text,
  issuer_name text not null,
  exchange text,
  country_code text,
  is_active boolean not null default true,
  created_at_utc timestamptz not null default now(),
  updated_at_utc timestamptz not null default now()
);

create table if not exists symbol_master (
  symbol_id text primary key,
  issuer_id text references issuer_master(issuer_id),
  ticker text not null,
  venue text,
  asset_type text,
  currency text,
  start_date date,
  end_date date,
  is_primary boolean not null default true,
  created_at_utc timestamptz not null default now(),
  updated_at_utc timestamptz not null default now()
);

create table if not exists job_runs (
  job_run_id uuid primary key,
  job_name text not null,
  source_name text,
  run_date date,
  status text not null,
  started_at_utc timestamptz not null default now(),
  finished_at_utc timestamptz,
  records_seen integer not null default 0,
  records_written integer not null default 0,
  records_skipped integer not null default 0,
  error_count integer not null default 0,
  error_summary text,
  metadata_json jsonb not null default '{}'::jsonb
);
```

나머지 DDL은 migration 파일에서 분리 관리합니다.

## 6. 스키마 검증 체크포인트

- 동일 accession 문서 중복 적재가 없어야 합니다.
- 동일 symbol/date/day-bar 중복 적재가 없어야 합니다.
- parser output은 event와 별도 테이블에 버전별 보존되어야 합니다.
- raw_object가 없으면 documents를 만들 수 없어야 합니다.
- issuer/symbol 매핑 실패는 nullable 허용하되 경고 로그를 남겨야 합니다.
