Claude와 로컬 LLM을 엮어서 자동 매매 시스템을 만들고 있는데, 끝이 안 보여요
며칠째 모니터 앞에서 코드와 씨름하고 있어요. Claude랑 로컬에서 돌리는 오픈소스 LLM을 연계해서 주식 자동 매매 시스템을 만들고 있는데, 이게 생각보다 훨씬 지옥이에요. "LLM이 알아서 판단하고 알아서 사고팔면 되지 않나?"라는 가벼운 마음으로 시작한 건 절대 아니었는데, 그래도 이 정도일 줄은 몰랐어요.
오늘은 그 고달픈 개발기를 좀 풀어보려고 해요. 아직 한참 미완성이라 시스템 자체의 세부 구조를 공개하긴 어렵지만, "LLM 기반 자동 매매"라는 걸 실제로 만들어보면 어떤 일들이 벌어지는지, 그 현실적인 이야기를 해보고 싶어요.
왜 이런 걸 만들기 시작했나
직장인 개미투자자의 가장 큰 적은 뭘까요. 저는 "시간"이라고 생각해요.
장이 열려 있는 시간에 모니터를 볼 수 없고, 뉴스가 터져도 한참 뒤에야 확인하고, 손절 타이밍을 놓쳐서 물리고, 익절 타이밍을 놓쳐서 수익이 녹아내리고. 이 모든 게 "실시간으로 시장을 못 보니까" 발생하는 문제예요.
그래서 생각했어요. LLM이 뉴스를 읽고, 시황을 분석하고, 매매 판단까지 해주면 어떨까. Claude는 추론 능력이 뛰어나고, 오픈소스 LLM은 로컬에서 빠르게 돌릴 수 있으니까, 이 둘을 적절히 엮으면 꽤 쓸만한 시스템이 나오지 않을까.
아이디어 자체는 나쁘지 않았어요. 문제는 구현이었죠.
"LLM이 주식을 산다"는 게 얼마나 무서운 일인지
코드를 처음 짜기 시작했을 때는 금방 될 줄 알았어요. LLM한테 시장 데이터를 주고, "사? 말아?" 물어보고, 그 답을 증권사 API로 넘기면 끝 아닌가. 구조만 놓고 보면 단순해요.
근데 "내 돈"이 걸려 있는 순간, 모든 게 달라져요.
LLM이 헛소리를 하면 어떡하죠? 갑자기 전 재산을 한 종목에 몰빵하라고 하면? 시장이 폭락하는데 "강력 매수"를 외치면? JSON을 깨먹어서 파싱이 안 되면? 네트워크가 끊겨서 LLM 응답이 안 오면?
실제 돈이 움직이는 시스템에서 LLM을 쓴다는 건, LLM을 "절대 신뢰하지 않으면서도 판단은 맡기는" 모순적인 구조를 만들어야 한다는 뜻이에요. 이게 이 프로젝트에서 가장 어려운 부분이었어요.
Claude와 로컬 LLM, 역할 분담의 고민
시스템 구조를 잡으면서 가장 오래 고민한 건 "어디까지를 Claude에게 맡기고, 어디까지를 로컬 LLM에게 맡길 것인가"였어요.
Claude는 확실히 추론 능력이 좋아요. 복잡한 상황을 종합적으로 판단하는 데 강하고, 프롬프트 지시를 잘 따르고, 출력 포맷도 안정적이에요. 그런데 API 호출이라 비용이 들고, 레이턴시도 있어요. 실시간으로 1분마다 호출하기에는 부담이 크죠.
반면 로컬 LLM은 비용이 0원이에요. 홈서버에서 돌리니까요. 대신 추론 능력은 Claude에 비하면 한참 부족하고, 특히 한국어 품질이 들쭉날쭉해요. GPT-OSS 120B를 포함해 여러 모델을 테스트해봤는데, 같은 프롬프트를 줘도 어떤 때는 완벽한 JSON을 뱉고, 어떤 때는 뜬금없이 마크다운을 섞어서 내놓고, 어떤 때는 아예 한국어가 깨져서 나와요.
그래서 결국 "빠른 판단은 로컬 LLM, 중요한 판단은 Claude"라는 구조로 잡았어요. 자세한 건 나중에 시스템이 어느 정도 안정되면 공유할게요. 지금은 아직 매일같이 구조가 바뀌고 있어서요.
로컬 LLM의 배신 - reasoning model의 함정
개발 초반에 가장 황당했던 사건 중 하나가 로컬 LLM의 reasoning model 이슈였어요.
요즘 오픈소스 LLM 중에 "reasoning" 모드를 지원하는 모델들이 있어요. 생각 과정을 먼저 쭉 출력한 다음 최종 답변을 내놓는 방식이죠. 문제는 이 "생각 과정"이 별도의 special token으로 감싸져서 나오는데, 이걸 제대로 처리하지 않으면 LLM의 내부 독백이 최종 응답에 섞여 들어온다는 거예요.
처음엔 이걸 몰랐어요. LLM한테 "JSON으로만 답변해"라고 시켰는데, 돌아오는 응답이 이상한 거예요. JSON 앞에 "음, 이 뉴스는 삼성전자에 부정적인 영향을..." 같은 독백이 붙어 있는 거죠. 당연히 JSON 파싱이 깨지고, 시스템이 에러를 뱉고, 한참을 삽질했어요.
결국 모델 서빙 프레임워크의 설정을 뒤져서 reasoning 분리 옵션을 찾아 해결했는데, 이런 식의 "LLM 인프라 레벨의 함정"이 정말 많았어요. 문서에도 잘 안 나와 있고, 커뮤니티에서도 비슷한 이슈를 겪는 사람이 드물어서 혼자 삽질하는 시간이 길었어요.
끝없는 로직 오류의 지옥
이 프로젝트에서 제일 체감이 큰 고통은 뭐니뭐니해도 "로직 오류"예요. 정말 끝이 없어요.
자동 매매 시스템이라는 게, 단순히 "사고팔기"만 하는 게 아니거든요. 매수 주문을 넣었는데 일부만 체결되면? 미체결 주문은 언제 취소하지? 체결된 부분은 포트폴리오에 어떻게 반영하지? 정산은? 수수료는? 현금 잔고 계산은?
하나하나가 다 엣지 케이스 덩어리예요.
예를 들어 "부분 체결" 하나만 해도 이래요. 주문을 100주 넣었는데 30주만 체결됐어요. 그러면 나머지 70주는 어떻게 하죠? 다음 정산 시점에 증권사 API로 확인해서 추가 체결됐으면 반영하고, 아직 미체결이면 얼마나 기다렸다가 취소하지? 취소하면 그 금액은 다시 투자 가능 금액으로 돌려야 하고, 근데 그 사이에 다른 주문이 또 들어가면?
이런 시나리오가 수십 개, 수백 개씩 나와요. 하나 고치면 다른 데서 터지고, 그걸 고치면 또 다른 데서 터지고. 지난 며칠간 수십 건의 버그를 잡았는데, 고칠 때마다 "이제 끝이겠지" 싶다가 또 새로운 게 나와요.
특히 골치 아팠던 것들을 좀 적어보면 이래요.
주문 접수와 체결은 다른 거다. 이게 처음에 구분이 잘 안 됐어요. 주문을 넣는 순간 포트폴리오에 반영하면 안 되고, 실제로 체결이 확인된 후에 반영해야 해요. 근데 체결 확인은 비동기적으로 오니까, 그 사이 시간 동안의 상태 관리가 정말 까다로워요.
동시 실행 문제. 여러 개의 스케줄링 작업이 동시에 돌아가다 보니, 같은 자원에 동시에 접근하는 경합 조건이 생겨요. A 프로세스가 "매수 가능"이라고 판단한 그 찰나에 B 프로세스가 먼저 주문을 넣어버리면? 이중 주문이 나가요. 이거 잡느라 파일 락, 뮤텍스, 실행 순서 보장 같은 것들을 한참 만졌어요.
날짜와 시간의 함정. 한국 주식시장은 KST 기준이고, 서버는 UTC로 돌아가고, 뉴스 피드의 시간대는 제각각이에요. 여기에 공휴일 처리, 영업일 계산, 정산일(T+2) 계산까지 들어가면 시간 관련 버그만 해도 양손 양발로 셀 수 없을 만큼 나왔어요. 2026년부터 2028년까지 한국 증시 휴장일을 수동으로 다 넣어야 했어요.
LLM 출력의 불확실성. 아무리 프롬프트를 정교하게 짜도, LLM은 가끔 예상 밖의 응답을 해요. "가격" 필드에 0을 넣어서 0원짜리 주문이 나갈 뻔한 적도 있고, 종목코드를 엉뚱한 걸로 바꿔서 내놓은 적도 있어요. 그래서 LLM 출력 하나하나를 검증하는 가드 로직을 겹겹이 쌓아야 했어요.
안전장치, 그리고 또 안전장치
"내 돈"이 왔다갔다하는 시스템이니까, 안전장치에 대해서는 편집증 수준으로 집착했어요.
기본적인 원칙은 이래요. LLM이 뭘 하라고 해도, 코드 레벨에서 한 번 더 검증한다. LLM의 판단을 100% 믿지 않아요. LLM이 "매수"라고 해도, 정말 매수해도 되는 상황인지를 코드가 독립적으로 확인해요. 잔고는 충분한지, 이미 같은 종목에 미체결 주문이 있지는 않은지, 시장이 열려 있는 시간인지, 손절 라인에 걸리지는 않는지.
그리고 킬 스위치. Slack에서 한 번의 커맨드로 전체 시스템을 즉시 동결시킬 수 있어요. 이건 초기부터 가장 먼저 구현한 기능이에요. 아무리 자동화를 해도, 사람이 최종 제어권을 쥐고 있어야 한다고 생각했거든요.
그리고 DRY RUN 모드. 지금은 실제 주문을 넣지 않는 시뮬레이션 모드로 돌리고 있어요. 모든 로직은 실제와 동일하게 돌아가되, 마지막 주문 실행 단계에서만 "실행한 척"을 하는 거예요. 이 모드로 충분히 검증한 다음에 실전 모드로 전환할 계획이에요.
이 정도면 충분할 것 같죠? 아뇨, 전혀요. 개발하면서 "이것도 막아야 해? 이런 경우도 있어?"라는 상황이 계속 나왔어요. 안전장치 위에 안전장치를 쌓고, 그 위에 또 안전장치를 쌓는 느낌이에요.
뉴스를 읽는 건 쉬운데, "해석"이 어렵다
자동 매매에서 뉴스 분석은 핵심 중 하나예요. 시장에 영향을 줄 만한 뉴스가 터지면 빠르게 감지하고 대응해야 하니까요.
뉴스를 수집하는 건 쉬워요. RSS 피드에서 긁어오면 돼요. 문제는 "이 뉴스가 내 포트폴리오에 영향을 주는가?"를 판단하는 거예요.
예를 들어 "미국 반도체 수출 규제 강화"라는 뉴스가 떴다고 해볼게요. 이게 내가 가진 종목에 악재인가, 호재인가, 무관한가? 반도체 관련 종목이면 악재겠지만, 어느 정도의 악재인지는? 이미 시장에 반영된 뉴스인지, 새로운 정보인지는?
이런 판단을 LLM에게 맡기는데, 단순히 "좋아/나빠"를 넘어서 확신도(certainty)와 영향도(impact)를 2축으로 평가하도록 만들었어요. 그리고 점수가 일정 이상이면 더 심층적인 검증을 하도록 했어요. 웹 검색을 돌려서 관련 기사가 여러 매체에서 나오고 있는지, 공식 발표가 있는지를 확인하는 거죠.
이 과정에서 "호재인데 왜 악재로 분류했지?" 같은 오분류가 정말 많이 나왔어요. 프롬프트를 수십 번 고쳤어요. 전쟁이나 거시경제 뉴스는 개별 종목에 직접 영향을 안 줄 수도 있으니까 별도로 처리하고, 호재와 악재를 분리해서 판정하고, 같은 이슈에 대한 뉴스가 여러 개 올라오면 클러스터링해서 하나의 이슈로 묶고. 이런 세부 로직들이 하나하나 다 씨름의 대상이었어요.
테스트의 끝은 어디인가
현재 테스트가 600개를 넘었어요. 솔직히 이 규모가 될 줄은 전혀 예상 못 했어요.
처음에는 "핵심 로직만 테스트하면 되지"라고 생각했는데, 안전장치를 추가할 때마다 테스트도 추가해야 했고, 엣지 케이스를 발견할 때마다 그걸 재현하는 테스트를 추가해야 했고, DB를 도입하면서 통합 테스트도 추가해야 했어요.
특히 신경 썼던 건 테스트 안전성이에요. 테스트를 돌렸는데 실제 슬랙 메시지가 날아가거나, 실제 주문이 들어가면 안 되잖아요. 모든 외부 통신을 mock으로 감싸서, 테스트 환경에서는 절대로 실제 API가 호출되지 않도록 했어요.
여기에 외부 리뷰까지 받았어요. Claude한테 전체 코드베이스를 던져주고 "이 시스템에 치명적인 결함이 있으면 찾아줘"라고 시켰거든요. 이 과정을 여러 차례 반복했는데, 매번 "아, 이걸 놓쳤네..." 하는 것들이 나왔어요. 킬 스위치를 눌렀는데 일부 경로에서 매매가 계속 되는 허점이라든가, 투자 한도가 비정상적으로 부풀려지는 버그라든가.
사람 눈으로는 못 잡는 걸 AI 리뷰어가 잡아주는 게 참 아이러니하면서도 고마웠어요. AI로 만드는 시스템을 AI가 검증해주는 셈이니까요.
DB 도입기 - JSON 파일의 한계
처음에는 상태 관리를 전부 JSON 파일로 했어요. 포트폴리오 상태, 매매 기록, 가드 설정 같은 것들을 각각 .json 파일에 저장했죠. 개발 초기에는 이게 간편하고 직관적이었어요.
근데 시스템이 복잡해지면서 한계가 확 드러났어요. 동시에 여러 프로세스가 같은 JSON 파일을 읽고 쓰면 데이터가 깨지고, 종목이 늘어나면 파일도 덩달아 늘어나고, 히스토리를 추적하려면 파일을 뒤져야 하고, 트랜잭션 개념이 없으니 중간에 크래시가 나면 반쪽짜리 데이터가 남고.
결국 PostgreSQL을 도입했어요. 스키마를 설계하고, 기존 JSON 기반 로직을 전부 DB 쿼리로 바꾸고, 마이그레이션 스크립트를 짜고. 이 전환 작업이 예상보다 훨씬 컸어요. 코드베이스 전체를 건드려야 했으니까요.
근데 바꾸고 나니까 확실히 좋았어요. 데이터 무결성이 보장되고, 종목별 쿼리가 깔끔해지고, 수익률 계산 같은 복잡한 집계도 SQL 한 방으로 뽑을 수 있게 됐거든요.
수익률 계산이라는 늪
"시스템이 돈을 벌고 있는지 잃고 있는지"를 정확하게 측정하는 것, 이게 생각보다 정말 어려워요.
단순히 "(현재 평가금액 - 투자 원금) / 원금"이면 될 것 같지만, 현실은 그렇게 단순하지 않아요. 중간에 입금을 하면? 출금을 하면? 배당금이 들어오면? 수수료는? 그리고 "이 시스템이 잘 하고 있는지"를 판단하려면 벤치마크 대비 성과(알파)도 계산해야 해요.
결국 TWR(Time-Weighted Return)이라는 입출금 보정 수익률 방식을 구현했고, 벤치마크는 Buy & Hold 전략, 시장 지수(KOSPI) 대비, 그리고 Sharpe Ratio까지 3가지 축으로 측정하도록 만들었어요.
배당금 처리는 또 다른 늪이었어요. DART에서 배당 공시를 자동 수집하고, 로컬 LLM으로 공시 본문을 파싱해서 배당 금액과 지급일을 추출하고, 기준일이 지나면 확정 처리하고, 지급일이 도래하면 수익률에 반영하고. 이 하나의 기능을 위해 들어간 코드량이 어마어마했어요.
멀티 종목으로 넘어가면 복잡도가 폭발한다
처음에는 한 종목만으로 개발했어요. 한 종목이 제대로 돌아가면 나중에 종목을 추가하면 되지, 라고 생각했죠.
근데 막상 멀티 종목을 지원하려고 하니까, 거의 처음부터 다시 짜는 수준이었어요.
뉴스 분석을 할 때 "이 뉴스가 어떤 종목에 관련된 건지"를 분류해야 하고, 종목별로 독립적인 매매 판단을 내려야 하고, 킬 스위치도 전체 동결과 종목별 동결을 분리해야 하고, 수익률 계산도 종목별 + 포트폴리오 전체로 나눠야 하고.
하드코딩되어 있던 종목코드를 전부 동적으로 바꾸는 작업만 해도 코드베이스 곳곳을 뒤져가며 고쳐야 했어요. "설마 여기에도 하드코딩이?" 싶은 곳에서 발견될 때마다 한숨이 나왔어요.
Slack이 운영 콘솔이 된 사연
이 시스템의 운영 인터페이스는 Slack이에요. 웹 대시보드 같은 걸 따로 만들 여력이 없으니까, Slack의 Slash Command를 활용해서 운영 명령어를 만들었어요.
시스템 상태 확인, 킬 스위치, 종목 추가/제거, 수익률 확인, 입출금 기록 같은 것들을 전부 Slack에서 할 수 있어요. LLM이 뉴스를 분석하면 Slack으로 알림이 오고, 매매 판단이 나오면 그것도 Slack으로 오고, 에러가 나도 Slack으로 와요.
처음에는 단순히 텍스트 메시지만 보냈는데, 가독성이 너무 떨어져서 Slack의 Block Kit이라는 걸 활용해 구조화된 형태로 바꿨어요. 종목 코드 옆에 종목명을 붙이고, 수익률은 색깔로 구분하고, 리포트는 파일로 첨부하고.
Slack 포맷팅 개선에만 며칠을 썼어요. "운영 편의성"이라는 게 결국 사소한 디테일의 축적이더라고요.
Claude를 코드 리뷰어로 쓰는 맛
이 프로젝트에서 Claude의 역할은 두 가지예요. 하나는 시스템 안에서 매매 판단을 내리는 LLM으로서의 역할, 다른 하나는 개발 과정에서 코드 리뷰어이자 버그 헌터로서의 역할.
특히 두 번째 역할이 없었으면 이 프로젝트는 진작에 포기했을 거예요. 전체 코드베이스를 던져주고 "치명적인 결함을 찾아줘"라고 하면, 사람이 며칠 걸려 찾을 법한 것들을 순식간에 짚어줘요.
물론 Claude가 지적한 게 다 맞는 건 아니에요. 가끔 존재하지 않는 문제를 만들어내기도 하고, 이미 처리되어 있는 걸 다시 지적하기도 해요. 그래도 전체적인 히트율로 보면, 외부 시니어 개발자한테 코드 리뷰를 받는 것과 비슷한 수준의 가치가 있었어요.
리뷰를 여러 차례 반복했는데, 한 번 리뷰 때마다 CRITICAL급 이슈가 2~3건, HIGH급이 5~6건씩 나왔어요. "QA 끝" 선언을 하고 나서도 한 번 더 돌리면 또 나오고, 또 돌리면 또 나오고. 끝이 없었어요. 그래도 그때마다 시스템이 조금씩 더 단단해지는 게 느껴져서, 힘들지만 보람은 있었어요.
지금 어디쯤 와 있나
현재 상태를 한 마디로 요약하면, "돌아는 가는데, 아직 실전 투입은 못 하는" 단계예요.
DRY RUN 모드로 시뮬레이션은 돌리고 있어요. 뉴스를 수집하고, 분석하고, 매매 판단을 내리고, 그 결과를 Slack으로 받아보고 있어요. 실제 주문만 안 나갈 뿐, 나머지는 다 실제와 동일하게 동작해요.
이걸로 한동안 더 돌려보면서, 시스템의 판단이 합리적인지, 예상 못 한 엣지 케이스가 또 나오지는 않는지를 지켜볼 생각이에요. 시뮬레이션 결과가 안정적으로 나오면 그때 조금씩 실전 모드로 전환하려고요.
테스트는 600개를 넘겼고, 전부 통과하고 있어요. 근데 테스트가 600개라도, 실제 시장은 테스트 케이스에 없는 상황을 매일 만들어내니까. 아직은 겸손하게 시뮬레이션 단계에 머물러 있는 거예요.
배운 것들
아직 미완성이지만, 여기까지 오면서 몇 가지 뼈에 사무치게 느낀 게 있어요.
LLM 기반 시스템은 "LLM을 믿지 않는 시스템"이어야 한다. LLM의 출력을 무조건 신뢰하면 안 돼요. 반드시 코드 레벨의 검증이 필요하고, 검증을 통과한 것만 실행해야 해요. LLM은 "판단의 참고"이지, "최종 결정권자"가 되면 안 돼요.
안전장치는 아무리 많아도 부족하다. "이 정도면 충분하지"라고 생각할 때가 가장 위험해요. 외부 리뷰를 받을 때마다 "이 경우는 생각 못 했는데..."가 나왔어요.
JSON 파일은 프로토타입까지만. 상태 관리를 JSON으로 시작하면 빠르지만, 시스템이 조금만 커지면 반드시 DB로 전환해야 해요. 나중에 바꿀수록 비용이 커져요.
멀티 종목은 처음부터 고려하자. 한 종목으로 시작해서 나중에 확장하면 된다는 건 환상이에요. 멀티 종목을 나중에 끼워넣으면 코드 구석구석에 박혀 있는 하드코딩과 싸워야 해요.
Slack은 생각보다 훌륭한 운영 도구다. 별도의 웹 대시보드를 만들 여력이 없는 개인 프로젝트라면, Slack Slash Command + Block Kit 조합이 꽤 실용적이에요.
앞으로의 계획
당분간은 DRY RUN 모드를 유지하면서 시스템의 안정성을 계속 검증할 거예요. 그러면서 시뮬레이션 수익률 리포트를 쌓아가고, 실전 전환 전에 최소 몇 주는 안정적으로 돌아가는 걸 확인하려고 해요.
그리고 종목을 좀 더 추가해서 멀티 종목 환경에서의 동작도 충분히 검증해야 해요. 한 종목일 때는 괜찮았던 것들이 종목이 늘어나면 또 어떤 식으로 깨질지 모르니까요.
시스템이 안정되면 아키텍처나 핵심 로직에 대한 기술적인 글도 써볼 생각이에요. 지금은 아직 매일 구조가 바뀌고 있어서, 글을 쓰면 다음 날 바로 무효화될 것 같아서요.
마치며
LLM 기반 자동 매매 시스템. 말로 하면 멋있는데, 만들고 있으면 정말이지 고달파요.
"Claude한테 시키면 되지", "AI가 알아서 해주지" 같은 말은 현실에서는 통하지 않아요. LLM은 도구이고, 도구를 제대로 쓰려면 도구의 한계를 정확히 알아야 하고, 한계를 보완하는 장치를 겹겹이 쌓아야 해요.
그래도 매일 조금씩 시스템이 나아지고 있다는 느낌이 있어요. 어제보다 오늘이 더 안전하고, 오늘보다 내일이 더 안정적이겠죠. 이 끝없어 보이는 로직 오류 지옥에서도 결국은 빠져나올 거라고 믿고 있어요.
지옥에서 함께 코딩하고 계신 모든 개발자 분들, 화이팅이에요.