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

주문가능 잔고조회 - 파이썬 업비트 비트코인 자동매매

Tech&Fin 2021. 6. 11. 08:37
반응형

이번 시간에는 매도 로직에 사용할 수 있는 주문가능 잔고를 조회하는 기능을 살펴 보겠습니다.

 

파이썬을 이용해서 비트코인 자동매매 프로그램을 만들때 공통으로 사용하는 기능은 공통 모듈에 작성하면 필요할 때 간편하게 사용할 수 있습니다. 공통 모듈을 만드는 방법은 아래 포스트를 참고 부탁 드립니다.

 

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

 

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

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

technfin.tistory.com

 

 

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

     

    잔고 조회

    프로그램을 이용해서 매도를 하려면 보유하고 있는 종목의 수량을 알고 있어야 합니다.

     

    업비트에는 원화로 거래할 수 있는 KRW마켓, 비트코인(BTC)로 거래할 수 있는 BTC마켓 그리고 USDT로 거래할 수 있는 마켓이 있는데요. 잔고의 개념은 마켓과는 무관합니다. 

     

    예를들어 원화 마켓에서 도지코인(DOGE)을 구매하려면 KRW-DOGE라는 티커 코드를 사용하게 되고 BTC 마켓에서 구매하려면 BTC-DOGE라는 티커 코드를 사용합니다.

     

    원화 마켓에서 100 DOGE를 구매하고 BTC마켓에서 100 DOGE를 구매했다고 가정하는 경우 잔고를 조회하면 마켓 구분없이 200 DOGE가 조회 됩니다. 잔고 조회 시 코드는 앞에 마켓이 없이 그냥 DOGE로 조회 됩니다.

     

    그래서 잔고를 조회하려면 티커에서 마켓코드를 빼고 조회해야 합니다.

     

    잔고 조회 로직

    # -----------------------------------------------------------------------------
    # - Name : get_balance
    # - Desc : 주문가능 잔고 조회
    # - Input
    #   1) target_item : 대상 종목
    # - Output
    #   2) rtn_balance : 주문가능 잔고
    # -----------------------------------------------------------------------------
    def get_balance(target_item):
        try:
    
            # 주문가능 잔고 리턴용
            rtn_balance = 0
    
            # 최대 재시도 횟수
            max_cnt = 0
    
            # 잔고가 조회 될 때까지 반복
            while True:
    
                # 조회 회수 증가
                max_cnt = max_cnt + 1
    
                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)
                my_asset = res.json()
    
                # 해당 종목에 대한 잔고 조회
                # 잔고는 마켓에 상관없이 전체 잔고가 조회됨
                for myasset_for in my_asset:
                    if myasset_for['currency'] == target_item.split('-')[1]:
                        rtn_balance = myasset_for['balance']
    
                # 잔고가 0 이상일때까지 반복
                if Decimal(str(rtn_balance)) > Decimal(str(0)):
                    break
    
                # 최대 100회 수행
                if max_cnt > 100:
                    break
    
                logging.info("[주문가능 잔고 리턴용] 요청 재처리중...")
    
            return rtn_balance
    
        # ----------------------------------------
        # Exception Raise
        # ----------------------------------------
        except Exception:
            raise

    종목코드를 조회하거나 매수/매도를 하기 위해서는 KRW-DOGE와 같이 앞에 마켓 코드를 붙여야 하기 때문에 잔고조회 로직에도 마켓 코드를 붙인 종목코드를 입력 받도록 했습니다.

     

    잔고를 조회한 후에는 target_item.split('-')[1]을 이용하여 마켓 코드를 제거하고 비교하여 원하는 종목의 잔고만 리턴하게 됩니다.

     

    간혹 매수를 하자마자 잔고를 조회하면 반영 시간이 모자라 잔고가 조회되지 않는 경우가 있어 잔고가 조회 될 때까지 반복하는 로직이 추가되어 있습니다.

     

    하지만 무한 루프에 빠질 수 있기 때문에 총 조회는 100번까지만 수행하도록 했습니다.

     

    최종 코드

    공통모듈(upbit.py)

    import time
    import logging
    import requests
    import jwt
    import uuid
    import hashlib
    
    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_balance
    # - Desc : 주문가능 잔고 조회
    # - Input
    #   1) target_item : 대상 종목
    # - Output
    #   2) rtn_balance : 주문가능 잔고
    # -----------------------------------------------------------------------------
    def get_balance(target_item):
        try:
    
            # 주문가능 잔고 리턴용
            rtn_balance = 0
    
            # 최대 재시도 횟수
            max_cnt = 0
    
            # 잔고가 조회 될 때까지 반복
            while True:
    
                # 조회 회수 증가
                max_cnt = max_cnt + 1
    
                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)
                my_asset = res.json()
    
                # 해당 종목에 대한 잔고 조회
                # 잔고는 마켓에 상관없이 전체 잔고가 조회됨
                for myasset_for in my_asset:
                    if myasset_for['currency'] == target_item.split('-')[1]:
                        rtn_balance = myasset_for['balance']
    
                # 잔고가 0 이상일때까지 반복
                if Decimal(str(rtn_balance)) > Decimal(str(0)):
                    break
    
                # 최대 100회 수행
                if max_cnt > 100:
                    break
    
                logging.info("[주문가능 잔고 리턴용] 요청 재처리중...")
    
            return rtn_balance
    
        # ----------------------------------------
        # Exception Raise
        # ----------------------------------------
        except Exception:
            raise

     

    모듈 호출하기

    import sys
    import logging
    import traceback
    import time
    
    from module import upbit
    from decimal import Decimal
    
    # -----------------------------------------------------------------------------
    # - Name : main
    # - Desc : 메인
    # -----------------------------------------------------------------------------
    if __name__ == '__main__':
    
        # noinspection PyBroadException
        try:
            # 로그레벨 설정(DEBUG)
            upbit.set_loglevel('I')
            
            # 주문가능 잔고 조회
            balance = upbit.get_balance('KRW-DOGE')
            
            # 잔고 출력
            logging.info(balance)
            
        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)        

     

    실행 결과

    프로그램을 수행하면 잔고가 조회 됩니다.

    반응형