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

MACD 지표 구하기 - 파이썬 업비트 비트코인 자동매매

Tech&Fin 2021. 8. 3. 13:21
반응형

MACD는 보조 지표중에서 장기 및 단기 이동평균선의 차이를 이용하여 매매 시점을 포착하는데 유용하게 사용되는 지표중에 하나 입니다.

 

차트를 보면 거의 대부분 단기 이동평균선과 장기 이동평균선이 멀어졌다 가까워졌다를 반복하게 되는데 이를 이용하여 코인의 흐름과 추세를 판단하고 두 개의 이동평균선이 교차하는 지점을 이용하여 매수 및 매도의 신호로 활용할 수 있습니다.

 

MACD(Moving Average Convergence & Divergence, 이동평균 수렴확산지수)는 상당히 기본적인 지표중의 하나로 신뢰도가 높긴 하지만 MACD만으로는 매매 신호의 정확도를 높이기는 어렵습니다. 그래서 다른 보조지표인 RSI 또는 MFI 지표등과 혼합하여 전략을 세우는 것이 좋습니다.

 

이번 시간에는 업비트API 및 파이썬을 이용하여 MACD 지표값을 구하는 방법에 대해서 살펴 보도록 하겠습니다.

 

RSI 상대강도지수를 구하는 방법은 아래 링크를 이용하여 이전 포스트에서 확인하실 수 있습니다.

 

2021.07.27 - [프로젝트/비트코인 자동매매] - RSI 상대강도지수 구하기 - 파이썬 업비트 비트코인 자동매매

 

RSI 상대강도지수 구하기 - 파이썬 업비트 비트코인 자동매매

주식과는 마찬가지로 코인 시장에서도 차트를 이용한 매매 기법을 사용하실 수 있습니다. 그 중에서도 RSI(상대강도지수)는 상당히 많이 사용되는 보조지표중에 하나 입니다. 앞으로 업비트 API

technfin.tistory.com

 

MFI 자금흐름지수를 구하는 방법은 아래 포스트에서 확인하실 수 있습니다.

 

2021.07.29 - [프로젝트/비트코인 자동매매] - MFI 자금흐름지수 구하기 - 파이썬 업비트 비트코인 자동매매

 

MFI 자금흐름지수 구하기 - 파이썬 업비트 비트코인 자동매매

지난 시간에 업비트API와 파이썬을 사용하여 과매수/과매도 구간을 확인하는데 유용한 지표인 RSI 보조지표를 구하는 방법에 대해서 살펴 보았습니다. 오늘은 가격 상승과 하락 변화량에 대한 상

