프로젝트/비트코인 자동매매

올라운더를 위한 업비트 모든 종목 매수 프로그램 - 파이썬 업비트 비트코인 자동매매

Tech&Fin 2021. 7. 17. 16:40
반응형

예전에 비트코인 관련 카페에서 업비트 원화마켓에서 거래되는 모든 종목을 매수하고 매일마다 수익률을 카페에 공유하던 분이 계셨는데요.

 

그 분은 이런 매수 방법을 올라운더라고 불렀던 것으로 기억하며 그 때 당시에는 장이 좋았던 터라 수익률도 상당히 높았던 것으로 기억 합니다.

 

하지만 업비트에서 거래되는 코인 갯수는 2021년 7월 17일 포스팅하는 날 기준 154개나 되며 이 중 KRW 원화 마켓에서 거래되는 코인의 개수는 102개 입니다.

 

하나씩 손으로 매수하기에는 너무 힘들고 시간도 오래 걸리겠지만 우리는 자동매매 프로그램을 만들 수 있기 때문에 클릭 한번에 전 종목을 1~2분내로 매수할 수 있습니다.

 

오늘은 그 동안 부분적으로 설명 드렸던 로직들 중 몇 가지를 이용하여 전 종목을 매수하는 올라운더 매수 프로그램을 작성해 보도록 하겠습니다.

 

자동매매 프로그램 및 올라운더 매수 방법은 수익을 보장하는 프로그램이 아니며 시장 상황에 따라 상당히 큰 손실을 보게 되실 수도 있으니 가급적 처음에는 연습용으로 시도해 보시기 바랍니다.

본 블로그는 가상화폐 매수/매도에 관련하여 일체 관여하지 않으며 모든 책임은 본인에게 있음을 안내 드립니다.

 

 

