
⚠️ 이 글은 어떤 종목을 사라고 권하는 글이 아닙니다. 직접 만들어 본 경험을 적은 "개발 후기"입니다.
글에 나오는 백테스트 숫자와 매매 방식은 공부하고 만들어 본 과정을 설명하려고 든 예시일 뿐이고, 돈을 벌 수 있다는 뜻이 아닙니다. 실제 투자는 꼭 스스로 충분히 따져보고 본인 책임으로 결정하세요.
회사에서 DB와 ETL(데이터를 모아서 옮기고 정리하는 작업) 일을 하면서, 취미로 키움증권 Open API를 이용해 자동매매봇을 만들어 본 지 꽤 됐습니다. 인터넷에 찾아보면 "키움 Open API 연결하는 법" 같은 따라하기 글은 많습니다. 그런데 봇을 실제로 돌리면서 어떤 에러를 만났는지, 백테스트 결과와 실제 결과가 얼마나 달랐는지, 그걸 어떻게 고쳤는지를 솔직하게 적은 글은 생각보다 적었습니다. 그래서 제가 헤맸던 과정을 개발 일기처럼 정리해 보려고 합니다.
시작한 이유는 간단했습니다. 평소에 오라클 ETL 배치를 만들면서 "정해진 조건대로 데이터를 계속 처리하는 코드"는 많이 짜 봤는데, 이걸 주식 데이터에 써 보면 어떨까 궁금했습니다. 처음에는 "API 연결하고 조건문 몇 개 넣으면 끝나겠지" 하고 쉽게 생각했습니다. 하지만 막상 해 보니 API의 특이한 구조부터 수시로 바뀌는 시장 데이터까지, 생각보다 훨씬 까다로운 일이었습니다.
키움 Open API를 처음 써 보고 느낀 점
가장 먼저 놀란 건 키움 Open API가 아직도 32비트 COM 방식으로 만들어져 있다는 점이었습니다. 요즘 개발 환경에 익숙한 분이라면 이 방식 자체가 낯설 수 있습니다. 파이썬에서 쓰려면 pywin32라는 도구로 COM 객체를 불러와야 하고, 파이썬도 32비트 버전을 써야 제대로 돌아갑니다. 처음엔 이걸 몰라서 이유를 알 수 없는 오류를 계속 만났습니다.
두 번째로 적응하기 힘들었던 건 모든 게 "이벤트 방식"으로 움직인다는 점이었습니다. 로그인 요청을 보내도 결과가 바로 오지 않고, OnEventConnect라는 이벤트가 올 때까지 기다려야 합니다. 종목 조회(TR) 요청도 결과가 OnReceiveTrData 이벤트로 따로 들어옵니다. 저는 보통 요청하면 바로 답이 오는 방식에 익숙해서, 처음엔 요청을 보내자마자 다음 줄에서 결과를 쓰려다가 계속 빈 값만 받았습니다. 결국 이벤트가 오면 알려주는 장치(조건 변수나 큐)를 써서 "답이 올 때까지 기다리는" 구조로 짜야 한다는 걸 직접 겪으면서 배웠습니다.
세 번째는 조회 횟수 제한이었습니다. 1초에 몇 번, 하루에 몇 번까지 조회할 수 있는지 정해져 있는데, 이걸 넘으면 한동안 조회 자체가 막혀 버립니다. 처음엔 이 제한을 몰라서 여러 종목을 반복문으로 빠르게 조회하다가, 중간에 봇이 멈춰 버리는 일이 자주 있었습니다. 그 뒤로는 요청 사이에 일부러 쉬는 시간을 넣고, 요청을 줄 세워 두는 대기열(큐)을 만들어서 1초에 보내는 요청 수를 스스로 조절하도록 바꿨습니다.
네 번째는 화면(UI) 스레드 문제였습니다. COM 방식 API는 반드시 메인(STA) 스레드에서 준비하고 이벤트도 거기서 처리해야 합니다. 그런데 무심코 다른 스레드에서 API를 건드렸다가 원인을 알 수 없는 예외가 여러 번 터졌습니다. 그래서 API와 관련된 일은 전부 한 스레드에서만 하고, 매매 계산이나 데이터 정리처럼 무거운 작업은 다른 스레드로 빼서, 결과만 메시지 큐로 주고받도록 구조를 정리했습니다.
백테스트로 알게 된 것, 그리고 실제로는 달랐던 것
코드를 어느 정도 완성한 뒤에는 지난 데이터로 백테스트(과거에 이 방식대로 샀다면 어땠을지 시험해 보는 것)를 해 봤습니다. 여기서 배운 게 생각보다 많았습니다.
1) 백테스트 결과가 좋다고 실제 결과도 좋은 건 절대 아니었습니다. 특정 기간 데이터에 맞춰 설정값을 계속 고치다 보니, 그 기간에만 잘 맞는 코드가 되어 버렸습니다. 이걸 과적합(오버피팅)이라고 합니다. 백테스트 승률이 높게 나와서 좋아했는데, 기간만 살짝 바꿔 돌려 보니 성적이 확 떨어지는 일을 몇 번 겪었습니다. 그 뒤로는 "이 결과를 얼마나 믿어도 될까"를 훨씬 조심스럽게 보게 됐습니다.
2) 처음엔 슬리피지와 호가 차이를 전혀 생각하지 않았습니다. 백테스트에서는 "이 가격에 샀다"고 치고 계산하지만, 실제로는 주문을 넣는 순간과 거래가 되는 순간 사이에 가격이 이미 움직여 있는 경우가 많습니다. 이 차이를 슬리피지라고 합니다. 특히 거래가 적은 종목이나 가격이 크게 오르내릴 때는 원하던 가격보다 한참 다른 가격에 사고팔리기도 했습니다. 백테스트에 슬리피지를 일부러 넣어 계산하도록 바꾼 뒤에야 실제 결과와의 차이가 조금씩 줄었습니다.
3) 장중 데이터와 백테스트용 봉 데이터의 시간 차이도 문제였습니다. 저는 분봉 데이터로 사고팔 신호를 계산했습니다. 실제 봇은 그 봉이 완전히 끝난 뒤에야 신호를 알 수 있는데, 처음 짠 백테스트 코드는 이 시간 차이를 제대로 반영하지 못했습니다. 그래서 실제로는 불가능한, 마치 "미래를 살짝 보고 한" 것 같은 결과가 나온 적도 있었습니다. 이걸 발견했을 때 꽤 당황했습니다. 봉이 끝나는 시점과 신호를 계산하는 시점을 확실히 나눈 뒤에야 믿을 수 있는 백테스트가 됐습니다.
이 세 가지를 겪고 나서 백테스트 결과를 보는 습관이 완전히 바뀌었습니다. 이제는 한 기간의 성적 숫자 하나만 보지 않고, 여러 기간으로 나눠 돌렸을 때 결과가 얼마나 들쭉날쭉한지, 즉 꾸준한지를 먼저 봅니다. 한 번 크게 잘 나온 결과보다, 여러 번 평범하지만 꾸준히 나온 결과가 훨씬 믿을 만하다는 걸 직접 겪으며 배웠습니다.
실제로 돌리면서 만난 에러와 해결 과정
봇을 며칠씩 실제로 돌리면서 만난 에러들을 로그와 함께 정리해 봤습니다.
가장 자주 본 에러: 조회·주문을 너무 많이 보내서 생기는 에러코드
짧은 시간에 조회를 계속하거나 주문을 반복해서 보내면 제한에 걸렸다는 에러가 로그에 계속 찍혔습니다. 앞에서 말한 것처럼 요청 대기열 관리가 부족했던 게 원인이었고, 요청 간격을 조절하는 코드를 넣은 뒤로 훨씬 줄었습니다.
1) 자정 무렵 날짜 처리 버그. 특정 시간 이후의 데이터를 모으는 코드를 짜면서 "날짜가 다음 날로 넘어가는" 경우를 빠뜨렸습니다. 그래서 자정이 지나는 순간 그날 데이터가 이상하게 섞였습니다. 새벽에 로그를 보다가 날짜가 하루 전으로 찍혀 있는 걸 보고 원인을 찾았는데, 날짜를 비교하는 코드에 특정 날짜를 전제로 고정해 둔 부분이 있었던 게 문제였습니다.
2) 장 마감 직후 남아 있는 주문 문제. 장이 끝날 때 아직 체결되지 않은 주문을 제대로 정리하지 않았습니다. 그러자 다음 날 장이 열릴 때 봇이 그 주문을 새 주문으로 잘못 알고 한 번 더 처리하려고 한 적이 있었습니다. 실제 돈이 걸린 부분이라 가장 긴장했던 버그였습니다. 그 뒤로는 장이 끝날 때 남은 주문을 모두 취소하거나, 봇 상태를 완전히 초기화하고 다음 날 새로 시작하도록 안전장치를 만들었습니다.
3) 연결이 끊긴 뒤 자동으로 다시 연결되지 않는 문제. 인터넷이 잠깐 끊기거나 컴퓨터가 절전 모드로 들어가면서 API 연결이 끊겼습니다. 그런데 봇은 이걸 알아채지 못하고 아무 일 없다는 듯 계속 돌고 있었습니다. 그래서 연결 상태를 일정 시간마다 확인하는 스레드(헬스체크)를 따로 만들고, 끊겼으면 자동으로 다시 로그인하도록 고쳤습니다.
4) 이벤트가 돌아오는 순서가 뒤섞이는 문제. 여러 종목을 한꺼번에 조회해 보니, 요청을 보낸 순서와 답이 돌아오는 순서가 항상 같지 않다는 걸 뒤늦게 알았습니다. 처음 코드는 순서만 보고 요청과 답을 짝지었기 때문에, 가끔 한 종목의 데이터가 다른 종목 처리 코드로 잘못 들어가는 사고가 났습니다. 그래서 요청할 때마다 고유한 번호를 붙여서 관리하는 방식으로 바꿔 해결했습니다.
한마디로 정리하면, 자동매매봇을 만들면서 가장 어려웠던 건 매매 방법 자체가 아니었습니다. "이벤트 방식으로 움직이는 API를 안전하게 다루는 것"과 "백테스트와 실제 결과의 차이를 줄이는 것"이 훨씬 어려웠습니다. 멋진 전략보다 이런 기본기가 훨씬 중요하다는 걸 크게 느낀 프로젝트였습니다.
돌이켜 보면 처음 몇 주는 매매 방법을 더 세밀하게 다듬는 데 시간을 쓰고 싶었습니다. 하지만 실제로는 API 연결을 끊기지 않게 유지하고, 에러를 로그로 남겨서 나중에 추적할 수 있게 만드는 기초 작업에 훨씬 많은 시간을 썼습니다. 지나고 보니 이런 기초가 약하면 아무리 좋은 전략이라도 실제로는 엉뚱한 곳에서 문제가 터지기 마련이라, 전혀 시간 낭비가 아니었다고 생각합니다.
다음 글에서는 요청 대기열과 헬스체크 스레드를 실제로 어떻게 만들었는지, 코드 구조를 더 자세히 다뤄 보겠습니다. 다시 말씀드리지만 이 글은 투자 조언이 아니라 개발 경험을 나누는 글입니다. 실제 투자는 충분히 알아보고 본인 책임으로 결정하시길 바랍니다.
⚠️ 이 글은 어떤 종목을 사라고 권하는 글이 아닙니다. 직접 만들어 본 경험을 적은 "개발 후기"입니다.
글에 나오는 백테스트 숫자와 매매 방식은 공부하고 만들어 본 과정을 설명하려고 든 예시일 뿐이고, 돈을 벌 수 있다는 뜻이 아닙니다. 실제 투자는 꼭 스스로 충분히 따져보고 본인 책임으로 결정하세요.
회사에서 DB와 ETL(데이터를 모아서 옮기고 정리하는 작업) 일을 하면서, 취미로 키움증권 Open API를 이용해 자동매매봇을 만들어 본 지 꽤 됐습니다. 인터넷에 찾아보면 "키움 Open API 연결하는 법" 같은 따라하기 글은 많습니다. 그런데 봇을 실제로 돌리면서 어떤 에러를 만났는지, 백테스트 결과와 실제 결과가 얼마나 달랐는지, 그걸 어떻게 고쳤는지를 솔직하게 적은 글은 생각보다 적었습니다. 그래서 제가 헤맸던 과정을 개발 일기처럼 정리해 보려고 합니다.
시작한 이유는 간단했습니다. 평소에 오라클 ETL 배치를 만들면서 "정해진 조건대로 데이터를 계속 처리하는 코드"는 많이 짜 봤는데, 이걸 주식 데이터에 써 보면 어떨까 궁금했습니다. 처음에는 "API 연결하고 조건문 몇 개 넣으면 끝나겠지" 하고 쉽게 생각했습니다. 하지만 막상 해 보니 API의 특이한 구조부터 수시로 바뀌는 시장 데이터까지, 생각보다 훨씬 까다로운 일이었습니다.
키움 Open API를 처음 써 보고 느낀 점
가장 먼저 놀란 건 키움 Open API가 아직도 32비트 COM 방식으로 만들어져 있다는 점이었습니다. 요즘 개발 환경에 익숙한 분이라면 이 방식 자체가 낯설 수 있습니다. 파이썬에서 쓰려면 pywin32라는 도구로 COM 객체를 불러와야 하고, 파이썬도 32비트 버전을 써야 제대로 돌아갑니다. 처음엔 이걸 몰라서 이유를 알 수 없는 오류를 계속 만났습니다.
두 번째로 적응하기 힘들었던 건 모든 게 "이벤트 방식"으로 움직인다는 점이었습니다. 로그인 요청을 보내도 결과가 바로 오지 않고, OnEventConnect라는 이벤트가 올 때까지 기다려야 합니다. 종목 조회(TR) 요청도 결과가 OnReceiveTrData 이벤트로 따로 들어옵니다. 저는 보통 요청하면 바로 답이 오는 방식에 익숙해서, 처음엔 요청을 보내자마자 다음 줄에서 결과를 쓰려다가 계속 빈 값만 받았습니다. 결국 이벤트가 오면 알려주는 장치(조건 변수나 큐)를 써서 "답이 올 때까지 기다리는" 구조로 짜야 한다는 걸 직접 겪으면서 배웠습니다.
세 번째는 조회 횟수 제한이었습니다. 1초에 몇 번, 하루에 몇 번까지 조회할 수 있는지 정해져 있는데, 이걸 넘으면 한동안 조회 자체가 막혀 버립니다. 처음엔 이 제한을 몰라서 여러 종목을 반복문으로 빠르게 조회하다가, 중간에 봇이 멈춰 버리는 일이 자주 있었습니다. 그 뒤로는 요청 사이에 일부러 쉬는 시간을 넣고, 요청을 줄 세워 두는 대기열(큐)을 만들어서 1초에 보내는 요청 수를 스스로 조절하도록 바꿨습니다.
네 번째는 화면(UI) 스레드 문제였습니다. COM 방식 API는 반드시 메인(STA) 스레드에서 준비하고 이벤트도 거기서 처리해야 합니다. 그런데 무심코 다른 스레드에서 API를 건드렸다가 원인을 알 수 없는 예외가 여러 번 터졌습니다. 그래서 API와 관련된 일은 전부 한 스레드에서만 하고, 매매 계산이나 데이터 정리처럼 무거운 작업은 다른 스레드로 빼서, 결과만 메시지 큐로 주고받도록 구조를 정리했습니다.
백테스트로 알게 된 것, 그리고 실제로는 달랐던 것
코드를 어느 정도 완성한 뒤에는 지난 데이터로 백테스트(과거에 이 방식대로 샀다면 어땠을지 시험해 보는 것)를 해 봤습니다. 여기서 배운 게 생각보다 많았습니다.
1) 백테스트 결과가 좋다고 실제 결과도 좋은 건 절대 아니었습니다. 특정 기간 데이터에 맞춰 설정값을 계속 고치다 보니, 그 기간에만 잘 맞는 코드가 되어 버렸습니다. 이걸 과적합(오버피팅)이라고 합니다. 백테스트 승률이 높게 나와서 좋아했는데, 기간만 살짝 바꿔 돌려 보니 성적이 확 떨어지는 일을 몇 번 겪었습니다. 그 뒤로는 "이 결과를 얼마나 믿어도 될까"를 훨씬 조심스럽게 보게 됐습니다.
2) 처음엔 슬리피지와 호가 차이를 전혀 생각하지 않았습니다. 백테스트에서는 "이 가격에 샀다"고 치고 계산하지만, 실제로는 주문을 넣는 순간과 거래가 되는 순간 사이에 가격이 이미 움직여 있는 경우가 많습니다. 이 차이를 슬리피지라고 합니다. 특히 거래가 적은 종목이나 가격이 크게 오르내릴 때는 원하던 가격보다 한참 다른 가격에 사고팔리기도 했습니다. 백테스트에 슬리피지를 일부러 넣어 계산하도록 바꾼 뒤에야 실제 결과와의 차이가 조금씩 줄었습니다.
3) 장중 데이터와 백테스트용 봉 데이터의 시간 차이도 문제였습니다. 저는 분봉 데이터로 사고팔 신호를 계산했습니다. 실제 봇은 그 봉이 완전히 끝난 뒤에야 신호를 알 수 있는데, 처음 짠 백테스트 코드는 이 시간 차이를 제대로 반영하지 못했습니다. 그래서 실제로는 불가능한, 마치 "미래를 살짝 보고 한" 것 같은 결과가 나온 적도 있었습니다. 이걸 발견했을 때 꽤 당황했습니다. 봉이 끝나는 시점과 신호를 계산하는 시점을 확실히 나눈 뒤에야 믿을 수 있는 백테스트가 됐습니다.
이 세 가지를 겪고 나서 백테스트 결과를 보는 습관이 완전히 바뀌었습니다. 이제는 한 기간의 성적 숫자 하나만 보지 않고, 여러 기간으로 나눠 돌렸을 때 결과가 얼마나 들쭉날쭉한지, 즉 꾸준한지를 먼저 봅니다. 한 번 크게 잘 나온 결과보다, 여러 번 평범하지만 꾸준히 나온 결과가 훨씬 믿을 만하다는 걸 직접 겪으며 배웠습니다.
실제로 돌리면서 만난 에러와 해결 과정
봇을 며칠씩 실제로 돌리면서 만난 에러들을 로그와 함께 정리해 봤습니다.
가장 자주 본 에러: 조회·주문을 너무 많이 보내서 생기는 에러코드
짧은 시간에 조회를 계속하거나 주문을 반복해서 보내면 제한에 걸렸다는 에러가 로그에 계속 찍혔습니다. 앞에서 말한 것처럼 요청 대기열 관리가 부족했던 게 원인이었고, 요청 간격을 조절하는 코드를 넣은 뒤로 훨씬 줄었습니다.
1) 자정 무렵 날짜 처리 버그. 특정 시간 이후의 데이터를 모으는 코드를 짜면서 "날짜가 다음 날로 넘어가는" 경우를 빠뜨렸습니다. 그래서 자정이 지나는 순간 그날 데이터가 이상하게 섞였습니다. 새벽에 로그를 보다가 날짜가 하루 전으로 찍혀 있는 걸 보고 원인을 찾았는데, 날짜를 비교하는 코드에 특정 날짜를 전제로 고정해 둔 부분이 있었던 게 문제였습니다.
2) 장 마감 직후 남아 있는 주문 문제. 장이 끝날 때 아직 체결되지 않은 주문을 제대로 정리하지 않았습니다. 그러자 다음 날 장이 열릴 때 봇이 그 주문을 새 주문으로 잘못 알고 한 번 더 처리하려고 한 적이 있었습니다. 실제 돈이 걸린 부분이라 가장 긴장했던 버그였습니다. 그 뒤로는 장이 끝날 때 남은 주문을 모두 취소하거나, 봇 상태를 완전히 초기화하고 다음 날 새로 시작하도록 안전장치를 만들었습니다.
3) 연결이 끊긴 뒤 자동으로 다시 연결되지 않는 문제. 인터넷이 잠깐 끊기거나 컴퓨터가 절전 모드로 들어가면서 API 연결이 끊겼습니다. 그런데 봇은 이걸 알아채지 못하고 아무 일 없다는 듯 계속 돌고 있었습니다. 그래서 연결 상태를 일정 시간마다 확인하는 스레드(헬스체크)를 따로 만들고, 끊겼으면 자동으로 다시 로그인하도록 고쳤습니다.
4) 이벤트가 돌아오는 순서가 뒤섞이는 문제. 여러 종목을 한꺼번에 조회해 보니, 요청을 보낸 순서와 답이 돌아오는 순서가 항상 같지 않다는 걸 뒤늦게 알았습니다. 처음 코드는 순서만 보고 요청과 답을 짝지었기 때문에, 가끔 한 종목의 데이터가 다른 종목 처리 코드로 잘못 들어가는 사고가 났습니다. 그래서 요청할 때마다 고유한 번호를 붙여서 관리하는 방식으로 바꿔 해결했습니다.
한마디로 정리하면, 자동매매봇을 만들면서 가장 어려웠던 건 매매 방법 자체가 아니었습니다. "이벤트 방식으로 움직이는 API를 안전하게 다루는 것"과 "백테스트와 실제 결과의 차이를 줄이는 것"이 훨씬 어려웠습니다. 멋진 전략보다 이런 기본기가 훨씬 중요하다는 걸 크게 느낀 프로젝트였습니다.
돌이켜 보면 처음 몇 주는 매매 방법을 더 세밀하게 다듬는 데 시간을 쓰고 싶었습니다. 하지만 실제로는 API 연결을 끊기지 않게 유지하고, 에러를 로그로 남겨서 나중에 추적할 수 있게 만드는 기초 작업에 훨씬 많은 시간을 썼습니다. 지나고 보니 이런 기초가 약하면 아무리 좋은 전략이라도 실제로는 엉뚱한 곳에서 문제가 터지기 마련이라, 전혀 시간 낭비가 아니었다고 생각합니다.
다음 글에서는 요청 대기열과 헬스체크 스레드를 실제로 어떻게 만들었는지, 코드 구조를 더 자세히 다뤄 보겠습니다. 다시 말씀드리지만 이 글은 투자 조언이 아니라 개발 경험을 나누는 글입니다. 실제 투자는 충분히 알아보고 본인 책임으로 결정하시길 바랍니다.