백테스트 방법, 제가 코인이랑 미국주식 돌릴 때 순서가 서로 다른 이유를 공개합니다 🔀

“백테스트 방법”을 검색하면 보통 전략 하나를 기준으로 설명이 끝납니다. 데이터 받고, 신호 계산하고, 결과 보고.
저는 자산군이 둘(코인·미국주식)이라 코드를 따로 짜야 했는데, 먼저 짜둔 코인 쪽 코드를 그대로 복붙해서 미국주식에 돌리려다 한참 막힌 적이 있습니다. 매일 추세만 확인하면 끝나는 구조라고 믿었거든요. 안 돌아갔습니다. 지난 빌드로그에서 백테스트가 진입·청산·비용을 어떻게 계산하는지 이야기했는데, 오늘은 그 코드를 자산군 두 개에 각각 짜면서 발견한, 구조 자체가 달라야 했던 지점을 공개합니다.
코인 하네스는 매일 같은 질문 하나만 묻는다
코인(BTC·ETH) 쪽 코드는 하루 단위 루프 안에 질문이 하나뿐입니다. “오늘 종가가 이동평균선 위에 있나?” 위면 들고 있고, 아래면 현금입니다.
INIT, COST = 1_000_000, 0.001
def sim(cl, sm, a, b, trend=True):
eq = INIT; out = []; prev = 0.0
for i in range(a, b):
p = (1.0 if (sm[i] and cl[i] > sm[i]) else 0.0) if trend else 1.0
eq -= eq*abs(p-prev)*COST
if i < b-1: eq *= 1 + p*(cl[i+1]/cl[i]-1)
out.append(eq); prev = p
return out
이 루프엔 “어떤 자산을 들고 있을지 고르는” 단계가 없습니다. 이미 BTC·ETH로 정해져 있으니까요. 매일 하는 일은 그 하나를 들고 있을지 현금으로 도망갈지, 둘 중 하나를 다시 정하는 것뿐입니다. 포지션이 바뀔 때마다(abs(p-prev)) 비용 0.1%(COST=0.001)가 바로 깎입니다. 표에 올라가는 비교도 “그냥 들고 있기”와 이동평균 기간을 50·100·150·200일로 바꿔본 네 가지 추세값뿐, 선택 단계가 없으니 더 늘어날 게 없습니다.
미국주식 하네스는 매일 하는 일과 며칠마다 하는 일이 나뉘어 있다
미국주식 쪽은 후보가 대형주 약 50종목입니다. “어떤 자산”부터 매번 다시 골라야 하는데, 이 단계 자체가 코인 코드엔 없었던 겁니다.
FEE, SLIP = 0.0005, 0.0010 # 수수료+슬리피지
COST = FEE + SLIP
TOP_K, REBAL = 10, 5
def sim_select(use_bo):
cash=INIT; pos={}; out=[]
for i in range(start,N):
for s in list(pos): # 추세이탈 청산
if not up(s,i):
c=C(s,i)
if c: cash+=pos.pop(s)["q"]*c*(1-COST)
if (i-start)%REBAL==0: # 선택·리밸런싱은 5일마다만
cand=[]
for s in UNIV:
if not up(s,i): continue
if use_bo and not breakout(s,i): continue
m=mom(s,i)
if m is None: continue
cand.append((s,m))
cand.sort(key=lambda x:x[1],reverse=True)
tgt=[s for s,_ in cand[:TOP_K]]
매일 하는 일은 추세이탈 청산 체크 하나뿐입니다. 추세를 벗어난 자산이 있으면 그날 바로 정리합니다. 그런데 “새로 뭘 담을지” 고르는 일은 5일(REBAL=5)마다만 합니다. 추세 조건을 통과한 후보 중 최근 변화율(모멘텀) 순위로 상위 10개(TOP_K=10)만 추려 동일 비중으로 채웁니다. 표에 올라가는 비교도 코인 쪽보다 하나 더 늘어서, 그냥 지수 보유·동일비중 보유·모멘텀 선택·돌파선택 네 가지를 나란히 봅니다. “선택”이라는 변수가 하나 더 생겼으니 비교할 방식도 그만큼 늘어난 겁니다.
왜 순서 자체가 다른가 — 고를 게 2개냐 50개냐
코인 코드는 고를 필요가 없습니다. BTC·ETH 둘 중 뭘 들고 있을지 매일 다시 물으면 충분합니다. 미국주식은 후보가 50개라 “뭘 담을지” 자체가 매일 바뀔 수 있는 질문인데, 이걸 매일 다시 풀면 리밸런싱 비용이 매번 쌓여 결과가 비용에 먹힙니다. 그래서 “청산 체크는 매일, 선택은 N일마다”로 쪼갠 겁니다.
비용 모델도 그래서 다릅니다. 코인 쪽은 COST 하나(0.1%)로 묶었습니다. 자산이 둘뿐이고 매일 포지션을 켜고 끄는 정도라 턴오버 비용 하나로 충분합니다. 미국주식 쪽은 수수료(FEE=0.05%)와 슬리피지(SLIP=0.10%)를 따로 갈라 더해서 0.15%로 씁니다. 종목 수가 많아지면 한 번 리밸런싱할 때 동시에 체결되는 주문 수도 늘어서, 둘을 뭉개면 어느 쪽이 더 아픈지 안 보이기 때문입니다.
자산이 몇 개냐가 아니라 “뭘 고를지”를 매번 다시 물어야 하느냐가 코드의 실행 순서를 가른다.
따라 하기
두 하네스를 각각 돌려보면 이 차이가 바로 보입니다.
- 코인 하네스를 돌립니다
cd <프로젝트 폴더>
python3 binance_btc.py
BTC·ETH 각각 9년치를 받아와 추세 추종 성과를 계산합니다. 선택 단계가 없어서 코드도 100줄 남짓으로 끝납니다.
- 미국주식 하네스를 돌립니다
python3 us_stocks.py
대형주 약 50종목 데이터를 받아오는 데 시간이 더 걸립니다. 후보가 많아서 당연한 일인데, 처음엔 이것도 “왜 이렇게 오래 걸리지”로 한참 들여다봤습니다.
- 두 리포트를 나란히 엽니다
open binance_btc_report.html
open us_stocks_report.html
코인 쪽 표는 “들고 있을까 말까”의 변주(이동평균 기간 4가지)만 비교합니다. 미국주식 쪽 표는 거기에 “뭘 담을지” 선택 방식(동일비중·모멘텀 선택·돌파선택) 비교가 하나 더 붙습니다. 표 구성 자체가 “고를 게 있었는지”를 보여줍니다.
코드를 복붙했다가 안 돌아가서 다시 들여다본 그 밤이, 사실은 이 둘이 왜 다른 구조여야 하는지를 제대로 이해한 순간이었습니다.
오늘도 한 걸음. 🐴
이 글은 산업 구조와 만드는 과정을 정리한 개인 기록입니다.
특정 종목의 매수·매도 추천이 아니며, 투자 판단과 그 결과는 독자 본인에게 있습니다.