트레이딩봇 개발 삽질기 커버

“트레이딩봇 하나 만들어볼까?”

가벼운 마음으로 시작했다. API 연결하고, 시그널 만들고, 매수/매도 로직 짜면 끝이라고 생각했다. 그런데 실제로 만들어보니, 진짜 어려운 건 “언제 사고 언제 파느냐”가 아니었다. “떨어질 때 어떻게 버티느냐”가 핵심이었다. 이 글은 트레이딩봇을 만들면서 겪은 설계 고민, 버그, 그리고 PAPER 모드 첫 주 결과까지의 솔직한 기록이다.

시작: “일단 API부터 연결하자”

업비트 API 키를 발급받고 Python으로 연결하는 건 의외로 순탄했다. pyupbit 라이브러리 하나면 잔고 조회, 시세 확인, 주문까지 다 된다. 바이빗도 pybit으로 금방 붙였다.

# 연결 테스트 - 이 정도는 쉬웠다
balance = upbit.get_balance()
print(f"잔고: {balance:,}원")  # 잔고: 1,050,161원

여기까지는 “오, 생각보다 쉬운데?” 싶었다. 문제는 그 다음부터였다.

DCA 6단계 물타기 전략을 설계하기까지

왜 DCA인가?

처음엔 단순 추세추종 전략을 생각했다. “올라가면 사고, 내려가면 팔고.” 근데 암호화폐 시장을 조금이라도 아는 사람은 안다. 이 시장은 올라가다가 갑자기 -20%를 찍는다. 단순 추세추종으로는 손절만 반복하게 된다.

그래서 DCA(Dollar Cost Averaging), 즉 물타기 전략을 선택했다. 하락할 때마다 분할 매수해서 평단가를 낮추는 방식이다. 근데 “얼마나 떨어지면 얼마를 더 살 것인가?”를 정하는 게 생각보다 복잡했다.

6단계 설계 과정

DCA 전략 설계

처음엔 3단계로 시작했다. -10%, -20%, -30%에서 동일 금액을 추가 매수하는 단순한 구조. 시뮬레이션을 돌려봤더니 문제가 바로 드러났다.

문제 1: 하락 초기에 너무 많이 산다

-10%에서 이미 전체 예산의 33%를 써버리면, 진짜 바닥(-30% 이하)에서 쓸 돈이 없다. 시뮬레이션에서 BTC가 1억에서 5천만까지 떨어지는 시나리오를 돌려보니, 3단계 DCA로는 평단가가 겨우 85%까지밖에 안 내려갔다.

문제 2: 간격이 너무 넓다

-10% → -20% 사이에 아무것도 안 하고 기다리는 건 심리적으로도, 전략적으로도 비효율적이다. 실제 시장에서는 -5%씩 질질 흘러내리는 경우가 훨씬 많다.

그래서 6단계로 확장했다:

단계 하락폭 매수 비중 누적 투입
1차 진입 0% 1x 1x
2차 물타기 -5% 1x 2x
3차 물타기 -10% 1.5x 3.5x
4차 물타기 -18% 2x 5.5x
5차 물타기 -28% 2x 7.5x
6차 물타기 -40% 2.5x 10x

핵심은 뒤로 갈수록 더 많이 사는 것이다. 진짜 바닥에서 화력을 집중해야 평단가를 효과적으로 낮출 수 있다.

시뮬레이션 결과, BTC가 1억에서 -50% 폭락해도 평단가는 약 76% 수준을 유지했다. 본전 복구에 필요한 반등폭은 약 53%. 물타기 없이 그냥 들고 있으면 100% 반등해야 하니까, 상당한 차이다.

# 시뮬레이션 결과 (추상화)
진입가 대비 -50% 폭락 시:
  - 물타기 없음: 본전 복구 = +100% 반등 필요
  - 6단계 DCA: 본전 복구 = +53% 반등 필요
  - 평단가 절감 효과: 약 24%

방어정책과 물타기가 싸우던 날

DCA 전략을 만들고 나서 자신감이 붙었다. “이제 떨어져도 괜찮아. 물타기하면 되니까.” 근데 여기서 예상 못한 문제가 터졌다.

충돌 발생

