본문 바로가기
Project/🚀 AI Finance 2026

#3 Alchemetric ERD

by 로띠 2026. 2. 3.

1. 데이터베이스 개요

Alchemetric 프로젝트는 PostgreSQL 15를 프로덕션 데이터베이스로 사용하며, 로컬 개발 환경에서는 SQLite를 지원한다. ORM은 SQLAlchemy 2.0+를 사용하여 두 DBMS 간 호환성을 유지한다.

전체 데이터베이스는 18개 테이블로 구성되며, 크게 다섯 가지 영역으로 분류

코어 데이터 5개 — 주가, 종목정보, 펀더멘털, 재무제표, 거시경제 지표
NLP / 대체 데이터 3개 — 뉴스 센티먼트, DART 공시, KRX 수급
ML 파이프라인 4개 — 피처스토어, 모델 메타데이터, 학습 이력, 예측 결과
스케줄러 3개 — 스케줄 설정, 실행 로그, ML 학습 전용 설정
백테스트 3개 — 백테스트 실행, 거래 기록, 일별 에쿼티 커브
테이블명 영역 용도 예상 Row 수 상태
stock_price 코어 일봉 가격 데이터 (OHLCV) ~10M 운영중
stock_info 코어 종목 마스터 정보 ~10K 운영중
stock_fundamental 코어 일별 밸류에이션 + 투자자별 매매 ~5M 운영중
financial_statement 코어 분기별 재무제표 (DART) ~100K 운영중
macro_indicator 코어 거시경제 지표 11종 (EAV 구조) ~30K 운영중
news_sentiment NLP 뉴스 기사 + KR-FinBert-SC 센티먼트 ~500K 운영중
dart_disclosure NLP DART 전자공시 + 센티먼트 ~200K 운영중
krx_supply_demand NLP KRX 공매도 + 프로그램 매매 ~5M 운영중
feature_store ML 종목별 일별 피처 저장소 (59컬럼) ~10M 운영중
ml_model ML 학습된 모델 메타데이터 + 성능 지표 ~500 운영중
ml_training_log ML 모델 학습 이력 + Optuna 하이퍼파라미터 ~2K 운영중
ml_prediction ML 모델 예측 결과 + 매매 시그널 ~1M 운영중
schedule_job 스케줄러 크론 기반 스케줄 설정 ~50 운영중
schedule_log 스케줄러 스케줄 실행 이력 ~10K 운영중
job_target_code 스케줄러 스케줄 잡 대상 종목 리스트 (1:N) ~200 운영중
pipeline_step_log 스케줄러 파이프라인 스텝별 실행 이력 (1:N) ~50K 운영중
backtest_run 백테스트 백테스트 실행 메타데이터 + 성과 지표 ~500 운영중
backtest_trade 백테스트 개별 거래 기록 ~50K 운영중
backtest_daily 백테스트 일별 에쿼티 커브 ~100K 운영중

2. ER 다이어그램

