
AI가 작성한 코드는 297개의 테스트를 모두 통과했다. 하지만 사양은 정반대였다
요약
AI를 이용한 암호자산 자동 매매 봇 개발 과정에서 발생한 테스트의 한계와 실무적 오류를 다룹니다. 테스트가 통과하더라도 비즈니스 로직의 전제가 틀릴 수 있음을 경고하며, 외부 데이터와의 대조 및 환경 관리의 중요성을 강조합니다.
핵심 포인트
- 테스트는 코드의 동작을 검증할 뿐, 비즈니스 로직의 전제(사양) 오류는 잡아내지 못함
- AI가 작성한 코드는 외부 공식 문서(수수료, 단위 등)와 반드시 대조해야 함
- 컨테이너 배포 환경에서 상태 파일(STOP 파일)로 인한 기동 오류 주의 필요
- 설정 변경 사항이 실제 환경에 반영되지 않는 배포 프로세스의 위험성 인지
AI에게 코드를 작성하게 하여, 암호자산 자동 매매 Bot을 만들었습니다. 거래소 API에 연결하고, Docker화하여 클라우드에 상주시켜 24시간 구동하고 있습니다. 테스트는 737개가 있습니다.
아직 단 1엔의 실탄(실제 자금)도 투입하지 않았습니다. 그리고 이 기사에 나오는 문제는 전부 그전에 발견되었습니다.
AI는 놀라울 정도로 빠르게 코드를 작성합니다. 다만, 여기에 나오는 문제 중 그 어떤 것도 테스트에서는 걸러지지 않았습니다. 걸러낸 것은 전부 다른 것이었습니다.
이 기사는 "AI 개발은 위험하다"라는 이야기가 아닙니다. 어느 부분이 위험하지 않은 채로 남아 있고, 어느 부분이 위험한 채로 남아 있는지를 실제로 일어난 일을 통해 분류한 기록입니다.
1. 테스트는 「전제」 자체를 검증해 버린다
거래소 API 구현을 AI에게 작성하게 했습니다. 테스트 297개, 전부 통과.
하지만 숫자에 위화감이 있었습니다. _JPY가 붙은 종목 코드의 수수료가 0%이고, 표시가 없는 쪽에 0.09%가 붙어 있었습니다.
거래소가 공식적으로 공표하고 있는 현물 거래 수수료와 일치하는 쪽은 표시가 없는 쪽이었습니다. 그리고 수수료 0%는 레버리지 거래의 특징입니다.
즉, 현물과 레버리지를 반대로 구현하고 있었습니다.
실탄을 투입해 구동했다면 이렇게 됩니다.
- 매수 주문 → 현물 구매가 아니라 레버리지 포지션(建玉) 진입이 됨
- 결제 의도의 매도 → 포지션 결제가 아니라 **신규 공매도(空売り)**가 됨
즉, 닫으려 했던 포지션이 계속 늘어나게 됩니다.
테스트는 무엇을 하고 있었나
테스트는 올바르게 작성되어 있었습니다. "이 종목 코드로 주문을 낸다", "응답을 이렇게 해석한다"를 제대로 검증하고 있었습니다.
틀린 것은 전제 쪽이었고, 테스트는 그 전제 자체를 검증하고 있었습니다.
이는 AI에 국한된 이야기가 아닙니다. 사람이 작성해도 똑같은 일이 일어납니다. 다만 AI를 사용하면 전제를 확인하는 공정이 생략되기 쉽습니다. 코드가 즉시 나오기 때문에, "이 종목 코드가 정말 현물인가?"라고 멈춰 서서 의심할 계기가 없는 것입니다.
효과적인 대책: 코드 베이스 외부에 있는 권위와 대조하기
발견한 것은 공식 사이트가 공표하고 있는 수수료 표였습니다. 내 코드도, 테스트도, AI도 아니었습니다.
- 단위, 코드 체계, 수수료, 상한값과 같은 외부에서 정의된 값은 코드 베이스 외부에 있는 1차 정보와 한 번 대조할 것
- "AI가 그렇게 작성했으니까"는 근거가 되지 않는다. AI는 학습 데이터에 있었던 다른 상품의 사양을 작성하고 있을 가능성이 있다.
2. 환경이 상태를 가지고 있다
2-1. 배포할 때마다 Bot이 죽어 있었다
배포할 때마다 매매 루프만 기동되지 않고 있었습니다.
원인은 다음과 같습니다.
- 배포 시 컨테이너로
SIGTERM이 전달됨 - 프로세스 감독 역할이 안전 정지를 위해
STOP파일을 생성함 - 그 파일이 **영구 볼륨(Persistent Volume)**에 남음
- 새로운 머신이 기동하면서 "
STOP파일이 있으니 기동하지 않는다"라고 판단함
까다로웠던 점은, "사용자가 의도적으로 멈춘 STOP"과 "배포 편의를 위한 STOP"을 동일한 빈 파일로 구분할 수 없다는 것이었습니다. 기동 시 단순히 삭제하면, 의도적인 정지까지 무시하는 또 다른 사고가 됩니다.
결국 "셧다운 시작 시점에 이미 존재했는가"를 기록하여, 자신이 만든 경우에만 정리하는 방식으로 만들었습니다.
로그를 확인하니 3번 발생했습니다. 알아챈 계기는 테스트도 모니터링도 아닌, 사람이 "멈춰 있지 않은가?"라고 직접 확인하러 간 것이었습니다.
2-2. 설정을 변경하여 배포했는데, 11일 동안 반영되지 않았다
이쪽이 더 무서운 사고였습니다.
로컬의 config.yaml을 수정하여 배포했습니다. 테스트도 통과했습니다. 배포도 성공했습니다.
하지만 본방의 동작은 아무것도 변하지 않았습니다.
기동 프로세스가 다음과 같이 되어 있었기 때문입니다.
def bootstrap_data_dir(code_dir: Path, data_dir: Path) -> None:
"""기존의 config.yaml이 있으면 일절 변경하지 않고 유지한다
(사용자가 조정한 설정을 재배포할 때마다 지우지 않기 위해)."""
...
이것은 의도적인 설계입니다. 사용자가 대시보드에서 조정한 설정을 재배포 시 날려버리지 않기 위해서입니다. 설계로서는 올바릅니다.
다만 그 결과, 영구 볼륨 상의 최초 복사본이 계속 사용되었습니다. 11일 전의 설정입니다.
게다가 질이 나쁜 것은, 설정을 읽는 측에 기본값(Default value)이 있었다는 점입니다.
cfg["sizing_mode"] = str(raw.get("sizing_mode", "fixed"))
오래된 설정 파일에는 sizing_mode 행이 존재하지 않습니다. 그래서 기본값(Default value)인 fixed가 사용됩니다. 검증을 통해 결정한 값은 vol_adjusted였습니다.
에러는 발생하지 않습니다. 로그에도 아무것도 남지 않습니다. 프로그램이 죽지도 않습니다. 다른 설정으로 정상적으로 계속 작동합니다.
효과적인 대책: 운영 환경의 실물을 읽기
배포에 성공했다는 것이 설정이 반영되었다는 것을 의미하지는 않습니다.
- 배포 후에 운영 환경의 설정 파일 그 자체를 읽어서, 수중에 있는 것과 키(Key) 단위로 차이를 비교한다.
- 기본값이 포함된 무엇이 기본값으로 동작하고 있는지 기동 시점에 로그로 남긴다.
get(key, default)는 키가 사라져도 알아차릴 수 없다. - 영구 볼륨(Persistent Volume), 환경 변수(Environment Variable), 시크릿(Secret), 관리형 DB(Managed DB) — "코드 외부에 살아남는 것들"을 목록화한다. 이 부분이 배포의 맹점이 된다.
3. "측정 대상"이 달랐다
여기서부터는 버그가 아닙니다. 버그보다 찾아내기 어려운 것입니다.
3-1. 자동화된 합격 기준을 모두 통과한 것이 틀렸다
백테스트(Backtest)의 합격 판정을 자동화하고 있었습니다.
- 전 기간 흑자
- 전반기·후반기 모두 흑자
- 연도별로 보았을 때 과반수가 흑자
- 인접한 파라미터(Parameter)도 흑자
어떤 전략 가설이 이 4가지를 모두 통과했습니다. 최상의 후보는 순손익 +84,004엔, 프로핏 팩터(Profit Factor) 2.38. 학술적인 뒷받침도 있는 가설이었습니다.
보통이라면 여기서 채택했을 것입니다.
하지만 만일을 위해, 정반대의 전략(가설을 반전시킨 것)을 동일한 조건으로 측정하고 있었습니다.
| 합격 수 | 평균 순손익 |
|---|---|
| 주 가설 | 9건 |
| 대조군(정반대) | 15건 |
정반대 쪽이 더 이기고 있었습니다.
즉 "그 가설이 옳아서 이긴 것"이 아니라, "이 조건에서는 무엇을 골라도 이기는 후보가 섞여 있다"는 것뿐이었습니다.
자동화된 4가지 기준은 "선택한 지점이 안정적인가"만을 측정하고 있습니다. "그 가설 자체가 옳은가"는 측정하지 않습니다. 그리고 "대조군도 측정하자"라는 판단은 자동화된 기준 안에 포함되어 있지 않았습니다. AI도 제안하지 않았습니다.
3-2. 두 가지 도구가 동일한 구성에 대해 다른 숫자를 내놓았다
"설정을 이렇게 바꾸면 성적이 23.9% → 30.3%로 개선된다"라는 결과를 채택하고 있었습니다.
나중에 다른 도구로 측정했더니, **동일한 구성이 20.2%**였습니다.
원인은 도구의 내부 모델(Internal Model) 차이였습니다. 한쪽은 "매주 모든 자금을 재배분한다"는 전제였고, 다른 한쪽은 "프레임별 독립 채산". 운영 환경은 후자입니다.
즉, 운영 환경과 다른 전제를 가진 시뮬레이터가 내놓은 개선 폭을 보고 운영 환경의 설정을 결정하고 있었습니다.
숫자는 정확하게 계산되었습니다. 전제가 운영 환경과 달랐을 뿐입니다. 그리고 전제는 코드 어디에도 "이것은 운영 환경과 다르다"라고 적혀 있지 않았습니다.
효과적인 대책: 반전시켜 측정하기 / 전제를 명문화하기
- 가설을 반전시킨 것을 동일한 조건으로 측정한다. 이는 코드 한 줄 추가로 끝나는 경우가 많아 효과가 가장 컸다.
- 판정 기준은 한 곳에만 둔다. 공통 모듈로 집약하여, 검증 도중에 "이번만 완화하자"가 불가능한 형태로 만들었다. 매번 다시 쓰면 기준이 미묘하게 흔들리고, 게다가 자신에게 유리한 방향으로 흔들린다.
- 여러 도구로 측정한 숫자를 나란히 비교할 때는, 내부 모델이 동일한지를 먼저 확인한다.
요약
발생한 일을 분류하면 다음과 같습니다.
| 종류 | 실례 | 잡아낸 것 |
|---|---|---|
| 테스트가 전제 자체를 검증했다 | 현물과 레버리지의 혼동 | 외부 공표 값과의 대조 |
| ... |
테스트로 잡아낸 것이 단 하나도 없습니다. 737건을 작성했음에도 말입니다.
AI는 코드를 작성하는 속도를 높였습니다. 줄어들지 않은 것은 "이것이 정말로 옳은가"를 의심하는 업무입니다. 오히려 늘어났습니다. 코드가 빠르게 늘어나기 때문에 의심할 대상도 빠르게 늘어나기 때문입니다.
자신을 의심하는 메커니즘은 마음가짐이 아니라 코드와 절차에 두어야 합니다. 자신에게 유리하게 기준을 완화할 여지를 스스로 없애 두어야 합니다. 현재로서는, 그 부분이 남아 있는 업무라고 생각합니다.
이 봇은 아직 dry_run으로 동작하고 있습니다. 실제 자금을 투입하는 것은 일정 거래 수와 가동 기간 조건을 충족한 이후로 하고 있습니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기