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

#12 [모듈1-6] opendartreader + 규칙 기반 공시 분류

by 로띠 2026. 3. 6.
데이터 수집

opendartreader + 규칙 기반 공시 분류

DART 공시를 ML 피처로 바꾸는 전체 여정

DART API opendartreader 규칙 분류 NLP 센티먼트 Feature Store ML 학습

주가를 예측하려면 기업이 공식적으로 발표하는 정보를 빠뜨릴 수 없습니다. 이 글에서는 금융감독원의 DART 공시를 수집하고, 7가지 유형으로 자동 분류한 뒤, ML 피처로 변환하는 전체 과정을 초보자도 이해할 수 있도록 하나씩 풀어봅니다.

1. DART가 뭔데? 왜 중요한가?

DART = 전자공시시스템

DARTData Analysis, Retrieval and Transfer System의 약자로, 금융감독원이 운영하는 전자공시시스템입니다. 한국에 상장된 모든 기업은 중요한 경영 사항을 이 시스템을 통해 의무적으로 공개해야 합니다.

쉽게 말하면, 공시는 “회사가 공식적으로 발표하는 뉴스”입니다.
인터넷 루머나 찌라시가 아니라, 법적 책임이 따르는 팩트입니다.

어떤 내용이 공시될까?

기업이 DART에 공시하는 대표적인 사례를 살펴보겠습니다.

공시 유형 예시 비유
실적 발표 “영업(잠정)실적(공정공시)” 성적표 공개
유상증자 “유상증자결정” 새 주식 발행 (기존 주주 가치 희석)
합병 “합병결정” 두 회사가 하나로
자사주 매입 “자기주식취득결정” 회사가 자기 주식을 사들임 (호재 신호)
정기보고 “사업보고서” 연간 종합 건강검진표

왜 주가 예측에 중요한가?

공시는 주가에 즉각적이고 강력한 영향을 미칩니다. “역대 최대 실적” 공시 직후 상한가를 치는 경우도, “유상증자 결정” 공시 직후 주가가 급락하는 경우도 흔합니다.

ML 관점에서 핵심: 공시가 나왔다는 사실 자체가 중요한 피처입니다. “무슨 공시인가”(유형)도 중요하지만, “최근에 공시가 갑자기 많아졌다”(빈도 변화)는 것만으로도 기업에 무언가 중대한 일이 진행 중이라는 시그널이 됩니다.

공시와 주가의 관계 (비유)

  일반 뉴스  =  “카더라” (불확실)
  DART 공시  =  “공식 발표” (법적 책임)

  “삼성전자 영업(잠정)실적(공정공시)”
       
  투자자: “실적이 좋네? / 나쁘네?”
       
  주가 급등 or 급락

2. opendartreader — DART API를 쉽게

DART API를 직접 쓰면?

금융감독원은 DART의 데이터를 프로그램으로 조회할 수 있도록 Open DART API를 제공합니다. 하지만 직접 사용하려면 꽤 복잡합니다.

해야 할 일 설명
API 키 발급 DART에 회원가입 후 API 키 신청
인증 처리 매 요청마다 API 키를 헤더에 포함
페이징 처리 공시가 많으면 여러 페이지로 나뉨 → 반복 요청 필요
XML/JSON 파싱 응답 데이터를 직접 파싱해야 함
에러 처리 API 제한, 네트워크 오류 등 예외 처리

이걸 매번 직접 구현하는 건 비효율적입니다. 여기서 opendartreader가 등장합니다.

opendartreader: 한 줄이면 충분

opendartreader는 DART API를 파이썬에서 한 줄로 사용할 수 있게 만든 오픈소스 라이브러리입니다. 인증, 페이징, 파싱을 모두 내부에서 처리해주고, 결과를 pandas DataFrame으로 깔끔하게 반환합니다.


# 설치
pip install opendartreader

# 사용법
import OpenDartReader

dart = OpenDartReader("YOUR_API_KEY")