아래 다이어그램은 Alchemetric 데이터베이스의 전체 테이블 관계를 나타냅니다. 실선 화살표는 외래키(FK) 관계, 점선은 논리적 조인 관계를 의미

  ┌───────────────────────────────────────────────────────────────────────────┐
  │                          CORE DATA TABLES (5)                             │
  └───────────────────────────────────────────────────────────────────────────┘

  stock_info                      macro_indicator
  ┌────────────────────┐           ┌─────────────────────┐
  │ id           PK    │           │ id            PK    │
  │ market       UQ ┐  │           │ date          UQ ┐  │
  │ code         UQ ┘  │           │ indicator_name UQ ┘ │
  │ name               │           │ value               │
  │ sector             │           │ change_pct          │
  │ industry           │           │ source              │
  │ updated_at         │           └─────────────────────┘
  └────────┬───────────┘                     :
           │                                 :
           │ (market, code)                  : (date, indicator_name)
           │                                 :
     ┌─────┼──────────────┐                  :
     │     │              │                  :
     ▼     ▼              ▼                  :
  stock_price    stock_fundamental   financial_statement
  ┌──────────────┐ ┌──────────────────┐ ┌───────────────────┐
  │ id       PK  │ │ id          PK   │ │ id           PK   │
  │ market   UQ ┐│ │ market      UQ ┐ │ │ market       UQ ┐ │
  │ code     UQ ││ │ code        UQ │ │ │ code         UQ │ │
  │ date     UQ ┘│ │ date        UQ ┘ │ │ period       UQ ┘ │
  │ open/high    │ │ per/pbr/eps      │ │ period_date       │
  │ low/close    │ │ bps/market_cap   │ │ revenue           │
  │ volume       │ │ div_yield        │ │ operating_profit  │
  │ created_at   │ │ foreign_ratio    │ │ net_income        │
  └──────┬───────┘ │ inst_net_buy     │ │ roe/roa           │
         :         │ foreign_net_buy  │ │ operating_margin  │
         :         │ individual_net.. │ │ net_margin        │
         :         └────────┬─────────┘ │ debt_ratio        │
         :                  :           │ source            │
         :                  :           └─────────┬─────────┘
         :                  :                     :

  ┌───────────────────────────────────────────────────────────────────────────┐
  │                    NLP / ALTERNATIVE DATA TABLES (3)                      │
  └───────────────────────────────────────────────────────────────────────────┘

  news_sentiment           dart_disclosure          krx_supply_demand
  ┌──────────────────┐    ┌──────────────────┐    ┌──────────────────────┐
  │ id          PK   │    │ id          PK   │    │ id            PK     │
  │ date             │    │ date             │    │ market        UQ ┐   │
  │ market           │    │ market           │    │ code          UQ │   │
  │ code (nullable)  │    │ code             │    │ date          UQ ┘   │
  │ title            │    │ corp_name        │    │ short_selling_vol    │
  │ description      │    │ report_nm        │    │ short_selling_ratio  │
  │ url         UQ ┐ │    │ rcept_no    UQ ┐ │    │ program_buy_vol      │
  │ source      UQ ┘ │    │ code        UQ ┘ │    │ program_sell_vol     │
  │ sentiment_score  │    │ report_type      │    │ margin_balance       │
  │ sentiment_label  │    │ type_score       │    │ source               │
  └────────┬─────────┘    │ sentiment_score  │    └──────────┬───────────┘
           :              │ sentiment_label  │               :
           :              └────────┬─────────┘               :
           :                       :                         :
           :·······················:·························:
           :                       :                         :
           ▼                       ▼                         ▼

  ┌────────────────────────────────────────────────────────────────────────────┐
  │                       ML PIPELINE TABLES (4)                               │
  └────────────────────────────────────────────────────────────────────────────┘

  feature_store (joins: 5 core + 3 NLP/대체 데이터 테이블)
  ┌───────────────────────────────────────────────────────────────────────────┐
  │ id PK │ market, code, date UQ │ Phase 1: 24 기술적 + Phase 2: 9 재무         │
  │       │                       │ + Phase 3: 11 거시 + Phase 4: 5 뉴스        │
  │       │                       │ + Phase 5: 10 공시/수급 + 4 targets         │
  │       │                       │ = 59 feature columns + 4 targets          │
  └───────────────────────────────────────────────────────────────────────────┘

  ml_model ──────────┬──── 1:N (FK) ────── ml_prediction
  ┌─────────────────┐ │                   ┌──────────────────────┐
  │ id          PK   │ │                   │ id              PK   │
  │ model_name  UQ ┐ │ │                   │ model_id    FK ──────┤ CASCADE
  │ version     UQ ┘ │ │                   │ market, code         │
  │ model_type       │ │                   │ prediction_date      │
  │ algorithm        │ │                   │ target_date          │
  │ market           │ │                   │ predicted_class      │
  │ target_column    │ │                   │ probability_up/down  │
  │ hyperparameters  │ │                   │ predicted_return     │
  │ feature_columns  │ │                   │ signal               │
  │ train dates      │ │                   │ confidence           │
  │ accuracy/f1/auc  │ │                   └──────────────────────┘
  │ mse/rmse/mae/r2  │ │
  │ model_path       │ │
  │ is_active        │ │
  └──────────────────┘ │
                       │
                       └──── 1:N (FK) ────── ml_training_log
                                            ┌──────────────────────┐
                                            │ id              PK   │
                                            │ model_id    FK ──────┤ SET NULL
                                            │ algorithm            │
                                            │ train/val dates      │
                                            │ status               │
                                            │ metrics_json         │
                                            │ feature_importance   │
                                            │ optuna_trials        │
                                            │ best_trial_value     │
                                            └──────────────────────┘

  ┌────────────────────────────────────────────────────────────────────────────┐
  │                       SCHEDULER TABLES (5)                                 │
  └────────────────────────────────────────────────────────────────────────────┘

  schedule_job ──────┬──── 1:N ──── job_step
  ┌──────────────────┐ │              ┌────────────────────┐
  │ id          PK   │ │              │ id            PK   │
  │ job_name    UQ   │ │              │ job_id    FK ──────┤
  │ market           │ │              │ step_type     UQ   │
  │ cron_expr        │ │              │ step_order         │
  │ days_back        │ │              │ enabled            │
  │ enabled          │ │              │ config     (JSON)  │
  │ description      │ │              └────────────────────┘
  └──────────────────┘ │
                       ├──── 1:N ──── job_target_code
                       │              ┌────────────────────┐
                       │              │ id            PK   │
                       │              │ job_id    FK ──────┤
                       │              │ code          UQ   │
                       │              │ name               │
                       │              └────────────────────┘
                       │
                       └──── 1:N ──── schedule_log
                                      ┌────────────────────┐
                                      │ id            PK   │
                                      │ job_id    FK ──────┤
                                      │ trace_id           │
                                      │ status             │
                                      │ started/finished   │
                                      │ total_codes        │
                                      │ success/failed     │
                                      │ trigger_by         │
                                      │ message            │
                                      └─────────┬──────────┘
                                                │
                                                └── 1:N ── pipeline_step_log
                                                    ┌──────────────────┐
                                                    │ id          PK   │
                                                    │ log_id  FK ──────┤
                                                    │ trace_id         │
                                                    │ step_type   UQ   │
                                                    │ step_order       │
                                                    │ status           │
                                                    │ duration_sec     │
                                                    │ saved_count      │
                                                    │ error_message    │
                                                    │ log_text         │
                                                    └──────────────────┘

  ┌────────────────────────────────────────────────────────────────────────────┐
  │                       BACKTEST TABLES (3)                                  │
  └────────────────────────────────────────────────────────────────────────────┘

  backtest_run ─────────┬──── 1:N (FK) ────── backtest_trade
  ┌──────────────────────┐ │                   ┌──────────────────────┐
  │ id              PK   │ │                   │ id              PK   │
  │ name                 │ │                   │ run_id      FK ──────┤ CASCADE
  │ market               │ │                   │ market, code         │
  │ strategy             │ │                   │ trade_date           │
  │ start_date/end_date  │ │                   │ action (BUY/SELL)    │
  │ config_json          │ │                   │ price/shares/amount  │
  │ initial_capital      │ │                   │ fee/tax              │
  │ transaction_fee      │ │                   │ signal_source        │
  │ tax_rate             │ │                   │ signal_confidence    │
  │ codes_json           │ │                   │ probability_up       │
  │ total_return         │ │                   │ cash_after           │
  │ annualized_return    │ │                   │ portfolio_value_after│
  │ sharpe/sortino/calmar│ │                   └──────────────────────┘
  │ max_drawdown         │ │
  │ win_rate             │ │
  │ profit_factor        │ │
  │ total_trades         │ │
  │ benchmark_return     │ │
  │ alpha                │ │
  │ status               │ │
  └──────────────────────┘ │
                           │
                           └──── 1:N (FK) ────── backtest_daily
                                                ┌──────────────────────┐
                                                │ id              PK   │
                                                │ run_id      FK ──────┤ CASCADE
                                                │ date          UQ ┐   │
                                                │ (run_id)      UQ ┘   │
                                                │ portfolio_value      │
                                                │ cash                 │
                                                │ positions_value      │
                                                │ daily_return         │
                                                │ cumulative_return    │
                                                │ drawdown             │
                                                │ benchmark_value      │
                                                │ benchmark_return     │
                                                │ positions_json       │
                                                └──────────────────────┘
  

