AlgoLab Blog · 실전 코드 · 2026 작동 보증

바이낸스 자동매매 봇 30분 안에 만들기 — Python 풀코드

바이낸스 2026-05-18 · 약 12분 읽기 / 실습 30분 · 알고랩 AlgoLab
한 줄 요약 이 글의 코드를 그대로 복붙하면 바이낸스에서 30분 안에 작동하는 자동매매 봇을 만들 수 있습니다. EMA 크로스오버 + RSI 필터 전략 / 백테스트 / 페이퍼 트레이딩 / 실거래 전환을 한 파일에 담았습니다. 단, 이 전략 자체가 수익을 보장하지는 않습니다 — 코드 구조를 익히는 학습용입니다.

"바이낸스 자동매매 봇" 검색 결과에는 마케팅성 글이 많지만, 코드를 끝까지 따라가면 봇이 실제로 도는 글은 의외로 적습니다. 이 글은 알고랩이 80건+ 바이낸스 봇을 제작하며 만든 가장 기본적인 봇 골격을 30분 안에 따라할 수 있게 압축했습니다.

전제는 단 하나, 바이낸스 API 키가 발급되어 있다는 것입니다. 아직 없다면 먼저 → 바이낸스 API 발급 완벽 가이드 (2026 최신 화면)를 보고 오세요. 권한은 Read + Spot Trading만 ON, 출금은 반드시 OFF여야 합니다.

30분 안에 만들 것

  1. 1단계 — 환경 세팅 (5분)
  2. 2단계 — 시세 받기 (5분)
  3. 3단계 — 전략 로직 작성: EMA 크로스 + RSI 필터 (5분)
  4. 4단계 — 페이퍼 트레이딩 모드 (5분)
  5. 5단계 — 백테스트로 전략 검증 (5분)
  6. 6단계 — 실거래 전환 + 안전장치 (5분)
  7. 통합 풀코드 (한 번에 복붙)
  8. 자주 실수하는 5가지

1환경 세팅5분

Python 3.10+ 가 설치되어 있다고 가정합니다(없다면 python.org에서 설치).

# 가상환경 생성 + 활성화
python -m venv venv
source venv/bin/activate   # Windows: venv\Scripts\activate

# 필수 패키지 설치
pip install python-binance pandas python-dotenv

프로젝트 폴더에 .env 파일을 만들고 API 키를 저장합니다:

# .env (반드시 .gitignore 에 추가)
BINANCE_API_KEY=발급받은_API_KEY
BINANCE_API_SECRET=발급받은_SECRET_KEY

⚠️ .env 를 깃 리포에 커밋하지 마세요. 가장 흔한 사고 유형입니다. 깃허브에 푸시되는 즉시 봇 자동 스캐너가 키를 가져갑니다(5분 내). 자세한 대응은 API 발급 가이드 참고.

2시세 받기5분

바이낸스에서 캔들(klines) 데이터를 가져오는 것부터 시작합니다. 15분봉 BTCUSDT 200개:

import os
import pandas as pd
from binance.client import Client
from dotenv import load_dotenv

load_dotenv()
client = Client(os.getenv("BINANCE_API_KEY"), os.getenv("BINANCE_API_SECRET"))

def get_klines(symbol="BTCUSDT", interval="15m", limit=200):
    """캔들 데이터 → pandas DataFrame"""
    raw = client.get_klines(symbol=symbol, interval=interval, limit=limit)
    df = pd.DataFrame(raw, columns=[
        "open_time","open","high","low","close","volume",
        "close_time","qav","trades","tbbav","tbqav","ignore"
    ])
    df["close"] = df["close"].astype(float)
    df["high"]  = df["high"].astype(float)
    df["low"]   = df["low"].astype(float)
    df["volume"] = df["volume"].astype(float)
    df["open_time"] = pd.to_datetime(df["open_time"], unit="ms")
    return df[["open_time","open","high","low","close","volume"]]

df = get_klines()
print(df.tail())

실행 후 최근 5개 캔들이 출력되면 성공입니다. 출력 안 되거나 401/403 에러가 나오면 키 또는 IP 화이트리스트를 점검하세요.

3전략 로직 — EMA 크로스 + RSI 필터5분

가장 단순하면서도 추세 추종의 기본인 전략입니다:

def add_indicators(df):
    """EMA 9/21, RSI 14 컬럼 추가"""
    df["ema9"]  = df["close"].ewm(span=9, adjust=False).mean()
    df["ema21"] = df["close"].ewm(span=21, adjust=False).mean()

    # RSI 14 (Wilder smoothing 근사)
    delta = df["close"].diff()
    gain = delta.clip(lower=0).rolling(14).mean()
    loss = -delta.clip(upper=0).rolling(14).mean()
    rs = gain / loss
    df["rsi"] = 100 - (100 / (1 + rs))
    return df

