Loading the catalog…
Loading the catalog…
실제로 돌아가는 대시보드의 실시간 시장 스캐너 하루 동안 만난 버그 5개와 해결 순서 "로컬 LLM으로 자동매매 시켜보면 재밌겠다"에서 시작했다 KOSPI/KOSDAQ 모의투자 대시보드를 FastAPI + SQLite, Next.js로 만들고 있다. 로컬에서 돌아가는 LLM(Ollama + EXAONE)이 실시간으로 매수·매도 판단을 내리는 구조다. 자체 시뮬레이터로만 굴리다가 오늘 한국투자증권(KIS) Open API의 모의투자 서버에 실제로 붙여봤는데, 코드보다 개념 정리에서 더 오래 헤맸다. 그 기록을 남긴다. 구조 backend/ FastAPI + SQLite ai/ 자율매매 엔진 (로컬 / KIS 두 개, 독립 운용) execution/ 주문 실행 계층 (SimulatedExecutor / KisPaperExecutor) integrations/kis.py KIS Open API 클라이언트 api/ REST 엔드포인트 frontend/ Next.js + shadcn/ui 시세 조회와 주문 실행을 인터페이스로 분리해뒀다. 나중에 시뮬레이터를 실제 브로커로 갈아끼울 때 다른 코드는 안 건드리게 하려는 목적이었는데, 실제로 KIS를 붙일 때 이 결정 덕을 많이 봤다. "모의투자"가 사실 두 개다 KIS Open API를 처음 만지면 거의 반드시 헷갈리는 지점이 있다. "모의투자"라는 이름을 쓰는 서비스가 완전히 별개로 두 개 있다. 하나는 Open API 모의투자(VTS, openapivts.koreainvestment.com )로, 개발자용 상시 테스트 서버다. 앱키만 있으면 바로 쓸 수 있고 잔고·주문은 API로만 조회된다. 다른 하나는 KIS 웹·MTS 앱에서 신청하는 프로모션 "모의투자 대회"로, 완전히 다른 시스템이다. 계좌번호가 우연히 같은 걸 쓸 수 있게 되어 있어서, 처음엔 "내가 대회 순위에 안 뜨는 게 버그인가?" 하고 한참 찾아봤다. API로 낸 주문은 대회 순위에 절대 반영 안 된다. 서로 다른 시스템이라 그렇다. 오늘 만난 버그들 1. SQLite가 "database is locked"를 뱉는다 자율매매 엔진이 백그라운드 스레드에서 5분마다 DB를 건드리는데, 동시에 API 요청이 들어오면 락이 걸렸다. 기본 SQLite는 동시 쓰기에 관대하지 않다. conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA busy_timeout=5000") 새 커넥션 열 때마다 이 두 줄만 추가하면 된다. WAL 모드는 리더가 라이터를 막지 않게 해주고, busy_timeout은 락이 걸려도 바로 에러 내지 않고 잠깐 기다렸다 재시도하게 해준다. 2. 죽은 커넥션을 재사용해서 RemoteDisconnected KIS 클라이언트가 requests.Session() 하나를 프로세스 전체에서 재사용하는 구조였다. 오래 살아있는 세션의 keep-alive 커넥션을 KIS 서버가 일방적으로 끊어버리는 경우가 있는데, 그 상태에서 재사용하면 RemoteDisconnected 가 터진다. from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry retry = Retry(total=2, connect=2, read=2, backoff_factor=0.3) session.mount("https://", HTTPAdapter(max_retries=retry)) 이거 하나로 간헐적 에러가 거의 다 사라졌다. 대신 죽은 커넥션을 새로 맺느라 응답이 몇 초 더 걸리는 트레이드오프는 생긴다. 3. 대량 시장가 주문이 미체결로 쌓인다 자율매매 엔진이 예산을 한 종목에 다 밀어넣으려다, 그 종목의 실제 유동성보다 훨씬 큰 수량을 시장가로 냈다. 절반도 안 체결되고 나머지는 미체결로 남았는데, 더 큰 문제는 이 미체결 주문 하나 때문에 엔진 전체가 다음 판단을 멈춰버렸다는 거다. 종목의 평균 거래량 대비 참여율로 주문 수량 자체를 상한선 걸어서 해결했다. 시장이 실제로 소화 가능한 물량만 주문하게 하니 미체결 문제가 거의 사라졌다. 4. 잔고 정산 지연 때문에 현금이 마이너스로 찍혔다 엔진이 "지금 남은 현금"을 기준으로 다음 매수 수량을 계산하는데, KIS 잔고 API의 정산 반영이 실시간이 아니었다. 사이클마다 "아직 현금 있네" 하고 계속 사들이다가, 실제로는 이미 다 써버린 상태가 됐다. 계좌의 추정 인출가능금액이 마이너스까지 갔다. 매수 직전에 KIS가 그 순간 실시간으로 계산해주는 진짜 주문가능금액을 한 번 더 확인하는 걸로 고쳤다. 우리가 계산한 예상치가 아니라, 브로커가 확인해주는 값을 최종 권위로 쓰는 게 맞다. 5. useSWR에 fetcher 함수를 잘못 넘긴 프론트엔드 실수 이건 좀 창피하다. // 버그 useSWR("/api/kis/buying-power", getKisBuyingPower); // 수정 useSWR("/api/kis/buying-power", () => getKisBuyingPower()); SWR은 fetcher(key) 형태로 호출한다. 첫 번째 인자를 종목코드로 기대하는 함수에 SWR 키 문자열이 그대로 들어가버려서, 실제로는 code=/api/kis/buying-power 라는 요청이 나가고 있었다. 로그에 이게 200번 넘게 쌓인 걸 보고서야 알아챘다. 백테스트 결과는 화려하지 않았다 여기까지 고치고 "그래서 이 AI가 돈은 버냐"가 궁금해서 워크포워드 백테스트를 돌렸다. 결과를 그대로 옮긴다. 수익률 -2.77% 벤치마크 대비 알파 큰 폭 마이너스 승률 21.7% 손익비 0.45 회전율 317% 단순 기술지표 스코어링 + 손절/익절만으로는 잦은 매매 비용을 이길 엣지가 없다는 뜻이다. 이 숫자를 그대로 공개하는 이유는, 파이프라인을 만드는 것과 그 파이프라인이 알파를 만드는 것은 완전히 다른 문제라는 걸 스스로 계속 확인하고 싶어서다. 오늘 배운 것 브로커 API 연동은 코드보다 개념 정리에서 더 오래 걸린다. 멀티스레드에서 SQLite를 쓴다면 WAL 모드는 옵션이 아니라 기본값이어야 한다. 그리고 자동매매 시스템에서는 "우리가 계산한 값"과 "브로커가 실제로 확인해주는 값"이 다를 수 있다는 걸 항상 염두에 둬야 한다 — 최종 주문 직전엔 후자를 믿는다. 다음 글에서는 로컬 LLM을 자율매매 판단에 붙인 구조를 좀 더 깊게 다뤄볼 생각이다. 이 프로젝트는 학습·실험 목적의 모의투자 시스템이며 투자 조언이 아닙니다. GitHub 저장소: sunwoo8478/kis-paper-trading
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
한국투자증권 Open API로 AI 자동매매 봇 만들다가 하루 종일 삽질한 기록. 실제로 돌아가는 대시보드의 실시간 시장 스캐너 하루 동안 만난 버그 5개와 해결 순서 "로컬 LLM으로 자동매매 시켜보면 재밌겠다"에서 시작했다 KOSPI/KOSDAQ 모의투자 대시보드를 FastAPI + SQLite, Next.js로 만들고 있다. 로컬에서 돌아가는 LLM(Ollama + EXAONE)이 실시간으로 매수·매도 판단을 내리는 구조다. 자체 시뮬레이터로만 굴리다가 오늘 한국투자증권(KIS) Open API의 모의투자 서버에 실제로 붙여봤는데, 코드보다 개념 정리에서 더 오래 헤맸다. 그 기록을 남긴다. 구조 backend/ FastAPI + SQLite ai/ 자율매매 엔진 (로컬 / KIS 두 개, 독립…
Open source