목차 - 클릭하면 이동합니다.

     

    올라운더 매수 프로그램 작성 플랜

    종목 리스트 조회

    전 종목을 매수하려면 먼저 종목 리스트를 가져와야 합니다. 이전에 다루었던 시세 종목 조회 하기를 이용하면 거래되는 모든 종목을 가져올 수 있습니다.

     

    2021.06.06 - [프로젝트/비트코인 자동매매] - 비트코인 자동매매 - 시세 종목 조회 하기

     

    비트코인 자동매매 - 시세 종목 조회 하기

    이번 시간에는 비트코인 자동매매 연재의 시작으로 업비트 API를 이용한 간단한 종목 조회하기 프로그램을 만들어 보겠습니다. 목차 - 클릭하면 이동합니다. 목표 업비트API를 이용한

    technfin.tistory.com

     

    매수 금액 계산

    종목 리스트를 가져왔으면 종목을 하나씩 매수 해야 하는데 이 때 얼마를 매수해야 할지 계산해야 합니다. 금액은 직접 입력하는 방법으로 하셔도 되며 KRW 잔고를 조회하여 진행할 수도 있습니다.

     

    참고로 업비트에서 최소 매수 금액은 종목당 5,000원이기 때문에 종목 154개를 구매하려면 최소 77만원 이상의 비용(수수료 고려 시 78만원 정도)이 소요 됩니다.

     

    KRW 잔고를 조회하여 수수료를 제외한 가용 금액을 계산하는 로직은 아래 포스트를 참고하시면 좋습니다.

     

    2021.07.07 - [프로젝트/비트코인 자동매매] - KRW 원화 잔고 조회 - 파이썬 업비트 비트코인 자동매매

     

    KRW 원화 잔고 조회 - 파이썬 업비트 비트코인 자동매매

    종목을 매수할 때 원하는 금액을 입력하여 매수할 수도 있지만 현재 보유하고 있는 원화 잔고 전부를 사용하여 매수를 하고자 할 때도 있습니다. 이번 시간에는 현재 KRW 잔고 정보를 조회하여

    technfin.tistory.com

     

    시장가 매수

    종목 리스트를 가져왔고 얼마를 매수할지를 계산했다면 이제 한 종목씩 매수를 진행하면 됩니다. 지정가 매수는 체결이 안될 가능성이 있기 때문에 시장가 매수를 진행하는 것으로 하겠습니다.

     

    시장가 매수 로직은 아래 포스트를 참고하시면 됩니다.

     

    2021.06.09 - [프로젝트/비트코인 자동매매] - 파이썬 업비트 비트코인 자동매매 - 시장가 매수 로직

     

    파이썬 업비트 비트코인 자동매매 - 시장가 매수 로직

    지난 시간에 공통적인 모듈을 이용해서 전체적인 프로젝트 구조를 만드는 방법에 대해서 살펴 보았는데요. 앞으로 공통 모듈에 여러가지 기능들을 심어갈 예정입니다. 이번 시간에는 시장가 매

    technfin.tistory.com

     

    전체 코드

    공통 코드

    import time
    import logging
    import requests
    import jwt
    import uuid
    import hashlib
    import math
    import os
    
    from urllib.parse import urlencode
    from decimal import Decimal
     
    # Keys
    access_key = '업비트에서 발급받은 Access Key'
    secret_key = '업비트에서 발급받은 Secret Key'
    server_url = 'https://api.upbit.com'
     
    # -----------------------------------------------------------------------------
    # - Name : set_loglevel
    # - Desc : 로그레벨 설정
    # - Input
    #   1) level : 로그레벨
    #     1. D(d) : DEBUG
    #     2. E(e) : ERROR
    #     3. 그외(기본) : INFO
    # - Output
    # -----------------------------------------------------------------------------
    def set_loglevel(level):
        try:
     
            # ---------------------------------------------------------------------
            # 로그레벨 : DEBUG
            # ---------------------------------------------------------------------
            if level.upper() == "D":
                logging.basicConfig(
                    format='[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d]:%(message)s',
                    datefmt='%Y/%m/%d %I:%M:%S %p',
                    level=logging.DEBUG
                )
            # ---------------------------------------------------------------------
            # 로그레벨 : ERROR
            # ---------------------------------------------------------------------
            elif level.upper() == "E":
                logging.basicConfig(
                    format='[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d]:%(message)s',
                    datefmt='%Y/%m/%d %I:%M:%S %p',
                    level=logging.ERROR
                )
            # ---------------------------------------------------------------------
            # 로그레벨 : INFO
            # ---------------------------------------------------------------------
            else:
                # -----------------------------------------------------------------------------
                # 로깅 설정
                # 로그레벨(DEBUG, INFO, WARNING, ERROR, CRITICAL)
                # -----------------------------------------------------------------------------
                logging.basicConfig(
                    format='[%(asctime)s][%(levelname)s][%(filename)s:%(lineno)d]:%(message)s',
                    datefmt='%Y/%m/%d %I:%M:%S %p',
                    level=logging.INFO
                )
     
        # ----------------------------------------
        # Exception Raise
        # ----------------------------------------
        except Exception:
            raise
            
     
    # -----------------------------------------------------------------------------
    # - Name : send_request
    # - Desc : 리퀘스트 처리
    # - Input
    #   1) reqType : 요청 타입
    #   2) reqUrl : 요청 URL
    #   3) reqParam : 요청 파라메타
    #   4) reqHeader : 요청 헤더
    # - Output
    #   4) reponse : 응답 데이터
    # -----------------------------------------------------------------------------
    def send_request(reqType, reqUrl, reqParam, reqHeader):
        try:
     
            # 요청 가능회수 확보를 위해 기다리는 시간(초)
            err_sleep_time = 0.3
     
            # 요청에 대한 응답을 받을 때까지 반복 수행
            while True:
     
                # 요청 처리
                response = requests.request(reqType, reqUrl, params=reqParam, headers=reqHeader)
     
                # 요청 가능회수 추출
                if 'Remaining-Req' in response.headers:
     
                    hearder_info = response.headers['Remaining-Req']
                    start_idx = hearder_info.find("sec=")
                    end_idx = len(hearder_info)
                    remain_sec = hearder_info[int(start_idx):int(end_idx)].replace('sec=', '')
                else:
                    logging.error("헤더 정보 이상")
                    logging.error(response.headers)
                    break
     
                # 요청 가능회수가 3개 미만이면 요청 가능회수 확보를 위해 일정시간 대기
                if int(remain_sec) < 3:
                    logging.debug("요청 가능회수 한도 도달! 남은횟수:" + str(remain_sec))
                    time.sleep(err_sleep_time)
     
                # 정상 응답
                if response.status_code == 200 or response.status_code == 201:
                    break
                # 요청 가능회수 초과인 경우
                elif response.status_code == 429:
                    logging.error("요청 가능회수 초과!:" + str(response.status_code))
                    time.sleep(err_sleep_time)
                # 그 외 오류
                else:
                    logging.error("기타 에러:" + str(response.status_code))
                    logging.error(response.status_code)
                    break
     
                # 요청 가능회수 초과 에러 발생시에는 다시 요청
                logging.info("[restRequest] 요청 재처리중...")
     
            return response
     
        # ----------------------------------------
        # Exception Raise
        # ----------------------------------------
        except Exception:
            raise
            
            
    # -----------------------------------------------------------------------------
    # - Name : get_items
    # - Desc : 전체 종목 리스트 조회
    # - Input
    #   1) market : 대상 마켓(콤마 구분자:KRW,BTC,USDT)
    #   2) except_item : 제외 종목(콤마 구분자:BTC,ETH)
    # - Output
    #   1) 전체 리스트 : 리스트
    # -----------------------------------------------------------------------------
    def get_items(market, except_item):
        try:
    
            # 조회결과 리턴용
            rtn_list = []
    
            # 마켓 데이터
            markets = market.split(',')
    
            # 제외 데이터
            except_items = except_item.split(',')
    
            url = "https://api.upbit.com/v1/market/all"
            querystring = {"isDetails": "false"}
            response = send_request("GET", url, querystring, "")
            data = response.json()
    
            # 조회 마켓만 추출
            for data_for in data:
                for market_for in markets:
                    if data_for['market'].split('-')[0] == market_for:
                        rtn_list.append(data_for)
    
            # 제외 종목 제거
            for rtnlist_for in rtn_list[:]:
                for exceptItemFor in except_items:
                    for marketFor in markets:
                        if rtnlist_for['market'] == marketFor + '-' + exceptItemFor:
                            rtn_list.remove(rtnlist_for)
    
            return rtn_list
    
        # ----------------------------------------
        # Exception Raise
        # ----------------------------------------
        except Exception:
            raise        
            
            
    # -----------------------------------------------------------------------------
    # - Name : buycoin_mp
    # - Desc : 시장가 매수
    # - Input
    #   1) target_item : 대상종목
    #   2) buy_amount : 매수금액
    # - Output
    #   1) rtn_data : 매수결과
    # -----------------------------------------------------------------------------
    def buycoin_mp(target_item, buy_amount):
        try:
    
            query = {
                'market': target_item,
                'side': 'bid',
                'price': buy_amount,
                'ord_type': 'price',
            }
    
            query_string = urlencode(query).encode()
    
            m = hashlib.sha512()
            m.update(query_string)
            query_hash = m.hexdigest()
    
            payload = {
                'access_key': access_key,
                'nonce': str(uuid.uuid4()),
                'query_hash': query_hash,
                'query_hash_alg': 'SHA512',
            }
    
            jwt_token = jwt.encode(payload, secret_key)
            authorize_token = 'Bearer {}'.format(jwt_token)
            headers = {"Authorization": authorize_token}
    
            res = send_request("POST", server_url + "/v1/orders", query, headers)
            rtn_data = res.json()
    
            logging.info("")
            logging.info("----------------------------------------------")
            logging.info("시장가 매수 완료!")
            logging.info(rtn_data)
            logging.info("----------------------------------------------")
    
            return rtn_data
    
        # ----------------------------------------
        # Exception Raise
        # ----------------------------------------
        except Exception:
            raise        
           
           
    # -----------------------------------------------------------------------------
    # - Name : get_krwbal
    # - Desc : KRW 잔고 조회
    # - Input
    # - Output
    #   1) KRW 잔고 Dictionary
    #     1. krw_balance : KRW 잔고
    #     2. fee : 수수료
    #     3. available_krw : 매수가능 KRW잔고(수수료를 고려한 금액)
    # -----------------------------------------------------------------------------
    def get_krwbal():
        try:
    
            # 잔고 리턴용
            rtn_balance = {}
    
            # 수수료 0.05%(업비트 기준)
            fee_rate = 0.05
    
            payload = {
                'access_key': access_key,
                'nonce': str(uuid.uuid4()),
            }
    
            jwt_token = jwt.encode(payload, secret_key)
            authorize_token = 'Bearer {}'.format(jwt_token)
            headers = {"Authorization": authorize_token}
    
            res = send_request("GET", server_url + "/v1/accounts", "", headers)
    
            data = res.json()
    
            for dataFor in data:
                if (dataFor['currency']) == "KRW":
                    krw_balance = math.floor(Decimal(str(dataFor['balance'])))
    
            # 잔고가 있는 경우만
            if Decimal(str(krw_balance)) > Decimal(str(0)):
                # 수수료
                fee = math.ceil(Decimal(str(krw_balance)) * (Decimal(str(fee_rate)) / Decimal(str(100))))
    
                # 매수가능금액
                available_krw = math.floor(Decimal(str(krw_balance)) - Decimal(str(fee)))
    
            else:
                # 수수료
                fee = 0
    
                # 매수가능금액
                available_krw = 0
    
            # 결과 조립
            rtn_balance['krw_balance'] = krw_balance
            rtn_balance['fee'] = fee
            rtn_balance['available_krw'] = available_krw
    
            return rtn_balance
    
        # ----------------------------------------
        # Exception Raise
        # ----------------------------------------
        except Exception:
            raise

    14~15 라인에 업비트에서 발급받은 Access Key와 Secret Key를 넣어 줍니다. 업비트 Key 발급 방법은 아래 포스트를 참고하시면 됩니다.

     

    2021.06.09 - [프로젝트/비트코인 자동매매] - 업비트 자동매매 API KEY 발급받기

     

    업비트 자동매매 API KEY 발급받기

    이번 시간에는 업비트를 이용하여 코인 자동매매를 하기 위해 API KEY를 발급받는 방법에 대해서 살펴 보겠습니다. 앞으로 진행할 비트코인 자동매매 프로그램 관련에 대해서는 아래 포스트에서

    technfin.tistory.com

     

    공통모듈을 어떻게 어느 파일에 작성해야 하는지 모르는 분들은 아래의 '프로그램 구조 만들기' 포스트를 참고하시면 좋을 것 같습니다.

     

    2021.06.06 - [프로젝트/비트코인 자동매매] - 비트코인 자동매매 - 프로젝트 구조 만들기

     

    비트코인 자동매매 - 프로젝트 구조 만들기

    이번 시간에는 본격적으로 로직을 만들고 살을 붙이기 전에 프로젝트 구조를 만들어 보도록 하겠습니다. 비트코인 자동매매 프로그램을 만드는 것이 얼마나 간단한지는 아래 포

    technfin.tistory.com

     

    전 종목 매수 프로그램

    import os
    import sys
    import logging
    import math
    import traceback
    
    # 공통 모듈 Import
    sys.path.append(os.path.dirname(os.path.dirname(__file__)))
    from lib import upbit as upbit  # noqa
    
    # -----------------------------------------------------------------------------
    # - Name : main
    # - Desc : 메인
    # -----------------------------------------------------------------------------
    if __name__ == '__main__':
    
        # noinspection PyBroadException
        try:
    
            print("***** USAGE ******")
            print("[1] 로그레벨(D:DEBUG, E:ERROR, 그외:INFO)")
    
            # 로그레벨(D:DEBUG, E:ERROR, 그외:INFO)
            upbit.set_loglevel('I')
    
            # ---------------------------------------------------------------------
            # Logic Start!
            # ---------------------------------------------------------------------
            # 전 종목 리스트 조회
            item_list = upbit.get_items('KRW', '')
    
            logging.info(len(item_list))
            logging.info(item_list)
    
            # 원화 매수 가능 금액 조회
            krw_bal = upbit.get_krwbal()
    
            logging.info(krw_bal)
    
            # 종목별 매수 금액 설정(일정 금액 직접 입력)
            #item_buy_amt = 5000
    
            # 종목별 매수 금액 설정(KRW잔고 이용)
            item_buy_amt = math.floor(krw_bal['available_krw'] / len(item_list))
    
            logging.info(item_buy_amt)
    
            # 종목별 처리
            for item_list_for in item_list:
    
                logging.info('종목코드:' + item_list_for['market'])
    
                # 시장가 매수
                # ★ 실제 매수가 될 수 있어 아래 주석 처리함.
                # 정말로 매수를 원하면 아래 주석을 풀면 됩니다.
                #upbit.buycoin_mp(item_list_for['market'], item_buy_amt)
    
            logging.info('전 종목 매수 완료')        
    
        except KeyboardInterrupt:
            logging.error("KeyboardInterrupt Exception 발생!")
            logging.error(traceback.format_exc())
            sys.exit(1)
    
        except Exception:
            logging.error("Exception 발생!")
            logging.error(traceback.format_exc())
            sys.exit(1)

    ① 종목 리스트 조회

    item_list = upbit.get_items('KRW', '')

     

    먼저 전 종목 리스트 조회 합니다. 첫번째 변수에는 원하는 마켓을 입력하면 되기 때문에 원화 마켓코드인 KRW를 변수로 넘기고 두번째 변수는 만약 매수하고 싶지 않은 종목이 있다면 콤마(,) 구분자로 넣어주면 해당 종목은 제외하고 결과를 만들어 냅니다.

     

    item_list = upbit.get_items('KRW', 'BTC,ETH')

     

    위와 같이 조회하게 되면 비트코인과 이더리움을 제외한 종목을 조회할 수 있습니다.

     

    ② KRW 잔고 조회

    krw_bal = upbit.get_krwbal()

     

    종목별 매수하기 위한 금액은 직접 입력해서 사용해도 되고 현재 KRW 잔고를 이용해서 매수할 수도 있습니다. 

     

    item_buy_amt = math.floor(krw_bal['available_krw'] / len(item_list))


    현재 잔고를 이용해서 매수하려면 수수료를 제외한 매수 가능 KRW 금액을 종목수로 나누어서 종목별 매수 금액을 구하면 됩니다.

     

    ③ 종목별 시장가 매수 처리
    for item_list_for in item_list:
        logging.info('종목코드:' + item_list_for['market'])

        # 시장가 매수
        upbit.buycoin_mp(item_list_for['market'], item_buy_amt)

    종목 리스트를 for 문을 이용하여 반복 처리 하면서 종목 코드를 이용하여 시장가 매수를 진행합니다. 이 때 앞서 구하거나 입력한 종목별 매수 금액을 입력하여 매수 처리 합니다.

     

    생각보다 간단히 만들 수 있었네요. 다음 시간에는 올라운더 매도 프로그램을 한번 작성해 보도록 하겠습니다.

     

    자동매매 프로그램은 수익을 보장하는 프로그램이 아닙니다. 게다가 실수로 코딩을 하는 경우는 상당한 손실을 초래할 수 있으니 프로그램 실행전에 반드시 많은 검토를 진행하시기 바라며 중요 매수/매도 로직은 주석처리한 채로 이전 로직이 잘 작동하는지 확인 후 최종 실행하는 것이 좋습니다.

    본 블로그는 가상화폐 매매에 전혀 관여하지 않습니다. 모든 투자 및 실행의 책임은 본인 스스로에게 있습니다.
    반응형