def get_signal(df):
    """마지막 캔들 기준 매매 신호: 'BUY' / 'SELL' / None"""
    if len(df) < 22:
        return None
    last = df.iloc[-1]
    prev = df.iloc[-2]

    # 골든크로스 + RSI 70 미만
    if prev["ema9"] <= prev["ema21"] and last["ema9"] > last["ema21"] and last["rsi"] < 70:
        return "BUY"
    # 데드크로스
    if prev["ema9"] >= prev["ema21"] and last["ema9"] < last["ema21"]:
        return "SELL"
    return None

💡 왜 EMA 크로스 + RSI 필터인가: EMA 크로스는 추세장에서 잘 작동하지만 횡보장에서 휩쏘(whipsaw)가 심합니다. RSI 70 필터로 "이미 너무 오른" 신호를 제거해 횡보 손실을 줄입니다. 완벽하지는 않지만 학습용 골격으로 적절합니다.

4페이퍼 트레이딩 모드5분

실거래 전에 가상으로 매매를 돌립니다. 잔고 / 매수가 / 손익을 메모리에서 시뮬레이션:

class PaperTrader:
    """실거래 대신 가상 잔고로 시뮬레이션"""
    def __init__(self, cash=1000.0, fee=0.001):  # 수수료 0.1%
        self.cash = cash
        self.position = 0.0  # 보유 코인 수량
        self.entry_price = 0.0
        self.fee = fee
        self.trades = []

    def buy(self, price):
        if self.position > 0:
            return  # 이미 보유 중
        qty = (self.cash * (1 - self.fee)) / price
        self.position = qty
        self.entry_price = price
        self.cash = 0
        self.trades.append(("BUY", price, qty))
        print(f"[PAPER BUY] price={price:.2f} qty={qty:.6f}")

    def sell(self, price):
        if self.position == 0:
            return
        proceeds = self.position * price * (1 - self.fee)
        pnl = proceeds - (self.entry_price * self.position)
        self.cash = proceeds
        self.trades.append(("SELL", price, self.position, pnl))
        print(f"[PAPER SELL] price={price:.2f} pnl={pnl:+.2f} USDT")
        self.position = 0
        self.entry_price = 0

    def value(self, price):
        return self.cash + self.position * price

5백테스트로 전략 검증5분

실거래 / 페이퍼 모두 시작 전에 과거 데이터로 전략이 어떻게 행동했는지 확인합니다:

def backtest(symbol="BTCUSDT", interval="15m", limit=1000, initial=1000):
    df = get_klines(symbol, interval, limit)
    df = add_indicators(df).dropna().reset_index(drop=True)
    pt = PaperTrader(cash=initial)

    for i in range(22, len(df)):
        window = df.iloc[:i+1]
        sig = get_signal(window)
        price = window.iloc[-1]["close"]
        if sig == "BUY":
            pt.buy(price)
        elif sig == "SELL":
            pt.sell(price)

    final = pt.value(df.iloc[-1]["close"])
    bnh = initial * df.iloc[-1]["close"] / df.iloc[22]["close"]  # 매수후 보유
    print(f"\n[Backtest] start={initial} end={final:.2f} (+{(final/initial-1)*100:.2f}%)")
    print(f"[Buy&Hold] {bnh:.2f} (+{(bnh/initial-1)*100:.2f}%)")
    print(f"거래 수: {len([t for t in pt.trades if t[0]=='BUY'])}")
    return pt

if __name__ == "__main__":
    backtest()

결과의 두 숫자를 비교하세요: 전략 수익률 vs 단순 보유(Buy & Hold) 수익률. 전략이 보유보다 못하다면 그 전략은 의미가 없습니다.

💡 백테스트 함정: 1,000개 캔들로 한 백테스트는 단지 "최근의 한 시기"입니다. 횡보장 / 상승장 / 하락장을 모두 포함한 다년치 데이터, 워크포워드 분석, 슬리피지 가정이 추가로 필요합니다. 자세히는 백테스트와 실전 괴리 줄이기 참고.

6실거래 전환 + 안전장치5분

충분히 검증됐다면 실거래 모드로 전환합니다. paper_trade=False 플래그 하나로 전환되도록 만들어야 사고가 적습니다:

import time
from binance.enums import SIDE_BUY, SIDE_SELL, ORDER_TYPE_MARKET

def live_order(symbol, side, quote_amount=None, qty=None):
    """실거래 시장가 주문. quote_amount=USDT 금액 / qty=코인 수량"""
    if side == SIDE_BUY and quote_amount:
        return client.order_market_buy(symbol=symbol, quoteOrderQty=quote_amount)
    if side == SIDE_SELL and qty:
        return client.order_market_sell(symbol=symbol, quantity=qty)
    raise ValueError("buy는 quote_amount, sell은 qty 필요")

