AlgoLab Blog · 경제×자동매매 · 2026

DART 잠정실적 공시 API — 실적 시즌에 봇이 실적 발표일을 거르는 법

실적 시즌 · OpenDART 2026-10-01 · 알고랩 AlgoLab
한 줄 요약

잠정실적 발표일을 봇이 거르려면 금융감독원 OpenDART 공시검색 API(/api/list.json)를 pblntf_ty=I(거래소공시)·pblntf_detail_ty=I002(공정공시)로 당일 조회하고, report_nm에 (잠정)실적이 들어간 종목을 당일 신규진입 제외 목록에 넣으면 됩니다. 함정은 하나 — 응답의 rcept_dt는 날짜(YYYYMMDD)뿐이고 시각이 없습니다. 장중에 "몇 시에 떴나"가 필요하면 봇이 처음 본 시각을 직접 기록하거나, 한국투자증권 KIS API의 FHKST01011800(종합 시황/공시 제목, 실전 전용)의 data_tm으로 보완합니다.

10월 초·중순은 국내 상장사의 3분기 잠정실적 공시 시즌입니다. 삼성전자는 2025년 3분기 잠정실적을 10월 14일에 냈고(추석 연휴가 끼어 예년보다 늦었다), 대형주가 먼저 내고 나머지가 뒤따르는 패턴이 반복됩니다. 올해 개별 종목 일정은 회사 공시와 DART에서 직접 확인해야 합니다 — 이 글은 날짜를 예측하지 않고, 날짜가 오면 봇이 알아채는 구조만 다룹니다.

이 글은 OpenDART 재무제표 API — 데이터가 늦게 들어오는 이유의 후속입니다. 그 글이 "재무 숫자를 언제 쓸 수 있나"였다면, 여기서는 "실적 공시가 뜬 그날 봇이 무엇을 하나"를 다룹니다. 금통위·FOMC 같은 거시 이벤트 가드는 경제 이벤트 리스크 관리에 있고, 이 글은 그 빈칸인 개별 종목 이벤트입니다.

1. 잠정실적은 DART에 어떤 이름으로 뜨나

잠정실적은 거래소 공정공시로 제출됩니다. OpenDART 공식 가이드의 공시유형 표에서 I = 거래소공시, 그 아래 상세유형 I002 = 공정공시입니다. 봇이 잡아야 할 보고서명(report_nm)은 이렇게 생겼습니다.

report_nm뜻봇 처리
연결재무제표기준영업(잠정)실적(공정공시)연결 기준 잠정실적이벤트
영업(잠정)실적(공정공시)별도(개별) 기준 잠정실적이벤트
연결재무제표기준영업실적등에대한전망(공정공시)실적 전망(가이던스)잠정실적 아님 — 별도 판단
[기재정정]연결재무제표기준영업(잠정)실적(공정공시)이미 낸 공시의 정정같은 종목 두 번째 → 중복 제거

그래서 문자열 비교는 전체 이름 일치가 아니라 (잠정)실적 부분 일치로 합니다. 연결·별도 두 가지를 한 번에 잡고, "전망" 공시는 괄호 위치가 달라 자연스럽게 빠집니다. [기재정정]·[첨부정정] 같은 접두어는 공식 가이드가 정의한 정정 표시라서, 같은 날 같은 종목이 두 번 잡히는 원인이 됩니다.

응답의 rm(비고)도 씁니다. 공식 정의상 유=유가증권시장본부 소관, 코=코스닥시장본부 소관, 연=연결 포함, 정=이후 정정신고 있음, 철=철회입니다. rm에 철이 붙은 건은 이벤트에서 빼는 것이 맞습니다.

2. list.json으로 당일 공정공시 받기 — 실물 코드

요청 인자는 공식 개발가이드(DS001 공시검색) 기준입니다. 봇에 필요한 것만 추리면 다음과 같습니다.

인자값주의
crtfc_key40자리 인증키코드에 쓰지 말고 환경변수로
bgn_de / end_de당일 YYYYMMDDcorp_code 없이 조회하면 기간 최대 3개월
pblntf_ty / pblntf_detail_tyI / I002거래소공시 / 공정공시
corp_clsY(유가) · K(코스닥)복수 지정 불가 → 두 번 호출
page_count100최대 100, 넘으면 total_page만큼 반복
import os, requests

