자동매매 봇 만들 때 흔히 하는 실수 7가지 — 초보가 꼭 피해야 할 것들

🌐 English

자동매매 봇에서 돈을 잃는 원인은 대개 전략이 나빠서가 아니라 구현이 틀려서입니다. 백테스트에서는 잘 되던 게 실전에서 무너지는 이유는 거의 정해져 있습니다. 자주 반복되는 일곱 가지를 정리합니다.

⚠️ 이 글은 교육·정보 제공 목적입니다. 투자 권유가 아니며, 투자 손실 책임은 본인에게 있습니다.

1. 미래 데이터를 몰래 쓰는 것 (Look-ahead Bias)

가장 흔하고, 가장 치명적입니다. 오늘 종가로 계산한 신호로 오늘 종가에 체결하는 백테스트는 현실에 존재하지 않습니다. 종가는 장이 끝나야 확정되니까요.

# 잘못 — 오늘 종가를 알아야 나오는 신호로 오늘 매수

signal = df["close"] > df["ma20"]

df["position"] = signal.astype(int)

# 올바름 — 신호는 어제 것, 체결은 오늘

df["position"] = signal.shift(1).fillna(0).astype(int)

이 한 줄 차이로 백테스트 수익률이 몇 배씩 바뀝니다. 수익률이 비현실적으로 좋다면 십중팔구 여기부터 의심하세요.

2. 수수료와 슬리피지를 빼먹는 것

하루에 몇 번씩 매매하는 전략은 수수료만으로 수익이 사라집니다. 슬리피지(주문 낸 가격과 실제 체결 가격의 차이)까지 더하면 더 그렇습니다.

FEE      = 0.0005    # 편도 0.05% (거래소·증권사마다 다름)

SLIPPAGE = 0.0005    # 체결 미끄러짐 가정

trades = df["position"].diff().abs()      # 포지션이 바뀐 날 = 거래 발생

cost   = trades * (FEE + SLIPPAGE)

df["net_ret"] = df["ret"] * df["position"] - cost

print(f"비용 반영 누적: {(1 + df['net_ret']).prod() - 1:.2%}")

비용을 넣었을 때 수익이 남지 않는 전략은, 실전에서도 남지 않습니다. 이 검증을 나중으로 미루지 마세요.

3. 파라미터를 데이터에 억지로 맞추는 것 (과최적화)

이동평균 기간을 5부터 100까지 다 돌려보고 제일 좋은 값을 고르면, 그건 전략을 찾은 게 아니라 과거 데이터의 우연을 외운 것입니다.

# 위험 신호: 인접한 값끼리 결과가 널뛴다

# ma=19 → +12%   ma=20 → +87%   ma=21 → +8%

# → 20이 특별한 게 아니라, 그냥 운 좋은 구간에 맞은 것

건강한 파라미터는 주변 값에서도 완만하게 좋습니다. 하나만 뾰족하게 튀면 버리세요. 그리고 기간을 나눠 검증하세요 — 앞 70%로 정하고, 뒤 30%는 손대지 않은 채 확인용으로 남기는 방식입니다.

4. 예외 처리 없이 API를 호출하는 것

네트워크는 반드시 끊깁니다. 거래소는 반드시 점검을 합니다. 예외 처리가 없으면 봇은 포지션을 든 채로 죽습니다.

import time, requests

def safe_get(url, params=None, retries=3):

    for i in range(retries):

        try:

            res = requests.get(url, params=params, timeout=10)

            if res.status_code == 429:            # 호출 제한

                time.sleep(2 ** i)                # 점점 길게 대기

                continue

            res.raise_for_status()

            return res.json()

        except requests.RequestException as e:

            if i == retries - 1:

                notify(f"API 실패, 봇 정지: {e}")   # 사람에게 알림

                raise

            time.sleep(2 ** i)

핵심은 재시도가 아니라 "실패했을 때 사람이 안다"입니다. 조용히 죽은 봇이 제일 위험합니다.

5. 주문 중복 — 같은 신호로 두 번 사는 것

주문을 냈는데 응답이 늦으면, 코드는 실패로 판단하고 다시 냅니다. 그렇게 2배 물량이 들어갑니다.

# 주문 전에 현재 포지션을 먼저 확인

pos = get_position(symbol)

if pos["qty"] > 0:

    log.info("이미 보유 중 — 신규 진입 건너뜀")

    return

# 클라이언트 주문번호로 중복 방지 (거래소가 지원하는 경우)

client_id = f"{symbol}-{signal_date}-entry"     # 같은 신호면 같은 ID

place_order(symbol, qty, client_order_id=client_id)

원칙은 하나입니다. 주문 직전에 실제 잔고를 확인한다. 내부 변수만 믿으면 재시작·오류 때 어긋납니다.

6. 손절 규칙이 없는 것

진입 조건은 열심히 만들면서 청산 조건은 "오르면 판다"로 끝내는 경우가 많습니다. 손실은 규칙이 없으면 무한정 커집니다.

MAX_LOSS_PCT = 0.02          # 1회 거래 최대 손실 2%

def position_size(capital, entry, stop):

    risk_per_share = entry - stop

    if risk_per_share <= 0:

        raise ValueError("손절가가 진입가보다 높습니다")

    qty = (capital * MAX_LOSS_PCT) / risk_per_share

    return int(qty)

# 손절 폭이 넓으면 수량이 자동으로 줄어듭니다

qty = position_size(10_000_000, entry=71_000, stop=68_000)

"얼마 살까"가 아니라 "틀렸을 때 얼마 잃을까"부터 정하는 것 — 이 순서가 자금 관리의 전부입니다.

7. 실계좌부터 시작하는 것

가장 비싼 실수입니다. 코드가 논리적으로 맞아 보여도 실행 환경에서는 늘 예상 밖의 일이 생깁니다.

권장 순서는 이렇습니다.

  1. 백테스트 — 과거 데이터에서 비용 포함해 검증
  2. 페이퍼 트레이딩 — 실시간 시세로 돌리되 주문은 로그만. 최소 2~4주
  3. 모의투자 계좌 — 실제 주문 흐름 확인
  4. 소액 실전 — 잃어도 되는 금액으로 시작

2번에서 걸러지는 버그가 정말 많습니다. 데이터 지연, 휴장일 처리, 자정 넘김, 토큰 만료 — 전부 실시간으로 돌려봐야 드러납니다.

정리

  • 미래 참조 — 신호는 shift(1)로 미뤄 적용
  • 비용 — 수수료 + 슬리피지 반영 후에도 남는지 확인
  • 과최적화 — 주변 값에서도 좋아야 진짜
  • 예외 처리 — 실패 시 멈추고 알리기
  • 중복 주문 — 주문 전 실제 잔고 확인
  • 손절 — 손실 한도부터 정하고 수량 계산
  • 실계좌 직행 금지 — 페이퍼 트레이딩을 반드시 거칠 것

봇 개발의 목표는 화려한 전략이 아니라 내일도 같은 규칙대로 도는 시스템입니다. 위 일곱 개만 피해도 대부분의 사고는 일어나지 않습니다.

댓글

이 블로그의 인기 게시물

한국투자증권 KIS API로 실시간 시세 받기 (WebSocket 실전)

파이썬으로 업비트 API 연동하기 — 시세 조회부터 주문까지 기초

Go로 자동매매 신호봇 프레임워크 설계하기