def run_bot(symbol="BTCUSDT", interval="15m", trade_size_usdt=15, paper_trade=True):
    """봇 메인 루프. 매 캔들마다 신호 체크 → 매매"""
    pt = PaperTrader(cash=trade_size_usdt) if paper_trade else None
    position_held = False

    while True:
        try:
            df = get_klines(symbol, interval, limit=100)
            df = add_indicators(df).dropna().reset_index(drop=True)
            sig = get_signal(df)
            price = df.iloc[-1]["close"]

            if sig == "BUY" and not position_held:
                if paper_trade:
                    pt.buy(price)
                else:
                    res = live_order(symbol, SIDE_BUY, quote_amount=trade_size_usdt)
                    print(f"[LIVE BUY] {res['executedQty']} @ avg {res['fills'][0]['price']}")
                position_held = True

            elif sig == "SELL" and position_held:
                if paper_trade:
                    pt.sell(price)
                else:
                    bal = client.get_asset_balance(asset=symbol.replace("USDT",""))
                    qty = float(bal["free"])
                    # 바이낸스 LOT_SIZE 규격 절삭 (BTCUSDT는 보통 5자리)
                    qty = round(qty, 5)
                    res = live_order(symbol, SIDE_SELL, qty=qty)
                    print(f"[LIVE SELL] {qty} @ avg {res['fills'][0]['price']}")
                position_held = False

            print(f"[{time.strftime('%H:%M:%S')}] price={price:.2f} signal={sig} held={position_held}")
        except Exception as e:
            print(f"[ERROR] {e}")

        time.sleep(60)  # 1분마다 확인 (15분봉이라 충분)

if __name__ == "__main__":
    run_bot(paper_trade=True)   # ← 처음엔 무조건 True

✅ 실거래 전환 체크리스트

통합 풀코드 (한 번에 복붙)

위 단계를 모두 합친 단일 파일입니다. bot.py로 저장하고 python bot.py 실행:

import os, time
import pandas as pd
from binance.client import Client
from binance.enums import SIDE_BUY, SIDE_SELL
from dotenv import load_dotenv

load_dotenv()
client = Client(os.getenv("BINANCE_API_KEY"), os.getenv("BINANCE_API_SECRET"))

def get_klines(symbol="BTCUSDT", interval="15m", limit=200):
    raw = client.get_klines(symbol=symbol, interval=interval, limit=limit)
    df = pd.DataFrame(raw, columns=["open_time","open","high","low","close","volume",
        "close_time","qav","trades","tbbav","tbqav","ignore"])
    for c in ["open","high","low","close","volume"]: df[c] = df[c].astype(float)
    df["open_time"] = pd.to_datetime(df["open_time"], unit="ms")
    return df[["open_time","open","high","low","close","volume"]]

def add_indicators(df):
    df["ema9"]  = df["close"].ewm(span=9, adjust=False).mean()
    df["ema21"] = df["close"].ewm(span=21, adjust=False).mean()
    delta = df["close"].diff()
    gain = delta.clip(lower=0).rolling(14).mean()
    loss = -delta.clip(upper=0).rolling(14).mean()
    df["rsi"] = 100 - (100 / (1 + gain/loss))
    return df

def get_signal(df):
    if len(df) < 22: return None
    p, l = df.iloc[-2], df.iloc[-1]
    if p["ema9"] <= p["ema21"] and l["ema9"] > l["ema21"] and l["rsi"] < 70: return "BUY"
    if p["ema9"] >= p["ema21"] and l["ema9"] < l["ema21"]: return "SELL"
    return None

class PaperTrader:
    def __init__(self, cash=1000.0, fee=0.001):
        self.cash, self.position, self.entry, self.fee = cash, 0.0, 0.0, fee
    def buy(self, price):
        if self.position > 0: return
        qty = (self.cash * (1 - self.fee)) / price
        self.position, self.entry, self.cash = qty, price, 0
        print(f"[PAPER BUY] {price:.2f} qty={qty:.6f}")
    def sell(self, price):
        if self.position == 0: return
        proceeds = self.position * price * (1 - self.fee)
        pnl = proceeds - (self.entry * self.position)
        print(f"[PAPER SELL] {price:.2f} pnl={pnl:+.2f}")
        self.cash, self.position, self.entry = proceeds, 0, 0