3. 코어 데이터 테이블

3.1 stock_price

모든 종목의 일봉(OHLCV) 가격 데이터를 저장합니다. yfinance를 통해 한국(KOSPI/KOSDAQ) 및 미국(NYSE/NASDAQ) 시장 데이터를 수집하며, (market, code, date) 조합으로 유니크 제약을 설정하여 중복 삽입을 방지합니다.

CREATE TABLE stock_price (
    id            SERIAL          PRIMARY KEY,
    market        VARCHAR(10)     NOT NULL,    -- KOSPI, KOSDAQ, NYSE, NASDAQ
    code          VARCHAR(20)     NOT NULL,    -- 005930, AAPL
    date          DATE            NOT NULL,
    open          NUMERIC(15,2),
    high          NUMERIC(15,2),
    low           NUMERIC(15,2),
    close         NUMERIC(15,2),
    volume        BIGINT,
    created_at    TIMESTAMP       DEFAULT now(),

    CONSTRAINT uq_stock_price
        UNIQUE (market, code, date)
);

CREATE INDEX idx_stock_price_lookup
    ON stock_price (market, code, date);

3.2 stock_info

종목 마스터 테이블입니다. 종목 코드, 이름, 섹터, 산업 분류를 저장하며 (market, code)로 유니크 제약을 설정한다. 섹터별 필터링을 위해 (market, sector) 복합 인덱스를 제공

CREATE TABLE stock_info (
    id            SERIAL          PRIMARY KEY,
    market        VARCHAR(10)     NOT NULL,
    code          VARCHAR(20)     NOT NULL,
    name          VARCHAR(100),
    sector        VARCHAR(50),
    industry      VARCHAR(100),
    updated_at    TIMESTAMP       DEFAULT now(),

    CONSTRAINT uq_stock_info
        UNIQUE (market, code)
);

CREATE INDEX idx_stock_info_sector
    ON stock_info (market, sector);

3.3 stock_fundamental

KIS API를 통해 수집하는 일별 밸류에이션 지표(PER, PBR, EPS, BPS)와 투자자별 순매수 데이터를 저장합니다. 외국인 보유 비율, 기관/외국인/개인 순매수량을 포함하여 ML 피처의 펀더멘털 영역을 구성합니다.

CREATE TABLE stock_fundamental (
    id                  SERIAL          PRIMARY KEY,
    market              VARCHAR(10)     NOT NULL,
    code                VARCHAR(20)     NOT NULL,
    date                DATE            NOT NULL,

    -- 밸류에이션
    per                 NUMERIC(10,2),
    pbr                 NUMERIC(10,2),
    eps                 NUMERIC(15,2),
    bps                 NUMERIC(15,2),
    market_cap          BIGINT,
    div_yield           NUMERIC(8,4),

    -- 외국인 보유
    foreign_ratio       NUMERIC(8,4),

    -- 투자자별 순매수
    inst_net_buy        BIGINT,
    foreign_net_buy     BIGINT,
    individual_net_buy  BIGINT,

    created_at          TIMESTAMP       DEFAULT now(),

    CONSTRAINT uq_stock_fundamental
        UNIQUE (market, code, date)
);

CREATE INDEX idx_fundamental_lookup
    ON stock_fundamental (market, code, date);

3.4 financial_statement

DART API에서 수집하는 분기별 재무제표 데이터. 손익계산서(매출, 영업이익, 순이익), 수익성 지표(ROE, ROA, 영업이익률, 순이익률), 안정성 지표(부채비율)를 포함합니다. period 컬럼은 2024Q4, 2024A 형식으로 분기 및 연간 데이터를 구분한다.

CREATE TABLE financial_statement (
    id                SERIAL          PRIMARY KEY,
    market            VARCHAR(10)     NOT NULL,
    code              VARCHAR(20)     NOT NULL,
    period            VARCHAR(10)     NOT NULL,    -- 2024Q4, 2024A
    period_date       DATE            NOT NULL,    -- 분기 말일

    -- 손익계산서
    revenue           BIGINT,
    operating_profit  BIGINT,
    net_income        BIGINT,

    -- 수익성 지표
    roe               NUMERIC(10,2),
    roa               NUMERIC(10,2),
    operating_margin  NUMERIC(10,2),
    net_margin        NUMERIC(10,2),

    -- 안정성 지표
    debt_ratio        NUMERIC(10,2),

    source            VARCHAR(30)     DEFAULT 'dart',
    created_at        TIMESTAMP       DEFAULT now(),

    CONSTRAINT uq_financial_statement
        UNIQUE (market, code, period)
);

CREATE INDEX idx_financial_lookup
    ON financial_statement (market, code, period_date);

3.5 macro_indicator

거시경제 지표를 EAV(Entity-Attribute-Value) 구조로 저장한다. 하나의 테이블에 11개 지표를 indicator_name 컬럼으로 구분하여 유연한 지표 확장이 가능하다. 수집 소스는 yfinance, FinanceDataReader, FRED API 3종을 사용한다. (date, indicator_name)으로 유니크 제약을 설정

CREATE TABLE macro_indicator (
    id              SERIAL          PRIMARY KEY,
    date            DATE            NOT NULL,
    indicator_name  VARCHAR(50)     NOT NULL,    -- KRW_USD, VIX, ..., FED_RATE, USD_INDEX, US_CPI
    value           NUMERIC(15,4)   NOT NULL,
    change_pct      NUMERIC(10,6),               -- 전일 대비 변화율
    source          VARCHAR(30),                 -- yfinance / fdr / fred
    created_at      TIMESTAMP       DEFAULT now(),

    CONSTRAINT uq_macro_indicator
        UNIQUE (date, indicator_name)
);

