토스증권 Open API로 나만의 포트폴리오 리포트 만들기 (1) — 매일 쌓고, 과거는 복원하고
시리즈: 토스증권 Open API로 나만의 포트폴리오 리포트 만들기
1편: 매일 쌓고, 과거는 복원하고 (이 글)
2편: Claude Code·Codex CLI로 종목 AI 분석 붙이기
왜 만들었나
토스증권 앱은 지금 상태를 보기엔 편하다. 그런데 "한 달 전 내 포트폴리오 수익률은 어땠지?", "이 종목 비중이 언제부터 이렇게 커졌지?" 같은 질문에는 답을 주지 않는다. 지나간 날의 계좌 상태가 남지 않기 때문이다.
토스증권이 Open API를 열었길래, 계좌와 보유종목을 매일 저장해 두는 개인용 리포트를 만들기로 했다. 목표는 단순하다.
- 매일 계좌 스냅샷을 쌓아서 일별·누적 수익률을 본다.
- 보유종목·관심종목을 차트와 보조지표로 본다.
- API에 없는 정보(밸류에이션, 공시, 배당, 실현손익)는 다른 데서 채운다.
개발은 거의 전부 Claude Code와 같이 했다. 계획 문서를 먼저 쓰게 하고, 확인한 다음 구현하는 식으로 진행했다.

설계 원칙: 최대한 가볍게
혼자 쓰는 앱이라 운영할 게 많으면 금방 안 쓰게 된다. 그래서 처음부터 원칙을 정했다.
- 서버 프로세스 하나, DB는 SQLite 파일 하나.
- 프런트는 Flask + Jinja 서버 렌더링에 순수 JS/CSS. 빌드 단계 없음.
- React, Node 서버, Redis, Celery, ORM은 쓰지 않는다.
- 무거운 수집은 배치에서만 한다. 웹 요청 중에는 pandas/pykrx를 import하지 않는다.
- 금액은
REAL이 아니라 API가 준 decimal 문자열을TEXT로 저장하고Decimal로 계산한다.
배포는 Docker Compose로 web(gunicorn)과 batch(cron) 컨테이너 두 개만 띄운다. 웹은 배치가 채워 둔 SQLite를 읽기만 하고, 새로고침·호가창처럼 실시간이 필요한 것만 그때그때 토스 API를 부른다.

데이터는 어디서 오나
| 출처 | 가져오는 것 |
| 토스 Open API | 계좌, 보유종목, 시세, 일봉, 주문내역, 수급, 호가, 시장 캘린더, 랭킹 |
| pykrx | 국내 종목 PER/PBR 등 밸류에이션, 시가총액, ETF 정보, 지수 |
| DART | 종목별 최근 공시 |
| 토스 웹 화면 | 입출금, 배당금, 실현손익 (API에 없음) |
원칙은 "토스에서 얻을 수 있으면 토스 걸 쓴다"이다. pykrx는 토스에 없는 밸류에이션·ETF 정보만 보충한다.
매일 쌓기: cron 배치
배치는 각자 독립 실행되는 sync_*.py 스크립트로 나눴다. 시간대는 데이터가 실제로 준비되는 시점에 맞췄다.
| 시각(KST) | 작업 | 이유 |
| 08:00 | 계좌·보유종목 스냅샷 | 미장 마감 이후 |
| 08:15 | Trend Score 계산 | 국장·미장 당일 캔들이 다 반영된 뒤 |
| 08:25 | DART 공시 | 아침 배치 묶음에 같이 |
| 08:30 | SQLite 백업 | 앞 작업이 끝난 뒤 |
| 18:10 | pykrx 밸류에이션 | KRX가 장 마감 후에 공표 |
| 일 09:00 | 종목 검색 카탈로그 | 주 1회면 충분 |
실패하면 30분 뒤 한 번만 재시도하고, 그래도 실패하면 텔레그램으로 알림을 보낸다. 국장·미장이 둘 다 휴장인 날은 토스 시장 캘린더 API로 확인해서 건너뛴다.
여기서 한 번 막힌 게 있다. cron 작업은 컨테이너 환경변수를 물려받지 못한다. 그래서 컨테이너가 뜰 때 환경변수를 파일로 떠 두고, 각 작업 전에 source하도록 바꿨다.
과거는 복원하기: 히스토리 백필
매일 쌓기 시작하면 그날부터의 이력만 생긴다. 처음 차트를 띄웠을 때 점이 두 개뿐이었다. 사실상 빈 기능이었다.
그래서 과거를 재구성하는 backfill_history.py를 만들었다. 토스 주문내역 API는 커서로 끝까지 넘기면 2023-05-26까지 나온다. 이걸로 다음 순서를 밟는다.
- 주문내역과 액면분할·합병 같은 기업 이벤트를 시간순으로 리플레이해서 종목별 보유수량·평단가 타임라인을 만든다.
- 그날그날의 종가를 붙인다. 국내는 pykrx, 해외는 토스 캔들.
- 환율은 새 API 없이, 실제 환전 기록(원화·외화 쌍)에서 역산한다.
- 일별 손익은 "평가손익 변화 + 그날 실현손익"으로 계산한다. 입금이나 매수 때문에 가짜 수익이 생기지 않게 하기 위해서다.
실데이터로 돌려 보니 문제가 또 나왔다. 일부 종목은 체결가와 시세가 5~10배씩 차이가 났다. 종목코드 재사용이나 합병 때문으로 보인다. 그대로 두면 누적수익률이 하루 만에 몇백 %로 튀었다. 이런 종목은 자동으로 감지해서 가격 계산에서 빼도록 안전장치를 넣었다.
마지막 날 재구성한 보유수량을 실제 보유와 비교해 검증했다. 대부분 맞았고, 안 맞는 건 재구성 이후 기간의 실거래로 설명이 됐다.