트레이딩봇에는 손실을 제한하는 방어정책이 있었다. “평단가 대비 -X% 이하로 떨어지면 손절 매도”하는 로직이다. 그런데 DCA 물타기 로직은 “떨어지면 더 사라”고 한다.

방어정책: “지금 -12%야! 빨리 팔아!” DCA 로직: “지금 -10%야! 3차 물타기 들어가!”

둘 다 동시에 실행되니 벌어지는 상황:

  1. 가격이 -10% 하락
  2. DCA: “3차 물타기 매수!” → 추가 매수 실행
  3. 방어정책: “손실 한도 초과! 전량 매도!” → 방금 산 것까지 전부 매도
  4. 결과: 물타기하자마자 바로 손절. 수수료만 날림.

이건 로그에서 처음 발견했을 때 진짜 멘붕이었다. 테스트넷에서 돌리고 있어서 실제 돈을 잃진 않았지만, 로그를 보면서 “이게 라이브였으면…” 하는 생각에 식은땀이 났다.

Phase 분리로 해결

고민 끝에 Phase 개념을 도입했다.

  • Phase 1 (DCA 예산 남음): 물타기만 한다. 방어정책 비활성화.
  • Phase 2 (DCA 예산 소진): 물타기 끝. 방어정책 활성화.
# 핵심 로직 (추상화)
if dca_budget_remaining > 0:
    # Phase 1: 물타기 모드
    if price_drop >= next_dca_level:
        execute_dca_buy()
    # 방어정책 → 비활성화
else:
    # Phase 2: 방어 모드
    if unrealized_loss >= stop_loss_threshold:
        execute_stop_loss()

이렇게 하니 깔끔하게 해결됐다. DCA 예산이 남아있는 동안은 하락을 “기회”로 보고 물타기하고, 예산이 다 소진된 뒤에야 “위험”으로 판단하고 손절하는 구조다.

교훈: 전략 하나만으로는 안 된다. 전략들이 서로 충돌하지 않게 “우선순위”와 “단계”를 명확히 나눠야 한다.

하이브리드 시그널: 랜덤이 왜 거기서 나와?

매매 시그널을 어떻게 만들 것인가도 큰 고민이었다. 뉴스 감성분석만으로? 기술적 지표만으로? 결론부터 말하면, 둘 다 단독으로는 불안정했다.

감성분석은 뉴스가 없는 시간대에 무용지물이고, 기술적 지표는 암호화폐 시장의 급변동을 못 따라간다. 그래서 만든 게 하이브리드 시그널이다.

초기 버전은 이랬다:

하이브리드 스코어 = (랜덤 × 50%) + (감성분석 × 50%)

맞다. 랜덤이 50%다. 이게 말이 되냐고? 처음엔 나도 그렇게 생각했다. 하지만 이유가 있다.

왜 랜덤을 넣었나

  1. 시장은 예측 불가능하다: 어떤 지표를 써도 미래를 맞출 수 없다. 랜덤 요소를 넣으면 특정 패턴에 과적합(overfitting)되는 걸 방지한다.
  2. 분산 매수 효과: 랜덤 덕분에 매수 타이밍이 자연스럽게 분산된다. 하루에 한 번 정해진 시간에 사는 것보다 분산도가 높다.
  3. 감성분석의 한계 보완: “뉴스 없음 = NEUTRAL”인데, 실제로는 뉴스 없이도 가격은 움직인다.

물론 버전을 올리면서 랜덤 비중을 줄이고 기술적 지표(RSI, 볼륨)를 추가했다:

v3 스코어 = (랜덤 × 30%) + (감성분석 × 40%) + (기술지표 × 30%)

하지만 완전히 랜덤을 빼지는 않았다. 시뮬레이션에서 랜덤 0%보다 랜덤 30%가 MDD(최대 낙폭)를 줄여주는 아이러니한 결과가 나왔기 때문이다.

PAPER 모드 첫 주: 현실의 벽

PAPER 모드 트레이딩 결과

전략 설계를 마치고 PAPER 모드(모의매매)로 봇을 돌렸다. 실제 시장 가격으로 매매하되, 진짜 돈은 들어가지 않는 테스트 모드다. 결과는… 겸허해지는 경험이었다.

첫 주 주요 사건들

Day 1-2: ETH 매수 시그널

봇이 ETH에서 BUY 시그널을 잡았다. 1차 진입가 약 290만원. DCA 6단계까지 물타기 플랜이 자동으로 세팅됐다.

