시작은 작은 지적이었다
손익 화면에서 수수료와 세금이 하나의 값으로 합쳐져 표시되고 있다는 지적에서 이 작업이 시작됐다. 단순히 “두 값을 나눠서 보여주면 되겠지”라고 생각했는데, 확인해보니 합산 구조만 문제가 아니었다. 요율 자체가 틀려 있었다.
가정으로 두지 않고, 실제 체결 47건(KIS API의 TTTC8715R, 기간별 매매손익 조회)을 역산해서 실제 청구액과 코드가 계산한 값을 대조했다.
발견한 오차 세 가지
| 항목 | 기존 코드 | 실측 | 영향 |
|---|---|---|---|
| 수수료 | 0.015% | 0.0035427% | 4.1배 과대 |
| 거래세 | 0.18% | 0.20% | 과소 (2026-01-01 시행 반영 누락) |
| ETF 거래세 | 0.18% 부과 | 면제 | ETF 손익 과소계산 |
1. 위탁수수료는 지금 면제 상태였다
실측값은 862원 ÷ 왕복거래대금 24,331,905원 = 0.0035427%. 남는 건 유관기관 제비용(0.0036396%)뿐이었는데, 이건 거래소와 예탁원이 걷는 비용이라 증권사가 면제해줄 수 있는 항목이 아니다. 나중에 면제가 끝나면 뱅키스 기준 0.0140527%를 더하면 된다는 것도 함께 기록해뒀다.
2. 거래세는 2026년부터 0.20%로 바뀌었다
코스피는 거래세 0.05%+농특세 0.15%, 코스닥은 0.20%로 구성 방식은 다르지만 합계는 같아서 단일 값으로 처리해도 충분했다. 실측한 25종목 모두 0.1981~0.2000% 범위 안에 들어왔다.
3. ETF는 거래세가 아예 면제였다
실측에서 KODEX·ACE·SOL 계열 ETF 4종 모두 세금이 0원이었다. 그런데 기존 코드는 여기에도 0.18%를 그대로 물리고 있었다. ETF 종목의 실제 수익성을 체계적으로 낮게 잡고 있었던 셈이다.
어떻게 고쳤나
calc_sell_cost라는 함수 하나에 뭉쳐 있던 수수료와 세금 계산을 calc_sell_commission과 calc_tax로 분리했다. 성격이 다르기 때문이다 — 수수료는 증권사 정책에 따라 면제될 수 있지만, 거래세는 면제되지 않는다. 하나로 묶어두면 나중에 수수료 면제가 끝났을 때 세금까지 같이 바뀌는 실수를 하기 쉽다.
요율 세 가지는 실측값으로 교정했고(CommissionConfig, BacktestConfig 양쪽 모두), ETF 거래세 면제도 반영했다(KISClient.is_etf). 매도 알림에는 수수료와 거래세를 각각 따로 표시하도록 해서, 면제가 적용되고 있는지를 눈으로 바로 확인할 수 있게 했다.
ETF 판별을 왜 API 기준으로 바꿨나
처음에는 종목명 접두어(KODEX, TIGER 등)로 ETF 여부를 판별하려고 했다. 그런데 검증 과정에서 로컬에 있던 종목명 테이블이 코드→이름을 잘못 반환하는 경우를 실측으로 발견했다.
| 코드 | 실제 종목명 | 로컬 조회 결과 |
|---|---|---|
| 433500 | ACE 원자력TOP10 | 433500 (조회 실패) |
| 466920 | SOL 조선TOP3플러스 | KODEX 원자력 (오매칭) |
| 395160 | KODEX AI반도체TOP2 | TIGER Fn반도체TOP10 (오매칭) |
이름을 추측하는 방식 대신, KIS API 응답에 들어있는 rprs_mrkt_kor_name 필드를 근거로 삼기로 했다. 종목의 속성(ETF 여부)은 시간이 지나도 바뀌지 않으므로, 프로세스가 살아있는 동안은 이 결과를 캐시해서 API 호출 제한(rate limit, 에러코드 EGW00215)에 걸리지 않도록 했다.
그리고 판별이 애매하거나 실패하는 경우에는 과세로 처리하기로 했다. 비용을 낮게 잡는 실수가 손익을 과대하게 보여주는 쪽으로 이어지는 게 더 위험하다고 판단했기 때문이다.
검증
실계좌 체결 29건을 수정된 계산식과 대조했다.
| 지표 | 수정 전 | 수정 후 |
|---|---|---|
| 유의한 편차 | 1건 (ETF 오탐) | 0건 |
| 세금 상대오차 | 1.250% | 0.068% |
남은 오차는 원 단위 절사 규칙 차이(건당 1~2원)에서 오는 것이라, 요율 자체는 정확하다고 볼 수 있었다. 관련 테스트는 test_commission, test_config, test_realtime_engine, test_notifier, test_api_client를 합쳐 275건이 통과했다.
배포할 때 남긴 주의사항
config.py는 Docker 이미지에 그대로 구워지는 경로라서, 코드를 고쳤다고 호스트에 마운트된 파일만 갱신하면 이미지 안의 값과 버전이 어긋난다. 재빌드가 필요하다는 점을 명시적으로 남겼고, 실거래 중인 시스템이라 배포는 장 마감 후로 미루도록 기록해뒀다.
남는 교훈
이 작업 전체를 관통하는 태도는 “가정하지 말고 실측하라”였다. 수수료율 하나만 틀린 게 아니라, 세율 개정 미반영과 ETF 오판까지 겹쳐 있었는데, 이 셋은 서로 다른 원인에서 왔다 — 하나는 오래된 상수를 갱신하지 않은 것, 하나는 법 개정을 놓친 것, 하나는 참조하던 데이터 소스 자체가 신뢰할 수 없었던 것.
세 가지 모두 코드만 들여다봐서는 찾을 수 없는 종류의 문제였다. 실제 체결 내역이라는, 시스템 바깥의 진실과 대조해야만 드러나는 오차였다.