한 파일이 1,500줄이 되기까지
이 시대 코드는 파일 하나입니다. 통신도, 지표 계산도, 진입 판단도, 주문 실행도, 화면 출력도 전부 한 .py 안에 있었어요. 처음엔 그게 편했습니다. 뭘 고치려면 그 파일만 열면 되니까.
단일 파일이 나쁘다고 단정할 순 없습니다. 오히려 초기에는 유리해요. 파일을 옮겨 다닐 필요가 없고, import 구조를 고민할 필요도 없고, 어떤 값이 어디 있는지 검색 한 번이면 나옵니다. 하루에 열 번 고치던 리듬에는 이게 맞았습니다.
문제는 임계점이 있다는 겁니다. 대략 몇백 줄까지는 단일 파일이 빠르고, 그 위로 가면 급격히 느려집니다. 그리고 그 임계점은 서서히 오는 게 아니라 어느 날 갑자기 체감돼요.
버전 번호가 이 시기에 이상하게 움직입니다. v091에서 시작해 v9xx까지 가는데, 백 자리가 바뀌면 대개 큰 변화가 있었다는 뜻이었습니다.
v09x 빗썸에서 막 넘어온 직후
v1xx 구조 정리 1차
v2xx 지표 확장
v3xx 진입 조건 재설계
v4xx 청산 로직 재설계
v5xx ~ v7xx 반복 개선
v9xx 마지막 세대 — v901 에서 스캐너 역할 축소
코드 길이: 1,500 ~ 2,000줄 (단일 파일)
버전 번호가 백 단위로 뛴 건 자신감이 아니라 시행착오의 개수였다목록을 다시 보면 재밌는 게 있습니다. v3xx가 진입 재설계, v4xx가 청산 재설계예요. 진입을 먼저 다 만지고 나서야 청산으로 넘어갔다는 뜻입니다. 대부분 이 순서로 갑니다. 사는 게 재밌고 파는 건 귀찮으니까요. 그런데 실제로 성적을 결정하는 비중은 반대에 가깝습니다. 이 순서를 되짚어보게 되는 게 훨씬 나중이에요.
무엇이 문제였나
모놀리스의 문제는 코드가 길다는 게 아닙니다. 어디를 고치면 어디가 부서지는지 알 수 없다는 게 문제입니다.
진입 조건을 하나 바꿨는데 화면 출력이 깨지고, 화면 출력을 고쳤더니 상태 저장이 안 되고. 이유는 단순합니다. 같은 변수를 여기저기서 직접 만지고 있었으니까요. 전역 변수 하나를 열 군데에서 읽고 쓰면, 그 변수의 값이 언제 어떻게 바뀌는지는 아무도 모릅니다.
┌──▶ 스캔 루프에서 읽고 쓴다
positions ─┼──▶ 청산 판정에서 읽고 쓴다
(전역) ├──▶ 화면 출력에서 읽는다
├──▶ 상태 저장에서 읽는다
└──▶ 수동 제어에서 직접 수정한다
--------------------------------------------------
질문: 이 값이 지금 왜 이렇게 돼 있나?
답: 다섯 군데를 다 읽어봐야 안다
현행: 상태는 한 모듈이 소유하고 나머지는 함수로 요청이런 구조에서는 버그를 고치는 데 걸리는 시간이 코드 길이에 비례하지 않고 연결 개수에 비례해서 늡니다. 줄이 두 배가 되면 시간이 두 배가 아니라 네 배가 돼요. 하루에 열 번 고치던 리듬이 하루에 세 번으로 떨어지고, 결국 하루에 한 번이 됩니다.
여기에 더 큰 문제가 있었습니다. 매매 기록을 체계적으로 안 남겼습니다. 봇은 돌고 있었고 사고팔고 있었는데, 그 기록이 분석 가능한 형태로 쌓이지 않았어요. 그래서 파라미터를 바꿀 때 근거가 없었습니다. “이게 나을 것 같다"로 바꾸고, 며칠 지켜보고, 느낌이 안 좋으면 되돌리고. 지금 돌아보면 이 시기 버전 절반은 안 만들어도 되는 것들이었습니다. 기록이 있었다면 데이터가 답을 줬을 테니까요.
근거 없는 튜닝이 만드는 순환
기록 없이 파라미터를 만지면 어떤 일이 벌어지는지 정리해둘 만합니다. 이 시기 버전 절반이 여기서 나왔거든요.
값을 바꾼다
↓
며칠 지켜본다 ← 표본이 너무 작다
↓
성적이 나쁘다
↓
값 때문인지 장세 때문인지 모른다
↓
되돌리거나 또 바꾼다
↓
처음으로
--------------------------------------------------
이 순환에서는 아무것도 축적되지 않는다
같은 값을 몇 주 뒤에 또 시도하게 된다며칠은 표본이 아닙니다. 특히 진입 빈도가 낮은 전략에서는 며칠에 매매가 몇 건밖에 안 나와요. 그 몇 건으로 값의 우열을 판단하는 건 동전 세 번 던져서 앞면이 많이 나왔으니 이 동전은 앞면이 잘 나온다고 결론 내는 것과 같습니다.
더 나쁜 건 되돌린 기록도 안 남는다는 겁니다. 그러면 몇 주 뒤에 같은 값을 다시 시도하게 돼요. 실제로 이 시기 버전 목록을 보면 비슷한 변경이 여러 번 반복됩니다. 앞에서 이미 해봤다는 걸 몰랐던 거죠.
지금은 파라미터를 바꿀 때 매매기록 전체를 그 값으로 다시 돌려봅니다. 며칠이 아니라 축적된 전 구간이요. 그리고 결과 수치를 커밋 메시지에 같이 넣습니다. 그러면 몇 주 뒤에 같은 값을 떠올려도 “이미 해봤고 결과는 이랬다"가 바로 나옵니다. 기록은 실험을 반복하지 않게 해주는 장치입니다.
왜 그때 안 쪼갰나
지금 이 글을 읽는 분은 당연히 이렇게 생각할 겁니다. 진작 파일을 쪼개지 그랬냐고. 저도 같은 생각을 여러 번 했습니다. 그래서 왜 안 했는지를 정직하게 적어두겠습니다.
1. 쪼개는 동안 개발이 멈춘다
그날 고치고 싶은 게 있는데 리팩터링부터 하라니
2. 어디서 쪼개야 할지 몰랐다
경계를 잘못 그으면 모듈이 서로를 계속 호출한다
= 파일만 늘고 복잡도는 그대로
3. 지금도 돌아가고 있다
돈을 벌든 잃든 봇은 실행 중이었다
멈추고 뜯는 데 심리적 저항이 컸다
--------------------------------------------------
2번이 진짜 이유였다 — 경계는 겪어봐야 보인다2번이 핵심입니다. 리팩터링의 어려움은 쪼개는 작업 자체가 아니라 어디서 쪼갤지 정하는 것이에요. 경계를 잘못 그으면 모듈 A가 모듈 B를 부르고 B가 다시 A를 부르는 구조가 됩니다. 파일 수만 늘고 복잡도는 그대로거나 오히려 나빠져요.
그리고 좋은 경계는 코드를 노려본다고 보이지 않습니다. 어디가 자주 같이 바뀌는지를 봐야 보여요. 늘 함께 바뀌는 코드는 한 모듈에 있어야 하고, 따로 바뀌는 코드는 나뉘어야 합니다. 그 패턴은 여러 달 고쳐본 뒤에야 드러납니다.
그래서 나중에 실제로 쪼갤 때는 경계를 별로 고민하지 않았습니다. 이미 알고 있었거든요. 상수와 상태, 체결과 청산, 스캔과 게이트, 원장 대조, 화면 출력. 이 다섯 덩어리는 이 시대 내내 서로 다른 이유로 바뀌던 것들입니다. 모놀리스 시절의 고통이 곧 설계도였습니다.
그래도 남긴 것
이 시대에 확립된 개념 중 지금까지 이어지는 게 몇 개 있습니다.
게이트 개념 조건을 여러 개 통과해야 진입 — 지금은 10개
랭킹 점수 통과한 종목이 여럿일 때 순서를 정하는 방식
스캔 로그 어떤 종목이 왜 떨어졌는지 남기기 시작
v901 "스나이퍼 강등" 스캐너가 진입까지 결정하던 걸 후보 추천으로 축소
마지막 항목이 특히 중요했다 — 종목 선정과 진입 결정의 분리게이트라는 표현을 쓰기 시작한 것 자체가 사고방식의 변화였습니다. 그전에는 조건들을 점수로 합쳐서 일정 점수 이상이면 사는 식이었어요. 게이트는 다릅니다. 하나라도 통과 못 하면 탈락입니다. 곱셈이 아니라 AND죠.
[ 점수 합산 ]
조건 A 90점 + 조건 B 10점 = 100점 → 통과
한 항목이 아주 좋으면 다른 결함이 가려진다
→ "거래량이 거의 없지만 RSI 가 완벽한" 종목이 통과
[ 게이트 ]
조건 A 통과 · 조건 B 탈락 → 진입 안 함
치명적 결함이 다른 장점으로 상쇄되지 않는다
————————————————–
현행: 게이트로 거르고 → 통과한 것들끼리만 점수로 정렬
이 구분이 실전에서 꽤 중요합니다. 점수 합산은 결함을 상쇄시켜요. 거래량이 거의 없는 종목인데 다른 지표가 완벽해서 통과하는 경우가 생깁니다. 그런 종목은 사면 못 팝니다. 진입 자체가 성립해도 청산이 안 되는 거죠. 게이트로 바꾸면 이런 종목이 처음부터 걸러집니다.
지금 봇은 둘 다 씁니다. 게이트로 거른 다음, 통과한 것들끼리 점수로 순서를 정해요. 역할을 나눈 겁니다 — 게이트는 자격을 보고 점수는 우선순위를 봅니다.
v901, 역할을 쪼갠 순간
v901의 변화를 좀 더 설명하면, 그전까지 스캐너는 종목을 고르고 바로 사는 것까지 했습니다. 이걸 “후보만 추천하고, 살지 말지는 별도 로직이 판단한다"로 바꿨어요. 역할을 쪼갠 겁니다.
[ 이전 ] 스캐너 = 발견 + 판단 + 주문
스캐너를 고치면 주문 동작이 바뀐다
진입 규칙을 바꾸려면 스캔 코드를 건드려야 한다
[ v901 ] 스캐너 = 발견만
스캐너 → 후보 목록 → 진입 판단 → 주문
각각 따로 고칠 수 있다
————————————————–
이 분리가 다음 시대 모듈화의 예고편이 된다
이 분리가 왜 중요했냐면, 바꾸고 싶은 것과 건드려야 하는 것을 일치시켰기 때문입니다. 진입 규칙만 바꾸고 싶은데 스캔 코드를 건드려야 하는 구조에서는 매번 관계없는 부분이 부서질 위험을 감수해야 해요. 역할이 나뉘면 그 위험이 사라집니다.
그리고 여기서 하나 더 얻는 게 있습니다. 각 단계의 출력을 따로 볼 수 있게 됩니다. 스캐너가 뽑은 후보 목록과 진입 판단이 실제로 산 종목을 비교하면, 어느 단계에서 무엇이 걸러졌는지 보여요. 문제가 발견 단계인지 판단 단계인지 구분할 수 있게 된 겁니다.
스캔 로그, 반쪽짜리 시작
스캔 로그도 이때 시작합니다. 통과 못 한 종목이 어느 조건에서 떨어졌는지 기록하기 시작한 건데, 이게 나중에 파라미터를 데이터로 정하는 기반이 됩니다. 다만 이때는 남기기만 하고 분석은 안 했어요.
게이트별 탈락 수를 세어보면
--------------------------------------------------
탈락 0건인 게이트 아무 일도 안 하고 있다
→ 있으나 마나 · 없애거나 조여야
탈락 99%인 게이트 혼자 다 거르고 있다
→ 나머지 게이트가 무의미해진다
고르게 분포 각 게이트가 제 역할을 한다
--------------------------------------------------
이 분포를 안 보면 게이트를 10개 만들어놓고
실제로는 2개만 작동하는 상태를 모른다나중에 실제로 이 분석을 해보니, 조건을 추가했는데 통행량만 죽고 성적은 안 좋아진 사례가 나옵니다. 좋은 필터를 얻었다고 생각했는데 그냥 매매 자체를 줄인 거였어요. 매매가 줄면 손실도 줄어서 잠깐은 좋아 보입니다. 그런데 기대값이 개선된 게 아니라 표본이 줄어든 것뿐이에요.
이 시기에 로그를 남기기만 하고 안 본 게 아쉽습니다. 다만 남기기라도 한 게 다행이었어요. 나중에 분석 도구가 생겼을 때 돌아볼 데이터가 있었으니까요. 분석할 여유가 없어도 기록은 남겨두라는 게 이 시대의 교훈입니다.
지표를 늘리면 좋아질 줄 알았습니다
v2xx 구간이 “지표 확장"인데, 여기서 배운 게 있습니다. 지표를 추가한다고 판단이 좋아지지는 않는다는 것.
처음 생각은 단순했습니다. 지표 하나로 판단하면 틀릴 수 있으니 여러 개를 보자. 그래서 지표를 붙이기 시작했어요. RSI, 이동평균, 거래량, 변동성. 각각은 다 나름의 논리가 있습니다.
지표 4개 · 각 지표에 파라미터 2개
= 조합 가능한 설정이 폭발적으로 증가
--------------------------------------------------
· 검증해야 할 조합이 는다 (기록도 없는데)
· 지표끼리 같은 얘기를 하는 경우가 많다
예: 여러 추세 지표는 대체로 함께 움직인다
· 서로 반대 신호를 낼 때 우선순위 규칙이 또 필요하다
→ 판단의 정확도보다 설정의 복잡도가 먼저 는다특히 두 번째가 함정입니다. 서로 다른 지표를 봤다고 생각하는데 실은 같은 정보를 다른 방식으로 본 것일 때가 많아요. 추세를 보는 지표들은 대체로 함께 움직입니다. 세 개를 다 보고 “세 지표가 모두 동의한다"고 판단해도, 실제로는 하나를 세 번 본 것에 가까울 수 있습니다.
그래서 지금 봇의 게이트는 서로 다른 성격의 조건들로 구성돼 있습니다. 가격 위치, 모멘텀, 거래량, 유동성, 주문 가능성. 각각이 다른 종류의 실패를 막습니다. 지표 개수가 아니라 커버하는 실패 유형의 개수가 중요하다는 걸 이 시기에 지표를 잔뜩 붙여보고 나서야 알았어요.
청산이 진입보다 늦게 온 이유
v3xx가 진입, v4xx가 청산이라고 했는데 이 순서에 대해 더 적어두겠습니다.
진입 로직은 만드는 게 재밌습니다. 조건을 설계하고, 신호가 뜨고, 봇이 사는 걸 보면 뭔가 하고 있다는 느낌이 들어요. 반면 청산은 지루합니다. 손절선을 정하고 익절선을 정하는 게 전부처럼 보이거든요.
진입은 한 번의 결정
청산은 보유 기간 내내의 결정
--------------------------------------------------
손절선 얼마나 버틸 것인가
익절 방식 목표가에서 전량? 분할? 트레일링?
시간 청산 오래 안 움직이면 뺄 것인가
본전 사수 이익이 났을 때 어디까지 되돌릴 것인가
전체 수확 계좌 단위로 정리하는 기준
--------------------------------------------------
진입 조건을 아무리 다듬어도 청산이 엉성하면 결과는 안 바뀐다이 시대에 청산을 재설계하고 나서 성적이 눈에 띄게 달라졌던 기억이 있습니다. 다만 기록이 없어서 얼마나 달라졌는지는 숫자로 못 남겼어요. 그게 이 시대의 한계입니다 — 뭔가 나아진 건 아는데 얼마나인지 모릅니다.
지금 봇에서 방어 규칙이 절반을 차지하는 것도 결국 이 순서를 뒤늦게 뒤집은 결과입니다. 손절, 본전 사수, 일일 한도, 주간 한도, 서킷브레이커, 레짐 차단. 전부 사는 것과 무관한 규칙들이에요. 만드는 재미는 없지만 계좌를 지키는 건 이쪽입니다.
코드가 아니라 사이클이 병목이었습니다
이 시대의 진짜 비용은 코드 품질이 아니라 한 번 고치고 확인하는 데 걸리는 시간이었습니다.
초기 고친다(5분) → 돌린다 → 눈으로 본다 = 빠름
중기 고친다(20분) → 다른 데가 깨진다
→ 그것도 고친다 → 다시 돌린다 = 느림
말기 고치기 전에 어디가 깨질지 예측이 안 됨
→ 고치는 걸 미루게 된다 = 정지
--------------------------------------------------
개발이 멈추는 건 의욕이 아니라 예측 불가능성 때문이다마지막 단계가 위험합니다. 고치면 뭐가 깨질지 모르니까 안 고치게 돼요. 명백히 개선인 변경도 미루게 됩니다. 이 상태가 되면 코드가 개발자를 통제하기 시작한 겁니다.
그리고 이 시기 개발의 상당 부분이 버그를 고치는 게 아니라 버그를 찾는 데 들어갔습니다. 증상은 화면에서 나타나는데 원인은 스캔 로직에 있고, 그 둘을 잇는 경로가 전역 변수라 추적이 안 됩니다. 이런 디버깅은 실력으로 극복이 안 돼요. 구조를 바꿔야 합니다.
끝
이 시대가 끝난 이유는 단순합니다. 감당이 안 됐습니다. 파일 하나를 열면 스크롤을 한참 내려야 원하는 함수가 나오고, 함수 하나를 고치면 뭐가 깨질지 예측이 안 됐어요.
그래서 두 가지 방향이 동시에 나옵니다. 하나는 여러 전략을 각각 별도 프로젝트로 떼어내서 실험해보는 것(다음 시대), 다른 하나는 결국 모듈로 쪼개는 것(그다음 시대).
[ 갈래 1 ] 전략을 의심한다
"지금 이 전략이 맞긴 한 건가?"
→ 여러 전략을 별도 프로젝트로 만들어 비교
→ 4시대 병렬 실험기
[ 갈래 2 ] 구조를 의심한다
“이 파일로는 뭘 해도 못 검증한다”
→ 역할별 모듈 분리 · git 도입
→ 6시대 VV 모듈
————————————————–
둘 다 이 시대의 답답함에서 나왔다
지금 돌아보면 이 시기의 답답함이 없었으면 그 두 결정 다 안 나왔을 겁니다. 불편함이 충분히 쌓여야 구조를 바꾸게 되더군요. 진작 쪼갤 걸 그랬다는 생각도 들지만, 실은 쪼개야 하는 이유를 몸으로 알기 전에 쪼갰으면 잘못된 경계로 쪼갰을 겁니다.
120개를 다시 세어보며
이 시대 버전 목록을 유형별로 나눠보면 어디에 시간을 썼는지가 보입니다.
파라미터 조정 근거 없이 바꾸고 되돌린 것들
상당수가 반복 시도였다
버그 수정 같은 원인이 다른 증상으로 재발
구조 문제라 근본 해결이 안 됨
구조 변경 v901 역할 분리 등
개수는 적은데 효과는 컸다
관측 추가 스캔 로그 등
당장은 무의미해 보였으나 자산이 됨
--------------------------------------------------
개수는 위 둘이 많고 · 가치는 아래 둘이 컸다이 표가 이 시대의 요약입니다. 많이 한 일과 값진 일이 달랐어요. 그리고 그 사실을 그때는 몰랐습니다. 파라미터를 만지는 건 즉각적인 만족감이 있고 구조를 바꾸는 건 티가 안 나니까요.
한 가지 덧붙이면, “관측 추가"의 가치는 항상 시차를 두고 나타납니다. 로그를 남기는 커밋은 그날의 성적을 1원도 안 바꿔요. 그런데 몇 주 뒤 그 로그가 답을 줍니다. 이런 종류의 작업을 계속하려면 당장 효과가 없는 일의 가치를 인정하는 태도가 필요한데, 이건 규율의 문제지 실력의 문제가 아니더군요.
이 시대에서 가져갈 것
첫째, 단일 파일은 나쁜 게 아니라 유효 기간이 있는 겁니다. 초기에는 오히려 빠릅니다. 문제는 임계점을 지났는데도 계속 쓰는 것이고, 그 임계점은 줄 수가 아니라 “고치기 전에 결과를 예측할 수 있는가"로 판단해야 합니다.
둘째, 게이트와 점수는 역할이 다릅니다. 자격을 보는 조건과 우선순위를 보는 조건을 한 산식에 섞으면 치명적 결함이 장점으로 상쇄됩니다. 통과·탈락으로 판정할 것과 정렬에 쓸 것을 분리하세요.
셋째, 분석할 여유가 없어도 기록은 남기세요. 이 시기에 스캔 로그를 남기기만 하고 안 본 게 나중에 큰 자산이 됐습니다. 반대로 매매 기록을 아예 안 남긴 구간은 지금도 복원이 불가능합니다. 기록은 나중을 위한 저축이고, 안 남긴 시간은 영원히 안 돌아옵니다.
넷째, 고통은 설계 정보입니다. 어디가 자주 부서지고 어디를 고칠 때 겁이 나는지가 곧 경계선입니다. 리팩터링을 미룬 몇 주가 낭비만은 아니었어요. 그동안 어디서 쪼개야 할지가 정해졌으니까요. 다만 그걸 알았으면 미루는 대신 기록이라도 해뒀을 겁니다. “오늘 또 여기가 깨졌다"를 세어두는 것만으로 설계도가 됩니다.
120개 버전, 1,500줄. 이 시대의 결론은 “더 열심히 고치면 된다"가 틀렸다는 것이었습니다.
같은 파일을 백스무 번 고치면서 알게 된 건, 문제가 코드의 품질이 아니라 코드에 대해 질문할 수 있는 능력이었다는 겁니다. 이 값이 왜 이런지, 이 조건이 실제로 뭘 거르는지, 어제와 오늘 중 뭐가 나은지. 단일 파일에 기록 없이 돌아가는 봇은 그 어떤 질문에도 답을 못 합니다. 다음 두 시대는 그 답할 수 있는 상태를 만들러 가는 여정입니다.