[PAPER] BUY KRW-ETH
  1차 진입: 2,906,000원
  총 예산: 6단계 DCA
  대기 물타기 주문: 5건

“좋아, 시작이 좋은데?” 이때까지는 기분이 좋았다.

Day 3: 시장 급락

BTC가 -4.3%, ETH가 -5%, DOGE가 -3.5% 빠졌다. ETH에서 2차 DCA가 자동 체결됐다.

✅ 2차 물타기 체결: 평단가 290만 → 280만

물타기가 의도대로 작동했다! 근데 기분이 좋지만은 않았다. 왜냐면…

Day 3 야간: 버그 5개 동시 발견

이게 진짜 이야기의 하이라이트다.

버그 1: 15분 크론 중복 매수

버그 수정 과정

15분마다 도는 급변감지 크론이 있었다. 이 크론의 원래 역할은 “가격 변동 모니터링”인데, 프롬프트에 “LIVE면 실행”이라고만 써놨다. 그래서 이 크론이 알아서 매매까지 해버린 것이다.

결과: ETH 보유량이 의도한 것의 2배가 됨.

# 수정 전 (문제)
"LIVE 모드이므로 매매를 실행합니다"

# 수정 후
"매매 절대 금지. 모니터링만 수행. 가격 변동만 보고."

교훈: AI 에이전트 기반 크론은 프롬프트가 곧 코드다. 애매하게 쓰면 AI가 “알아서” 해석해서 예상치 못한 행동을 한다.

버그 2: 크론에서 텔레그램 직접 전송 실패

크론 프롬프트에 “텔레그램으로 즉시 알려줘”라고 썼더니, 크론 세션에서는 텔레그램 API 접근이 안 됐다. 크론은 격리된 세션이니까 당연한 건데, 이걸 몰랐다.

해결: 크론에서 직접 전송하지 말고, 리턴값만 돌려주면 시스템이 알아서 전달하는 announce 모드를 활용.

버그 3: 워치리스트 불일치

4시간 크론 프롬프트에 “BTC, ETH, XRP, SOL, DOGE 5종목 분석”이라고 되어있었다. 근데 실제 전략은 ETH, DOGE 2종목만 운용하기로 바꾼 상태였다. 프롬프트를 업데이트 안 해서 크론이 XRP에 진입해버렸다. 수동으로 취소하고 매도하느라 진땀 뺐다.

교훈: 설정을 바꾸면 관련된 모든 곳을 동시에 바꿔야 한다. 코드만 바꾸고 프롬프트를 안 바꾸면 AI가 옛날 지시를 따른다.

버그 4: 헤지 리밸런스 시 스탑로스 초기화

바이빗에서 선물 헤지 포지션의 리밸런스(비율 조절)를 하면, 기존 포지션을 청산하고 새로 진입한다. 이때 기존에 설정해둔 스탑로스가 날아간다. 새 포지션에는 SL이 안 걸려있으니, 갑자기 방어막 없이 노출되는 상태가 되는 거다.

# 리밸런스 후 SL 자동 재설정 추가
def rebalance_hedge():
    close_position()
    new_position = open_new_position()
    set_stop_loss(new_position)  # 이걸 빼먹었었다!

버그 5: 지정가 체결 시 DCA 레벨 미업데이트

DCA 물타기를 지정가 주문으로 걸어두는데, 이게 체결돼도 봇이 “아직 1단계”라고 인식하고 있었다. 지정가 체결 여부를 체크하는 로직이 없었던 것이다. 결과적으로 같은 가격에 중복 주문을 넣을 뻔했다.

# 15분 모니터에 체결 확인 로직 추가
def check_orders():
    for order in pending_orders:
        if is_filled(order):
            update_dca_level()
            register_take_profit()  # 익절도 자동 재설정

첫 주 성적표

지표 결과
총 손익 약 -0.17%
발견된 버그 5개
수동 개입 횟수 3회
심장이 쫄깃한 횟수 셀 수 없음

솔직히 수익은 거의 본전이다. 하지만 버그를 5개나 잡았다는 게 PAPER 모드의 가치다. 이게 라이브였으면 돈을 잃었을 뿐 아니라, 의도하지 않은 포지션이 쌓이면서 더 큰 손실로 이어졌을 것이다.

