teji

tede

AI 에이전트, 트레이딩 봇 개발 중인 남편의 일기 🤖
화장품에 진심인 간호사 아내의 코덕로그 💄

전체보기

클로드코드(Claude Code) 자동매매 봇 (5) 운영 사고 4건과 그때 정한 운영 원칙: 장부 초기화, 좀비 포지션과 차트 스팸 120장, 자본 이중계상 +37%, 재시작 누락 5일

최종 업데이트: 2026-09-22

이 글은 투자 조언이 아니에요. 제 자동매매 봇을 운영하며 낸 사고와 그 뒤 정한 절차를 기록한 글이고, 손실 금액은 적지 않았어요.

클로드코드 자동매매 봇 5편: 봇 사고 4건과 운영 원칙 썸네일

클로드코드(Claude Code)로 만든 자동매매 봇을 2026년 4월부터 넉 달 운영하면서 사고를 네 번 냈어요. PC 이관 후 장부(state) 파일이 비어버린 사고, 거래소에서 직접 청산한 포지션이 장부에 남아 텔레그램에 같은 차트를 17시간 동안 120장 보낸 사고, 유니파이드 계정의 잠긴 마진이 이중계상돼 자본이 37% 부풀려 보인 사고, 설정을 바꾸고 재시작을 안 해 5일간 옛 규칙으로 매매한 사고예요. 네 건 중 코드 버그는 한 건뿐이고 나머지 셋은 “봇을 켜둔 채 손댔다”, “바꾸고 재시작을 안 했다” 는 사람 쪽 순서 문제였어요. 그래서 이 글의 결론은 코드가 아니라 절차예요. 멈추고, 고치고, 켜고, 켜졌는지 확인한다.

이 글은 4편 백테스트 리서치의 다음 편이고, 1편에서 적은 안전장치 중 하나가 실전에서 실제로 작동한 기록이기도 해요. 실제 로그와 실제 코드를 붙였어요.

자동매매 봇 사고 4건은 무엇이었나요?

자동매매 봇 사고 4건 요약: 장부 초기화, 좀비 포지션과 차트 스팸, 자본 이중계상, 재시작 누락

# 날짜 증상 원인 정한 원칙
① 4/21 PC 이관 후 장부 파일이 빈 파일로 재생성. 거래소엔 포지션 2개, 장부엔 0개 이관 스크립트가 상태 파일과 폴더 연결(junction)을 빠뜨림 이관 체크리스트, 상태 파일은 백업 먼저
② 5/6~7 거래소에서 직접 청산한 포지션이 장부에 계속 남고, 텔레그램에 같은 차트 120장 봇을 켜둔 채 장부 파일만 정리. 5분마다 메모리가 파일을 도로 덮어씀 멈추고 → 고치고 → 켠다
③ 6/26 포지션을 열자마자 자본이 37% 튀어 보임. 가짜 일간 수익 선물 계정가치 + 현물 USDC 를 더해서 잠긴 마진이 이중계상 (유일한 코드 버그) 잔고는 분해 값까지 기록, 포지션 유무로 교차 확인
④ 7/4~9 설정을 바꿨는데 5일간 옛 규칙으로 매매 파일만 고치고 봇을 안 껐다 켬 적용의 증거는 새 PID 와 시작 로그뿐

사고 ①: 이관했더니 장부가 왜 비어 있었나요? (4/21)

봇을 다른 폴더 구조로 옮긴 날, 옮기고 켰더니 첫 사이클에서 “장부와 실포지션이 다르다” 는 경고가 떴어요. 장부 파일이 빈 껍데기였고, 거래소에는 트랜치 2개가 살아 있는데 봇은 아무것도 안 들고 있다고 믿는 상태였어요. 원인은 이관 스크립트가 상태 파일을 옮기지 않았고, 코드가 기대하는 윈도우 junction 폴더 연결도 만들지 않아 모듈을 못 찾는 에러까지 함께 난 것이었어요.

복구는 거래 기록 CSV 의 진입 줄 2개를 보고 장부를 손으로 다시 썼고, 그 전에 빈 파일도 백업으로 남겼어요. 이때 정한 원칙은 상태 파일은 손대기 전에 날짜와 사유를 붙여 백업하고, 이관에는 체크리스트를 둔다는 거예요. 같은 날 재기동 첫 사이클에 아주 작은 매수·매도가 찍혀서 놀랐는데, 진입가 기준 금액과 현재가 기준 금액의 차이를 맞추는 리밸런스였고 설계대로였어요. 사고가 아니라 “알고 있어야 안 놀라는 소음” 으로 분류했어요.