# 공시 목록 조회 (삼성전자, 2024년 1분기)
df = dart.list("005930", start="20240101", end="20240331")
# → DataFrame: report_nm, rcept_no, rcept_dt, corp_name, flr_nm

# 재무제표 조회
fs = dart.finstate("005930", 2023)
# → ROE, 부채비율, 매출액 등

비유: DART API를 직접 쓰는 건 “외국 관공서에서 서류를 직접 발급받는 것”이고, opendartreader를 쓰는 건 “대행 서비스를 이용하는 것”입니다. 결과물은 같지만, 과정이 훨씬 간단합니다.

반환 데이터 구조

dart.list()가 반환하는 DataFrame의 주요 컬럼을 알아보겠습니다.

컬럼명 설명 예시
report_nm 공시 제목 “영업(잠정)실적(공정공시)”
rcept_no 접수번호 (고유 ID) 20240305000123
rcept_dt 접수일자 20240305
corp_name 회사명 삼성전자
flr_nm 공시 제출인 삼성전자

이 중에서 ML에 가장 중요한 것은 report_nm (공시 제목)입니다. 공시 제목에는 유형 정보가 담겨 있기 때문입니다. “영업(잠정)실적”이라는 제목만 봐도 “아, 실적 발표 공시구나”라고 알 수 있죠.

Alchemetric에서의 실제 사용

실제 프로젝트에서는 DisclosureFetcher 클래스가 opendartreader를 감싸서 사용합니다. API 키 관리, 에러 처리, 날짜 변환까지 내부에서 처리합니다.


# disclosure_fetcher.py (실제 코드 발췌)

class DisclosureFetcher:
    def __init__(self):
        self.api_key = settings.DART_API_KEY
        self._dart = None   # Lazy init (필요할 때만 생성)

    def _get_dart(self):
        if self._dart is None:
            import OpenDartReader
            self._dart = OpenDartReader(self.api_key)
        return self._dart

    def fetch_disclosures(self, code, start_date, end_date):
        dart = self._get_dart()
        df = dart.list(code, start=start_yyyymmdd, end=end_yyyymmdd)

        records = []
        for _, row in df.iterrows():
            report_type, type_score = _classify_report(row["report_nm"])
            records.append({
                "report_nm": row["report_nm"],
                "report_type": report_type,   # ← 규칙 분류 결과
                "type_score": type_score,     # ← 중요도 점수
            })
        return records

3. 규칙 기반 공시 분류 — 7가지 유형

왜 분류가 필요한가?

DART에서 가져온 공시 목록을 그대로 사용하면 문제가 있습니다. “사업보고서”“유상증자 결정”은 주가에 미치는 영향이 완전히 다릅니다. 사업보고서는 정기적으로 나오는 “일상적” 공시이고, 유상증자 결정은 주가에 직접 충격을 주는 “이벤트” 공시입니다.

비유: 우편물을 생각해보세요. “등기우편”(법원 서류, 세금 고지서)은 반드시 확인해야 하고, “광고 전단지”는 무시해도 됩니다. 공시도 마찬가지 — 중요도에 따라 분류해야 ML 모델이 “이건 중요한 공시다”를 구별할 수 있습니다.

7가지 유형 + 가중 점수

Alchemetric에서는 공시 제목(report_nm)에 특정 키워드가 포함되어 있는지를 검사하여 7가지 유형으로 분류합니다. 각 유형에는 type_score(0.2~1.0)를 부여하여 중요도를 수치화합니다.

유형 키워드 예시 type_score 주가 영향
실적 영업(잠정)실적, 매출액또는손익구조 1.0 매우 높음
구조변경 합병, 분할, 영업양수도 0.9 높음
주요사항 주요사항보고서, 소송 0.8 높음
지분 지분, 주식등의대량보유, 공개매수 0.7 중간
자본변동 유상증자, 무상증자, 전환사채 0.6 중간
정기보고 사업보고서, 분기보고서, 반기보고서 0.5 낮음
기타 위 키워드에 해당하지 않는 공시 0.2 미미

규칙 기반 분류 코드