DART_KEY = os.environ["DART_API_KEY"]        # 키는 코드·깃에 남기지 않는다
URL = "https://opendart.fss.or.kr/api/list.json"

def fetch_fair_disclosures(day: str, corp_cls: str) -> list:
    """day=YYYYMMDD, corp_cls='Y'(유가) 또는 'K'(코스닥) — 한 번에 하나만"""
    rows, page = [], 1
    while True:
        r = requests.get(URL, params={
            "crtfc_key": DART_KEY,
            "bgn_de": day, "end_de": day,
            "pblntf_ty": "I",              # 거래소공시
            "pblntf_detail_ty": "I002",    # 공정공시
            "corp_cls": corp_cls,
            "page_no": page, "page_count": 100,
        }, timeout=10).json()
        if r["status"] == "013":           # 조회된 데이터 없음 = 정상
            return rows
        if r["status"] != "000":
            raise RuntimeError(f'{r["status"]} {r["message"]}')
        rows += r["list"]
        if page >= int(r["total_page"]):
            return rows
        page += 1

def earnings_events(day: str) -> dict:
    hits = {}
    for cls in ("Y", "K"):
        for d in fetch_fair_disclosures(day, cls):
            if "(잠정)실적" not in d["report_nm"] or "철" in d["rm"]:
                continue
            if d["stock_code"]:
                hits.setdefault(d["stock_code"], d)   # 정정 재공시는 첫 건만
    return hits

응답은 이런 구조로 옵니다(필드 구성은 공식 가이드 기준, 값은 설명용 가상 값).

{
  "status": "000",
  "message": "정상",
  "page_no": 1,
  "page_count": 100,
  "total_count": 1,
  "total_page": 1,
  "list": [{
    "corp_cls": "Y",
    "corp_name": "예시전자",
    "corp_code": "00000000",
    "stock_code": "000000",
    "report_nm": "연결재무제표기준영업(잠정)실적(공정공시)",
    "rcept_no": "20261007900001",
    "flr_nm": "예시전자",
    "rcept_dt": "20261007",
    "rm": "유연"
  }]
}

rcept_no(14자리)는 공시뷰어 링크 https://dart.fss.or.kr/dsaf001/main.do?rcpNo=접수번호로 바로 이어지므로, 텔레그램 알림에 붙여 두면 사람이 원문을 1초 만에 엽니다(텔레그램 봇 모니터링).

3. 함정 4가지 — status 코드와 '시각 없음'

① rcept_dt에는 시각이 없다

공식 응답 정의에서 rcept_dt는 "공시 접수일자(YYYYMMDD)"입니다. 장 시작 전에 떴는지, 14시에 떴는지, 장 마감 후에 떴는지 list.json만으로는 모릅니다. 장중 봇이라면 폴링 루프에서 rcept_no를 처음 본 순간의 시각을 first_seen으로 저장하는 것이 유일한 방법입니다. 폴링 간격이 1분이면 오차도 최대 1분입니다.

② status "013"은 에러가 아니다

공식 메시지 표에서 013 = "조회된 데이타가 없습니다." 아침 이른 시간이나 공시가 없는 날에는 이게 정상 응답입니다. 000이 아니면 무조건 예외를 던지는 코드는 공시 없는 날마다 봇을 멈춥니다. 위 코드에서 013을 먼저 거른 이유입니다.

③ status "020" — 요청 제한

공식 가이드는 020을 "요청 제한을 초과하였습니다. 일반적으로는 20,000건 이상의 요청에 대하여" 발생한다고 적습니다. 계산해 보면 07:00~18:00를 1분 간격으로 돌려도 660분 × 유가·코스닥 2회 = 1,320회, 시즌 피크에 페이지가 3장씩 나와도 4,000회 안쪽입니다. 여유는 있지만 같은 키로 재무제표 수집까지 돌리면 합산된다는 점을 기억하십시오. 증권사 쪽 호출 제한은 별개입니다(KIS EGW00201 초당 제한).

④ corp_cls는 하나만, 기간은 3개월까지