API에 없는 데이터: 브라우저에서 긁어 오기
입출금, 배당금, 실현손익은 Open API에 엔드포인트 자체가 없다. 토스증권 웹 화면에만 있다.
여기서는 Claude in Chrome을 썼다. 화면별 추출 스펙(필드, 형식)을 문서로 만들어 주고, 브라우저에서 화면을 읽어 JSON으로 뽑게 했다. 그 JSON을 앱의 "데이터 가져오기" 모달로 올리면 SQLite에 들어간다. 테이블의 기본키가 곧 중복 제거 키라서 같은 파일을 두 번 올려도 괜찮다.
그대로 믿지는 않았다. 뽑은 데이터의 내부 합계를 맞춰 봤더니 배당금·실현손익은 전부 맞았다. 그런데 거래내역의 "거래 후 잔액" 필드는 상당 부분이 틀어져 있었다. 같은 시각에 몰린 체결에서 잔액을 엉뚱한 행에 짝지은 것 같았다. 이 필드는 아예 저장하지 않기로 했다.
AI가 뽑은 데이터는 검증할 방법을 같이 만들어야 한다는 걸 다시 느꼈다.

화면 구성
지금은 이런 메뉴가 있다.
- 포트폴리오 홈: 평가금액·손익·실현손익·예수금 카드, 기간별 수익률 차트(KOSPI/KOSDAQ/S&P500 비교), 국장/미장 비중, 오늘 시장 급등 Top 10
- 보유·관심·과거종목: 같은 표 포맷을 재사용. 과거종목엔 "팔지 않고 계속 들고 있었다면(If I Had Held)" 컬럼이 있다.
- 종목 상세: KLineChart 캔들(이동평균·볼린저·RSI·MACD·피보나치), 매수·매도 마커, Trend Score, 밸류에이션, 수급, 호가창, 공시, 주문 내역
- 실시간 현황: 보유종목 카드 대시보드, 등락률 순 정렬과 급등 배지
- 랭킹: 토스 랭킹 API 기반 필터 목록
Trend Score는 20/60/120 거래일 추세를 수익률, 이동평균 기울기, 변동성으로 묶어 0~100점으로 만든 값이다. 1~120일 기간별로 상승·보합·하락 배지도 붙는다.
색은 국내 관행대로 상승은 빨강, 하락은 파랑이다. 앱 전체가 .gain/.loss 클래스로만 색을 쓰게 해 둬서 CSS 변수 두 개만 바꾸면 됐다. 캔들 차트는 스타일 시스템이 따로라 직접 지정해야 했다.



그 밖에 부딪힌 것들
- KRX 로그인 필수화: 2025년 말부터 KRX 정보데이터시스템이 회원제로 바뀌어 pykrx가 익명으로 데이터를 못 가져온다. KRX 계정을 만들어 환경변수로 넘기는 걸로 해결했다. 간편가입 계정은 안 되고 아이디/비밀번호 계정이어야 한다.
- database is locked: 배치와 웹이 동시에 쓰다 보니 가끔 났다. WAL 모드에 busy timeout을 30초로 늘려서 정리했다.
- 토큰 만료: 백필처럼 오래 도는 작업 중에 토큰이 만료돼 401이 쏟아졌다. 자동 재발급을 넣었다.
- 미국 종목 이름: 일부가 티커 그대로 저장돼서 토스 종목 상세 API로 보정했다.
정리
혼자 쓰는 도구라 "운영할 게 없게"를 최우선으로 뒀다. Flask와 SQLite 파일 하나, cron 배치 몇 개로도 꽤 많은 걸 할 수 있었다. 데이터가 없으면 쌓고, 쌓을 수 없는 과거는 주문내역으로 복원하고, API에 없는 건 브라우저에서 가져왔다.
다음 편에서는 이렇게 모은 데이터와 차트 이미지를 Claude Code·Codex CLI에 넘겨서 종목별 AI 분석을 붙인 과정을 정리한다. 웹에서 CLI 인증을 처리하는 부분과, 자동 배치를 만들었다가 결국 없앤 이야기가 들어간다.
이전 글: 없음
다음 글: 2편 — Claude Code·Codex CLI로 종목 AI 분석 붙이기
※ 이 글은 AI의 도움을 받아 작성했고, 작성자가 내용을 직접 검토한 뒤 올렸다.