사고 ②: 청산한 포지션이 왜 살아나서 차트를 120장 보냈나요? (5/6~7)

규칙을 바꾸기 전에 잡혀 있던 숏 포지션을 거래소에서 직접 청산하고, 봇 장부 파일에서 그 포지션을 지웠어요. 여기까지는 맞아요. 문제는 봇을 켜둔 채로 했다는 거예요. 봇은 시작할 때 장부를 한 번 읽고 그 뒤로는 5분마다 메모리 내용을 파일에 저장하므로, 파일을 고쳐도 다음 사이클에 메모리가 파일을 도로 덮어써요. 좀비 포지션이 부활한 거예요.

자동매매 봇 실제 로그: state/live desync detected 경고가 5분 간격으로 반복된 기록

# 사고 ② 실제 로그 : 5/6 11:00 부터 5분마다 같은 줄이 270번
# logs/bot.log.2026-05-06  (156줄)  /  bot.log.2026-05-07  (114줄)
2026-05-06 11:00:26 [WARNING] state/live desync detected BEFORE signals; skipping cycle. Use --allow-desync to override.
2026-05-06 11:05:27 [WARNING] state/live desync detected BEFORE signals; skipping cycle. Use --allow-desync to override.
2026-05-06 11:10:28 [WARNING] state/live desync detected BEFORE signals; skipping cycle. Use --allow-desync to override.
  ... (5분 간격으로 반복)
2026-05-07 00:03:29 [WARNING] state/live desync detected BEFORE signals; skipping cycle. Use --allow-desync to override.

# 봇은 정확히 설계대로 했어요 : 장부(state)와 실포지션이 다르니 매 사이클 '건너뜀'
# 문제는 사람이었어요 : 봇을 켜둔 채 장부 파일만 고쳐서, 5분마다 메모리가 파일을 도로 덮어씀
# 그리고 차트 발송은 이 검사보다 '앞'에 있어서, 건너뛴 사이클마다 텔레그램에 차트가 한 장씩 → 17시간 120장

봇은 매 사이클 “장부와 실포지션이 다르니 건너뛴다” 를 정확히 수행했어요. 5분 간격으로 270번이에요. 부작용은 차트를 텔레그램으로 보내는 코드가 이 검사보다 앞에 있었고, 건너뛴 사이클에서는 “이 봉은 이미 처리했다” 는 표시가 갱신되지 않아 5분마다 같은 차트가 한 장씩, 17시간 동안 120장이 온 거예요. 조치는 봇 정지, 장부 정리(백업 포함), 차트 발송 직후 처리 표시를 갱신하는 한 줄 패치, 재시작이었어요. 여기서 나온 원칙이 “멈추고 → 고치고 → 켠다” 예요. 순서를 바꾸면 고친 것이 무효가 되고 부작용이 따라와요.

봇이 스스로 멈추는 검사는 어떻게 생겼나요?

reconcile.py 코드: 장부와 거래소 실포지션을 심볼별로 비교해 50달러 이상 어긋나면 사이클을 건너뛰는 검사

# 원칙 ② 봇이 스스로 멈추는 검사 : reconcile.py (실제 코드 요약)
def compare_state_to_live(state, live_notional_by_symbol, epsilon=50.0):
    """장부(state)가 기대하는 포지션 금액과 거래소 실포지션을 심볼별로 비교"""
    target = target_by_symbol(state)               # 장부 기준 심볼별 목표 명목금액
    diffs = []
    for symbol in sorted(set(target) | set(live_notional_by_symbol)):
        delta = target.get(symbol, 0.0) - live_notional_by_symbol.get(symbol, 0.0)
        if abs(delta) > epsilon:                    # $50 넘게 다르면 '어긋남'
            diffs.append(PositionDiff(symbol, target.get(symbol, 0.0), live, delta))
    return SyncReport(ok=len(diffs) == 0, diffs=diffs)

# run_live.py 매 사이클, 신호 계산보다 먼저
sync = compare_state_to_live(state, live_notional_by_symbol(broker))
if not sync.ok and not args.allow_desync:
    logger.warning("state/live desync detected BEFORE signals; skipping cycle.")
    continue                                        # 주문 안 냄. 사람이 볼 때까지 대기

