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

업비트 자동매매 봇 30분 안에 만들기 — Python 변동성 돌파 풀코드

업비트 2026-05-21 · 약 12분 읽기 / 실습 30분 · 알고랩 AlgoLab
한 줄 요약 이 글의 코드를 그대로 복붙하면 업비트에서 30분 안에 작동하는 자동매매 봇을 만들 수 있습니다. 한국 코인 자동매매의 클래식인 변동성 돌파 전략 / 백테스트 / 페이퍼 트레이딩 / 실거래 전환을 한 파일에 담았습니다. 단, 이 전략 자체가 수익을 보장하지는 않습니다 — 코드 구조를 익히는 학습용입니다.

"업비트 자동매매" 검색 결과에는 유료 프로그램 광고가 많지만, 코드를 끝까지 따라가면 봇이 실제로 도는 글은 의외로 적습니다. 이 글은 알고랩이 60건+ 업비트 봇을 제작하며 만든 가장 기본적인 봇 골격을 30분 안에 따라할 수 있게 압축했습니다.

전제는 업비트 Open API 키가 발급되어 있다는 것입니다. 마이페이지 → Open API 관리에서 발급하며, 자산조회·주문 권한만 ON, 출금은 반드시 OFF, IP 화이트리스트 등록을 권장합니다. API 인프라를 더 깊게 알고 싶다면 → 업비트 자동매매 REST·WebSocket 실전 아키텍처를 함께 보세요.

30분 안에 만들 것

  1. 1단계 — 환경 세팅 + JWT 인증 이해 (5분)
  2. 2단계 — 시세(캔들) 받기 (5분)
  3. 3단계 — 변동성 돌파 전략 로직 (5분)
  4. 4단계 — 페이퍼 트레이딩 모드 (5분)
  5. 5단계 — 백테스트로 전략 검증 (5분)
  6. 6단계 — 실거래 전환 + 안전장치 (5분)
  7. 통합 풀코드 (한 번에 복붙)
  8. 자주 실수하는 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)는 래리 윌리엄스가 고안한 전략으로, 한국 코인 자동매매에서 가장 널리 쓰이는 클래식입니다. 원리는 단순합니다:

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

✅ 실거래 전환 체크리스트

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

위 단계를 모두 합친 단일 파일입니다. 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 아키텍처 참고.

다음 단계 — 진짜 운용 가능한 봇으로

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

이 모든 것을 직접 구현하는 게 부담스럽다면, 알고랩이 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시간 빠른 답변 가능합니다.

무료 상담 시작하기