실제 프로젝트에서 사용하는 분류 로직입니다. 공시 제목에 키워드가 포함되어 있으면 해당 유형으로 분류합니다. 어떤 키워드에도 해당하지 않으면 “기타”(0.2)로 분류합니다.


# 공시 제목 키워드 → report_type + type_score
_TYPE_RULES = [
    # (키워드 리스트, report_type, type_score)
    (["영업(잠정)실적", "매출액또는손익구조", "실적"],    "실적",     1.0),
    (["주요사항보고서", "소송"],                     "주요사항",  0.8),
    (["지분", "임원ㆍ주요주주", "주식등의대량보유", "공개매수"], "지분",     0.7),
    (["유상증자", "무상증자", "전환사채", "신주인수권"],  "자본변동",  0.6),
    (["사업보고서", "분기보고서", "반기보고서"],        "정기보고",  0.5),
    (["합병", "분할", "영업양수도"],               "구조변경",  0.9),
]


def _classify_report(report_nm: str) -> tuple[str, float]:
    """공시 제목에서 유형과 가중치 판별"""
    for keywords, rtype, score in _TYPE_RULES:
        for kw in keywords:
            if kw in report_nm:
                return rtype, score
    return "기타", 0.2

어떻게 작동하는지 따라가보기

예를 들어, 공시 제목이 "삼성전자 영업(잠정)실적(공정공시)"라고 해봅시다.


입력: report_nm = "삼성전자 영업(잠정)실적(공정공시)"

1차 검사: ["영업(잠정)실적", "매출액또는손익구조", "실적"]
    "영업(잠정)실적" in report_nm?  True!
    return ("실적", 1.0)

결과: report_type = "실적", type_score = 1.0 (가장 높은 중요도)

또 다른 예: 공시 제목이 "임원ㆍ주요주주특정증권등소유상황보고서"라면?


입력: report_nm = "임원ㆍ주요주주특정증권등소유상황보고서"

1차 검사: ["영업(잠정)실적", ...]  매칭 없음
2차 검사: ["주요사항보고서", "소송"]  매칭 없음
3차 검사: ["지분", "임원ㆍ주요주주", ...]
    "임원ㆍ주요주주" in report_nm?  True!
    return ("지분", 0.7)

왜 “규칙 기반”인가?
머신러닝이 아니라 if/in 키워드 매칭으로 분류합니다. 공시 유형은 DART의 양식이 정해져 있어서 제목이 매우 규칙적입니다. ML이 필요 없을 만큼 패턴이 명확하기 때문에 규칙 기반이 빠르고 정확합니다.

4. 이중 분석 — 규칙 분류 + NLP 센티먼트

규칙 분류만으로는 부족한 이유

규칙 분류는 공시의 “유형”(실적, 합병, 증자 등)은 잘 잡아냅니다. 하지만 같은 유형이라도 방향성은 다를 수 있습니다.

같은 “실적” 유형이지만... 방향 주가 반응
“역대 최대 영업이익 달성” 긍정 급등
“영업이익 전년비 50% 급감” 부정 급락

규칙 분류 결과는 둘 다 report_type="실적", type_score=1.0입니다. 유형은 같지만 방향이 정반대입니다. 이 방향성을 잡아내기 위해 NLP 센티먼트 분석을 추가합니다.

KR-FinBert-SC 센티먼트 분석

Alchemetric은 서울대학교 NLP 연구실이 공개한 KR-FinBert-SC(snunlp/KR-FinBert-SC) 모델을 사용합니다. 한국어 금융 텍스트에 특화된 감성 분류 모델로, 텍스트를 positive / negative / neutral 3가지로 분류합니다.


# 내부 점수 매핑
_LABEL_SCORE = {
    "positive": +1.0,    # 긍정적 공시
    "negative": -1.0,    # 부정적 공시
    "neutral":  0.0,     # 중립적 공시
}

이중 분석: 유형 + 방향성

규칙 분류와 NLP 센티먼트를 결합하면, 공시의 “무엇인가”(유형)와 “좋은가 나쁜가”(방향)를 동시에 파악할 수 있습니다.


