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

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

Tech&Fin 2021. 7. 29. 17:53
반응형

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

 

오늘은 가격 상승과 하락 변화량에 대한 상대강도지수인 RSI 보조지표에 거래량을 포함한 MFI 보조지표를 구하는 방법에 대해서 살펴보려고 합니다.

 

RSI 보조지표를 구하는 방법은 아래 포스트를 참고하시면 좋을 것 같습니다.

 

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

 

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

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

technfin.tistory.com

 

 

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

     

    MFI 보조지표

    앞서 잠깐 설명드린 바와 같이 MFI 보조지표는 RSI 지표와 상당히 유사한 보조 지표로서 단순하게는 RSI 지표에 거래량이 더해진 지표라고 생각하시면 됩니다.

     

    또한 RSI 보조지표는 종가를 기준으로 계산되는 반면 MFI는 고가, 저가, 종가의 평균값인 Typical Price를 이용해 계산되기 때문에 지수의 움직임에 민감하게 반응하는 RSI에 비해 조금 느리게 반응하는 편입니다.

     

    MFI 지표가 RSI 지표보다 지수에 둔감하게 반응하는 것이 조금 더 신뢰를 줄 수 있다고 판단할 수도 있지만 이는 어떤 전략을 짜느냐에 따라 다를 수 있어 어느것이 더 좋거나 신뢰할 수 있다고 말하기는 어렵습니다.

     

    본인의 전략에 맞게 RSI 지표와 MFI 지표를 섞어서 사용하시는 것을 추천 드립니다.

     

    MFI 지표의 일반적인 해석

    ① MFI 값이 20 이하일 경우 과매도 구간으로 판단하여 매수 준비
    ② MFI 값이 80 이상일 경우 과매수 구간으로 판단하여 매도 준비

     

    MFI 지표를 활용한 매매법

    ① MFI가 20 미만으로 떨어져 과매도 구간에 돌입하고, MFI가 시그널선을 상향 돌파시 상승추세로 판단하여 매수
    ② MFI가 80을 초과하여 과매수 구간에 돌입하고, MFI가 시그널선을 하향 돌파시 하락추세로 판단하여 매도

     

    업비트 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_mfi
    # - Desc : MFI 조회
    # - 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) MFI 값
    # -----------------------------------------------------------------------------
    def get_mfi(target_item, tick_kind, inq_range, loop_cnt):
        try:
    
            # 캔들 데이터 조회용
            candle_datas = []
    
            # MFI 데이터 리턴용
            mfiList = []
    
            # 캔들 추출
            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))])
    
            # 캔들 데이터만큼 수행
            for candle_data_for in candle_datas:
    
                df = pd.DataFrame(candle_data_for)
                dfDt = df['candle_date_time_kst'].iloc[::-1]
    
                df['typical_price'] = (df['trade_price'] + df['high_price'] + df['low_price']) / 3
                df['money_flow'] = df['typical_price'] * df['candle_acc_trade_volume']
    
                positive_mf = 0
                negative_mf = 0
    
                for i in range(0, 14):
    
                    if df["typical_price"][i] > df["typical_price"][i + 1]:
                        positive_mf = positive_mf + df["money_flow"][i]
                    elif df["typical_price"][i] < df["typical_price"][i + 1]:
                        negative_mf = negative_mf + df["money_flow"][i]
    
                if negative_mf > 0:
                    mfi = 100 - (100 / (1 + (positive_mf / negative_mf)))
                else:
                    mfi = 100 - (100 / (1 + (positive_mf)))
    
                mfiList.append({"type": "MFI", "DT": dfDt[0], "MFI": round(mfi, 4)})
    
            return mfiList
    
        # ----------------------------------------
        # 모든 함수의 공통 부분(Exception 처리)
        # ----------------------------------------
        except Exception:
            raise

    다른 로직들과 마찬가지로 지표 계산 로직들은 공통 모듈에 작성하고 필요할 때 호출하여 사용하는 것이 편리하기 때문에 MFI 지표 계산 로직도 공통함수에 작성하는 것이 좋습니다.

     

    테크앤핀에서 진행하는 비트코인 자동매매 프로젝트에서는 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('D')
    
            # ---------------------------------------------------------------------
            # Logic Start!
            # ---------------------------------------------------------------------
            # 보유 종목 리스트 조회
            mfi_data = upbit.get_mfi('KRW-BTC', '30', '200', 10)
    
            for mfi_data_for in mfi_data:
                logging.info(mfi_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)

    mfi_data = upbit.get_mfi('KRW-BTC', '30', '200', 10)

     

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

     

    호출 결과

    [2021/07/29 05:46:53 PM][INFO][test_module.py:33]:{'type': 'MFI', 'DT': '2021-07-29T17:30:00', 'MFI': 46.4102}
    [2021/07/29 05:46:53 PM][INFO][test_module.py:33]:{'type': 'MFI', 'DT': '2021-07-29T17:00:00', 'MFI': 52.0513}
    [2021/07/29 05:46:53 PM][INFO][test_module.py:33]:{'type': 'MFI', 'DT': '2021-07-29T16:30:00', 'MFI': 48.3574}
    [2021/07/29 05:46:53 PM][INFO][test_module.py:33]:{'type': 'MFI', 'DT': '2021-07-29T16:00:00', 'MFI': 44.5553}
    [2021/07/29 05:46:53 PM][INFO][test_module.py:33]:{'type': 'MFI', 'DT': '2021-07-29T15:30:00', 'MFI': 52.9156}
    [2021/07/29 05:46:53 PM][INFO][test_module.py:33]:{'type': 'MFI', 'DT': '2021-07-29T15:00:00', 'MFI': 52.6104}
    [2021/07/29 05:46:53 PM][INFO][test_module.py:33]:{'type': 'MFI', 'DT': '2021-07-29T14:30:00', 'MFI': 53.3788}
    [2021/07/29 05:46:53 PM][INFO][test_module.py:33]:{'type': 'MFI', 'DT': '2021-07-29T14:00:00', 'MFI': 47.1754}
    [2021/07/29 05:46:53 PM][INFO][test_module.py:33]:{'type': 'MFI', 'DT': '2021-07-29T13:30:00', 'MFI': 47.722}
    [2021/07/29 05:46:53 PM][INFO][test_module.py:33]:{'type': 'MFI', 'DT': '2021-07-29T13:00:00', 'MFI': 46.1016}

     

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

     

    업비트에서 ① 지표를 눌러 MFI를 선택한 후 ② 30분봉을 선택하면 30분봉 기준으로 조회된 MFI값이 맞는지 검증할 수 있습니다.

     

    다음 시간에는 MACD 값을 구하는 방법을 알아보도록 하겠습니다. 우측 상단 버튼을 눌러 블로그를 구독해 주시면 조금 더 빨리 소식을 받아보실 수 있습니다.

    반응형