사고 ①② 에서 봇이 주문을 내지 않고 버틴 것은 이 검사 덕분이에요. 매 사이클, 신호를 계산하기 전에 장부가 기대하는 포지션 금액과 거래소 실포지션을 심볼별로 비교하고, 50달러 넘게 다르면 그 사이클은 주문 없이 건너뛰고 사람이 볼 때까지 기다려요. 1편의 안전장치 4가지 중 이 검사가 실전에서 두 번 일했어요. 안전장치는 안 쓰이는 것이 목표가 아니라 쓰였을 때 손실을 막는 것이 목표예요.

사고 ③: 자본이 왜 37% 부풀려 보였나요? (6/26)

유일한 코드 버그예요. 3편의 AI 판단 봇을 라이브로 올린 날, 첫 포지션을 열자마자 텔레그램 알림의 일간 수익률이 +37% 로 찍혔어요. 원인은 잔고 계산이었어요. Hyperliquid 는 현물과 선물을 한 계정으로 쓰는 유니파이드 구조라 선물 계정가치 안에 이미 현물 USDC 에서 잠긴 담보가 들어 있는데, 코드는 선물 계정가치와 현물 USDC 를 그냥 더했고 포지션을 열어 마진이 잠기는 순간 그만큼 이중계상됐어요.

Hyperliquid 어댑터 get_balance 수정 코드: 현물 USDC 에 선물 미실현손익만 더해 자본 이중계상을 제거

# 사고 ③ 수정 후 코드 : exchanges/hyperliquid.py get_balance (요약)
# 전 : equity = perp_account_value + spot_usdc
#      유니파이드 계정에선 perp 의 담보(collateral)가 spot USDC 에서 잠긴 돈이라
#      둘을 더하면 잠긴 마진만큼 이중계상 → 자본이 37% 부풀려 보임 (가짜 일간 수익)

# 후 : spot 에 USDC 가 있으면 spot USDC + perp 미실현손익만 더한다
perp_upnl = sum(float(p["position"].get("unrealizedPnl", 0)) for p in state["assetPositions"])
equity = (unified_spot + perp_upnl) if unified_spot > 0.0 else perp_account_value

return {"equity": equity,
        "perp_account_value": perp_account_value,   # 디버그용으로 분해 값도 같이 반환
        "spot_usdc": unified_spot,
        "perp_upnl": perp_upnl}

# 발견 계기 : 포지션 없을 땐 숫자가 맞고, 포지션 열자마자 자본이 튀었어요
# 검증 : 같은 함수를 쓰는 다른 봇(sqz)도 함께 정정됨. 무포지션일 땐 동작 동일(무회귀)

고친 뒤엔 현물 USDC 에 선물 미실현손익만 더해요. 포지션이 없을 때는 예전과 같은 값이라 회귀는 없고, 같은 함수를 쓰는 4시간봉 봇도 함께 정정됐어요. 원칙은 숫자를 분해해서 기록한다는 거예요. 자본 하나만 찍지 않고 선물 가치, 현물, 미실현을 따로 반환하게 바꿔서 합이 이상하면 어느 항이 튀었는지 바로 보이게 했어요. 계정 구조는 Hyperliquid 공식 문서를 참고하세요.

사고 ④: 바꾼 설정이 왜 5일간 적용되지 않았나요? (7/4~9)

2주 리뷰에서 “AI 재량 청산은 효과가 없다”, “종목은 하나만” 같은 결론이 나와 설정 파일을 고쳤는데, 5일 뒤 리뷰에서 봇이 여전히 세 종목을 매매하고 재량 청산을 하고 있었어요. 재시작을 안 한 거예요. 파일은 바뀌었지만 돌고 있던 프로세스는 옛 설정을 메모리에 들고 있었어요. 손실보다 5일치 데이터가 “바꾼 규칙” 이 아니라 “옛 규칙” 의 데이터가 돼버린 것이 뼈아팠어요.

자동매매 봇에 손대는 날 6줄 체크리스트: 멈추기, 백업, 고치기, 켜기, PID 와 시작 로그 확인, 첫 사이클 동기화 확인

# 원칙 ①④ 사람 쪽 체크리스트 : 봇에 손대는 날 순서
# 1. 멈춘다        tasklist | findstr python  →  taskkill /PID <pid> /F
# 2. 백업한다      copy operation_state.json operation_state.json.bak_YYYYMMDD_<사유>
# 3. 고친다        state 정리 / 코드 패치 / config 변경
# 4. 켠다          set SQZ_USE_V3=true && python -m operation.run_live
# 5. 확인한다      새 PID + 시작 로그의 설정값이 내가 바꾼 값인지
>                 [INFO] univ=[BTCUSDT, ETHUSDT, SOLUSDT] disc_exit=False min_conv=0.68
# 6. 첫 사이클     "State and live positions are in sync."  이 줄이 떠야 끝