백테스트: 과거는 미래를 보장하지 않지만

전략 v1과 v2를 3개월 데이터(2025년 11월~2026년 2월)로 백테스트했다. 이 기간 BTC는 약 -24%가 빠졌다. 꽤 험한 장이었다.

# 백테스트 결과 요약 (3개월, BTC 기준)
바이앤홀드: -23.81%
v1 전략:    -3.01% (MDD 5.07%)
v2 전략:    -5.09% (MDD 6.00%)

바이앤홀드 대비 훨씬 나은 결과다. 시장이 -24% 빠지는 동안 손실을 -3~5%로 억제한 셈이다. DCA 물타기가 제 역할을 한 것이다.

하지만 여기서 중요한 포인트: 백테스트는 과거 데이터에 대한 것이다. 미래에도 이렇게 작동한다는 보장은 없다. 특히 -50% 이상 폭락이 오면 DCA 예산이 다 소진되고 Phase 2 방어 모드에 진입하게 되는데, 이 상황은 아직 실전에서 겪어보지 못했다.

PAPER에서 LIVE로: 떨리는 전환

PAPER 모드에서 어느 정도 안정성을 확인한 뒤, 소액으로 LIVE 전환을 했다. 전환하는 순간의 그 떨림이란… 코드 한 줄 바꾸는 건데 손이 떨렸다.

# 이 한 줄의 무게
MODE = "LIVE"  # was "PAPER"

LIVE 전환 후 실제로 ETH 1차 진입이 체결됐을 때, 업비트 앱에서 체결 알림이 뜨는 걸 보고 묘한 감정이 들었다. “내가 만든 봇이 진짜 내 돈으로 코인을 샀다.” 금액은 만 몇천 원이었지만, 그 의미는 컸다.

지금까지 배운 핵심 교훈 7가지

  1. 전략보다 방어가 중요하다: “얼마 벌 것인가”보다 “얼마까지 잃을 수 있는가”를 먼저 정해야 한다.

  2. 전략 간 충돌을 반드시 검증하라: DCA와 손절, 물타기와 헤징 등 여러 전략을 결합하면 반드시 충돌 지점이 생긴다. Phase 분리 같은 우선순위 체계가 필수다.

  3. PAPER 모드는 선택이 아니라 필수다: 버그 5개를 실전이 아니라 테스트에서 잡은 건 행운이 아니라 프로세스다.

  4. 크론 프롬프트 = 코드다: AI 에이전트 기반이라면, 프롬프트의 한 문장이 수만 원의 차이를 만든다. 명확하고 제한적으로 써야 한다.

  5. 설정 변경은 원자적(atomic)으로: 전략 파라미터 하나를 바꾸면, 관련된 크론, 프롬프트, 설정 파일을 전부 한 번에 업데이트해야 한다.

  6. 백테스트를 과신하지 마라: 과거 데이터에서 잘 작동한 전략이 미래에도 잘 된다는 보장은 없다. 백테스트는 “최소한의 검증”이지 “성공의 보증”이 아니다.

  7. 소액으로 시작하라: LIVE 전환 시 전체 자금의 일부만 투입하고, 안정성을 확인한 뒤 점진적으로 늘려야 한다.

마무리: 트레이딩봇은 “만드는 것”이 아니라 “키우는 것”

트레이딩봇 개발을 시작한 지 약 일주일. 코드는 계속 바뀌고 있고, 전략은 v3까지 올라갔고, 버그는 아직도 나올 것 같은 불안감이 있다. 하지만 확실한 건, 이 과정에서 시장에 대해, 코드에 대해, 그리고 위험 관리에 대해 정말 많은 걸 배웠다는 것이다.

다음 글에서는 Rocky Linux에서 이 모든 걸 돌리기 위한 서버 세팅 삽질기를 다룰 예정이다. 크롬 CDP 연결, 한글 입력 문제, Wayland 이슈 등… 서버 세팅도 트레이딩봇 못지않게 험난했다.

⚠️ 면책 조항: 이 글은 개인적인 개발 경험을 공유하기 위한 것이며, 투자 조언이 아닙니다. 암호화폐 투자는 원금 손실의 위험이 있으며, 모든 투자 판단은 본인의 책임입니다.


관련 포스트

참고