0. 시작하기 전에
461개 버전을 처음부터 다시 열어보기로 했다. 이유는 단순하다. 지금 돌아가는 봇의 파라미터 하나하나에 “왜 이 값인가"를 설명할 수 있어야 하는데, 그 근거의 절반쯤은 초기 몇 주에 만들어졌기 때문이다. 그때 뭘 겪었는지 모르면 지금 값이 왜 그 자리에 있는지도 모른다.
그래서 아카이브를 시각 순으로 정렬했다. 그리고 첫 화면에서 좀 웃었다. 가장 오래된 파일이 코드가 아니었다.
1. 봇보다 먼저 있었던 건 메모 한 장
1-1. Futuring Request.txt, 2026년 5월 25일
목록의 맨 위에는 Futuring Request.txt가 있다. 봇에게 뭘 고쳐달라고 적어둔 요구사항 메모다. 그 옆에는 윈도우에서 봇을 띄우던 배치파일이 같이 남아 있다.
C:\Trader> type RunTrader.bat
@echo off
cd /d "C:\codezero"
python "bithumb_pure_bot.py"
pause
C:\Trader> dir /od
Futuring Request.txt ← 최초 요구사항 메모 (보존 최고문서)
bithumb.txt / BAPI.txt ← 당시 API 규격 메모
bithum.pdf ← 거래소 문서
v81~v90_slot_states.json배치파일 네 줄이 이 시절을 다 설명한다. C:\codezero로 들어가서 파이썬을 띄우고 pause. 마지막 줄이 특히 그렇다. pause가 있다는 건 더블클릭으로 켰다는 뜻이다. 콘솔에서 실행했다면 창이 저절로 닫히지 않으니 pause가 필요 없다. 아이콘을 두 번 눌러 봇을 켜고, 검은 창에 뜬 로그를 눈으로 읽고, 다 보면 아무 키나 눌러 닫던 시절이다.
지금은 맥북에서 터미널 세션에 붙여놓고 돌린다. 켜는 방식 하나가 그 시절 운영 방식 전부를 요약한다. 더블클릭으로 켜는 봇은 필연적으로 사람이 앞에 앉아 있는 봇이다. 창을 닫으면 죽고, PC가 잠들면 죽고, 윈도우 업데이트가 재부팅을 걸면 죽는다. 그러니까 이 시절의 운영이란 결국 “봇을 지키고 앉아 있기"였다.
목록에 같이 남아 있는 bithumb.txt, BAPI.txt, bithum.pdf도 같은 시절의 증거다. 거래소 API 문서를 텍스트로 뜯어 옆에 펼쳐놓고 코드를 짰다는 뜻이다. 지금 같으면 문서 링크 하나 북마크해두면 끝인데, 그때는 규격 메모를 로컬 파일로 들고 다녔다. 왜 그랬는지는 뒤에서 나온다 — 인증 규격 하나 틀리면 아무것도 안 되는데, 그 “하나"가 문서 어디에 적혀 있는지 계속 확인해야 했기 때문이다.
v81~v90_slot_states.json은 훨씬 나중 파일이 같은 폴더에 섞여 들어온 흔적이다. 이 시절엔 없던 개념 — 슬롯 — 이 파일 이름에 들어 있다. 아카이브를 시각 순으로 정렬해야 하는 이유가 이런 데 있다. 폴더는 거짓말을 한다.
1-2. 메모 안에 이미 들어 있던 것
메모 쪽이 훨씬 재밌다. 내용의 대부분은 UI 요청이다. 시총 화면 색을 통일해달라, 연결 버튼을 연결/연결해지 둘로 쪼개달라, 입력창 글자를 키워달라. 지금 읽으면 자동매매 요구사항이라기보단 화면 불만 목록에 가깝다.
그런데 그 사이에 문장 두 개가 섞여 있다.
[ UI 요청 ]
· 시총 화면 색 통일
· 연결 버튼 → 연결 / 연결해지 분리
· 입력창 글자 크게
[ 매매 요청 ]
· 목표 수익률 도달 시 50% 물량 즉시 익절
· 나머지 50%는 트레일링 스톱으로 추세를 끝까지 추적
[ 안정성 요청 ]
· 2중으로 창 실행을 막는 로직을 추가해줘
첫날부터 익절 폭, 분할 청산, 트레일링 스톱이 요구사항에 있었다는 뜻이다. 코드 한 줄 없던 시점에 이미 “얼마에 팔고 나머지는 어떻게 끌고 갈지"를 문장으로 정해뒀다.
그리고 마지막 한 줄. “2중으로 창 실행을 막는 로직을 추가해줘.” 이건 전략이 아니라 사고 방지다. 같은 봇이 두 번 떠 있으면 잔고를 두 번 읽고 주문을 두 번 낸다. 실제로 이 문장은 바로 다음 버전 코드의 파일 맨 첫 줄에 그대로 박힌다.
1-3. 이 메모가 남긴 것
기록을 정리하면서 알게 된 건, 코드를 짜기 전에 문장을 먼저 썼다는 사실이다. 그 문장들은 대부분 화면 이야기였지만, 그 안에 이미 손익 처리의 뼈대가 들어 있었다.
재밌는 건 메모의 비율이다. UI 요청이 대여섯 줄인데 매매 요청은 두 줄이다. 화면 이야기가 훨씬 많다. 그럴 수밖에 없다 — 그때 내가 매일 마주하던 건 전략이 아니라 창이었으니까. 봇의 성능을 판단하는 유일한 창구가 그 창이었고, 창이 불편하면 봇이 잘 돌아가는지도 알 수 없었다.
지금은 정반대다. 화면은 터미널 대시보드 한 장이고, 요구사항의 100%가 게이트 조건과 파라미터다. 그 사이 두 달 동안 “보는 문제"가 “판단하는 문제"로 옮겨간 것이다. 다만 순서는 이게 맞았다고 생각한다. 볼 수 없으면 판단도 못 한다.
461개 버전을 다시 여는 이유도 여기 있다. 뭘 고쳤는지보다 왜 고쳐야 했는지가 안 남으면, 다음에 또 같은 자리에서 넘어진다. 지금 커밋마다 근거 수치를 같이 적는 습관도 결국 이 메모의 후손이다. 형식은 텍스트 파일에서 git 커밋으로 바뀌었지만, 하는 일은 똑같다 — 미래의 나에게 이유를 남기는 것.
2. 615줄짜리 시조
2-1. 계보의 0번
보존된 코드 중 가장 오래된 파일은 615줄, 2026년 5월 26일 밤 9시 11분이다. 461개 버전의 시조다. 파일 머리에 적힌 메모는 딱 한 줄인데, 전날 메모의 마지막 문장과 정확히 같다.
================= [ 2중 실행 방지 로직 ] =================
import tkinter as tk
from tkinter import ttk, messagebox
import pybithumb
# --- 시세·차트는 라이브러리로 ---
mkt_price = pybithumb.get_current_price(ticker)
df = pybithumb.get_ohlcv(ticker)
# --- 인증이 필요한 구간만 직접 구현 ---
class CustomBithumbAPI:
def _req(...) # HMAC-SHA512 서명
def get_balance(...)
def buy_market(...)
def sell_market(...)요구사항 메모가 파일 헤더가 되는 흐름이 꽤 정직하다. 문장 → 주석 → 코드. 지금은 그 자리를 커밋 메시지가 대신한다.
2-2. 한 파일에 전부
구조를 그려보면 이렇다.
bithumb_bot_v1_4.py
├─ CustomBithumbAPI 인증 통신 엔진
│ ├─ _req() HMAC-SHA512 서명 + POST
│ ├─ get_balance()
│ ├─ buy_market()
│ └─ sell_market()
├─ TraderApp(tk.Tk) GUI + 매매 로직
│ ├─ build_ui() tkinter 위젯 배치
│ ├─ on_tick() 시세 폴링 루프
│ └─ 손절 판정 개별 포지션 단위
└─ pybithumb 공개 데이터 전담클래스 하나가 API를 감싸고, 다른 하나가 창을 그린다. 그리고 그 창을 그리는 클래스 안에 매매 판단까지 같이 들어 있다. 화면과 전략이 분리돼 있지 않다는 뜻이다.
이게 왜 문제가 되는지는 나중에 아주 비싸게 배운다. 화면을 그리는 코드와 판단하는 코드가 한 클래스에 있으면, 전략을 검증할 방법이 없다. 백테스트를 돌리려면 창을 띄워야 하고, 창을 띄우면 사람이 앉아 있어야 한다. 검증이 불가능한 구조다. 지금 봇이 매매기록 시뮬레이션으로 모든 파라미터를 검증하고 배포하는 규율을 갖게 된 건, 이 구조를 뜯어내는 데 몇 시대가 걸린 뒤였다.
지금 구조와 나란히 놓으면 이렇다.
[ 2026-05-26 · 1 파일 615줄 ]
통신 + 화면 + 판단 ─ 전부 한 덩어리
검증 방법: 없음 (눈으로 본다)
되돌리기: 파일 복사
[ 현행 · 6 모듈 · git ]
통신 / 데이터 / 게이트 / 슬롯 / 청산 / 알림
검증 방법: 매매기록 시뮬레이션
되돌리기: git revert
공통: 읽는다 → 판단한다 → 주문한다
2-3. 왜 하필 tkinter였나
지금 보면 의아한 선택이다. 웹 대시보드를 만들 수도 있었고, 그냥 콘솔에 로그만 찍어도 됐다. 그런데 창을 그렸다.
이유는 단순하다. 버튼이 필요했기 때문이다. 자동매매를 처음 돌리는 사람이 제일 무서워하는 건 “지금 당장 멈추는 방법이 없는 것"이다. 콘솔 로그만 있으면 이상한 걸 보고도 할 수 있는 게 없다. 창에 빨간 버튼이 하나 있으면 다르다.
✔ 파이썬 표준 라이브러리 (설치 불필요)
✔ 버튼·입력창을 몇 줄로 만든다
✔ 윈도우에서 더블클릭으로 바로 뜬다
✘ 화면 코드와 로직 코드가 섞이기 쉽다
✘ 창이 떠 있어야만 봇이 산다
✘ 폰트·해상도 환경을 타면 레이아웃이 밀린다
→ 장점 셋은 첫 주에, 단점 셋은 그 뒤 두 달에 걸쳐 나타난다tkinter는 파이썬에 기본 포함돼 있어 아무것도 설치할 필요가 없다. 진입 장벽이 0이다. 그 대가로 화면과 로직이 섞이고, 창이 곧 프로세스가 된다. 뒤에 나올 폰트 사고도 이 선택의 연장선이다.
2-4. 왜 인증만 직접 짰나
여기가 이 버전에서 제일 마음에 드는 선택이다. 통신이 두 갈래로 나뉘어 있다.
- 시세, 분봉처럼 인증이 필요 없는 공개 데이터 →
pybithumb라이브러리를 그대로 사용 - 잔고 조회, 주문처럼 돈이 움직이는 구간 → 직접 구현
인증 요청은 공개 요청과 성격이 다르다. 요청 본문을 정해진 규칙대로 문자열로 조립하고, 비밀키로 해시를 만들어 헤더에 같이 실어 보낸다. 서버는 같은 규칙으로 해시를 다시 만들어 비교한다. 하나라도 어긋나면 거절이다 — 문자열 조립 순서, 인코딩, 구분자, 심지어 해시 결과를 어떤 타입으로 헤더에 넣는지까지.
payload → 문자열 조립 순서·구분자 틀리면 실패
→ 인코딩 UTF-8 아니면 실패
→ HMAC-SHA512 키 오타면 실패
→ base64 타입 틀리면 실패
→ 헤더에 싣기 헤더 이름 틀리면 실패
그리고 전부 같은 메시지로 돌아온다: 인증 실패라이브러리를 쓰면 저 다섯 단계가 한 줄 뒤로 숨는다. 잘 될 땐 편하다. 문제는 안 될 때다. 어느 단계에서 틀렸는지 알 방법이 없다. 남의 코드가 어디서 실패하는지 모르는 채로 실주문을 내보내는 게 더 무서웠다. 그래서 인증 구간만 손으로 짰다.
여기서 nonce라는 값도 처음 만난다. 요청마다 달라지는 숫자 — 보통 밀리초 단위 시각 — 를 서명에 섞어 넣는 장치다. 같은 요청을 누가 가로채서 그대로 다시 보내도 서버가 “이미 지나간 nonce"라며 거절한다. 재전송 공격을 막는 표준적인 방법이다.
1차 요청 nonce=1748232011234 → 통과
가로채기 동일 요청 그대로 재전송
2차 요청 nonce=1748232011234 → 거절 (지나간 값)
--------------------------------------------------
부작용: PC 시계가 서버와 크게 어긋나면
정상 요청도 거절된다
→ 인증 실패의 원인 후보가 하나 더 늘어난다문제는 이게 인증 실패 원인 목록을 하나 더 늘린다는 점이다. 서명이 틀린 건지, 키가 틀린 건지, 시계가 어긋난 건지 — 서버는 전부 같은 코드로 답한다. 뒤에 나올 34분짜리 사건의 배경이 여기 다 깔려 있다.
공개 데이터를 라이브러리에 맡긴 것도 게으름은 아니었다. 시세와 분봉은 초 단위로 계속 불러야 하는데, 그 반복 요청을 안정적으로 돌리는 코드는 생각보다 손이 많이 간다. 재시도, 타임아웃, 호출 간격 조절 같은 것들이다. 그리고 그쪽은 틀려도 돈이 안 나간다. 잘못 읽으면 안 사면 그만이다.
이 감각은 지금도 그대로다. 편한 건 라이브러리에 맡기고, 위험한 건 직접 짠다. 경계선은 하나다 — 틀렸을 때 돈이 움직이는가.
2-5. 있는 리스크와 없는 리스크
추정이지만, 이 시점에 손절은 있었고 그 위의 통제는 없었다. 재진입 통제도, 일일 손실 한도도, 시장 상황 판단도 없다.
✔ 개별 포지션 손절
✘ 동시 보유 슬롯 제한
✘ 재진입 쿨다운
✘ 일일 손실 한도
✘ 시장 레짐 판단 (하락장 진입 차단)
→ 개별 거래의 리스크는 있고 계좌 전체의 리스크는 없다한 종목이 손절선에 닿으면 자른다. 그런데 다섯 종목이 동시에 물려도 봇은 그걸 하나의 사건으로 인식하지 못한다. 손절이 다섯 번 따로 일어날 뿐이다.
차이가 어디서 벌어지는지 보면 이렇다. 개별 손절만 있는 봇은 시장이 통째로 빠지는 날에 가장 나쁘게 행동한다. 다 물리고, 다 잘리고, 그다음에도 조건이 서면 또 산다. 손절이 잘 작동할수록 손실이 규칙적으로 쌓인다.
[ 개별 손절만 있는 구조 ]
시장 하락 → 5종목 동시 진입 → 5회 손절
→ 조건 재성립 → 또 진입 → 또 손절
봇은 아무 잘못도 안 했고 계좌만 녹는다
[ 계좌 단위 통제가 있는 구조 ]
BTC 4시간봉 하락 → 신규 진입 자체를 차단
동시 보유 4슬롯 → 노출 상한 고정
안 사는 것이 전략이 된다
지금 봇에 4슬롯 제한이 있고, BTC 4시간봉이 하락일 때 신규 진입 자체를 차단하는 레짐 로직이 있는 이유가 이 빈칸이다. 이후 두 달은 저 ✘ 다섯 줄을 하나씩 지우는 과정이었다. 그리고 순서가 재밌다 — 진입 조건을 정교하게 만드는 작업보다, 안 사는 조건을 만드는 작업이 훨씬 뒤에 왔다. 대부분의 초보가 그 순서로 간다고 생각한다.
2-6. 615줄에 대한 소회
615줄은 지금 보면 귀엽다. 현행 봇은 모듈이 여섯 개고 git으로 관리되고, 파라미터는 매매기록 시뮬레이션을 통과해야만 배포된다.
그런데 뼈대는 안 바뀌었다. 읽고, 판단하고, 주문한다. 늘어난 건 오직 하나 — “안 사는 이유"의 개수다. 615줄에서 지금까지, 코드가 늘어난 양의 대부분은 매수를 막는 조건이다.
3. 34분 만에 심장을 갈아끼웠다
3-1. 최신을 버리고 구버전으로
직전 버전으로부터 34분 뒤. 629줄, +40/-26줄. 시간은 짧은데 바꾼 건 봇의 심장이다.
- class CustomBithumbAPI: # V2 규격
- data = {"endpoint":…, "nonce":…}
- query = quote(json.dumps(data))
- "Api-Sign": b64encode(sig) # bytes!
+ class CustomBithumbAPI_V1: # API 1.0 규격
+ str_data = urllib.parse.urlencode(data)
+ query = endpoint + chr(0) + str_data + chr(0) + nonce
+ "Content-Type": "application/x-www-form-urlencoded"
+ "Api-Sign": b64encode(sig).decode('utf-8')
--------------------------------------------------
⚠️ 로그인 실패: {'status': '5300'}
🚀 V1.0 엔진 로그인 성공!V2 대응 클래스를 지우고 API 1.0 전용 클래스를 새로 썼다. 최신 규격을 버리고 구버전으로 후퇴한 것이다.
3-2. 서명 문자열이 다르다
두 규격의 차이는 “무엇을 해시하는가"에 있다.
[ 이전 ] JSON 직렬화 후 URL 인코딩
{"endpoint":"/info/balance","nonce":"1748..."}
→ quote(json.dumps(data))
→ %7B%22endpoint%22%3A...
[ 이후 ] 폼 인코딩 + NULL 구분자
endpoint=/info/balance¤cy=BTC
→ endpoint + chr(0) + str_data + chr(0) + nonce
→ /info/balance\0endpoint=…¤cy=BTC\0 1748…
↑ chr(0) = NULL 문자. 눈에 안 보이는 구분자다
chr(0)은 화면에 아무것도 안 그려진다. 로그로 찍어봐도 그냥 붙어 보인다. 서명 문자열을 눈으로 검증하기가 사실상 불가능하다는 뜻이다. 대신 규격이 단순하다 — 사전 순 정렬도, JSON 공백 처리도 신경 쓸 게 없다. 눈으로 못 보는 대신 틀릴 여지가 적은 방식을 고른 셈이다.
JSON 방식이 왜 까다로운지도 짚어둘 만하다. 같은 데이터를 JSON으로 만들어도 결과 문자열은 여러 가지가 나온다. 키 순서가 다를 수 있고, 콜론 뒤 공백을 넣을 수도 안 넣을 수도 있고, 한글이 들어가면 이스케이프 방식이 갈린다. 사람 눈에는 다 같은 데이터인데 바이트로는 전부 다른 문자열이다. 해시는 바이트 하나만 달라도 완전히 다른 값을 뱉는다.
{"a":1,"b":2} ← 공백 없음
{"a": 1, "b": 2} ← 공백 있음
{"b":2,"a":1} ← 키 순서 다름
--------------------------------------------------
사람 눈: 전부 같은 데이터
해시 : 전부 다른 결과
폼 인코딩은 이 함정이 훨씬 적다폼 인코딩은 키=값&키=값 한 줄이라 변형의 여지가 작다. 후퇴처럼 보이지만, 실은 변수를 줄이는 선택이었다.
3-3. .decode('utf-8') 한 줄
더 결정적인 건 마지막 한 줄이다. 서명 결과에 .decode('utf-8')이 붙었다. 이전엔 base64 인코딩 결과가 bytes 객체 그대로 헤더에 실리고 있었다.
>>> sig = base64.b64encode(h.digest())
>>> sig
b'YWJjZGVm...' ← bytes
>>> headers = {"Api-Sign": sig}
HTTP 라이브러리가 알아서 처리해 줄 때도 있고
b'...' 접두어까지 문자열로 만들어 보낼 때도 있다
--------------------------------------------------
>>> sig.decode('utf-8')
'YWJjZGVm...' ← str. 논쟁의 여지가 없다이런 실수는 최악의 방식으로 실패한다. 어떤 환경에선 조용히 통과하고 어떤 서버에선 거절된다. 심지어 같은 코드가 라이브러리 버전에 따라 다르게 동작한다. 재현이 안 되는 버그가 되는 것이다.
3-4. 왜 34분이었나
추정하자면 인증이 계속 실패했을 것이다. 그것도 왜 실패하는지 메시지가 안 나오는 종류로. 로그에 남은 status 5300은 그냥 “인증 실패"다. 어느 단계가 틀렸는지는 안 알려준다.
[A] V2 규격을 계속 디버깅한다
→ 후보가 다섯 개(조립·인코딩·키·타입·헤더)
→ 서버 응답은 전부 같은 코드
→ 소요 시간 예측 불가
[B] 확실히 동작하는 규격으로 후퇴한다
→ 문서가 오래돼 예제가 많다
→ 조립 규칙이 단순하다
→ 34분
34분 만에 엔진을 갈아엎었다는 건 원인 추적을 포기하고 확실히 되는 길로 갈아탔다는 뜻이다.
같은 버전에서 키 입력창에 눈 아이콘 토글을 붙인 것도 같은 흔적이다. 마스킹된 키에서는 오타를 찾을 방법이 없다. 앞에 여덟 자리가 맞는지, 마지막에 공백이 하나 붙었는지 — 별표만 보고는 알 수 없다. 그것마저 의심하고 있었다는 뜻이다. 서명 로직을 의심하고, 규격을 의심하고, 결국 자기가 붙여넣은 문자열까지 의심했다.
디버깅이 막히면 대개 이 순서로 간다. 코드를 의심하다가 → 문서를 의심하다가 → 입력을 의심한다. 그리고 셋 다 아니면 규격을 바꾼다. 34분은 그 네 단계를 다 밟기에 딱 맞는 시간이다.
한 가지 더. 이 버전에서 만든 후퇴 판단이 좋았던 이유는 되돌릴 수 있었기 때문이다. 이전 버전 파일이 그대로 남아 있으니, V1 엔진도 안 되면 다시 열면 그만이다. git이 없던 시절인데도 후퇴가 안전했던 건 파일을 복사해두는 습관 덕이었다. 이 습관이 얼마나 뿌리 깊은지는 다음 글에서 아주 극단적인 형태로 다시 나온다.
3-5. 그날 밤에 정리된 디버깅 순서
이 사건 이후로 내 디버깅 순서가 하나 굳었다. 인증이든 주문이든, 외부 시스템과 통신하다 막히면 아래 순서로 좁힌다.
1. 최소 요청 하나만 남긴다 잔고 조회 등 가장 단순한 것
2. 서명 대상 문자열을 찍는다 repr() 로. 눈에 안 보이는 문자까지
3. 타입을 확인한다 bytes 인가 str 인가
4. 입력값을 의심한다 키 앞뒤 공백, 복사 누락
5. 시계를 확인한다 nonce 가 서버 기준에서 유효한가
6. 여기까지 아니면 규격을 바꾼다
--------------------------------------------------
2번의 repr() 하나가 그날 밤 34분을 아꼈을 수도 있다특히 2번이 중요하다. print로 찍으면 chr(0) 같은 문자는 보이지 않지만 repr로 찍으면 \x00으로 나온다. 눈에 안 보이는 것을 눈에 보이게 만드는 게 디버깅의 절반이다. 지금 봇의 로그가 숫자를 찍을 때 자릿수를 명시하고, 게이트 판정 결과를 통과/탈락 이유까지 같이 찍는 것도 같은 원칙이다. 판단의 근거를 화면에 남긴다.
4. 이틀치 타임라인
세 파일을 시각으로 늘어놓으면 이렇게 된다.
05-25 Futuring Request.txt 요구사항 메모 (코드 없음)
· 50% 익절 + 50% 트레일링
· 2중 실행 방지
05-26 21:11 v1.04 615줄 시조. 메모 → 파일 헤더
05-26 21:45 v1.05 629줄 +40/-26 · 인증 엔진 전면 교체
34분 만의 후퇴
--------------------------------------------------
이틀 동안 늘어난 코드: 615 → 629줄 (+14)
이틀 동안 바뀐 것 : 봇의 심장줄 수만 보면 14줄 늘었다. 이 시기 기록을 줄 수로 읽으면 아무것도 안 보인다는 걸 잘 보여주는 구간이다. 그래서 이 아카이브는 줄 수와 함께 무엇이 바뀌었는지를 항상 같이 적는다. 지금 커밋 메시지에 근거 수치를 동봉하는 규칙도 같은 이유다. 숫자만 남기면 나중의 내가 못 읽는다.
한 가지 덧붙이면, 이 이틀은 “하루에 열 개씩 버전을 뽑던 시기"의 시작이기도 하다. 34분 간격, 20분 간격으로 파일이 쌓인다. 지금 기준으로는 커밋 하나로 묶였을 변경들이 전부 별도 버전 번호를 받았다. 버전 번호가 곧 저장 버튼이었기 때문이다.
그게 나쁘기만 한 건 아니었다. 되돌아갈 지점이 촘촘하다는 뜻이니까. 나쁜 쪽은 따로 있다 — 어떤 변경이 어떤 결과를 냈는지 짝을 지을 수 없다. 34분 안에 서명 방식도 바꾸고, 헤더 타입도 바꾸고, 입력창 토글도 붙였다. 그중 무엇이 로그인 성공을 만들었는지 이 기록만으로는 확정할 수 없다. 지금 봇에서 파라미터를 한 번에 하나씩만 바꾸고 시뮬레이션을 돌리는 규율은, 정확히 이 답답함의 반작용이다.
5. 이틀이 남긴 것
세 개의 파일 — 메모, 615줄, 그리고 34분짜리 후퇴 — 을 나란히 놓고 보면 이후 두 달의 방향이 이미 보인다.
첫째, 문장을 먼저 쓴다. 요구사항 메모가 파일 헤더가 되고, 파일 헤더가 코드가 됐다. 지금은 커밋 메시지에 근거 수치를 같이 적는다. 형식만 바뀌었지 순서는 같다. 왜 바꾸는지 먼저 쓰고 그다음에 바꾼다.
둘째, 돈이 오가는 구간은 남에게 안 맡긴다. 시세는 라이브러리로 받고 주문은 직접 짠 그 선택이 지금까지 유지된다. 편의와 통제 중에 통제를 고르는 자리가 어디인지, 그 경계는 첫 코드에서 정해졌다.
그 경계선은 지금도 매 버전 다시 그어진다. 새 라이브러리를 붙일 때마다 같은 질문을 한다 — 이게 틀리면 주문이 나가나, 아니면 안 사고 넘어가나. 후자면 붙이고, 전자면 직접 짜거나 최소한 결과를 한 번 더 검증한다.
셋째, 새로 나온 것보다 확실히 동작하는 것. 34분의 후퇴가 알려준 건 두 가지다. 하나는 최신 규격이 항상 정답은 아니라는 것. 다른 하나는 안 되는 걸 붙들고 있는 시간보다 되는 길로 갈아타는 결단이 쌀 때가 있다는 것. 지금 봇이 시뮬레이션으로 검증되지 않은 값을 절대 배포하지 않는 것도 같은 원칙의 다른 얼굴이다. 검증된 것만 실계좌에 올린다.
넷째, 실패는 조용할수록 비싸다. 인증 실패는 그래도 로그에 뭔가 찍혔다. 앞으로 나올 버그들은 그마저도 없다. 주문이 거부됐는데 봇은 다음 루프로 넘어가고, 화면엔 아무 일도 안 일어난 것처럼 보인다. 이때의 status 5300이 차라리 친절했다는 걸 그다음 날 알게 된다.
그리고 하나 더. 이 이틀 동안 계좌 전체를 지키는 로직은 한 줄도 없었다. 손절은 있었지만 슬롯도, 쿨다운도, 레짐 판단도 없었다. 그게 없다는 걸 이때는 몰랐다.
몰랐다는 사실 자체가 기록으로 남아 있는 게 아카이브의 쓸모다. 지금 시점에서 “그때 슬롯 제한을 넣었어야지"라고 쓰는 건 쉽다. 하지만 그때의 나는 주문이 나가는 것만으로도 벅찼다. 순서를 건너뛸 수 있었다고 말하는 건 대체로 거짓말이다. 다만 어느 지점에서 다음 단계로 올라갔어야 하는지는 기록해둘 수 있다. 그게 이 아카이브가 하는 일이다.
이틀 동안 만들어진 건 결국 세 가지다 — 문장, 615줄, 그리고 후퇴할 줄 아는 습관. 뒤의 459개 버전은 여기에 “안 사는 이유"를 붙여나간 기록이다.
마지막으로 하나. 이 시절 코드에는 인증 식별자나 서버 주소 같은 값이 파일에 그대로 박혀 있었다. 지금은 안 하는 방식이라 이 글의 어떤 화면에도 실제 값은 옮기지 않았다. 아카이브를 공개 기록으로 정리할 때 제일 먼저 하는 일이 그 값들을 골라내는 작업이었다. 초기 코드를 다시 여는 사람이 있다면 이 순서를 권한다 — 읽기 전에 지운다.
다음 글에서는 이 봇이 실제로 주문을 못 내던 이야기가 나온다. 인증은 뚫었는데 숫자가 발목을 잡는다. 그리고 그 뒤엔 숫자가 아니라 사람이 발목을 잡는다.
폴 튜더 존스는 방어가 공격보다 중요하다고 했다. 이 이틀의 방어는 2중 실행 방지 하나뿐이었지만, 그걸 파일 맨 위에 적어뒀다는 게 방향은 맞았다는 증거다.
제시 리버모어는 시장이 틀린 게 아니라 사람이 틀린 거라고 했다. 코드도 같다. API가 이상한 게 아니라 내 서명이 틀린 거였다. 그걸 인정하는 데 34분이 걸렸다.