technfin.tistory.com

 

 

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

     

    MACD 보조지표

    MACD는 앞서 설명드린바와 같이 장/단기 이동평균선을 이용하여 매매신호를 포착하는데 도움을 주는 보조지표이며 장기 및 단기 그리고 시그널에 사용되는 이동평균선은 아래와 같습니다.

     

    ① 장기 이동평균선 : 26일 이동평균선

    ② 단기 이동평균선 : 12일 이동평균선

    ③ 시그널 : 9일 이동평균선

     

    관련된 값을 구하는 공식은 아래와 같습니다.

     

    ① MACD = 단기이동평균선(12일선) - 장기이동평균선(26일선)

    ② 오실레이터 = MACD값 - 시그널값

     

    MACD 지표의 일반적인 해석

    ① MACD > 0 | 단기적으로 상승한 과열 구간으로 판단

    ② MACD < 0 | 단기적으로 하락한 침체 구간으로 판단

    ③ MACD = 0 | 장/단기 이동평균선이 교차 돌파하는 구간으로 골든 크로스 혹은 데드 크로스가 생기는 지점

    일반적으로 MACD 값이 0선 위에서 시그널 선을 상향 돌파하면 급등이 나오는 경우가 많고 MACD선이 시그널 선을 돌파하지 못하고 0선 아래로 떨어지게 되면 급락이 자주 출현합니다. 하지만 항상 그런 것은 아니오니 참고하며 실제 매매 로직에 적용하기 전에 가격의 흐름을 모니터링 하시는 것이 좋습니다.

     

    MACD 지표를 활용한 매매신호 포착 방법

    MACD 지표는 아래와 같이 활용할 수 있지만 항상 맞지는 않기 때문에 다른 보조지표와 함께 전략을 세워야 하며 실전 매매에서는 오실레이터 값을 이용하는 것이 일반적입니다.

     

    ① 오실레이터가 0선 위에서 양봉으로 이어지며 계속해서 값이 증가하다가 양봉이 줄어들기 시작하면 시세하락으로 판단하여 매도

    ② 오실레이터가 0선 아래에서 음봉으로 계속 값이 내려가다가 음봉이 줄어들기 시작하면 상승 전환으로 판단하여 매수

     

    업비트 API를 이용한 파이썬 코드

    공통코드

    import logging
    import requests
    import time
    import smtplib
    import jwt
    import sys
    import uuid
    import hashlib
    import math
    import numpy
    import pandas as pd
    from datetime import datetime, timedelta
    from decimal import Decimal
    from urllib.parse import urlencode
     
    # 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_candle
    # - Desc : 캔들 조회
    # - Input
    #   1) target_item : 대상 종목
    #   2) tick_kind : 캔들 종류 (1, 3, 5, 10, 15, 30, 60, 240 - 분, D-일, W-주, M-월)
    #   3) inq_range : 조회 범위
    # - Output
    #   1) 캔들 정보 배열
    # -----------------------------------------------------------------------------
    def get_candle(target_item, tick_kind, inq_range):
        try:
     
            # ----------------------------------------
            # Tick 별 호출 URL 설정
            # ----------------------------------------
            # 분붕
            if tick_kind == "1" or tick_kind == "3" or tick_kind == "5" or tick_kind == "10" or tick_kind == "15" or tick_kind == "30" or tick_kind == "60" or tick_kind == "240":
                target_url = "minutes/" + tick_kind
            # 일봉
            elif tick_kind == "D":
                target_url = "days"
            # 주봉
            elif tick_kind == "W":
                target_url = "weeks"
            # 월봉
            elif tick_kind == "M":
                target_url = "months"
            # 잘못된 입력
            else:
                raise Exception("잘못된 틱 종류:" + str(tick_kind))
     
            logging.debug(target_url)
     
            # ----------------------------------------
            # Tick 조회
            # ----------------------------------------
            querystring = {"market": target_item, "count": inq_range}
            res = send_request("GET", server_url + "/v1/candles/" + target_url, querystring, "")
            candle_data = res.json()
     
            logging.debug(candle_data)
     
            return candle_data
     
        # ----------------------------------------
        # Exception Raise
        # ----------------------------------------
        except Exception:
            raise
            
    
    # -----------------------------------------------------------------------------
    # - Name : get_macd
    # - Desc : MACD 조회
    # - Input
    #   1) target_item : 대상 종목
    #   2) tick_kind : 캔들 종류 (1, 3, 5, 10, 15, 30, 60, 240 - 분, D-일, W-주, M-월)
    #   3) inq_range : 캔들 조회 범위
    #   4) loop_cnt : 지표 반복계산 횟수
    # - Output
    #   1) MACD 값
    # -----------------------------------------------------------------------------
    def get_macd(target_item, tick_kind, inq_range, loop_cnt):
        try:
    
            # 캔들 데이터 조회용
            candle_datas = []
    
            # MACD 데이터 리턴용
            macd_list = []
    
            # 캔들 추출
            candle_data = get_candle(target_item, tick_kind, inq_range)
    
            # 조회 횟수별 candle 데이터 조합
            for i in range(0, int(loop_cnt)):
                candle_datas.append(candle_data[i:int(len(candle_data))])
    
            df = pd.DataFrame(candle_datas[0])
            df = df.iloc[::-1]
            df = df['trade_price']
    
            # MACD 계산
            exp1 = df.ewm(span=12, adjust=False).mean()
            exp2 = df.ewm(span=26, adjust=False).mean()
            macd = exp1 - exp2
            exp3 = macd.ewm(span=9, adjust=False).mean()
    
            for i in range(0, int(loop_cnt)):
                macd_list.append(
                    {"type": "MACD", "DT": candle_datas[0][i]['candle_date_time_kst'], "MACD": round(macd[i], 4), "SIGNAL": round(exp3[i], 4),
                     "OCL": round(macd[i] - exp3[i], 4)})
    
            return macd_list
    
        # ----------------------------------------
        # 모든 함수의 공통 부분(Exception 처리)
        # ----------------------------------------
        except Exception:
            raise

    MACD 지표 역시 다른 지표들과 마찬가지로 공통 모듈에 작성하고 필요할 때 호출하여 사용하는 것이 편리합니다.
     
    테크앤핀에서 진행하는 비트코인 자동매매 프로젝트에서는 upbit.py 라는 공통 모듈을 만들어서 대부분의 로직을 공통 모듈에 구현하고 전략 마다 짜는 프로그램들에서 공통적으로 사용하도록 하고 있습니다. 공통 모듈 구조를 잡는 방법은 아래 포스트를 참고하시면 도움이 될 것 같습니다.

     

    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!
            # ---------------------------------------------------------------------
            # MACD 조회
            macd_data = upbit.get_macd('KRW-BTC', '30', '200', 10)
    
            for macd_data_for in macd_data:
                logging.info(macd_data_for)
    
        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)

    macd_data = upbit.get_macd('KRW-BTC', '30', '200', 10)

     

    MACD 보조지표를 구하고 싶은 곳에서 위와 같이 호출하면 MACD의 데이터를 가져올 수 있습니다. 위의 호출 방법은 비트코인(KRW-BTC)의 30분봉 기준으로 총 10개의 MACD 데이터를 구하는 예시 입니다.

     

    호출 결과

    [2021/08/03 11:54:47 AM][INFO][test_module.py:33]:{'type': 'MACD', 'DT': '2021-08-03T11:30:00', 'MACD': -139395.8096, 'SIGNAL': -141178.9369, 'OCL': 1783.1273}
    [2021/08/03 11:54:47 AM][INFO][test_module.py:33]:{'type': 'MACD', 'DT': '2021-08-03T11:00:00', 'MACD': -143602.9705, 'SIGNAL': -141624.7187, 'OCL': -1978.2517}
    [2021/08/03 11:54:47 AM][INFO][test_module.py:33]:{'type': 'MACD', 'DT': '2021-08-03T10:30:00', 'MACD': -145764.0671, 'SIGNAL': -141130.1558, 'OCL': -4633.9113}
    [2021/08/03 11:54:47 AM][INFO][test_module.py:33]:{'type': 'MACD', 'DT': '2021-08-03T10:00:00', 'MACD': -125020.3896, 'SIGNAL': -139971.678, 'OCL': 14951.2884}
    [2021/08/03 11:54:47 AM][INFO][test_module.py:33]:{'type': 'MACD', 'DT': '2021-08-03T09:30:00', 'MACD': -126761.7991, 'SIGNAL': -143709.5001, 'OCL': 16947.701}
    [2021/08/03 11:54:47 AM][INFO][test_module.py:33]:{'type': 'MACD', 'DT': '2021-08-03T09:00:00', 'MACD': -142617.0265, 'SIGNAL': -147946.4254, 'OCL': 5329.3989}
    [2021/08/03 11:54:47 AM][INFO][test_module.py:33]:{'type': 'MACD', 'DT': '2021-08-03T08:30:00', 'MACD': -149274.1782, 'SIGNAL': -149278.7751, 'OCL': 4.5969}
    [2021/08/03 11:54:47 AM][INFO][test_module.py:33]:{'type': 'MACD', 'DT': '2021-08-03T08:00:00', 'MACD': -139003.5001, 'SIGNAL': -149279.9243, 'OCL': 10276.4242}
    [2021/08/03 11:54:47 AM][INFO][test_module.py:33]:{'type': 'MACD', 'DT': '2021-08-03T07:30:00', 'MACD': -143116.1473, 'SIGNAL': -151849.0304, 'OCL': 8732.883}
    [2021/08/03 11:54:47 AM][INFO][test_module.py:33]:{'type': 'MACD', 'DT': '2021-08-03T07:00:00', 'MACD': -160945.5095, 'SIGNAL': -154032.2511, 'OCL': -6913.2584}

     

    예시와 같이 호출하면 위와 같이 30분봉 기준으로 총 10개의 MACD 값을 가져올 수 있습니다.

     

    업비트에서 MACD 지표를 추가하고 확인하면 값을 비교해 볼 수 있습니다.

     

    값을 비교해보니 소수점 2번째 자리부터 업비트의 값과 API를 조회하여 계산한 값이 조금 다른데 이 부분은 원인을 조금 더 찾아봐야 할 것 같습니다.

     

    하지만 큰 오차는 아니기 때문에 일반적인 추세를 판단하는데 사용하는데는 무리 없을 것이라 생각 됩니다. 다음 시간에는 지금까지 알아본 지표를 한 번에 조회하는 방법을 알아보도록 하겠습니다.

     

    우측 상단 버튼을 눌러 블로그를 구독해 주시면 조금 더 빨리 소식을 받아보실 수 있습니다.

    반응형