CREATE INDEX idx_macro_lookup
    ON macro_indicator (indicator_name, date);

현재 수집 중인 11개 거시경제 지표는 다음과 같다.

indicator_name 수집 소스 티커/심볼 설명
KRW_USD yfinance KRW=X 원/달러 환율
VIX yfinance ^VIX CBOE 변동성 지수 (공포 지수)
KOSPI yfinance ^KS11 KOSPI 종합지수
SP500 yfinance ^GSPC S&P 500 지수
US_10Y yfinance ^TNX 미국 10년 국채 금리
KR_3Y FinanceDataReader INVESTING: 한국 3년 국채 금리
WTI yfinance CL=F WTI 원유 선물 가격
GOLD yfinance GC=F 금 선물 가격
FED_RATE FRED API FEDFUNDS 미국 연방기금 기준금리
USD_INDEX FRED API DTWEXBGS 달러 인덱스 (무역가중)
US_CPI FRED API CPIAUCSL 미국 소비자물가지수 (CPI)

4. NLP / 대체 데이터 테이블

Phase 4~5에서 추가된 NLP 및 대체 데이터 테이블. 네이버 뉴스 API + KR-FinBert-SC 모델로 뉴스 센티먼트를 분석하고, DART 전자공시와 KRX 수급(공매도/프로그램매매) 데이터를 수집

4.1 news_sentiment

네이버 뉴스 API를 통해 수집한 뉴스 기사에 KR-FinBert-SC(한국어 금융 센티먼트 모델)로 감성 점수를 부여한다. code가 NULL이면 시장 전체 뉴스, 값이 있으면 종목별 뉴스

CREATE TABLE news_sentiment (
    id                SERIAL          PRIMARY KEY,
    date              DATE            NOT NULL,
    market            VARCHAR(10)     NOT NULL DEFAULT 'KR',
    code              VARCHAR(20),                 -- NULL = 시장 전체
    title             VARCHAR(500)    NOT NULL,
    description       TEXT,
    url               VARCHAR(1000),
    source            VARCHAR(30)     DEFAULT 'naver',

    -- KR-FinBert-SC 센티먼트
    sentiment_score   NUMERIC(6,4),               -- -1.0 ~ +1.0
    sentiment_label   VARCHAR(20),                -- positive / negative / neutral

    created_at        TIMESTAMP       DEFAULT now(),

    CONSTRAINT uq_news_sentiment
        UNIQUE (url, code)
);

CREATE INDEX idx_news_lookup    ON news_sentiment (market, code, date);
CREATE INDEX idx_news_date      ON news_sentiment (date);
CREATE INDEX idx_news_code_date ON news_sentiment (code, date);

4.2 dart_disclosure

DART API를 통해 수집하는 전자공시 데이터. 공시 유형(실적, 지분변동 등)에 따라 type_score 가중치를 부여하며, KR-FinBert-SC로 공시 제목의 센티먼트를 분석한다. rcept_no(접수번호)가 공시 고유 식별자

CREATE TABLE dart_disclosure (
    id                SERIAL          PRIMARY KEY,
    date              DATE            NOT NULL,
    market            VARCHAR(10)     NOT NULL,
    code              VARCHAR(20)     NOT NULL,
    corp_name         VARCHAR(100),
    report_nm         VARCHAR(500)    NOT NULL,    -- 공시 제목
    rcept_no          VARCHAR(20)     NOT NULL,    -- 접수번호
    flr_nm            VARCHAR(100),                -- 제출인
    rcept_dt          VARCHAR(10),                 -- 접수일 (YYYYMMDD)

    -- 공시 유형 분류
    report_type       VARCHAR(50),                 -- 실적, 지분, 기타 등
    type_score        NUMERIC(4,2)    DEFAULT 0.2,  -- 실적=1.0, 지분=0.8, ...

    -- 센티먼트
    sentiment_score   NUMERIC(6,4),
    sentiment_label   VARCHAR(20),

    created_at        TIMESTAMP       DEFAULT now(),

    CONSTRAINT uq_dart_disclosure
        UNIQUE (rcept_no, code)
);

CREATE INDEX idx_disclosure_lookup ON dart_disclosure (market, code, date);
CREATE INDEX idx_disclosure_date   ON dart_disclosure (date);

4.3 krx_supply_demand

KRX(한국거래소) 수급 데이터를 저장한다. 공매도(대차거래) 거래량/비율과 프로그램 매매(기관 알고리즘) 매수/매도량을 포함. pykrx 라이브러리를 통해 수집하며, 향후 신용잔고 데이터도 확장 가능

CREATE TABLE krx_supply_demand (
    id                    SERIAL          PRIMARY KEY,
    market                VARCHAR(10)     NOT NULL,
    code                  VARCHAR(20)     NOT NULL,
    date                  DATE            NOT NULL,

    -- 공매도
    short_selling_volume  BIGINT,
    short_selling_ratio   NUMERIC(8,4),               -- %

    -- 프로그램 매매
    program_buy_volume    BIGINT,
    program_sell_volume   BIGINT,

    -- 신용잔고 (향후 확장)
    margin_balance        BIGINT,

    source                VARCHAR(30)     DEFAULT 'pykrx',
    created_at            TIMESTAMP       DEFAULT now(),

    CONSTRAINT uq_krx_supply_demand
        UNIQUE (market, code, date)
);

CREATE INDEX idx_supply_demand_lookup
    ON krx_supply_demand (market, code, date);

5. ML 파이프라인 테이블

5.1 feature_store

종목별 일별 피처를 저장하는 중심 테이블. Phase 5 기준 총 69개 피처 컬럼으로 구성되며, 코어 5개 테이블과 NLP/대체 데이터 3개 테이블의 데이터를 조인하여 계산

CREATE TABLE feature_store (
    id              SERIAL          PRIMARY KEY,
    market          VARCHAR(10)     NOT NULL,
    code            VARCHAR(20)     NOT NULL,
    date            DATE            NOT NULL,

    -- ═══════════════════════════════════════════════════════
    -- Phase 1: 기술적 지표 (24 columns)
    -- ═══════════════════════════════════════════════════════

    -- 가격 피처 (6)
    close           NUMERIC(15,2),
    return_1d       NUMERIC(10,6),
    return_5d       NUMERIC(10,6),
    return_20d      NUMERIC(10,6),
    volatility_20d  NUMERIC(10,6),
    volume_ratio    NUMERIC(10,4),

    -- 이동평균 (3)
    sma_5           NUMERIC(15,4),
    sma_20          NUMERIC(15,4),
    sma_60          NUMERIC(15,4),

    -- 지수이동평균 (2)
    ema_12          NUMERIC(15,4),
    ema_26          NUMERIC(15,4),

    -- RSI (1)
    rsi_14          NUMERIC(8,4),

    -- MACD (3)
    macd            NUMERIC(15,4),
    macd_signal     NUMERIC(15,4),
    macd_histogram  NUMERIC(15,4),

    -- 볼린저 밴드 (5)
    bb_upper        NUMERIC(15,4),
    bb_middle       NUMERIC(15,4),
    bb_lower        NUMERIC(15,4),
    bb_width        NUMERIC(10,6),
    bb_pctb         NUMERIC(10,6),

    -- OBV (1)
    obv             NUMERIC(20,2),

    -- 파생 피처 (4 = 24 - 20)
    price_to_sma20  NUMERIC(10,6),
    price_to_sma60  NUMERIC(10,6),
    golden_cross    INTEGER,          -- 1: 골든크로스, 0: 없음
    rsi_zone        INTEGER,          -- -1: 과매도, 0: 중립, 1: 과매수

    -- ═══════════════════════════════════════════════════════
    -- Phase 2: 재무 피처 (+9 columns = 33 total)
    -- ═══════════════════════════════════════════════════════
    per             NUMERIC(10,2),
    pbr             NUMERIC(10,2),
    eps             NUMERIC(15,2),
    market_cap      BIGINT,
    foreign_ratio   NUMERIC(8,4),
    inst_net_buy    BIGINT,
    foreign_net_buy BIGINT,
    roe             NUMERIC(10,2),
    debt_ratio      NUMERIC(10,2),

    -- ═══════════════════════════════════════════════════════
    -- Phase 3: 거시경제 피처 (+11 columns = 44 total)
    -- ═══════════════════════════════════════════════════════
    krw_usd         NUMERIC(10,4),
    vix             NUMERIC(8,4),
    kospi_index     NUMERIC(10,2),
    us_10y          NUMERIC(8,4),
    kr_3y           NUMERIC(8,4),
    sp500           NUMERIC(10,2),
    wti             NUMERIC(10,2),
    gold            NUMERIC(10,2),
    fed_rate        NUMERIC(8,4),               -- FRED: 미국 기준금리
    usd_index       NUMERIC(10,4),              -- FRED: 달러 인덱스
    us_cpi          NUMERIC(10,4),              -- FRED: 미국 CPI

    -- ═══════════════════════════════════════════════════════
    -- Phase 4: 뉴스 센티먼트 피처 (+5 columns = 49 total)
    -- ═══════════════════════════════════════════════════════
    news_sentiment      NUMERIC(6,4),           -- 일평균 센티먼트
    news_volume         INTEGER,                -- 당일 뉴스 건수
    news_sentiment_std  NUMERIC(6,4),           -- 센티먼트 표준편차
    market_sentiment    NUMERIC(6,4),           -- 시장 전체 센티먼트
    market_news_volume  INTEGER,                -- 시장 전체 뉴스 건수

    -- ═══════════════════════════════════════════════════════
    -- Phase 5A: 공시 피처 (+5 columns = 54 total)
    -- ═══════════════════════════════════════════════════════
    disclosure_count_30d     INTEGER,           -- 30일 공시 건수
    days_since_disclosure    INTEGER,           -- 최근 공시 경과일
    disclosure_sentiment     NUMERIC(6,4),      -- 공시 센티먼트
    disclosure_type_score    NUMERIC(4,2),      -- 공시 유형 가중치
    disclosure_volume_change NUMERIC(10,4),     -- 공시 전후 거래량 변화

    -- ═══════════════════════════════════════════════════════
    -- Phase 5B: 수급 피처 (+5 columns = 59 total)
    -- ═══════════════════════════════════════════════════════
    short_selling_volume  BIGINT,              -- 공매도 거래량
    short_selling_ratio   NUMERIC(8,4),        -- 공매도 비율 (%)
    program_buy_volume    BIGINT,              -- 프로그램 매수량
    program_sell_volume   BIGINT,              -- 프로그램 매도량
    program_net_volume    BIGINT,              -- 프로그램 순매수

    -- ═══════════════════════════════════════════════════════
    -- Targets (4 columns)
    -- ═══════════════════════════════════════════════════════
    target_class_1d   INTEGER,        -- 1일 후 상승(1)/하락(0)
    target_class_5d   INTEGER,        -- 5일 후 상승(1)/하락(0)
    target_return_1d  NUMERIC(10,6), -- 1일 후 수익률
    target_return_5d  NUMERIC(10,6), -- 5일 후 수익률

    created_at        TIMESTAMP       DEFAULT now(),

    CONSTRAINT uq_feature_store
        UNIQUE (market, code, date)
);

CREATE INDEX idx_feature_store_lookup ON feature_store (market, code, date);
CREATE INDEX idx_feature_store_date   ON feature_store (date);
Phase 1 (24 기술적) + Phase 2 (+9 재무) + Phase 3 (+11 거시경제) + Phase 4 (+5 뉴스 센티먼트) + Phase 5 (+10 공시/수급) = 59 feature columns + 4 target columns

5.2 ml_model

학습이 완료된 ML 모델의 메타데이터를 저장한다. 분류(classification) 지표(accuracy, precision, recall, F1, AUC-ROC)와 회귀(regression) 지표(MSE, RMSE, MAE, R2)를 모두 포함하며, is_active 플래그로 서빙 중인 모델을 관리

