바이낸스 자동매매 봇 30분 안에 만들기 — Python 풀코드
"바이낸스 자동매매 봇" 검색 결과에는 마케팅성 글이 많지만, 코드를 끝까지 따라가면 봇이 실제로 도는 글은 의외로 적습니다. 이 글은 알고랩이 80건+ 바이낸스 봇을 제작하며 만든 가장 기본적인 봇 골격을 30분 안에 따라할 수 있게 압축했습니다.
전제는 단 하나, 바이낸스 API 키가 발급되어 있다는 것입니다. 아직 없다면 먼저 → 바이낸스 API 발급 완벽 가이드 (2026 최신 화면)를 보고 오세요. 권한은 Read + Spot Trading만 ON, 출금은 반드시 OFF여야 합니다.
30분 안에 만들 것
- 1단계 — 환경 세팅 (5분)
- 2단계 — 시세 받기 (5분)
- 3단계 — 전략 로직 작성: EMA 크로스 + RSI 필터 (5분)
- 4단계 — 페이퍼 트레이딩 모드 (5분)
- 5단계 — 백테스트로 전략 검증 (5분)
- 6단계 — 실거래 전환 + 안전장치 (5분)
- 통합 풀코드 (한 번에 복붙)
- 자주 실수하는 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분
가장 단순하면서도 추세 추종의 기본인 전략입니다:
- EMA(9) 가 EMA(21) 위로 골든크로스 → 매수 신호
- 다만 RSI(14) < 70일 때만 (과매수 영역 회피)
- EMA(9) 가 EMA(21) 아래로 데드크로스 → 매도 신호
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
✅ 실거래 전환 체크리스트
- 최소 1주일 페이퍼 트레이딩으로 작동 확인
- 첫 실거래 금액은 잃어도 괜찮은 수준 (10~20 USDT)
- 봇이 도는 동안 첫 며칠은 30분에 한 번씩 로그 확인
- 이상한 동작 발견 시 즉시 paper_trade=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~2% 룰
- 여러 종목 동시 운영: 분산 효과로 변동성 감소
- WebSocket 실시간 시세: REST 폴링은 지연 발생
- VPS 24시간 무중단: 노트북 절전 → 봇 정지
- 텔레그램 알림: 매매 / 에러 즉시 통보
- 백테스트 강화: 워크포워드, 몬테카를로
이 모든 것을 직접 구현하는 게 부담스럽다면, 알고랩이 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시간 빠른 답변 가능합니다.