# 최종 레코드 구조 (DB에 저장되는 형태)
{
    "report_type":      "실적",         ← 규칙 분류 (what?)
    "type_score":       1.0,           ← 중요도 (0.2~1.0)
    "sentiment_score":  +0.87,         ← NLP 센티먼트 (-1 ~ +1)
    "sentiment_label":  "positive",    ← 방향 (good or bad?)
}

비유: 병원
접수처(규칙 분류)에서 “이분은 내과 환자입니다”라고 진료과를 배정합니다. 그 다음 의사(NLP)가 실제 진찰을 해서 “양성입니다 / 음성입니다”를 판단합니다.

접수처만으로는 “어느 과인지”만 알 수 있고, 의사까지 거쳐야 “좋은 결과인지 나쁜 결과인지”까지 알 수 있습니다.

코드 흐름


# disclosure_service.py (실제 코드 발췌)

def collect_disclosures(self, market, codes, ...):
    # Step 1: opendartreader로 공시 수집 + 규칙 분류
    result = self.disclosure_fetcher.fetch_all(codes, market, ...)
    # → 각 레코드에 report_type, type_score 포함

    # Step 2: 공시 제목에 NLP 센티먼트 분석
    if analyze_sentiment and result.records:
        result.records = self._analyze_sentiment(result.records)
        # → 각 레코드에 sentiment_score, sentiment_label 추가

    # Step 3: DB 저장 (dart_disclosure 테이블)
    saved = repo.upsert_disclosures(result.records)

5. ML 피처로의 변환 (Phase 5)

공시 데이터가 어떻게 ML 피처가 되는가?

DB에 저장된 개별 공시 레코드들은 그 자체로는 ML 모델에 넣을 수 없습니다. 날짜별로 정해진 개수의 숫자로 변환해야 합니다. Alchemetric은 공시 데이터를 5개의 숫자 피처로 변환합니다.

피처 계산 방식 ML에게 알려주는 것
disclosure_count_30d 최근 30일간 공시 건수 “요즘 이 회사에 공시가 많다/적다”
days_since_disclosure 마지막 공시 이후 경과일 “최근에 공시가 있었다/오래됐다”
disclosure_sentiment 30일 센티먼트 평균 “최근 공시들의 분위기가 긍정적/부정적”
disclosure_type_score 30일 type_score 평균 “최근 공시들이 중요한 유형인가/사소한 유형인가”
disclosure_volume_change 최근 30일 vs 이전 30일 건수 변화율 “공시 활동이 늘고 있다/줄고 있다”

Rolling Window 개념

위 피처들은 모두 30일 슬라이딩 윈도우로 계산됩니다. 특정 날짜의 피처를 구할 때, 그 날짜로부터 과거 30일간의 공시 데이터를 모아서 집계하는 방식입니다.


Rolling Window 시각화

날짜:  1월    2월    3월    4월    5월
       |------|------|------|------|

3월 15일의 피처를 계산할 때:
                    |======30일======|
                2/13          3/15
              이 구간의 공시를 모아서 집계

3월 16일의 피처를 계산할 때:
                     |======30일======|
                 2/14          3/16
              윈도우가 하루 밀림 (슬라이딩!)

비유: “최근 30일”이라는 돋보기를 들고 타임라인 위를 한 칸씩 밀며 들여다보는 것과 같습니다. 매일 새로운 공시가 들어오고, 30일 지난 공시는 빠져나갑니다.

volume_change 계산 예시

disclosure_volume_change는 “최근 30일 공시 건수”를 “그 전 30일 공시 건수”와 비교합니다.


# 계산 로직 (feature_engineer.py 발췌)

# 최근 30일: 3건, 이전 30일: 1건
count_30d     = 3
count_prev30d = 1

vol_change = (count_30d - count_prev30d) / count_prev30d
# = (3 - 1) / 1 = 2.0
# 의미: 공시 활동이 200% 증가!

# 이전 기간이 0건이면?
if count_prev30d == 0 and count_30d > 0:
    vol_change = 1.0  # 100% 증가로 처리