# 파일을 고쳤다 ≠ 봇에 적용됐다.  적용의 증거는 프로세스 ID 와 시작 로그 두 개뿐이에요
# 사고 ④ 는 3번까지 하고 4·5번을 5일간 안 한 사고예요

그래서 체크리스트가 생겼어요. 핵심은 5번과 6번이에요. 새 프로세스 ID 와 시작 로그에 찍힌 설정값이 내가 바꾼 값과 같은지 보고, 첫 사이클에 “장부와 실포지션 일치” 가 뜨는 것까지 확인해야 끝이에요. 파일을 고친 것은 적용의 증거가 아니에요.

네 사고에서 나온 운영 원칙 5가지

자동매매 봇 운영 원칙 4가지 카드: 멈추고 고치고 켠다, 봇이 먼저 멈추게, 숫자 분해 기록, PID 와 시작 로그로 확인

  1. 멈추고 → 고치고 → 켠다. 봇이 켜진 상태에서 장부와 설정을 만지지 않아요. 메모리가 파일을 이겨요.
  2. 봇이 먼저 멈추게 한다. 장부와 실포지션이 어긋나면 주문 없이 건너뛰고 사람을 기다려요. 이 검사가 두 번 일했어요.
  3. 숫자는 분해해서 기록한다. 자본 하나가 아니라 선물·현물·미실현을 따로 남기고, 포지션을 열고 닫으며 교차 확인해요.
  4. 적용의 증거는 PID 와 시작 로그. 파일 고침과 봇 적용은 다른 사건이에요.
  5. 봇 시작·정지·실주문은 사람 손. 클로드코드에는 명령을 제시하게만 하고 실행은 제가 눌러요. 사고 ② 도 이 규칙 덕분에 봇이 멋대로 주문을 내진 않았어요.

AI 는 코드를 고치는 것은 빠르지만 “그 코드가 지금 돌고 있는 프로세스에 들어갔는가” 는 사람이 확인해야 해요. 사고 ②④ 가 둘 다 그 틈에서 났어요. 지금은 클로드코드가 패치를 끝내면 반드시 재시작 명령과 확인할 로그 줄을 같이 내놓게 해뒀어요. 프로세스 재시작과 설정 반영의 일반 원칙은 The Twelve-Factor App 의 Config 항목이 잘 정리해요.

자주 묻는 질문

Q. 사고로 돈을 잃었나요?
네 건 중 직접 손실로 이어진 것은 없어요. ① 은 봇이 주문을 멈춰서, ② 도 건너뛰기만 반복해서, ③ 은 표시 문제라서, ④ 는 옛 규칙도 이미 돌리던 규칙이라 새 사고는 아니었어요. “어긋나면 멈춘다” 는 안전장치 덕이에요.

Q. 거래소에서 직접 청산하면 안 되나요?
해도 돼요. 대신 그 다음 순서가 봇 정지 → 장부 정리 → 재시작이에요. 봇을 켜둔 채 장부만 고치면 사고 ② 가 재현돼요.

Q. 상태 파일(장부)이 무엇인가요?
봇이 지금 어떤 포지션을 얼마에 들고 있는지 적어두는 JSON 파일이에요. 거래소가 진실이고 장부는 봇의 기억이라 둘이 다르면 봇이 잘못된 판단을 하므로 매 사이클 둘을 비교해요.

Q. 자본 이중계상은 어떻게 알아챘나요?
포지션이 없을 땐 숫자가 맞고 열자마자 튀었어요. “포지션 유무에 따라 값이 달라진다” 가 힌트였고, 그래서 지금은 잔고를 분해 값과 같이 기록해요.

Q. 클로드코드가 재시작까지 해주면 안 되나요?
기술적으로는 가능해요. 시키지 않는 이유는 돈이 걸린 프로세스의 시작과 정지는 제가 눌러야 무슨 일이 났는지 제가 알기 때문이에요. 대신 재시작 명령과 확인 로그를 항상 같이 내놓게 했어요.

함께 보면 좋은 글

출처: 제 봇의 실제 로그 파일(bot.log 2026-05-06·07), 상태 비교 코드(reconcile.py), 거래소 어댑터 수정 코드(2026-06-26), 운영 규칙 문서. 투자 조언이 아니에요. 다음 편은 3편의 AI 판단 봇을 2주 돌린 데이터 리뷰예요.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *