과제 선택 배경
AI로 무언가 필요한것을 뚝딱 뚝딱 만들어보는 것은 이것 저것 해 봤기에 어려움은 없었지만, 재테크에 대한 공부와 접목 시키는 도전은 처음인지라, 어떻게 시작해야 할지 감이 잘 오지 않았다.
지난 스터디 과정을 통해 공공 데이터에서 수많은 데이터를 받을수 있고, 그 데이터를 활용할 수 있다는 힌트를 얻을 수 있었다.
그렇다면 어떤 데이터를 수집해서 의미 있는 분석을 할 수 있을지 고민해 봤다.
처음엔 부동산 쪽부터 훑어봤다. 아파트 실거래가 추이, 전월세 전환율, 미분양·인허가 현황, 인구이동과 가격의 상관관계 — 아이템 후보는 꽤 많았다.
그런데 막상 방향을 정하려니 걸리는 게 있었다. 부동산은 단위금액이 너무 컸다.
억 단위 숫자를 다루는 분석은 학습 목적으로는 체감이 잘 안 됐다.
그래서 방향을 주식·거시경제 쪽으로 틀었다.
주식/거시경제 쪽에서도 후보는 여럿이었다 — 지수 대비 개별종목 베타·변동성 분석, 배당·증자 같은 권리 이벤트 전후 주가 반응, 금리 사이클별 자산군 성과 비교, 그리고 기업 재무데이터와 주가를 엮는 팩터 분석. 등...
이 중 마지막 아이디어가 가장 끌렸다.
그 이유는 아무래도 목표를 장기적인 관점으로 공부를 하겠다는 생각이었던것 같다. — 지금 당장 실전 투자를 하려는 게 아니라, 데이터를 어떻게 수집하고 시각화하고 분석하는지 그 과정 자체를 익히는 것.
기업이 실적을 발표하는 시점 전후로 주가가 실제로 어떻게 움직이는지, 혹시라도 그 흐름이 보이지 않을까? 하는 생각에 우선 어떤식으로든 데이터를 모으고, 시각화 하는것 까지를 구현해 보기로 했다.
표본 설계: 종목과 기간부터 정하기
방향이 정해지고 나니, 다음 질문은 "어떤 종목을, 얼마나 오랜 기간 볼 것인가"였다. 처음부터 크게 벌이지 않기로 했다. 파일럿은 5개 종목, 기간은 최근 3년(12개 분기)로 잡았다.
종목은 그냥 아무거나 고르지 않고, 일부러 성격이 다른 것들을 섞었다.
종목
업종
선정 이유
삼성전자
반도체/IT
압도적 유동성, 잠정실적을 정식 발표 전에 먼저 공시하는 특이 케이스
NAVER
플랫폼/IT서비스
시장 기대와 실적의 괴리가 잦아 서프라이즈 효과가 뚜렷
KB금 융
금융
실적 변동성이 낮은 대조군
LG화학
화학/2차전지
원자재 가격 영향으로 반응 강도가 클 것으로 예상
오리온
소비재(음식료)
경기방어주 성격, 완만한 반응의 대조군
뭐.. 솔직히 AI에게 물어보고 추천한 항목을 넣었다.
PRD 한 장으로 정리하기
표본이 정해지고 나서, 실제 코드를 짜기 전에 PRD(제품 요구사항 문서)부터 정리했다.
Claude Code에게 개발을 맡길 생각이었기 때문에, 개발 착수 전에 요구사항을 문서 한 장으로 명확히 해두는 게 먼저라고 판단했다.
물론 이 PRD 문서 만드는 것 또한 AI에게 시켰다.
내가 한 것은 최종 산출물을 Review하고 수정이 필요한 부분이 있으면 그 부분만 다듬었을 뿐....
PRD에는 크게 이런 내용을 담았다:
종목 선택 기능: 사전 정의된 5종목 풀에서 사용자가 종목을 고르는 구조
데이터 수집 기능: 선택 종목의 최근 3년 실적 발표일 과 재무데이터를 DART에서, 개별 종목·지수 시세를 data.go.kr에서 가져와 매칭하는 파이프라인
분석 기능: 초과수익률(AR)과 누적초과수익률(CAR) 계산, 실적 서프라이즈(YoY/QoQ 증감률) 방향에 따른 그룹 비교
시각화 기능: 이벤트 윈도우(D-5~D+5) 기준 CAR 곡선 — 이벤트 스터디의 표준 차트 형태
비기능 요구사항: API 키 관리, 캐싱, 호출 제한 대응
마일스톤 M1~M6: 인증키 발급부터 종목 확장, 그룹 비교 기능까지 단계별 계획
리스크 항목: 삼성전자 잠정실적 이중 공시 문제, 컨센서스 데이터 부재로 인한 서프라이즈 프록시의 한계, data.go.kr 응답 형식(XML/JSON 혼용, HWP 문서화)의 번거로움을 미리 적어뒀다
한국사이트 스크린샷
DART API 사전 준비
PRD에서 재무데이터 소스로 DART(전자공시시스템)를 쓰기로 했으니, 코드를 짜기 전에 준비할 게 있었다.
앞서 우리가 실습과정에서 공공데이터 포탈에서도 API를 사용하기 위해서는 계정 생성 및 API키 생성이 필요했으니, 이번에도 그 과정이 당연히 필요할 것이라 생각했다.
그 외에 필요한 사전 준비작업 또한 당연히 있으리라 생각했다. 이 과정 또한 Claude 에게 물어보고, 그 가이드에 따라 실행했다.
opendart.fss.or.kr 회원가입 — 이메일 인증만으로 간단히 완료
인증키 신청 — 개인 신청자는 등록 즉시 40자리 인증키 발급, 일일 호출 한도 20,000건
종목코드 ↔ corp_code 매핑 준비 — DART는 증권시장의 6자리 종목코드가 아니라 자체 8자리 고유번호(corp_code)로 기업을 식별한다.
corp_code.xml을 미 리 받아서 매핑 테이블을 만들어둬야 이후 조회가 가능했다필요한 API 그룹 파악 — 정기보고서 재무정보(매출/영업이익/순이익), 공시서류검색(공시 접수일자) 두 그룹이 핵심
응답 형식 특이사항 확인 — 일부 API는 ZIP으로 응답이 오는 등 예외가 있어 개발 전 미리 확인
라이브러리 선택 —
dart-fss나OpenDartReader중 하나를 골라 corp_code 매핑과 파싱을 라이브러리에 맡기기로 함
여기까지가 코드를 짜기 전, 종이 위에서 끝낸 준비 작업이었다.
이제부터는 실제로 코드를 만들어야 한다.
Claude Code에게 맡기다
PRD에는 6단계 마일스톤(M1~M6)과 5개 종목 풀을 적어뒀지만, 실제 코드를 만들고 실행할 때는 삼성전자 1종목으로 전체 파이프라인을 끝까지 완성하는 것(M1~M4)을 목표로 잡았다.
실질적으로 내가 실행한 것은 하나밖에 없다.
PRD 문서를 붙여 넣고, 이 문서 기반으로 프로그램을 만들어 달라는 요청을 한 것.
그 뒤로는 Claude Code가 알아서 실행 & 오류 발견 & 수정 과정을 거쳐서 최종 산출물을 끌어냈다.
과정을 다 지나고 나서 그 중간에 있었던 삽질들(?)을 살펴보니 다음과 같은 시행착오의 과정들을 살펴 볼 수 있었다.
시작하자마자 틀어진 계획: data.go.kr의 배신
PRD에는 주가·지수 데이터 소스로 data.go.kr의 "금융위원회_주식시세정보/지수시세정보/KRX상장종목정보" API를 적어뒀었다.
그런데 Claude가 Plan Mode에서 조사해보니 — 세 개 API 전부 403 Forbidden이었다.
사실 이건 처음 겪는 일이 아니었다.
이 프로젝트 초반에 부동산 실거래가를 수집할 때도 국토교통부 "아파트 전월세" API가 똑같이 403을 뱉었던 적이 있다.
data.go.kr은 서비스키 하나로 여러 API를 쓸 수 있는 게 아니라, API 상품 하나하나마다 별도로 활용신청 승인을 받아야 한다.
이번에도 같은 패턴이었다.
다행히 이 프로젝트에는 이미 검증된 대안이 있었다. 주식 시세 수집에 써봤던 FinanceDataReader가 KOSPI 지수(KS11)도 키 없이 바로 가져와줬다.
Claude는 이 사실을 확인하자마자 "PRD 원안 대신 이걸 쓰는 게 어떠냐"고 먼저 제안했고, 나는 바로 승인했다.
계획 문서에 적힌 방법이 항상 최선은 아니다 — 실제로 부딪혀보고 조정하는 게 맞다는 걸 다시 확인할 수 있었다.
DART API와의 진짜 싸움
키 문제가 해결되고 나니, 진짜 난이도는 DART API 쪽에 있었다. 겉보기엔 단순해 보이는 "분기 실적 가져오기"가 실제로는 버그의 연속이었다.
버그 1: 사라진 0 DART는 회사를 6자리 종목코드가 아니라 8자리 corp_code로 식별한다. 삼성전자는 00126380. 문제는 이 값을 CSV로 캐싱했다가 다시 읽어올 때, pandas가 "얘 숫자처럼 생겼네?" 하고 정수로 인식해버리면서 앞자리 0 두 개가 그냥 증발해버린 것. 00126380 → 126380이 되니 당연히 API가 회사를 못 찾았다. dtype=str을 명시하지 않은 게 원인이었다.
버그 2: 완전히 잘못된 재무 로직 이게 제일 골치 아팠다. 분기 실적은 사업보고서·반기보고서·분기보고서로 나뉘어 공시되는데, 나는 당연히 "반기보고서는 상반기 누적치일 테니, 2분기 = 반기 - 1분기로 계산하면 되겠지"라고 가정하고 코드를 짰다.
실행해보니 숫자가 말이 안 됐다. 1분기 매출이 71.9조인데 계산된 2분기 매출이 2.1조? 4분기는 갑자기 221조? 삼성전자가 아무리 커도 이런 변동은 있을 수 없다.
Claude가 DART 응답 원본을 직접 찍어봤더니 — 반기보고서의 thstrm_amount 필드는 이미 2분기 단독 값이었다. 누적치는 thstrm_add_amount라는 별도의 필드에 따로 들어 있었다.
내(정확히는 Claude의) 가정이 완전히 거꾸로였던 것이다. 유일하게 사업보고서만 "당기"가 연간 합계라서, 4분기는 사업보고서 금액 - 3분기보고서 누적치로 역산해야 했다.
이 구조를 파악하고 나서야 71~93조 사이를 오가는, 상식적인 분기별 매출 흐름이 나왔다.
버그 3: "당기순이익"이라는 이름은 사업보고서에만 있다 분기·반기보고서에는 "당기순이익"이 아니라 "분기순이익(손실)", "반기순이익(손실)"이라는 다른 계정명으로 잡힌다.
하드코딩된 이름 하나로 매칭하던 코드는 당연히 실패했다. 계정명 후보 목록을 두고 순서대로 시도하는 방식으로 바꿨다.
버그 4: 1분기와 3분기가 같은 날 공시된 것처럼 잡힌 사고 분기보고서는 1년에 두 번(1분기, 3분기) 나온다. report_nm에 (2024.03), (2024.09)처럼 결산월이 붙어있는데, 이걸 무시하고 "분기보고서"라는 키워드 + 연도만으로 매칭했더니 둘 다 같은 날짜로 잡히는 사고가 났다. 정확한 월 접미사까지 매칭하도록 고치고 나서야 4개 분기가 3개월 간격으로 깔끔하게 배열됐다.
버그 5: 스키마가 바뀐 줄 모르고 캐시에 잘못 꽂아넣기 컬럼 이름 규칙(operating_profit_yoy → PRD 스키마에 맞춰 op_yoy로 통일)을 바꾼 뒤 재실행했는데도 여전히 옛날 컬럼명 에러가 났다. 원인은 DuckDB에 이미 있던 테이블에 INSERT INTO ... SELECT *를 했더니, 컬럼명이 아니라 위치 기준으로 새 데이터가 옛날 스키마에 욱여넣어진 것. 스크립트를 고쳐가며 반복 실행하는 개발 중에 흔히 나는 사고라, 이제는 저장 전에 기존 테이블과 컬럼 구성이 다르면 조용히 망가뜨리는 대신 명확히 에러를 내고 멈추도록 안전장치를 추가했다.
검증은 타협하지 않았다
버그를 다 잡았다고 끝난 게 아니었다. 계산이 정말 맞는지 확인하는 절차가 따로 있었다.
실 제 가격 데이터 + 검증용 임의 이벤트로 AR/CAR 계산 로직을 먼저 단위 테스트
삼성전자 2025년 1분기 실적 발표일(D0, 2025-05-15)의 초과수익률을 손으로 직접 계산해서 함수 출력과 대조 →
0.005533으로 소수점까지 정확히 일치DuckDB를 다시 읽어서 저장된 행 수·기간이 예상과 같은지 재확인
이 "손으로 한 번 더 계산해서 대조"하는 단계가 없었다면 버그 2번(누적치 오해) 같은 건 숫자가 "그럴듯하게 틀린" 채로 넘어갔을 수도 있다. 실제로 처음엔 코드가 에러 없이 잘 돌았다 — 단지 결과가 조용히 틀렸을 뿐이었다.
결과물
최종적으로 나온 것:
earnings_cli.py --stock 005930한 줄로 끝까지 도는 파이프라인DuckDB에 쌓인 raw 테이블 3종(공시, 종목 시세, KOSPI 지수) — 종목 단위 캐싱으로 재실행해도 API를 다시 호출하지 않음
이벤트 윈도우(D-5~D+5) 평균 CAR 곡선, 서프라이즈 그룹별 비교, 산점도 — Plotly 인터랙티브 HTML 3종
9개 분기 이벤트 기준 평균 CAR 1.10%, 표준편차 2.37%
한국의 인구수를 보여주는 그래프와 한국의 인구수를 보여주는 그래프
한국 주식 시장의 성장을 보여주는 그래프