6. 전체 파이프라인 흐름

지금까지 설명한 내용을 하나의 흐름으로 정리하면 다음과 같습니다. DART에서 시작해서 ML 학습까지 가는 전체 여정입니다.


DART 공시 → ML 피처 전체 파이프라인

DART API (금감원 전자공시시스템)
     
opendartreader.list()
       report_nm, rcept_no, rcept_dt, corp_name, flr_nm
     
_classify_report() — 규칙 기반 7유형 분류
       + report_type, type_score
     
SentimentAnalyzer — KR-FinBert-SC 센티먼트
       + sentiment_score, sentiment_label
     
dart_disclosure 테이블 (DB 저장)
     
Feature Engineering (Phase 5A)
       30일 롤링 윈도우로 5개 피처 계산
     
       disclosure_count_30d
       days_since_disclosure
       disclosure_sentiment
       disclosure_type_score
       disclosure_volume_change
     
feature_store → ML 학습/예측

관련 파일 구조

파일 역할
disclosure_fetcher.py DART API 호출 + 규칙 분류 (_TYPE_RULES, _classify_report)
sentiment_analyzer.py KR-FinBert-SC 센티먼트 분석 (Singleton)
disclosure_service.py 수집 + 센티먼트 + 저장 오케스트레이션
disclosure.py (models) DartDisclosure 테이블 스키마 정의
feature_engineer.py _merge_disclosure_features() — 5개 피처 계산

7. 실제 사례로 이해하기

실제 공시 제목이 들어왔을 때 규칙 분류 + NLP 센티먼트가 어떤 결과를 내는지 살펴봅시다.

사례 1: 실적 공시 (긍정)


입력: "삼성전자 영업(잠정)실적(공정공시)"

규칙 분류:
  "영업(잠정)실적" in 제목    True
  report_type  = "실적"
  type_score   = 1.0       (가장 높은 중요도)

NLP 센티먼트:
  sentiment_score = +0.72
  sentiment_label = "positive"

해석: 중요한 실적 공시 + 긍정적  주가 상승 가능성 높음

사례 2: 기타 공시 (중립)


입력: "삼성전자 타법인 주식 및 출자증권 처분결정"

규칙 분류:
  어떤 키워드에도 매칭 안 됨
  report_type  = "기타"
  type_score   = 0.2       (가장 낮은 중요도)

NLP 센티먼트:
  sentiment_score = 0.0
  sentiment_label = "neutral"

해석: 사소한 공시 + 중립  주가 영향 미미

사례 3: 자본변동 공시 (부정)


입력: "삼성전자 유상증자결정"

규칙 분류:
  "유상증자" in 제목    True
  report_type  = "자본변동"
  type_score   = 0.6       (중간 중요도)

NLP 센티먼트:
  sentiment_score = -0.65
  sentiment_label = "negative"

해석: 주주 가치 희석 우려  주가 하락 가능성

3가지 사례 비교

공시 제목 유형 type_score 센티먼트 방향
영업(잠정)실적(공정공시) 실적 1.0 +0.72 positive
타법인 주식 처분결정 기타 0.2 0.0 neutral
유상증자결정 자본변동 0.6 -0.65 negative

이렇게 수집된 공시 데이터들이 30일 윈도우로 집계되어 5개 피처가 되고, 이 피처들은 가격 지표, 재무 지표, 뉴스 센티먼트 등 다른 피처들과 합쳐져서 최종적으로 ML 모델의 입력이 됩니다.

정리: DART 공시 분석의 핵심은 이중 레이어입니다.
1층 — 규칙 분류: 공시가 “무엇”인지 (유형 + 중요도)
2층 — NLP 센티먼트: 공시가 “좋은지 나쁜지” (방향성)

이 두 가지 정보가 합쳐져서 ML 모델에게 “이 시점에 이 회사에 어떤 공식 이벤트가 있었고, 그것이 긍정적이었는지 부정적이었는지”를 알려줍니다.
Alchemetric — AI 기반 주가 예측 플랫폼