corp_cls는 "복수조건 불가"라서 유가·코스닥을 한 요청에 못 묶습니다. 또 corp_code 없이 조회하면 검색 기간이 3개월로 제한되므로, 백테스트용으로 몇 년치를 모을 때는 분기 단위로 잘라 요청해야 합니다. 021(조회 가능한 회사 개수 초과, 최대 100건)도 같은 표에 있습니다.

4. 시각이 필요하면 — KIS FHKST01011800

한국투자증권 KIS Developers에는 종합 시황/공시(제목) API가 있습니다. 공식 저장소 examples_llm/domestic_stock/news_title 기준으로 URL은 /uapi/domestic-stock/v1/quotations/news-title, tr_id는 FHKST01011800이고, 응답에 data_dt(작성일자)·data_tm(작성시간)·hts_pbnt_titl_cntt(HTS 공시 제목)·dorg(자료원)·iscd1~iscd5(관련 종목코드)가 옵니다. 시각이 붙어 있다는 점이 DART list.json과 다릅니다.

# 공식 저장소 news_title() 사용 — 입력값 공백 = 현재 기준 전체
df = news_title(
    fid_news_ofer_entp_code="", fid_cond_mrkt_cls_code="",
    fid_input_iscd="005930",        # 공백이면 전 종목
    fid_titl_cntt="", fid_input_date_1="", fid_input_hour_1="",
    fid_rank_sort_cls_code="", fid_input_srno="",
)
hit = df[df["hts_pbnt_titl_cntt"].str.contains("잠정", na=False)]
print(hit[["data_dt", "data_tm", "hts_pbnt_titl_cntt", "dorg", "iscd1"]])

모의투자에서는 안 됩니다. 공식 저장소 legacy/README.md의 API 제공 목록에서 "종합 시황/공시(제목)"은 모의투자 제공 칸이 비어 있습니다. 모의 계좌로 테스트하는 봇은 이 경로를 쓸 수 없으니 DART 쪽을 1차로 두십시오(KIS 모의투자로 어디까지 검증되나). 또 제목 문구 형식은 자료원(dorg)마다 다를 수 있어, "잠정"이라는 키워드가 실제로 걸리는지 실데이터로 한 번 확인하고 쓰십시오.

5. 알아챈 다음 — 봇이 할 수 있는 3가지

공시를 잡는 것보다 중요한 건 잡은 뒤 무엇을 하느냐를 미리 정해 두는 것입니다. 실적 공시 직후에는 갭이나 급변이 생기기 쉽고, 급변 구간에서는 거래소 변동성완화장치(VI)가 발동해 2분간 단일가매매로 바뀔 수 있습니다(VI 발동 시 봇 처리). 선택지는 셋입니다.

list.json 폴링 report_nm에 (잠정)실적 A. 당일 신규진입 제외 신호가 나와도 매수 안 함 보유분은 그대로 돌파·단타형 전략 가장 흔한 기본값 B. 보유 한도 축소 공시 전 비중 상한을 낮추거나 손절 폭을 좁힘 집중 보유·레버리지 일정을 미리 알아야 함 C. 아무것도 안 함 기록·알림만 남김 규칙은 그대로 실행 분산·장기 리밸런싱형 백테스트가 이미 포함
잠정실적 공시 감지 후 선택지 — 어느 쪽이 맞는지는 전략의 보유 기간과 집중도가 정한다

A는 사후 대응이라 list.json만으로 됩니다. B는 사전 대응이라 "언제 낼지"를 미리 알아야 합니다. 회사가 IR 안내 등으로 일정을 미리 알리기도 하지만 방식이 제각각이라 list.json 한 줄로 자동화되지 않습니다 — 회사 IR 일정이나 사람이 넣는 달력이 필요합니다. C도 정당한 선택입니다. 수십 종목에 분산하고 월 단위로 리밸런싱하는 전략이라면 실적일을 거르는 것 자체가 백테스트와 다른 봇을 만드는 셈이기 때문입니다(리밸런싱 주기 실측).

# A안을 주문 직전 게이트로 — 신호 로직은 건드리지 않는다
blocked = set(earnings_events(today))          # {'000000', ...}

def can_enter(code: str) -> bool:
    if code in blocked:
        log.info("skip %s: 잠정실적 공시일", code)
        return False
    return True