CREATE TABLE ml_model (
    id                SERIAL          PRIMARY KEY,
    model_name        VARCHAR(100)    NOT NULL,
    model_type        VARCHAR(30)     NOT NULL,    -- classification / regression / reinforcement
    algorithm         VARCHAR(30)     NOT NULL,    -- random_forest / xgboost / lightgbm / ensemble / lstm / transformer / dqn / ppo
    market            VARCHAR(10)     NOT NULL,
    target_column     VARCHAR(50)     NOT NULL,    -- target_class_1d 등

    -- 학습 파라미터
    hyperparameters   VARCHAR(2000),               -- JSON
    feature_columns   VARCHAR(2000),               -- JSON
    train_start_date  DATE,
    train_end_date    DATE,
    train_sample_count INTEGER,

    -- 분류 성능 지표
    accuracy          NUMERIC(6,4),
    precision_score   NUMERIC(6,4),
    recall            NUMERIC(6,4),
    f1_score          NUMERIC(6,4),
    auc_roc           NUMERIC(6,4),

    -- 회귀 성능 지표
    mse               NUMERIC(15,6),
    rmse              NUMERIC(15,6),
    mae               NUMERIC(15,6),
    r2_score          NUMERIC(8,6),

    -- 모델 파일
    model_path        VARCHAR(500),
    is_active         BOOLEAN         DEFAULT FALSE,
    version           INTEGER         DEFAULT 1,

    created_at        TIMESTAMP       DEFAULT now(),
    updated_at        TIMESTAMP       DEFAULT now(),

    CONSTRAINT uq_ml_model
        UNIQUE (model_name, version)
);

CREATE INDEX idx_ml_model_active
    ON ml_model (market, model_type, is_active);

5.3 ml_training_log

모든 학습 시도의 이력을 기록합니다. 성공/실패와 무관하게 모든 학습 과정을 추적하며, Optuna 하이퍼파라미터 최적화 결과와 피처 중요도를 JSON으로 저장합니다. model_idSET NULL 외래키로, 모델 삭제 시에도 학습 로그가 보존됩니다.

CREATE TABLE ml_training_log (
    id                      SERIAL          PRIMARY KEY,
    model_id                INTEGER         REFERENCES ml_model(id)
                                            ON DELETE SET NULL,
    algorithm               VARCHAR(30)     NOT NULL,
    model_type              VARCHAR(30)     NOT NULL,
    market                  VARCHAR(10)     NOT NULL,
    target_column           VARCHAR(50)     NOT NULL,

    -- 학습 데이터 정보
    train_start_date        DATE,
    train_end_date          DATE,
    val_start_date          DATE,
    val_end_date            DATE,
    train_samples           INTEGER,
    val_samples             INTEGER,
    feature_count           INTEGER,

    -- 학습 결과
    status                  VARCHAR(20)     NOT NULL DEFAULT 'running',
    metrics_json            VARCHAR(3000),  -- JSON: 성능 지표
    feature_importance_json VARCHAR(5000),  -- JSON: 피처 중요도
    hyperparameters_json    VARCHAR(2000),  -- JSON: 하이퍼파라미터

    -- Optuna 최적화
    optuna_trials           INTEGER,
    best_trial_value        NUMERIC(10,6),

    started_at              TIMESTAMP       NOT NULL DEFAULT now(),
    finished_at             TIMESTAMP,
    error_message           VARCHAR(1000)
);

CREATE INDEX idx_training_log_model  ON ml_training_log (model_id);
CREATE INDEX idx_training_log_status ON ml_training_log (status, started_at);

5.4 ml_prediction

학습된 모델의 예측 결과를 저장한다. 분류 결과(상승/하락 확률)와 회귀 결과(예상 수익률)를 모두 포함하며, 최종적으로 BUY / SELL / HOLD 시그널과 신뢰도를 제공. 모델 삭제 시 관련 예측도 CASCADE로 함께 삭제

CREATE TABLE ml_prediction (
    id                SERIAL          PRIMARY KEY,
    model_id          INTEGER         NOT NULL
                                      REFERENCES ml_model(id)
                                      ON DELETE CASCADE,
    market            VARCHAR(10)     NOT NULL,
    code              VARCHAR(20)     NOT NULL,
    prediction_date   DATE            NOT NULL,
    target_date       DATE            NOT NULL,

    -- 분류 결과
    predicted_class   INTEGER,                     -- 1: 상승, 0: 하락
    probability_up    NUMERIC(6,4),
    probability_down  NUMERIC(6,4),

    -- 회귀 결과
    predicted_return  NUMERIC(10,6),

    -- 시그널
    signal            VARCHAR(10),                 -- BUY / SELL / HOLD
    confidence        NUMERIC(6,4),

    created_at        TIMESTAMP       DEFAULT now(),

    CONSTRAINT uq_ml_prediction
        UNIQUE (model_id, market, code, prediction_date)
);

CREATE INDEX idx_prediction_lookup ON ml_prediction (market, code, prediction_date);
CREATE INDEX idx_prediction_signal ON ml_prediction (signal, prediction_date);

6. 스케줄러 테이블

6.1 schedule_job

APScheduler와 연동되는 크론 기반 스케줄 설정 테이블. 작업 유형은 job_step 테이블의 스텝 조합으로 정의하며, 대상 종목은 job_target_code로 관리

CREATE TABLE schedule_job (
    id            SERIAL          PRIMARY KEY,
    job_name      VARCHAR(50)     UNIQUE NOT NULL,
    market        VARCHAR(10)     NOT NULL,
    cron_expr     VARCHAR(100)    NOT NULL,    -- "0 18 * * *"
    days_back     INTEGER         NOT NULL DEFAULT 7,
    enabled       BOOLEAN         NOT NULL DEFAULT TRUE,
    description   VARCHAR(200),
    created_at    TIMESTAMP       DEFAULT now(),
    updated_at    TIMESTAMP       DEFAULT now(),

    CONSTRAINT uq_schedule_job_name
        UNIQUE (job_name)
);

