업비트 자동매매 봇 30분 안에 만들기 — Python 변동성 돌파 풀코드
"업비트 자동매매" 검색 결과에는 유료 프로그램 광고가 많지만, 코드를 끝까지 따라가면 봇이 실제로 도는 글은 의외로 적습니다. 이 글은 알고랩이 60건+ 업비트 봇을 제작하며 만든 가장 기본적인 봇 골격을 30분 안에 따라할 수 있게 압축했습니다.
전제는 업비트 Open API 키가 발급되어 있다는 것입니다. 마이페이지 → Open API 관리에서 발급하며, 자산조회·주문 권한만 ON, 출금은 반드시 OFF, IP 화이트리스트 등록을 권장합니다. API 인프라를 더 깊게 알고 싶다면 → 업비트 자동매매 REST·WebSocket 실전 아키텍처를 함께 보세요.
30분 안에 만들 것
- 1단계 — 환경 세팅 + JWT 인증 이해 (5분)
- 2단계 — 시세(캔들) 받기 (5분)
- 3단계 — 변동성 돌파 전략 로직 (5분)
- 4단계 — 페이퍼 트레이딩 모드 (5분)
- 5단계 — 백테스트로 전략 검증 (5분)
- 6단계 — 실거래 전환 + 안전장치 (5분)
- 통합 풀코드 (한 번에 복붙)
- 자주 실수하는 5가지
1환경 세팅 + JWT 인증5분
Python 3.10+ 가 설치되어 있다고 가정합니다.
# 가상환경 생성 + 활성화
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 필수 패키지 설치
pip install pyupbit pandas python-dotenv
프로젝트 폴더에 .env 파일을 만들고 발급받은 키를 저장합니다:
# .env (반드시 .gitignore 에 추가)
UPBIT_ACCESS_KEY=발급받은_Access_Key
UPBIT_SECRET_KEY=발급받은_Secret_Key
업비트 인증 원리 — JWT
업비트 API는 JWT(JSON Web Token) 방식입니다. 모든 인증 요청마다 Access Key + 파라미터 해시를 Secret Key로 서명한 토큰을 헤더에 넣습니다. pyupbit는 이 과정을 내부에서 처리해주므로 입문 단계에서는 신경 쓸 필요가 없습니다. 직접 호출 시에는 다음과 같습니다(참고용):
import jwt, uuid, hashlib
from urllib.parse import urlencode
def make_jwt_token(access_key, secret_key, query=None):
payload = {"access_key": access_key, "nonce": str(uuid.uuid4())}
if query:
m = hashlib.sha512()
m.update(urlencode(query).encode())
payload["query_hash"] = m.hexdigest()
payload["query_hash_alg"] = "SHA512"
return jwt.encode(payload, secret_key)
# 헤더: {"Authorization": f"Bearer {token}"}
⚠️ .env 를 깃 리포에 커밋하지 마세요. 업비트 키가 깃허브에 노출되면 자동 스캐너가 즉시 가져갑니다. 출금 권한을 OFF로 발급했더라도 거래 권한만으로 자산을 망가뜨릴 수 있습니다. .gitignore에 .env를 꼭 추가하세요.
2시세(캔들) 받기5분
변동성 돌파 전략은 일봉을 사용합니다. pyupbit로 비트코인 일봉 데이터를 가져옵니다:
import pyupbit
import pandas as pd
def get_ohlcv(ticker="KRW-BTC", interval="day", count=200):
"""업비트 캔들 데이터 → pandas DataFrame"""
df = pyupbit.get_ohlcv(ticker, interval=interval, count=count)
# df 컬럼: open, high, low, close, volume, value
return df
df = get_ohlcv()
print(df.tail())
print("현재가:", pyupbit.get_current_price("KRW-BTC"))
실행 후 최근 캔들과 현재가가 출력되면 성공입니다. 업비트는 한국 거래소라 별도 IP 우회 없이 바로 조회됩니다.
💡 업비트 마켓 구분: 티커는 KRW-BTC(원화마켓), BTC-ETH(BTC마켓), USDT-BTC(USDT마켓) 형식입니다. 입문자는 거래량이 가장 크고 정산이 단순한 원화마켓(KRW-)으로 시작하세요.
3변동성 돌파 전략 로직5분
변동성 돌파(Volatility Breakout)는 래리 윌리엄스가 고안한 전략으로, 한국 코인 자동매매에서 가장 널리 쓰이는 클래식입니다. 원리는 단순합니다:
- 목표가 = 오늘 시가 + (어제 고가 − 어제 저가) × K (보통 K=0.5)
- 현재가가 목표가를 돌파하면 매수 (오늘 큰 움직임이 나왔다는 신호)
- 다음 날 시가에 전량 매도 (하루만 보유)
def get_target_price(ticker="KRW-BTC", k=0.5):
"""변동성 돌파 목표가 계산"""
df = pyupbit.get_ohlcv(ticker, interval="day", count=2)
yesterday = df.iloc[-2] # 어제 캔들
today_open = df.iloc[-1]["open"] # 오늘 시가
rng = yesterday["high"] - yesterday["low"] # 어제 변동폭
target = today_open + rng * k
return target
def get_signal(ticker="KRW-BTC", k=0.5):
"""매수 신호 판단: 현재가가 목표가 돌파 시 True"""
target = get_target_price(ticker, k)
current = pyupbit.get_current_price(ticker)
return current >= target, target, current
💡 K값의 의미: K가 낮으면(0.3) 목표가가 낮아 자주 매수하지만 가짜 신호가 늘고, K가 높으면(0.7) 매수가 드물지만 강한 추세만 잡습니다. 0.5가 일반적인 출발점이며, 실제로는 종목별·기간별 백테스트로 최적값을 찾습니다.
4페이퍼 트레이딩 모드5분
실거래 전에 가상으로 매매를 돌립니다. 업비트는 공식 테스트넷이 없으므로 이 페이퍼 트레이딩이 더욱 중요합니다.
class PaperTrader:
"""실거래 대신 가상 잔고로 시뮬레이션"""
def __init__(self, cash=1_000_000, fee=0.0005): # 업비트 수수료 0.05%
self.cash = cash
self.coin = 0.0
self.avg_price = 0.0
self.fee = fee
def buy(self, price):
if self.cash < 5000: # 업비트 최소 주문 5,000원
return
qty = (self.cash * (1 - self.fee)) / price
self.coin = qty
self.avg_price = price
print(f"[PAPER BUY] price={price:,.0f} qty={qty:.8f}")
self.cash = 0
def sell(self, price):
if self.coin == 0:
return
proceeds = self.coin * price * (1 - self.fee)
pnl = proceeds - (self.avg_price * self.coin)
self.cash = proceeds
print(f"[PAPER SELL] price={price:,.0f} pnl={pnl:+,.0f}원")
self.coin = 0
self.avg_price = 0
def value(self, price):
return self.cash + self.coin * price
5백테스트로 전략 검증5분
실거래·페이퍼 시작 전에 과거 데이터로 전략이 어떻게 행동했는지 확인합니다:
def backtest(ticker="KRW-BTC", k=0.5, count=200, initial=1_000_000):
df = pyupbit.get_ohlcv(ticker, interval="day", count=count)
df["range"] = (df["high"] - df["low"]).shift(1) # 어제 변동폭
df["target"] = df["open"] + df["range"] * k # 오늘 목표가
# 고가가 목표가를 넘었으면 목표가 매수 → 종가 보유 → 다음날 시가 매도
df["bought"] = df["high"] > df["target"]
fee = 0.0005
df["ret"] = 1.0
# 매수 성사된 날: (다음날 시가 / 목표가) 수익률
df.loc[df["bought"], "ret"] = (
df["open"].shift(-1) / df["target"] - fee * 2
)
df = df.dropna()
cumulative = df["ret"].cumprod()
final = initial * cumulative.iloc[-1]
bnh = initial * df["close"].iloc[-1] / df["close"].iloc[0]
print(f"\n[Backtest] {ticker} K={k} {len(df)}일")
print(f"전략 수익: {final:,.0f}원 ({(final/initial-1)*100:+.1f}%)")
print(f"단순보유: {bnh:,.0f}원 ({(bnh/initial-1)*100:+.1f}%)")
print(f"매수 발생: {int(df['bought'].sum())}일")
return df
if __name__ == "__main__":
backtest()
출력의 전략 수익 vs 단순보유(Buy & Hold)를 비교하세요. 전략이 단순보유보다 못하다면 그 K값·종목 조합은 의미가 없습니다.
💡 백테스트 함정: 위 백테스트는 슬리피지(목표가에 정확히 못 사는 현상), 거래량 부족, 동시 호가 경쟁을 무시한 단순 시뮬레이션입니다. 변동성 돌파는 특히 목표가 슬리피지가 크기 때문에 실거래는 백테스트보다 나쁩니다. 자세히는 → 백테스트와 실전의 괴리 줄이기 참고.
6실거래 전환 + 안전장치5분
충분히 검증됐다면 실거래로 전환합니다. paper_trade 플래그 하나로 전환되게 만드는 것이 사고를 줄이는 핵심입니다.
import pyupbit, os, time, datetime
from dotenv import load_dotenv
load_dotenv()
upbit = pyupbit.Upbit(os.getenv("UPBIT_ACCESS_KEY"), os.getenv("UPBIT_SECRET_KEY"))
def run_bot(ticker="KRW-BTC", k=0.5, invest_krw=10000, paper_trade=True):
"""변동성 돌파 봇 메인 루프"""
pt = PaperTrader(cash=invest_krw) if paper_trade else None
held = False
while True:
try:
now = datetime.datetime.now()
target = get_target_price(ticker, k)
current = pyupbit.get_current_price(ticker)
# 09:00:10 ~ 08:59:50 사이가 '오늘' (업비트 일봉 기준 09시)
# 매수: 목표가 돌파 + 미보유
if current >= target and not held:
if paper_trade:
pt.buy(current)
else:
krw = upbit.get_balance("KRW")
if krw > 5000:
# 시장가 매수는 '금액'으로 지정 (업비트 특성)
upbit.buy_market_order(ticker, krw * 0.9995)
print(f"[LIVE BUY] {ticker} {krw:,.0f}원")
held = True
# 매도: 매일 08:59 (다음 일봉 시작 직전) 전량 청산
if held and now.hour == 8 and now.minute == 59:
if paper_trade:
pt.sell(current)
else:
coin = upbit.get_balance(ticker)
if coin > 0:
# 시장가 매도는 '수량'으로 지정 (업비트 특성)
upbit.sell_market_order(ticker, coin)
print(f"[LIVE SELL] {ticker} {coin:.8f}")
held = False
time.sleep(60) # 중복 매도 방지
print(f"[{now:%H:%M:%S}] target={target:,.0f} cur={current:,.0f} held={held}")
except Exception as e:
print(f"[ERROR] {e}")
time.sleep(10) # 10초마다 확인
if __name__ == "__main__":
run_bot(paper_trade=True) # ← 처음엔 무조건 True
✅ 실거래 전환 체크리스트
- 최소 1주일 페이퍼 트레이딩으로 작동 확인
- 첫 실거래 금액은 업비트 최소 주문(5,000원) 수준부터
- 봇이 도는 동안 첫 며칠은 매일 09시 전후로 로그 확인
- 이상한 동작 발견 시 즉시 paper_trade=True로 복귀
통합 풀코드 (한 번에 복붙)
위 단계를 모두 합친 단일 파일입니다. upbit_bot.py로 저장하고 python upbit_bot.py 실행:
import os, time, datetime
import pyupbit
from dotenv import load_dotenv
load_dotenv()
upbit = pyupbit.Upbit(os.getenv("UPBIT_ACCESS_KEY"), os.getenv("UPBIT_SECRET_KEY"))
def get_target_price(ticker="KRW-BTC", k=0.5):
df = pyupbit.get_ohlcv(ticker, interval="day", count=2)
rng = df.iloc[-2]["high"] - df.iloc[-2]["low"]
return df.iloc[-1]["open"] + rng * k
class PaperTrader:
def __init__(self, cash=1_000_000, fee=0.0005):
self.cash, self.coin, self.avg, self.fee = cash, 0.0, 0.0, fee
def buy(self, price):
if self.cash < 5000: return
self.coin = (self.cash * (1 - self.fee)) / price
self.avg, self.cash = price, 0
print(f"[PAPER BUY] {price:,.0f} qty={self.coin:.8f}")
def sell(self, price):
if self.coin == 0: return
proceeds = self.coin * price * (1 - self.fee)
pnl = proceeds - (self.avg * self.coin)
print(f"[PAPER SELL] {price:,.0f} pnl={pnl:+,.0f}원")
self.cash, self.coin, self.avg = proceeds, 0, 0
def run_bot(ticker="KRW-BTC", k=0.5, invest_krw=10000, paper_trade=True):
pt = PaperTrader(cash=invest_krw) if paper_trade else None
held = False
while True:
try:
now = datetime.datetime.now()
target = get_target_price(ticker, k)
current = pyupbit.get_current_price(ticker)
if current >= target and not held:
if paper_trade: pt.buy(current)
else:
krw = upbit.get_balance("KRW")
if krw > 5000: upbit.buy_market_order(ticker, krw * 0.9995)
held = True
if held and now.hour == 8 and now.minute == 59:
if paper_trade: pt.sell(current)
else:
coin = upbit.get_balance(ticker)
if coin > 0: upbit.sell_market_order(ticker, coin)
held = False
time.sleep(60)
print(f"[{now:%H:%M:%S}] target={target:,.0f} cur={current:,.0f} held={held}")
except Exception as e:
print(f"[ERR] {e}")
time.sleep(10)
if __name__ == "__main__":
run_bot(paper_trade=True) # 실거래는 False, 검증 끝나면 변경
자주 실수하는 5가지
1. 시장가 매수/매도 파라미터 혼동
업비트 시장가 주문은 매수는 KRW 금액, 매도는 코인 수량으로 지정합니다. buy_market_order(ticker, 10000)은 1만원어치 매수, sell_market_order(ticker, 0.001)은 0.001 코인 매도입니다. 이걸 바꿔 넣으면 주문이 거부되거나 의도와 다르게 체결됩니다.
2. 잔고 전액 매수 → 수수료 부족으로 주문 실패
get_balance("KRW") 전액을 그대로 매수하면 수수료를 낼 돈이 없어 거부됩니다. 위 코드처럼 0.9995를 곱해 수수료 여유분을 남기세요.
3. paper_trade=False 로 두고 실수로 실행
코드 마지막 줄 기본값은 항상 True로. 실거래 전환은 명시적으로 False로 바꿀 때만.
4. 같은 봇 여러 번 실행 → 중복 매수
봇이 두 개 떠 있으면 목표가 돌파 시 매수가 두 번 들어가 잔고가 꼬입니다. PID 파일이나 lockfile, 또는 systemd 같은 프로세스 매니저로 단일 실행을 강제하세요.
5. Rate Limit 무시 → 429 에러
업비트 REST API는 초당 호출 제한이 있습니다(주문성 요청이 더 엄격). 빠른 루프로 현재가를 계속 조회하면 429 Too Many Requests가 납니다. 위 코드는 time.sleep(10)으로 여유를 뒀습니다. 다종목 운영 시에는 호출 간격 관리가 필수입니다 — 자세히는 → 업비트 REST·WebSocket 아키텍처 참고.
다음 단계 — 진짜 운용 가능한 봇으로
위 골격은 학습용 출발점입니다. 실제 운용하려면 다음을 추가해야 합니다:
- 손절 규칙: 매수 후 −N% 도달 시 즉시 청산 (변동성 돌파는 손절이 생명)
- 거래량·이동평균 필터: 하락 추세에서는 매수 안 함
- 다종목 분산: 상위 거래량 코인 여러 개에 자산 분할
- WebSocket 실시간 시세: REST 폴링은 지연·Rate Limit 위험
- VPS 24시간 무중단: 노트북은 절전 → 봇 정지
- 텔레그램 알림: 매수·매도·에러 즉시 통보
이 모든 것을 직접 구현하는 게 부담스럽다면, 알고랩이 1~3주 안에 위 골격 + 안전장치 + 모니터링 + 무중단 운영까지 통합된 업비트 봇을 만들어드립니다.
자주 묻는 질문
Q. pyupbit와 직접 API 호출 중 무엇을 쓰나요?
입문은 pyupbit 권장. JWT 토큰 생성·페이지네이션·데이터 정리를 모두 처리해줍니다. 정밀 제어가 필요하면 requests + PyJWT로 직접 호출.
Q. 변동성 돌파 전략으로 돈을 벌 수 있나요?
학습용 클래식 전략입니다. 추세장에서는 잘 작동하지만 횡보·하락장에서는 손실이 누적됩니다. 손절·자산배분·거래량 필터가 추가로 필요합니다.
Q. 업비트 API 키는 어떻게 발급하나요?
업비트 마이페이지 → Open API 관리. 자산조회·주문 권한만 ON, 출금 OFF, IP 화이트리스트 등록 권장.
Q. 실거래 전 안전한 테스트 방법은?
업비트는 공식 테스트넷이 없습니다. (1) 페이퍼 트레이딩 1주일 (2) 과거 데이터 백테스트 (3) 5,000원 수준 실거래 1주일 순서로.
Q. 봇을 24시간 돌리려면?
VPS(Vultr, AWS Lightsail, Oracle Free Tier)에서 systemd / PM2로 실행. 변동성 돌파는 일봉 기준이라 1분 주기 체크로 충분합니다.
Q. 바이낸스 봇과 코드 구조가 같나요?
구조는 비슷하지만 인증(JWT vs HMAC), 주문 파라미터, 마켓 체계가 다릅니다. 바이낸스 버전은 → 바이낸스 자동매매 봇 30분 만들기 참고.
업비트 봇 맞춤 제작
위 골격에 손절 규칙 / 다종목 분산 / WebSocket / 텔레그램 알림 / VPS 무중단 운영까지 포함한 통합 봇을 알고랩이 만들어드립니다.
24시간 빠른 답변 가능합니다.