게이트를 신호 계산이 아니라 주문 직전에 두는 이유는, 나중에 "이 규칙 때문에 놓친 매수가 몇 건이었나"를 로그로 셀 수 있게 하기 위해서입니다. 규칙이 성과에 준 영향을 모르면 다음 시즌에 유지할지 판단할 근거가 없습니다.

6. 백테스트에 넣을 때 — 공시 시각을 모른다는 사실까지

과거 잠정실적 공시일을 모아 백테스트에 "실적일 진입 제외"를 넣을 수 있습니다. 여기서 ①번 함정이 다시 등장합니다. rcept_dt가 날짜뿐이니 그날 장 시작 전에 알 수 있었는지 알 수 없습니다. 장 마감 후 공시를 "당일 아침에 알고 피했다"고 처리하면 미래 정보를 쓴 백테스트가 됩니다(룩어헤드 편향 찾는 법).

import pandas as pd

ev = pd.DataFrame(rows)[["stock_code", "rcept_dt"]]
ev["rcept_dt"] = pd.to_datetime(ev["rcept_dt"], format="%Y%m%d")

# 보수적 처리: 공시 당일은 진입 금지(시각 모름),
# 공시 '내용'을 신호에 쓰는 건 다음 거래일부터
block_days = set(zip(ev["stock_code"], ev["rcept_dt"]))

모을 때는 ④번 제약 때문에 bgn_de~end_de를 3개월 이내로 잘라 반복합니다. 결과가 "실적일 제외가 성과를 개선했다"로 나와도 그건 과거 구간에서의 결과일 뿐 미래를 보장하지 않습니다. 특히 한 분기에 대형 공시 몇 건이 결과를 좌우하는지 종목별로 쪼개 보십시오.

자주 묻는 것

잠정실적 공시는 OpenDART에서 어떤 공시유형으로 조회하나요?

거래소공시(pblntf_ty=I) 중 공정공시(pblntf_detail_ty=I002)입니다. 보고서명은 "연결재무제표기준영업(잠정)실적(공정공시)" 또는 "영업(잠정)실적(공정공시)"이므로 report_nm에 "(잠정)실적"이 포함됐는지로 거르면 연결·별도 두 가지를 함께 잡을 수 있습니다.

OpenDART list.json으로 공시 시각도 알 수 있나요?

아니요. 응답의 rcept_dt는 접수일자(YYYYMMDD)만 제공합니다. 장중 시각이 필요하면 폴링에서 접수번호(rcept_no)를 처음 본 시각을 직접 저장하거나, 한국투자증권 KIS API의 종합 시황/공시(제목) FHKST01011800 응답의 data_tm을 참고하십시오. 이 KIS API는 모의투자에서 제공되지 않습니다.

status 013이 나오는데 키가 잘못된 건가요?

아닙니다. 013은 "조회된 데이타가 없습니다"로, 해당 조건에 공시가 아직 없다는 정상 응답입니다. 키 문제는 010(등록되지 않은 키)·011(사용할 수 없는 키)·012(접근할 수 없는 IP)로 따로 옵니다.

OpenDART는 하루에 몇 번까지 호출할 수 있나요?

공식 가이드는 status 020(요청 제한 초과)이 일반적으로 20,000건 이상의 요청에서 발생한다고 안내합니다. 1분 간격 폴링을 유가·코스닥 두 번씩 해도 하루 수천 건 수준이지만, 같은 키로 재무제표 수집도 한다면 합산해서 계산하십시오.

고지. 이 글은 기술 자료이며 특정 종목·전략을 권유하거나 실적·주가 방향을 예측하지 않습니다. OpenDART 요청 인자·응답·메시지 코드는 금융감독원 OpenDART 개발가이드, KIS API는 한국투자증권 공식 GitHub 저장소(koreainvestment/open-trading-api)를 2026-10-01 기준으로 확인했습니다. API 스펙과 공시 일정은 바뀔 수 있으니 실제 운영 전 공식 자료를 다시 확인하십시오. 백테스트 결과는 과거 구간의 결과이며 미래 수익을 보장하지 않습니다.

실적 시즌 가드, 기존 봇에 붙이고 싶으신가요?

DART 공시 감지·진입 게이트·텔레그램 알림을 지금 쓰는 KIS·키움 봇에 맞춰 붙여 드립니다. 24시간 빠른 답변 가능합니다.

상담 문의하기