6.2 schedule_log

스케줄 실행의 상세 이력을 기록한다. trace_id로 하위 스텝 로그(pipeline_step_log)와 연결하며, 처리 대상 종목 수, 성공/실패/DB 저장 건수를 추적. 수동 실행(manual)과 자동 실행(scheduler)을 구분

CREATE TABLE schedule_log (
    id              SERIAL          PRIMARY KEY,
    job_id          INTEGER         NOT NULL
                                    REFERENCES schedule_job(id)
                                    ON DELETE CASCADE,
    trace_id        VARCHAR(36),                    -- UUID: 스텝 로그 연결 키
    status          VARCHAR(20)     NOT NULL
                                    DEFAULT 'running',
                                                    -- running / success / failed
    started_at      TIMESTAMP       NOT NULL DEFAULT now(),
    finished_at     TIMESTAMP,
    total_codes     INTEGER         DEFAULT 0,
    success_count   INTEGER         DEFAULT 0,
    failed_count    INTEGER         DEFAULT 0,
    db_saved_count  INTEGER         DEFAULT 0,
    trigger_by      VARCHAR(20)     NOT NULL
                                    DEFAULT 'manual',
                                                    -- manual / scheduler
    message         VARCHAR(500)
);

6.3 job_target_code

스케줄 잡의 대상 종목 리스트. schedule_job1:N 관계로, 잡 삭제 시 CASCADE로 함께 삭제된다. 동일 잡 내에서 종목 코드는 유일해야 한다

CREATE TABLE job_target_code (
    id            SERIAL          PRIMARY KEY,
    job_id        INTEGER         NOT NULL
                                  REFERENCES schedule_job(id)
                                  ON DELETE CASCADE,
    code          VARCHAR(20)     NOT NULL,
    name          VARCHAR(100),

    CONSTRAINT uq_job_target_code
        UNIQUE (job_id, code)
);

6.4 pipeline_step_log

파이프라인 스텝별 실행 이력. schedule_log1:N 관계로, 각 스텝의 시작/종료 시간, 소요시간, 저장 건수, 에러 메시지, 전체 로그 텍스트를 기록한다. 동일 실행 내에서 step_type은 유일해야 한다

CREATE TABLE pipeline_step_log (
    id              SERIAL          PRIMARY KEY,
    log_id          INTEGER         NOT NULL
                                    REFERENCES schedule_log(id)
                                    ON DELETE CASCADE,
    trace_id        VARCHAR(36)     NOT NULL,     -- UUID: 실행 추적 키
    step_type       VARCHAR(30)     NOT NULL,     -- price / fundamental / feature / ml …
    step_order      INTEGER         NOT NULL,
    status          VARCHAR(20)     DEFAULT 'pending',
                                                    -- pending / running / success / failed / skipped
    started_at      TIMESTAMP,
    finished_at     TIMESTAMP,
    duration_sec    INTEGER,
    saved_count     INTEGER         DEFAULT 0,
    summary         VARCHAR(500),
    error_message   VARCHAR(2000),
    log_text        TEXT,                             -- 전체 로그 텍스트

    CONSTRAINT uq_step_log_type
        UNIQUE (log_id, step_type)
);

7. 백테스트 테이블

7.1 backtest_run

백테스트 실행의 메타데이터와 성과 지표를 저장합니다. strategy 컬럼으로 ML 앙상블(ml_ensemble)과 단일 모델(single_model) 전략을 구분하며, Sharpe/Sortino/Calmar 비율, 최대 낙폭, 승률, 알파 등 핵심 성과 지표를 기록합니다.

CREATE TABLE backtest_run (
    id                INTEGER         PRIMARY KEY AUTOINCREMENT,
    name              VARCHAR(200)    NOT NULL,
    market            VARCHAR(10)     NOT NULL,
    strategy          VARCHAR(50)     NOT NULL,    -- ml_ensemble / single_model
    start_date        DATE            NOT NULL,
    end_date          DATE            NOT NULL,
    config_json       VARCHAR(5000),               -- JSON: model_ids, aggregation_method 등
    initial_capital   NUMERIC(15,2)   NOT NULL DEFAULT 10000000,
    transaction_fee   NUMERIC(8,6)    DEFAULT 0.00015,
    tax_rate          NUMERIC(8,6)    DEFAULT 0.0020,
    codes_json        VARCHAR(5000),               -- JSON: 종목 목록

    -- 성과 지표
    total_return       NUMERIC(10,6),
    annualized_return  NUMERIC(10,6),
    sharpe_ratio       NUMERIC(10,6),
    sortino_ratio      NUMERIC(10,6),
    max_drawdown       NUMERIC(10,6),
    calmar_ratio       NUMERIC(10,6),
    win_rate           NUMERIC(8,6),
    profit_factor      NUMERIC(10,4),
    total_trades       INTEGER,
    benchmark_return   NUMERIC(10,6),
    alpha              NUMERIC(10,6),

    -- 실행 상태
    status            VARCHAR(20)     DEFAULT 'running',
                                                  -- running / success / failed
    error_message     VARCHAR(1000),
    started_at        DATETIME,
    finished_at       DATETIME,
    created_at        DATETIME
);

CREATE INDEX idx_backtest_run_market
    ON backtest_run (market, status);
CREATE INDEX idx_backtest_run_date
    ON backtest_run (start_date, end_date);

7.2 backtest_trade

백테스트 실행 중 발생한 개별 거래 기록을 저장합니다. run_idbacktest_run과 연결되며, 매수/매도 시점의 가격, 수량, 수수료, 세금과 시그널 정보를 추적합니다. 백테스트 실행 삭제 시 관련 거래 기록도 CASCADE로 함께 삭제