def run_bot(symbol="BTCUSDT", interval="15m", trade_size_usdt=15, paper_trade=True):
    pt = PaperTrader(cash=trade_size_usdt) if paper_trade else None
    held = False
    while True:
        try:
            df = add_indicators(get_klines(symbol, interval)).dropna().reset_index(drop=True)
            sig = get_signal(df)
            price = df.iloc[-1]["close"]
            if sig == "BUY" and not held:
                if paper_trade: pt.buy(price)
                else: client.order_market_buy(symbol=symbol, quoteOrderQty=trade_size_usdt)
                held = True
            elif sig == "SELL" and held:
                if paper_trade: pt.sell(price)
                else:
                    qty = round(float(client.get_asset_balance(asset=symbol.replace("USDT",""))["free"]), 5)
                    client.order_market_sell(symbol=symbol, quantity=qty)
                held = False
            print(f"[{time.strftime('%H:%M:%S')}] {price:.2f} sig={sig} held={held}")
        except Exception as e:
            print(f"[ERR] {e}")
        time.sleep(60)

if __name__ == "__main__":
    run_bot(paper_trade=True)  # 실거래는 False, 검증 끝나면 변경

자주 실수하는 5가지

1. paper_trade=False 로 두고 실수로 실행

코드 마지막 줄을 가장 안전하게: 기본값은 항상 True. 실거래로 전환할 때만 명시적으로 False로 바꾸세요.

2. 같은 봇 여러 번 실행 → 동일 신호로 중복 주문

봇이 두 번 떠 있으면 매수 신호가 한 번 나올 때 매수가 두 번 들어갑니다. PID 파일이나 lockfile로 단일 실행을 강제하세요. systemd 등 프로세스 매니저 쓰면 자동 보장.

3. LOT_SIZE / MIN_NOTIONAL 무시 → 주문 실패

바이낸스는 심볼마다 최소 거래 단위최소 명목금액이 다릅니다. BTCUSDT는 0.00001 단위, 최소 5 USDT. 위반 시 -1013 Filter failure: LOT_SIZE 에러. client.get_symbol_info(symbol)로 사전 확인하세요.

4. 서버시간 동기화 안 됨 → -1021 에러

로컬 시계가 바이낸스 서버 시계와 ±1초 이상 차이나면 모든 인증 요청이 실패합니다. Windows 시계는 NTP가 자주 어긋납니다. 자세한 대응은 바이낸스 봇 함정 5가지 참고.

5. 백테스트 결과를 그대로 믿음

위 백테스트는 슬리피지, 수수료 외 비용, 시장 충격을 무시한 단순 시뮬레이션입니다. 실거래는 보통 백테스트보다 10~30% 나쁩니다. 실거래 전에 페이퍼 트레이딩 1주일 + 작은 금액 실거래 1주일을 거치세요.

다음 단계 — 진짜 수익 내는 봇으로

위 골격은 학습용 출발점입니다. 실제로 수익을 내려면 다음을 추가해야 합니다:

이 모든 것을 직접 구현하는 게 부담스럽다면, 알고랩이 1~3주 안에 위 골격 + 안전장치 + 모니터링 + 무중단 운영까지 통합된 봇을 만들어드립니다.

자주 묻는 질문

Q. Python 초보도 따라할 수 있나요?

변수, 함수, if/for 기본만 알면 가능합니다. 코드를 그대로 복붙해도 동작합니다. 다만 실거래 전 페이퍼 1주일은 필수.

Q. 이 코드로 돈을 벌 수 있나요?

이 글의 EMA + RSI 전략은 학습용 예제입니다. 모든 단순 추세 전략은 횡보장에서 손실을 봅니다. 검증되지 않은 전략으로 큰 금액 거래는 비추.

Q. 실거래 전에 어떻게 안전하게 테스트하나요?

(1) 위 페이퍼 트레이딩 모드로 1주일 (2) Spot Testnet(testnet.binance.vision)에서 모의 거래 (3) 실거래 10~20 USDT부터 시작.

Q. 봇이 24시간 돌려면?

VPS(Vultr, AWS Lightsail, Oracle Free Tier)에서 systemd / PM2 / supervisord로 실행. 노트북은 절전 / 네트워크 단절로 봇이 멈춥니다.

Q. 선물(Futures) 봇도 같은 코드로 되나요?

구조는 같지만 함수가 다릅니다: client.futures_klines(), client.futures_create_order(). 또한 레버리지, 마진 모드, 청산 가격 추적이 추가됩니다. 자세히는 바이낸스 선물 청산 방지 7가지 안전장치 참고.

바이낸스 봇 맞춤 제작

위 골격에 리스크 관리 / WebSocket / 텔레그램 알림 / VPS 무중단 운영까지 포함한 통합 봇을 알고랩이 만들어드립니다.
24시간 빠른 답변 가능합니다.

무료 상담 시작하기