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

지정가 매도 로직 - 파이썬 업비트 비트코인 자동매매

Tech&Fin 2021. 6. 17. 01:23
반응형

이번 시간에는 가격을 지정하여 매도하는 지정가 매도 로직에 대해서 살펴 보겠습니다.

 

관련된 로직으로 시장가 매수 및 매도 로직은 아래 포스트를 참고 부탁 드리겠습니다.

 

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

 

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

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

technfin.tistory.com

 

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

 

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

지난 시간에 이어 이번에는 시장가 매도 로직에 대해서 살펴 보겠습니다. 공통모듈 만들기 및 시장가 매수 로직 관련은 아래 포스트를 참고 부탁 드립니다. 2021.06.06 - [프로젝트/비트코인 자동매

technfin.tistory.com

 

 

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

     

    지정가 매도

    지정가 매도는 가격을 정하여 매도하는 방식으로 목표한 가격에 매도를 걸어두면 해당 가격 도달 시 매도가 되는 방식 입니다.

     

    하지만 해당 가격에 도달했다고 무조건 매도가 되는 것은 아니고 이미 같은 가격에 다른 사람이 매도를 걸어 두었다면 이전에 걸려 있는 수량이 모두 체결된 후에 우리가 걸어둔 수량이 체결되기 시작합니다.

     

    예를들어 150원에 매수한 도지코인이 100개있다고 가정하고 목표가를 170원으로 잡고 매도를 걸려고 할 때 이미 500개의 수량이 170원에 매도가 걸려 있었다면 170원에 매도를 걸면 호가창의 170원에 500+100 = 600개의 매도 수량이 걸리게 되고 먼저 걸려있는 500개의 수량이 매도된 후에 자신이 걸어둔 수량이 매도처리 되게 됩니다. 이 때 170원의 매수세가 550개였다면 우리가 걸어둔 100개중에 50개만 팔리게 됩니다.

     

    시장가 매도는 현재 형성된 시세에 따라 매도 가격이 달라지기 때문에 원하는 목표가가 정해져 있다면 시장가 매도보다는 지정가 매도를 하는 것이 좋습니다.

     

    지정가 매도 로직

    지정가 매도 로직 역시 공통 모듈에 작성하고 원하는 곳에서 편하게 호출하여 사용하는 것이 좋습니다.

     

    # -----------------------------------------------------------------------------
    # - Name : sellcoin_tg
    # - Desc : 지정가 매도
    # - Input
    #   1) target_item : 대상종목
    #   2) sell_price : 매도희망금액
    # - Output
    #   1) rtn_data : 매도결과
    # -----------------------------------------------------------------------------
    def sellcoin_tg(target_item, sell_price):
        try:
    
            # 잔고 조회
            cur_balance = get_balance(target_item)
    
            query = {
                'market': target_item,
                'side': 'ask',
                'volume': cur_balance,
                'price': sell_price,
                'ord_type': 'limit',
            }
    
            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

    지정가 매도 로직은 아래 변수를 입력하여 함수를 호출하면 됩니다.

     

    ① target_item : 대상종목
    ② sell_price : 매도희망금액

     

    시장가 매도와 마찬가지로 보유 잔고는 잔고 조회 로직을 이용해서 자동으로 가져오며 전량 매도처리 하도록 되어 있습니다.

     

    함수 호출 방법

    sellcoin_tg("KRW-DOGE", '170')

    원하는 곳에서 종목코드(마켓코드 포함)와 원하는 매도가격을 입력하면 지정가 매도 처리가 됩니다.

     

    최종 코드

    공통모듈(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
    
    
    # -----------------------------------------------------------------------------
    # - Name : sellcoin_tg
    # - Desc : 지정가 매도
    # - Input
    #   1) target_item : 대상종목
    #   2) sell_price : 매도희망금액
    # - Output
    #   1) rtn_data : 매도결과
    # -----------------------------------------------------------------------------
    def sellcoin_tg(target_item, sell_price):
        try:
    
            # 잔고 조회
            cur_balance = get_balance(target_item)
    
            query = {
                'market': target_item,
                'side': 'ask',
                'volume': cur_balance,
                'price': sell_price,
                'ord_type': 'limit',
            }
    
            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

     

    모듈 호출 예시

    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')
            
            # 지정가 매도
            result = upbit.sellcoin_tg('KRW-DOGE', '170')
            
            # 매도 결과 출력
            logging.info(result)
            
        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)

     

    활용 방법

    업비트에는 예약 매도라는 기능이 있는데요. 예약 매도는 당장 매수를 걸어두지 않고 가격을 감시하다 알림을 걸어둔 가격에 도달할 시 원하는 가격에 매도를 걸어주는 기능인데 해당 기능을 이용하게 되면 일반 거래보다 높은 수수료를 내야 합니다.

     

    지정가 매도 또는 시장가 매도를 잘 활용하게 되면 실시간으로 가격을 감시하다 원하는 가격에 도달하게 되면 매도로직을 호출하여 업비트의 예약 매도와 같은 기능을 구현할 수 있습니다. 로직을 직접 짜게 되면 수수료는 일반 거래와 동일하게 0.05%가 되기 때문에 수수료 이익을 볼 수 있습니다.

     

    참고사항

    손으로 매매를 하는 경우 지정가 매도를 할 때 1000원에 팔아야 하는데 0을 하나 빼먹고 100을 입력하는 경우와 같이 실수로 시장 가격보다 낮게 매도를 거는 경우가 발생할 수도 있는데요. 이렇게 지정가 매도를 할 때 시장가 보다 낮게 설정하면 어떻게 될까요?

     

    이런 경우 100원에 매도가 되는 것이 아니라 시장가격에 매도가 되기 때문에 크게 걱정하실 필요는 없습니다.

     

    예를들어 현재 150원에 매수세가 있고 155원에 매도세가 있다고 가정할 때 실수로 지정가 매도를 130원에 걸게 되면 130원에 매도가 되는 것이 아니라 현재 시장 매수가격인 150원에 매도 처리가 됩니다.

     

    프로그램을 통해 매매를 하는 경우에는 손으로 매도가를 입력하지 않기 때문에 실수를 줄일 수 있으며 또는 위와 같은 로직을 활용하여 급등하는 코인에서는 지정가 매도에서 시장가 매도의 기능을 일부러 활용할 수도 있습니다.

    반응형