CREATE TABLE backtest_trade (
    id                    INTEGER         PRIMARY KEY AUTOINCREMENT,
    run_id                INTEGER         NOT NULL
                                          REFERENCES backtest_run(id)
                                          ON DELETE CASCADE,
    market                VARCHAR(10)     NOT NULL,
    code                  VARCHAR(20)     NOT NULL,
    trade_date            DATE            NOT NULL,
    action                VARCHAR(10)     NOT NULL,    -- BUY / SELL
    price                 NUMERIC(15,2)   NOT NULL,
    shares                INTEGER         NOT NULL,
    amount                NUMERIC(15,2)   NOT NULL,
    fee                   NUMERIC(15,4),
    tax                   NUMERIC(15,4),

    -- 시그널 정보
    signal_source         VARCHAR(100),
    signal_confidence     NUMERIC(6,4),
    probability_up        NUMERIC(6,4),

    -- 거래 후 잔고
    cash_after            NUMERIC(15,2),
    portfolio_value_after NUMERIC(15,2),

    created_at            DATETIME
);

CREATE INDEX idx_backtest_trade_run
    ON backtest_trade (run_id);
CREATE INDEX idx_backtest_trade_code
    ON backtest_trade (run_id, code, trade_date);

7.3 backtest_daily

백테스트 실행의 일별 에쿼티 커브를 저장합니다. 포트폴리오 가치, 현금 잔고, 포지션 가치, 일별/누적 수익률, 낙폭, 벤치마크 대비 성과를 추적하며, positions_json에 종목별 보유 현황을 JSON으로 기록합니다. (run_id, date) 조합으로 유니크 제약을 설정

CREATE TABLE backtest_daily (
    id                INTEGER         PRIMARY KEY AUTOINCREMENT,
    run_id            INTEGER         NOT NULL
                                      REFERENCES backtest_run(id)
                                      ON DELETE CASCADE,
    date              DATE            NOT NULL,
    portfolio_value   NUMERIC(15,2)   NOT NULL,
    cash              NUMERIC(15,2),
    positions_value   NUMERIC(15,2),

    -- 수익률
    daily_return      NUMERIC(10,6),
    cumulative_return NUMERIC(10,6),
    drawdown          NUMERIC(10,6),

    -- 벤치마크
    benchmark_value   NUMERIC(15,2),
    benchmark_return  NUMERIC(10,6),

    -- 포지션 상세
    positions_json    VARCHAR(5000),               -- JSON: 종목별 보유 현황

    created_at        DATETIME,

    CONSTRAINT uq_backtest_daily
        UNIQUE (run_id, date)
);

CREATE INDEX idx_backtest_daily_run
    ON backtest_daily (run_id, date);

8. 인덱스 전략

인덱스 설계는 두 가지 원칙을 따른다.

 첫째, 유니크 제약 조건이 암묵적으로 생성하는 인덱스를 최대한 활용

 둘째, 조회 패턴(종목별 기간 조회, 날짜 범위 스캔, 상태 필터)에 맞춘 복합 인덱스를 별도로 생성

테이블 인덱스명 컬럼 용도
stock_price uq_stock_price market, code, date 중복 방지 + 종목별 기간 조회
stock_price idx_stock_price_lookup market, code, date 종목별 가격 조회 최적화
stock_info uq_stock_info market, code 종목 유니크 식별
stock_info idx_stock_info_sector market, sector 섹터별 종목 필터링
stock_fundamental uq_stock_fundamental market, code, date 중복 방지
stock_fundamental idx_fundamental_lookup market, code, date 펀더멘털 데이터 조회
financial_statement uq_financial_statement market, code, period 중복 방지
financial_statement idx_financial_lookup market, code, period_date 날짜 기반 재무제표 조회
macro_indicator uq_macro_indicator date, indicator_name 중복 방지
macro_indicator idx_macro_lookup indicator_name, date 지표별 시계열 조회
news_sentiment uq_news_sentiment url, code 중복 뉴스 방지
news_sentiment idx_news_lookup market, code, date 종목별 뉴스 조회
news_sentiment idx_news_date date 날짜별 뉴스 스캔
news_sentiment idx_news_code_date code, date 종목+날짜 복합 조회
dart_disclosure uq_dart_disclosure rcept_no, code 중복 공시 방지
dart_disclosure idx_disclosure_lookup market, code, date 종목별 공시 조회
dart_disclosure idx_disclosure_date date 날짜별 공시 스캔
krx_supply_demand uq_krx_supply_demand market, code, date 중복 방지
krx_supply_demand idx_supply_demand_lookup market, code, date 종목별 수급 조회
feature_store uq_feature_store market, code, date 중복 방지
feature_store idx_feature_store_lookup market, code, date 종목별 피처 조회
feature_store idx_feature_store_date date 날짜 범위 학습 데이터 추출
ml_model uq_ml_model model_name, version 모델 버전 유니크
ml_model idx_ml_model_active market, model_type, is_active 활성 모델 조회
ml_training_log idx_training_log_model model_id 모델별 학습 이력 조회
ml_training_log idx_training_log_status status, started_at 실행 상태별 조회
ml_prediction uq_ml_prediction model_id, market, code, prediction_date 중복 예측 방지
ml_prediction idx_prediction_lookup market, code, prediction_date 종목별 예측 조회
ml_prediction idx_prediction_signal signal, prediction_date 시그널별 종목 필터링 (BUY/SELL)
backtest_run idx_backtest_run_market market, status 시장별 백테스트 상태 조회
backtest_run idx_backtest_run_date start_date, end_date 기간별 백테스트 조회
backtest_trade idx_backtest_trade_run run_id 실행별 거래 기록 조회
backtest_trade idx_backtest_trade_code run_id, code, trade_date 종목별 거래 이력 조회
backtest_daily uq_backtest_daily run_id, date 중복 방지
backtest_daily idx_backtest_daily_run run_id, date 실행별 에쿼티 커브 조회
참고: PostgreSQL에서 UNIQUE 제약 조건은 자동으로 B-Tree 인덱스를 생성.
별도의 lookup 인덱스는 UNIQUE 제약과 컬럼 순서가 동일한 경우 중복으로 보일 수 있으나,
SQLite 호환성을 위해 명시적으로 선언.