맨위로버튼이미지

1. 들어가기 전에

혹시 예전 중고등학교 시절 배운 연역법과 귀납법에 대하여 기억을 하시나요? 수학 시간에 무언가를 증명하라는 문제가 나오면 대부분은 연역법을 사용하여 A=B, B=C 그러므로 A=C이다고 증명을 했을 것입니다. 하지만 가끔은 연역법을 사용할 수 없는 것들이 있습니다. 예를 들면 수열이 그렇습니다. f(n) = f(n-1) + 3이라는 수식이 주어 진다면 f(1)은 5라고 한다면 f(0)는 2가 되고 f(2) = f(1) + 3 = 8 .. 이런 식으로 증가하는 것은 알겠는데 그래서 f(x)는 뭐지? 수학을 잘하셨던 분들은 금방 f(x) = 3*x + 2 인 것을 눈치 채었을 듯합니다만 수학에 잰뱅이셨던 분들도 우리 주제를 읽어시는데는 아무 상관이 없습니다. 왜 장황한 이야기를 하느냐 하면 지금 우리가 하고 있는 python Bitcoin 자동매매 프로그램이 해야 하는 목표가 바로 f(n)식을 이용하여 f(x)식을 구해야 하는 일이기 때문입니다. 저는 1970년 생입니다만 우리 시절에는 미적분이 너무나도 어렵게 문제가 나오는 바람에 수열과 통계 부분은 너무나 하찮게 다루어 버려서 아마도 지금의 제 나이 때 분들은 우리가 하려는 일에 대한 이해도가 그렇게 높지 않을 수도 있겠다는 생각을 하면서 이 글을 작성하고 있습니다.

이번 회차에서는 이전에 만든 5분 데이터와 이전 회차에서 만든 보조지표 클래스를 어떻게 사용하는 지에 대한 간단한 예시를 통해서 보조지표에 대한 아이디어를 제시합니다. 여러분들이 다른 아이디어를 첨가한다면 더 완벽한 전략을 만들 수도 있을 것입니다. 

2. Condition Class 만들기

소스 내용(BithumbCondition.py)

import time as datetime
import pandas as pd
from addAuxiliaryData import Auxiliary
class BithumbTrading:
    def __init__ (self, logger, base_balance, buy_condition, sell_condition):
        self.logger = logger
        self.base_balance = base_balance
        self.buy_condition = buy_condition
        self.sell_condition = sell_condition
        self.ad = Auxiliary(self.logger)
        self.df = None

    def add_auxiliary(self):
        self.ad.add_auxiliary(self.df)
        
    def add_macd(self, short, long, signal):
        macd, macdsignal, macdhist = self.ad.add_macd(self.df, short, long, signal)
        self.df['macd1'] = macd
        self.df['signal1'] = macdsignal
        self.df['flag1'] = macd - macdsignal
        
    def append_dataframe(self, df):
        if df is None:
            return
        df=df.truncate(after=(df.index.max()-1))  #마지막 데이터는 짤라 낸다.
        
        if(self.df is None):
            self.df = df
        else:
            self.df = pd.concat([self.df, df], ignore_index=True)

        if self.df is None:
            return

        self.df.sort_values(by='time', axis = 0)
        self.df.reset_index(drop=True, inplace=True)

        self.df = self.df.drop_duplicates(['time'], keep='first', inplace=False, ignore_index=True)
        self.df.reset_index(drop=True, inplace=True)
        self.logger.debug("add new data")
        self.logger.debug(self.df)

    def buy_scenario(self, index):
        # print("buy_scenario max : %d : %d, return: %d" % (self.df.index.max(), index, self.buy_condition(index)))
        return self.buy_condition(index, self.df)

    def sell_scenario(self, index):
        # print("sell_scenario max : %d : %d, return: %d" % (self.df.index.max(), index, self.buy_condition(index)))
        return self.sell_condition(index, self.df)

    def sell_and_buy(self):
        self.origin_balance = self.base_balance    # 초기 투자금 100만원
        self.year_balance = self.origin_balance
        self.balance = self.origin_balance
        self.remains = 0
        self.sell_balance = 0
        self.unit_balance = 0

        for i in self.df.index:
            # print("수익률 계산 :%s"  % datetime.ctime(df.loc[i, 'time']/1000))
            self.close = self.df.loc[i, 'close']
            time = self.df.time.loc[i]/1000

            if (time % (365*24*60*60)) == 0:  # 1년에 한번씩 년간 수익률을 계산하기 위해서
                # print("unit : %d" % self.unit_balance)
                self.logger.debug(datetime.strftime('%Y년%m월%d일', datetime.localtime(self.df.loc[i, 'time']/1000)))
                if self.unit_balance > 0 :
                    self.sell_balance = self.unit_balance * self.close
                    self.balance = self.balance + self.sell_balance
                    self.year_balance = self.balance
                else:
                    self.year_balance = self.balance

                self.logger.info("조정 원금: %.3f" % self.year_balance)

            if self.buy_scenario(i) :  #매수
                self.sell_balance = self.balance - (self.balance % 1000)
                # print("buy condition sell_balance: %.2f balance: %.2f unit: %.2f" % (sell_balance, balance, unit_balance))

                if self.sell_balance > 0:
                    self.remains = self.balance - self.sell_balance
                    self.balance = self.remains
                    self.remains = 0
                    self.unit_balance = self.sell_balance / self.close
                self.df.loc[i, 'balance'] = self.balance
                self.df.loc[i, 'unit'] = self.unit_balance
            elif self.sell_scenario(i):   #매도
                # print("sell condition close: %.2f balance: %.2f unit: %.2f" % (close, balance, unit_balance))
                if self.unit_balance > 0:
                    self.sell_balance = self.unit_balance * self.close
                    self.balance = self.balance + self.sell_balance
                    self.unit_balance = 0
                    self.sell_balance = 0
                self.df.loc[i, 'balance'] = self.balance
                self.df.loc[i, 'unit'] =  self.unit_balance
            else:
                self.df.loc[i, 'balance'] = self.balance
                self.df.loc[i, 'unit'] = self.unit_balance

            if self.year_balance != 0:
                self.df.loc[i, 'year_rate'] = ((self.df.loc[i, 'unit'] * self.df.loc[i, 'close'] + self.df.loc[i, 'balance']) - self.year_balance) * 100 / self.year_balance
            else:
                self.df.loc[i, 'year_rate'] = 0

            self.df.loc[i, 'rate'] = ((self.df.loc[i, 'unit'] * self.df.loc[i, 'close'] + self.df.loc[i, 'balance']) - self.base_balance) * 100 / self.base_balance
        return self.df

첫줄에 time lib를 import 하면서 이름을 as로 datetime으로 변경했습니다. 이유는 pandas에서 컬럼명을 attribute형으로 사용하는 경우가 있는데 이때 우리 데이터 중에 time이 있어 혼돈이 있을 것을 대비하여 변경했습니다. 위 소스는 백테스팅을 고려해서 만든 것이라 지금은 당장 사용하지 않는 부분들도 포함이 되어 있습니다. 

자 그럼 main.py를 변경하여 데이터를 연동하도록 하겠습니다. 

 

소스내용(main.py)

from getBithumbData import ScrapCurrBithum
from BithumbCondition import BithumbTrading as BitTra
import time as datetime
import sqlite3

import logging
import logging.config
import json

config = json.load(open('./logging.json'))
logging.config.dictConfig(config)

buy_func = lambda x, dataframe: (x >= 2 and dataframe.loc[x, 'flag1'] > 0 and dataframe.loc[x-1, 'flag1'] <= 0 and dataframe.loc[x-2, 'flag1'] <= 0)
sell_func = lambda x, dataframe: (x >= 2 and dataframe.loc[x, 'flag1'] <= 0 and dataframe.loc[x-1, 'flag1'] > 0 and dataframe.loc[x-2, 'flag1'] > 0)

BASE_BALANCE = 1000000 # 초기 투자금 100만원

if __name__ == '__main__':
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.INFO)

    dbname = "bithum5mMACD1.db"
    sqlcon = sqlite3.connect(dbname) 

    bt = BitTra(logger, BASE_BALANCE, buy_func, sell_func)
    scb = ScrapCurrBithum()

    last_time = ""
    while True:
        start_time = datetime.time()
        df = scb.get_data()
        bt.append_dataframe(df)
        #전체 보조 지표를 포함하지 않고 개별 보조 지표를 사용하도록 하겠습니다.
        #bt.add_auxiliary()
        bt.add_macd(12, 26, 9)
        df = bt.sell_and_buy()
        savedf = df.tail(1)
        get_time= datetime.strftime("%Y%m%d%H%M%S", datetime.localtime(savedf.loc[savedf.index.max(), 'time']/1000))

        if last_time == "":
            df.to_sql(name=dbname, con=sqlcon, if_exists='replace')
        elif last_time != get_time:
            last_time = get_time
            savedf.to_sql(name=dbname, con=sqlcon, if_exists='append')

        logger.info("MACD1 최종:")
        logger.info(df)
        strdt1 = datetime.strftime("%Y%m%d", datetime.localtime(df.loc[df.index.max()-2, 'time']/1000))
        strdt2 = datetime.strftime("%Y%m%d", datetime.localtime(df.loc[df.index.max()-1, 'time']/1000))
        logger.info("마지막1전: %s, 마지막:%s", strdt1, strdt2)
        if(strdt1 != strdt2 ):  #날짜가 변경되었다면
            logger.info('자정이면 메모리를 일부 크리어한다.')
            df = df.tail(df.index.max() - 12*24)
            df.reset_index(drop=True, inplace=True)

        end_time = datetime.time()
        if (end_time - start_time) > 0 and (end_time - start_time) <= 5*60:
            datetime.sleep(5*60 - (end_time - start_time))

sqlite3을 이용하여 수신 데이터를 DB에 저장하므로 우리가 만든 폴더에 "bithum5mMACD1.db" 파일이 생성되었음을 확인할 수 있을 것입니다. 이 회차에서 가장 어렵고 중요한 부분은 "append_dataframe" 함수입니다. 

이전에 만든 getBithumbData.py에서 close와 volume은 float형으로 값을 변경했지만 time은 값을 변경하지 않았습니다. 이유는 time은 RDBMS의 key처럼 동작 하지만 우리가 주로 사용하는 intel cpu의 경우 부동소수점의 값은 "그때그때 달라요" 값입니다. 즉 float형을 key값으로 사용하면 여러분의 데이터는 유일성을 보장할 수 없습니다. 하지만 python은 어떤 면에서는 데이터형 검사가 엄격해서 정수형 데이터로는 어려운 수식 계산 시 오류가 발생합니다. 내용을 쭉 읽어 보면 double 형 데이터가 아니어서 계산이 불가하다는 내용입니다. python을 쉽게 봤던 사람들은 여기서 멘붕에 빠지게 됩니다. 참 그리고 우리가 받고 있는 Bithumb의 5분 데이터는 실제는 1주일치 데이터이며 마지막 한 줄은 "완전 실시간" 데이터입니다. 그러다 보니 마지막 한 줄이 보조지표의 정확성에 방해가 됩니다. 보조지표의 대원칙은 간격이 일정해야 하고 유일성이 보장되어야 합니다. 그렇지 않으면 계산은 아무런 의미가 없습니다. 

 

3. DB 파일 보기 (https://sqlitebrowser.org/dl/)

제목 옆에 링크를 클릭하시면 sqlitebrowser를 다운 받을 수 있는 사이트로 접속합니다. 거기서 자신에게 맞는 버전을 다운로드하시고 설치하시면 DB파일을 볼 준비가 완료됩니다. 

DB Browser를 실행한 다음 File--> 데이터베이스 열기-->해당 폴더의 db파일 지정하시면 위와 같이 DB파일을 보실 수 있습니다. 2021년 오늘 시점 일론머스크의 발언과 미정부의 자본세 발표 및 우리나라도 비트코인 규제 방침이 발표되면서 시장이 급락하고 있습니다. 마지막 컬럼 두 개가 일 년 수익률과 전체 수익률입니다. 현재 -16.7%를 기록하고 있네요. ^^

4. 데이터 분석

DB Browser에서 File-->내보내기-->CSV로 내보내기를 선택합니다. 같은 폴더에 bithum5mMACD1.csv로 파일을 저장합니다. Excel을 실행하여 csv파일을 엽니다. 그리고 MACD가 처음 시작하는 32행까지를 삭제하여 데이터를 정리 합니다. shift키를 눌은 상태에서 close컬럼과 flag1컬럼을 선택 합니다. 그다음 삽입에서 차트를 삽입합니다. 차트를 클릭하면 메뉴에 차트 디자인이 보일 겁니다. 차트 디자인에서 차트 종류 변경을 클릭합니다. 

그럼 아래 차트가 만들어집니다.

이번에는 close컬럼과 macd1, signal1컬럼을 선택하고 데이터 범위를 하루치(12X24=288)만 선택하여 차트를 만듭니다. 

그리고 차트 디자인에서 아래와 같이 클릭하고 확인을 클릭합니다.

아래의 차트를 보면서 데이터 분석을 해 보도록 하겠습니다.

먼저 오늘 데이터 연동을 하면서 적용한 보조지표는 MACD입니다. MACD는 단기이평선과 장기이평선이 서로 만났다가 떨어지고 다시 만나는 반복되는 상황에서 macd값이 상위에 있으면 매수 macd선이 하위로 오면 매도를 하게 프로그램하면 됩니다. 마지막 차트를 보면 거의 정확한 시점에서 매수와 매도가 발생하고 있음을 알 수 있습니다. 그럼에도 불구하고 전체 수익률은 -15~-17%사이를 움직이고 있습니다. 이는 macd의 약점 중에 하나인 허매수 신호가 발생하고 있기 때문입니다. 허매수 신호는 완전 폭락이 발생하는 구간보다는 옆으로 흔들리면서 이동하는 구간에서 많이 발생하고 있음을 차트로 알 수 있습니다. 전문가들이 macd의 가장 큰 약점이라고 하는 후시성은 위에 차트에서도 알 수 있듯이 5분 데이터에서는 큰 영향이 없습니다. 오히려 급락 구간을 잘 피해서 매수를 하고 있음을 알 수 있습니다. 그리고 급등 구간에서도 매수가 되어 큰 성과를 내고 있음을 알 수 있습니다.  그렇다면 flag값은 무엇일까요? MACD Oscillator라고도 하는 값입니다. 이 값이 양수이면 매수 이 값이 음수이면 매도를 하면 되기 때문에 프로그램에서 매수매도를 구현하기가 쉽습니다. BithumbCondition.py에 BithumbTrading class에 들어가는 lambda 함수 식의 의미가 바로 flag를 기준으로 매수 매도를 하는 공식입니다. 

 

5. 마치며

오늘 회차는 어떠셨나요? 데이터 연동 및 차트 분석과 데이터 분석에 대하여 알아보았습니다. 그런데 아무리 하락장이라고 하지만 수익률이 너무 심하지 않나 싶을 겁니다. 우리의 목표가 여러 보조 지표를 활용해서 기대 수익률을 낮추면서 하락장에서 수익률을 잃지 않는 프로그램 개발이라는 것을 잊지 마세요. 오히려 지금과 같은 과하락장이 우리 프로그램에는 좋은 기회입니다. 

반응형
LIST

맨위로버튼이미지

1. 들어가기 전에

우리가 하려는 python Bitcoin 자동 매매는 과학의 영역이 아닙니다.

치료 행위를 예를 들면 아직도 많은 양의 쪽 의사들은 한방 의술을 비과학이라고 하고 절대로 해서는 안 되는 행위라고 하시는 분들이 상당히 많습니다.

양의 쪽 의술들 중 상당수는 과학적인 검증이 완료되었지만 그래도 개개의 의사마다의 경험치가 있고 이 경험치가 환자의 생명에 지대한 영양을 미칩니다.

우리가 하려고 하는 프로그램은 이러한 경험치를 최대한 짧은 시간에 많이 늘리기 위한 것이며 따라서 아무리 잘 짠 프로그램이라고 해도 손실은 피할 수 없습니다.

모든 금융상품들은 급등락을 반복하고 일정 범위의 등락은 우리에게 큰 영향을 미치지 않지만 심한 급등락이 발생한다면 일반인뿐만 아니라 기관이라고 하는 사람들 조차 큰 피해를 보게 됩니다.

 

2020년의 코로나 사태나 2003년 동일본 대지진 2008년 리먼브라더스 사태 2001년 911 사태 1997년 한국의 IMF사태(같은 시기 아시아 다른 나라도 같은 위기를 겪었습니다.)등의

경기 급락 사건이 발생하게 되면 외국 지분의 상당수는 돈을 빌려서 우리나라의 주식에 투자하고 있기에

아무리 우량주라도 팔아서 빚을 갚는 전략을 치하게 되기 때문에

우리의 일반 논리로 저가 고가 우량주 잡주의 개념은 의미가 없게 됩니다.

기존의 방식은 말이죠 그랬다는 겁니다.

제가 궁극적으로 하고자 하는 것은 우리가 다른 일을 하고 있는 중에도 시장의 폭락을 예측이 아닌 분석을 하고

폭락장에 휘둘리지 않으면서 다시 장이 상승할 경우 가장 아래 꼭짓점을 감지하여 매수하여

최고점까지 매도하지 않고 버티는 비이성적인 프로그램을 만드는 것이 목표입니다.

 

제가 장황한 글을 쓰는 이유는 일부 사람 중 비과학을 과학적으로 사고하려는 사람들이 있는데

이런 분들은 결코 성공할 수 없다는 것을 말씀드리고자 함입니다.

 

비과학은 비이성적이고 비상식적이며 기존의 관념을 버릴 때 비로소 아이디어를 얻게 됩니다.

 

2. TA-LIB 설치하기

TA-LIB는 시리즈 데이터를 이용하여 기존 보조지표를 아주 빠르게 구해 주는 lib입니다.

그러나 불행히도 설치가 꽤 까다롭습니다. https://minjejeon.github.io/learningstock/2018/04/07/installing-and-using-talib.html 이 글을 참조해 보시기 바랍니다.

TA-LIB 홈페이지는 https://www.ta-lib.org/ 이 링크입니다. 

 

여기서 잠깐 왜 TA-LIB를 사용할까요?

아시다시피 pandas의 rolling 함수를 사용하면 쉽게 이동평균선을 구할 수 있습니다.

예를 들면 rolling(window=5).mean() 이렇게 하면 5일 이동평균이 구해집니다. 

1. 속도

TA-LIB는 어떤 pandas 모듈보다 7배 이상 빠릅니다.

이유는 내부적으로 c함수로 구현이 되어 있어서 훨씬 빠른 속도를 구현합니다.

단 그래서 설치하기가 쉽지 않습니다.

설치 파일은 python에 TA-LIB를 설치하려고 하는 순간 c compiler를 찾을 것입니다.

대부분의 분들은 당연히 설치가 안되어 있을 것이며 설치가 되어 있다고 해도 설치가 안 되는 것은 맞찬가지 입니다.

저의 경우는 ubuntu에서도 맞찬가지 였습니다.

-- 2024년 기준 지금은 잘 됩니다. 

그런데 먼저 c lib TA-LIB를 먼저 설치하고 그 다음 PIP로 TA-LIB를 설치한다면 바로 성공하실 수 있을 것 입니다. --

2. 종류

TA-LIB는 pandas로 구현하려면 알아야 하는 모든 공식을 미리 다 구현하여서 바로 사용하기만 하면 됩니다.

그래도 일부는 pandas를 사용합니다. 

3. 시간

우리에게는 많은 시간이 주어지지 않습니다.

어떤 경우 python의 for loop를 사용한다면 다음 데이터가 들어오는 5분 내에 많은 일을 해야 합니다.

1분 1초가 상당히 중요하게 다가옵니다. 

 

3. logger 설정하기

이전에 만들었던 main.py에 logger를 추가하도록 하겠습니다.

 

인터넷에 있는 자동매매 관련 프로그램들이 부족한 부분이 프로그램을 시스템화하는 부분에 상당히 부족한 부분을 보입니다.

그중 한 가지가 logger입니다.

주식이나 데이터 분석이 주업이신 분들의 특징은 python에서 이미 제공하는 기능을 새로 만들려고 한다는 것입니다.

하지만 직접 제작한 것은 이미 있는 것을 이기기가 쉽지 않습니다.

양쪽 모두 천재가 아니라면 말이죠.

 

소스 내용(main.py)

from getBithumbData import ScrapCurrBithum
import logging
import logging.config
import json

config = json.load(open('./logging.json'))
logging.config.dictConfig(config)

if __name__ == '__main__':
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.INFO)

    scb = ScrapCurrBithum()
    df = scb.get_data()
    logger.info(df)

소스내용(logging.json)

{
    "version": 1,
    "disable_existing_loggers": false,
    "formatters": {
      "basic": {
        "format": "%(asctime)s:%(module)s:%(levelname)s:%(message)s",
        "datefmt": "%Y-%m-%d %H:%M:%S"
      }
    },
    "handlers": {
      "console": {
        "class": "logging.StreamHandler",
        "level": "INFO",
        "formatter": "basic"
      },
      "file_debug": {
        "class": "logging.FileHandler",
        "level": "DEBUG",
        "formatter": "basic",
        "filename": "log/debug.log"
      },
      "file_error": {
        "class": "logging.FileHandler",
        "level": "ERROR",
        "formatter": "basic",
        "filename": "log/error.log"
      }
    },
    "loggers": {
      "__main__": {
        "level": "DEBUG",
        "handlers": ["console", "file_debug", "file_error"],
        "propagate": true,
        "filename": "macd.log"
      }
    }
  }

이전에 만들었던 bitcoin_trade1 폴더 아래 log폴더를 생성합니다. 그리고 python main.py를 실행해 봅니다. 

2021-05-23 13:05:57:main:INFO:               time       close     volume                 date
0     1621138200000  58900000.0   8.866844  2021-05-16 13:10:00
1     1621138500000  58886000.0   9.422226  2021-05-16 13:15:00
2     1621138800000  58889000.0   4.299981  2021-05-16 13:20:00
3     1621139100000  58862000.0   7.325870  2021-05-16 13:25:00
4     1621139400000  58650000.0   8.781141  2021-05-16 13:30:00
...             ...         ...        ...                  ...
1951  1621741200000  47258000.0  12.528850  2021-05-23 12:40:00
1952  1621741500000  47100000.0   8.641681  2021-05-23 12:45:00
1953  1621741800000  47116000.0   5.692299  2021-05-23 12:50:00
1954  1621742100000  47087000.0   2.980005  2021-05-23 12:55:00
1955  1621742400000  46890000.0  15.279345  2021-05-23 13:00:00

[1956 rows x 4 columns]

log폴더에 debug.log파일이 생성되고 아래와 같은 내용이 생겼다면 이제 여러분은 logger를 사용할 수 있게 되신 겁니다.

 

4. 보조지표 클래스 만들기

우리가 사용하려는 보조지표가 일반적일 수도 일반적이 아닐 수도 있습니다.

중요한 것은 이전의 관습을 버리고 오로지 우리의 아이디어 만으로 새로운 보조지표를 생성하시기 바랍니다. 

1. 기존 관습을 버려야 하는 이유

인터넷에 돌고 있는 자료 중 상당수가 '한빛미디어' 출판사의 '파이썬 증권 데이터 분석'을 토대로 한 변동성 돌파 전략에 대한 소스입니다.

 

유튜브에서도 상당수의 소스가 검증 없이 돌고 있습니다.

 

래리 윌리엄스의 변동성 돌파 전략이나 책이 잘못되었다는 게 아닙니다.

 

그런데 정말 이 변동성 돌파 전략은 안전한 투자 전략일까요?

 

여러분은 어떤 전략을 실행하기 전에 반듯이 백테스팅(과거 데이터를 이용하여 로직을 검증하는 테스트)을 실행해 보시고 데이터를 자신이 신뢰할 수 있을 때에 비로소 적용하시기 바랍니다.

 

다시 변동성 돌파 전략으로 돌아가서 변동성 돌파 전략이란 전일의 고가와 전일의 저가의 평균 또는 K상수를(일반적으로 0.5를 곱하면 평균이 되고 0.3을 곱하면 30%가 될 겁니다.)

곱하여 변동성을 구한 다음 오늘의 시가에 변동성 값을 더한 값 위로 상승하면 매수하여

종가에 모두 매도하는 전략입니다.

 

곰곰이 생각해 보시면 정말 의이 없는 미신이 따로 없습니다.

 

내가 생각하는 그 종목은 정말 저 방법 되로 움직일까요?

 

초반부터 하락한다면 매수조차 안돼서 아무런 피해가 없겠지만 대부분의 경우 장 초기에는 상승을 하다가 중반 이후에는 장이 빠지는 식의 거래가 주를 이루다 보니 이 전략의 약점은 바로 나타나기 시작했습니다.

 

매도 시점에는 이미 엄청난 손실을 기록하게 되는데 이게 끝이 아니라는 게 문제입니다. 

2. 그렇다면 다른 매매전략은?

제가 주목한 것은 MACD(Moving Average Convergence & Divergenc)를 이용한 매매 전략이었습니다.

MACD에 대한 내용은 https://md2biz.tistory.com/397 링크를 참조하시기 바랍니다. 

 

소스내용(addAuxiliaryData.py)

import time
import sys
import os
import talib
import time
import numpy as np

class Auxiliary:
    def __init__(self, logger):
        self.logger = logger

    ## NULL 데이터 인 경우 merge가 안된다.
    def add_closeminmax(self, df):
        try:
            self.logger.info("Close Min Max 구하기:%s"  % time.ctime(time.time()))
            df['clsmax'] =  df.close.rolling(window=30, min_periods=1).max()
            df['clsmin'] =  df.close.rolling(window=30, min_periods=1).min()
            df['clsminsig1'] = df.clsmin - df.clsmin.shift(2)    
            df['clsminsig2'] = df.close.shift(2) - df.clsmin    
            df['clsmaxsig1'] = df.clsmax - df.clsmax.shift(2)   
            df['clsmaxsig2'] = df.close.shift(2) - df.clsmax   

        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_closeminmax -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_moving_avg5(self, df):
        try:
            self.logger.info("5 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['sma5'] = talib.SMA(np.asarray(df.close), 5)
            df['sma5_incli'] = df.sma5 - df.sma5.shift()
            df['sma5vsclose'] = df.close - df.sma5
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_moving_avg5 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
    
    def add_moving_avg20(self, df):
        try:
            self.logger.info("20 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['sma20'] = talib.SMA(np.asarray(df.close), 20)
            df['sma20_incli'] = df.sma20 - df.sma20.shift()
            df['sma20vsclose'] = df.close - df.sma20
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_moving_avg20 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))
    
    def add_moving_avg60(self, df):
        try:
            self.logger.info("60 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['sma60'] = talib.SMA(np.asarray(df['close']), 60)
            df['sma60_incli'] = df.sma60 - df.sma60.shift()
            df['sma60vsclose'] = df.close - df.sma60
            df['sma60clsmax'] =  df.sma60.rolling(window=30, min_periods=1).max()
            df['sma60clsmin'] =  df.sma60.rolling(window=30, min_periods=1).min()
            df['sma60clsminsig1'] = df.sma60clsmin - df.sma60clsmin.shift(2)    
            df['sma60clsminsig2'] = df.sma60.shift(2) - df.sma60clsmin    
            df['sma60clsmaxsig1'] = df.sma60clsmax - df.sma60clsmax.shift(2)   
            df['sma60clsmaxsig2'] = df.sma60.shift(2) - df.sma60clsmax   
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_moving_avg60 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_moving_avg120(self, df):
        try:
            self.logger.info("120 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['sma120'] = talib.SMA(np.asarray(df['close']), 120)
            df['sma120_incli'] = df.sma120 - df.sma120.shift()
            df['sma120vsclose'] = df.close - df.sma120
            df['sma120clsmax'] =  df.sma120.rolling(window=30, min_periods=1).max()
            df['sma120clsmin'] =  df.sma120.rolling(window=30, min_periods=1).min()
            df['sma120clsminsig1'] = df.sma120clsmin - df.sma120clsmin.shift(2)    
            df['sma120clsminsig2'] = df.sma120.shift(2) - df.sma120clsmin       
            df['sma120clsmaxsig1'] = df.sma120clsmax - df.sma120clsmax.shift(2) 
            df['sma120clsmaxsig2'] = df.sma120.shift(2) - df.sma120clsmax       
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_moving_avg120 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_moving_avg240(self, df):
        try:
            self.logger.info("240 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['sma240'] = talib.SMA(np.asarray(df['close']), 240)
            df['sma240_incli'] = df.sma240 - df.sma240.shift()
            df['sma240vsclose'] = df.close - df.sma240
            df['sma240clsmax'] =  df.sma240.rolling(window=30, min_periods=1).max()
            df['sma240clsmin'] =  df.sma240.rolling(window=30, min_periods=1).min()
            df['sma240clsminsig1'] = df.sma240clsmin - df.sma240clsmin.shift(2)   
            df['sma240clsminsig2'] = df.sma240.shift(2) - df.sma240clsmin        
            df['sma240clsmaxsig1'] = df.sma240clsmax - df.sma240clsmax.shift(2)  
            df['sma240clsmaxsig2'] = df.sma240.shift(2) - df.sma240clsmax        
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_moving_avg240 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_moving_ema12(self, df):
        try:
            self.logger.info("지수 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['ema12'] = talib.EMA(np.asarray(df['close']), 12)
            df['ema12clsmax'] =  df.ema12.rolling(window=30, min_periods=1).max()
            df['ema12clsmin'] =  df.ema12.rolling(window=30, min_periods=1).min()
            df['ema12minsig1'] = df.ema12clsmin - df.ema12clsmin.shift(2)   
            df['ema12minsig2'] = df.ema12.shift(2) - df.ema12clsmin         
            df['ema12maxsig1'] = df.ema12clsmax - df.ema12clsmax.shift(2)   
            df['ema12maxsig2'] = df.ema12.shift(2) - df.ema12clsmax         
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_moving_ema12 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_moving_ema26(self, df):
        try:
            df['ema26'] = talib.EMA(np.asarray(df['close']), 26)
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_moving_ema26 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_moving_wma(self, df):
        try:
            self.logger.info("가중 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['wma12'] = talib.WMA(np.asarray(df['close']), 12)
            df['wma26'] = talib.WMA(np.asarray(df['close']), 26)
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_moving_wma -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_bbands(self, df):
        try:
            self.logger.info("볼리져 밴드 구하기:%s"  % time.ctime(time.time()))
            upper, middle, lower = talib.BBANDS(np.asarray(df['close']), timeperiod=20, nbdevup=2, nbdevdn=2, matype=0)
            df['dn']=lower
            df['mavg']=middle
            df['up']=upper
            df['bbands_sub']=df.up - df.dn
            df['bbands_submax'] =  df.bbands_sub.rolling(window=30, min_periods=1).max()
            df['bbands_submin'] =  df.bbands_sub.rolling(window=30, min_periods=1).min()
            df['bbands_subminsig1'] = df.bbands_submin - df.bbands_submin.shift(2)  
            df['bbands_subminsig2'] = df.bbands_sub.shift(2) - df.bbands_submin   
            df['bbands_submaxsig1'] = df.bbands_submax - df.bbands_submax.shift(2) 
            df['bbands_submaxsig2'] = df.bbands_sub.shift(2) - df.bbands_submax  

            df['pct8']=(df.close - df.dn)/(df.up - df.dn)
            df['pct8vsclose']=df.close - df.pct8
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_bbands -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_relative_strength(self, df):
        try:
            self.logger.info("상대 강도 지수 구하기:%s"  % time.ctime(time.time()))
            rsi14 = talib.RSI(np.asarray(df['close']), 14)
            df['rsi14'] = rsi14
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_relative_strength -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_macd(self, df, sort, long, sig):
        try:
            self.logger.info("MACD1 구하기:%s, sort:%d, long:%d, sig:%d"  % (time.ctime(time.time()), sort, long, sig))
            return talib.MACD(np.asarray(df['close']), sort, long, sig)
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_macd -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_volume_sma5(self, df):
        try:
            self.logger.info("5 거래량 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['vol_sma5'] = talib.SMA(np.asarray(df['volume']), 5)
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_volume_sma5 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_volume_sma20(self, df):
        try:
            self.logger.info("20 거래량 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['vol_sma20'] = talib.SMA(np.asarray(df['volume']), 20)
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_volume_sma20 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_volume_sma60(self, df):
        try:
            self.logger.info("60 거래량 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['vol_sma60'] = talib.SMA(np.asarray(df['volume']), 60)
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_volume_sma60 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_volume_sma120(self, df):
        try:
            self.logger.info("120거래량 이동 평균선 구하기:%s"  % time.ctime(time.time()))
            df['vol_sma120'] = talib.SMA(np.asarray(df['volume']), 120)
        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_volume_sma120 -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_volume_separation(self, df):
        try:
            volume_df = df[['vol_sma5', 'vol_sma20', 'vol_sma60', 'vol_sma120']]

            df['vol_sepr'] = volume_df.max(axis=1, skipna = True) - volume_df.min(axis=1, skipna = True)
            df['vol_seprmax'] =  df.vol_sepr.rolling(window=30, min_periods=1).max()
            df['vol_seprmin'] =  df.vol_sepr.rolling(window=30, min_periods=1).min()
            df['vol_seprminsig1'] = df.vol_seprmin - df.vol_seprmin.shift(2)   
            df['vol_seprminsig2'] = df.vol_sepr.shift(2) - df.vol_seprmin  
            df['vol_seprmaxsig1'] = df.vol_seprmax - df.vol_seprmax.shift(2) 
            df['vol_seprmaxsig2'] = df.vol_sepr.shift(2) - df.vol_seprmax  

        except Exception as ex:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logger.error("`add_volume_separation -> exception! %s ` : %s %d" % (str(ex) , fname, exc_tb.tb_lineno))

    def add_balance_init(self, df):
        df['balance'] = 0.0
        df['unit'] = 0.0
        df['year_rate'] = 0.0
        df['rate'] = 0.0

    def add_auxiliary(self, df):
        self.add_closeminmax(df)
        self.add_moving_avg5(df)
        self.add_moving_avg20(df)
        self.add_moving_avg60(df)
        self.add_moving_avg120(df)
        self.add_moving_avg240(df)
        self.add_moving_ema12(df)
        self.add_moving_ema26(df)
        self.add_moving_wma(df)
        self.add_bbands(df)
        self.add_relative_strength(df)

        macd, macdsignal, macdhist = self.add_macd(df, 12, 26, 9)
        df['macd1'] = macd
        df['signal1'] = macdsignal
        df['flag1'] = macd - macdsignal

        macd, macdsignal, macdhist = self.add_macd(df, 7, 15, 9)
        df['macd2'] = macd
        df['signal2'] = macdsignal
        df['flag2'] = macd - macdsignal

        self.add_volume_sma5(df)
        self.add_volume_sma20(df)
        self.add_volume_sma60(df)
        self.add_volume_sma120(df)
        self.add_volume_separation(df)
        self.add_balance_init(df)

        return df

5. 마치며

이번 회차에서는 TA-LIB를 이용한 보조지표 클래스를 만들었습니다.

 

다음 회차에서는 이 보조지표를 이용하여 Bitumb 5분 데이터와 연동해 보도록 하겠습니다.

 

연동은 각 지표별로 해 볼 예정이므로 제가 생각하는 보조지표의 의미와 함께

여러분의 백테스팅 방법을 한번 고민해 보시기 바랍니다.

 

6. 추가로

저는 윈도우 환경이 아니고 azure의 리눅스(우분투) vm을 사용하고 있습니다.

 

마찬가지로 TA-LIB가 깔리지 않습니다. 그런데 하다보니 원인을 알았습니다.

홈페이지에도 내용이 나오는데 자세히 안 읽은 것 같네요.

 

먼저 python의 버젼을 확인 합니다. python --version   저는 3.6.9버젼 입니다.

centos의 경우 sudo yum install python36-devel 을 설치 하고 python -m pip install ta-lib  하면 잘 깔립니다. 

 

우분투 또는 레미안 계열은 sudo apt-get install python-dev  설치 하고 맞찬가지로 pip install ta-lib 하면 에러 없이 잘 깔립니다.

혹시 root 권한이 없는 경우 sudo 하시는 건 기본적으로 아시죠.

windows 사용자는 사이트에서 wheel 파일 받아서 설치하면 잘 설치됩니다.

우리는 다음 데이터가 오는 5분 전에 모든 것을 해야 합니다.

 

그렇게 할려면  TA-LIB는 가장 중요한 모듈입니다.

 

반응형
LIST

맨위로버튼이미지

1. 들어가기 전에 

현재 인터넷에서 돌고 있는 거의 대부분의 소스를 분석하고 적용하고 실제 거래도 태워 보고 한 경험을 여러분과 공유하고자 이 블로그를 작성하게 되었습니다.

현재 인터넷에서 돌고 있는 대부분의 내용은 현실과 부합하지 않는 것이 많으며 특히 유튜브를 통해서 돌고 있는 내용들을 믿고 자동 매매를 실제 거래로 돌리시는 분들이 있다면 상당히 주의를 요한다는 것을 미리 말씀드리고 싶습니다. 

무엇보다 백테스팅이 중요하며 백테스팅 결과를 어떻게 적용할지와 예측 데이터를 어떻게 활용할지에 중점을 맞추고자 합니다.

 

현재 인터넷상에 있는 소스들의 특징은 대부분(상당히 잘 만든 소스도 분명히 있습니다만) 대학교 졸작으로도 부족한 내용 또는 중, 고등학생 학예회 수준의 소스가 많습니다. 

프로그램에는 분명 라이프사이클이 있습니다.

아무리 잘 짠 프로그램도 예외사항을 맞으면 다시 시작되어야 하고 특히나 자동 매매 프로그램은 여러분들의 소중한 돈을 직접 운영하는 어떻게 보면 세상에서 가장 비싼 프로그램이어야 하기에 프로그램에 꼭 필요한 기능인 로그분석, 매매 기록, 예외 처리, 자동 재시작, 버그 패치 방법 등의 기능이 필요합니다.

이 기능들이 없다면 여러분은 정말 어리석은 모험을 하고 있다고 말씀드리고 싶습니다.

분명히 말씀드리지만 저를 포함 인터넷에서 돌고 있는 어떤 프로그램도 투자손실에 대한 보상을 하지 않으며 모든 행위의 책임은 본인이 지고 있는다는 것을 명심하시기 바랍니다.

 

2. Bithumb 5분 데이터 구하기

사용하시는 브라우저에 https://api.bithumb.com/public/candlestick/BTC_KRW/5m 를 입력하시면 상당한 량의 데이터가 json타입으로 내려 옴을 보실 수 있을 것입니다.

이번회에서는 이 데이터를 수신하여 가공하는 부분까지 하고 다음에 이 데이터를 어떻게 활용할지 보조 지표는 어떻게 적용할 지에 대한 글을 이어 가겠습니다.

참고로 저는 VSCode를 사용해서 개발 중이며 python은 아무 버전이나 3.x 버전이면 상관없습니다.

( 32bit,64bit도 이번 주제에서는 상관이 없습니다.

다만 주식 매매 프로그램 작성 시에는 아직도 대부분의 증권사 API가 32bit 윈도우를 지원하고 있어서 향후에는 32bit python을 설치하셔야 할 것입니다.

32bit python의 경우 메모리 제약을 가지고 있어 많은 량의 pandas DataFrame을 사용하게 되면 프로그램이 정상 동작하지 않습니다. )

python의 기본지식(pip 사용법, 기본 문법)은 알고 계신 것으로 믿고 시작하겠습니다. 

 

1. MicroSoft의 VSCode는 Visula Studio나 Eclipse나 기타 툴과는 비교가 안될 정도의 메모리나 속도 관리가 좋습니다.

그리고 일반 Text Editor에 비해 프로젝트 프로그램에 필요한 대부분의 기능을 가지고 있습니다. 

VSCode는 프로젝트란 개념이 따로 없고 폴더 단위로 프로그램 단위를 구성합니다.

또한 Python툴로 사용할 경우 해당 폴더 단위로 실행되므로 폴더 이름이 중요합니다.

bitcoin_trade1 또는 적당한 프로젝트 이름으로 디렉토리를 만들고 VSCode-->파일-->폴더 열기로 해당 폴더를 열면 됩니다.

 

2.Bithumb의 API를 이용하여 데이터를 가져 오기 위해서는 http requests lib를 이용하여 데이터를 가져옵니다.

아시다시피 Bitcoin의 거래는 365일 24시간 이루어지고 있으며 우리나라가 전 세계 평균보다 약간 비싼 편입니다.

사실 우리가 하려는 목표와 이런 내용은 크게 의미가 없지만 Bitcoin의 거래가 365일 24시간 이루어진다는 것은 우리가 작성하려는 프로그램에는 아주 좋은 일입니다. (보조 지표 테스트에 아주 좋은 조건입니다.)

 

3. 아래 데이터에는 시간(timestamp * 1000), 시작가, 종가, 고가, 저가, 거래량을 제공 하고 있습니다.

주식에서는 의미 있는 이 값들이 사실상 Bitcoin의 경우 거의 의미가 없습니다.

그래서 시작가, 고가, 저가를 버리고 종가와 거래량만을 사용하여 추가적인 보조 지표를 다음 회차에서는 다루고자 합니다. 

 

4. 데이터는 Pandas DataFrame을 사용하여 저장되고 리턴 될 것입니다.

DataFrame의 마치 RDBMS처럼 동작하기도 하고 NoSQL DB처럼 동작하기도 합니다.

요령을 익힌다면 상당히 유용하지만 잘못 사용하면 데이터가 중복되거나 유실될 수도 있습니다.

그런 경우 보조 지표에는 아주 치명적일 수 있습니다. 

 

5. 모든 소스는 적당한 크기의 클래스 형태 또는 모듈 단위로 작성될 것입니다.

나중에는 이런 모듈들이 모여 적당한 lib가 갖쳐지게 되면  특정 디렉토리에 담고 import 절에서 [디렉토리명.모듈명]의 형식으로 호출하여 사용할 수 있습니다. 

 

소스 내용(getBithumbData.py)

import time
import requests as rq
import pandas as pd
 
#### 최근 5분 데이터 구하기 하지만 1주일치 임.
class ScrapCurrBithum:
    def get_his_5m(self):
        data=rq.get('https://api.bithumb.com/public/candlestick/BTC_KRW/5m')
        data=data.json()
        data=data.get("data")
        df=pd.DataFrame(data)
        df.rename(columns=0:'time',1:"start", 2:"close", 3:"high",4:"low",5:"volume"}, inplace=True)
        df=df.drop(['start', 'high', 'low'], axis=1)
        df.close=df.close.astype("float")
        df.volume=df.volume.astype("float")
        df["date"]=df["time"].apply(lambda x:time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(x/1000)))
        return df
 
    def get_data(self):
        return self.get_his_5m()

 

3. 마치며

이번 회차에서는 Bithumb의 5분 데이터를 받아서 DataFrame으로 저장하는 소스를 작성했습니다. 이 소스는 main함수가 없으므로 단독 실행이 되지 않습니다. 아래 소스를 같은 폴더안에 작성하여 실행하면 수집된 데이터를 확인할 수 있습니다.

 

소스 내용(main.py)

from getBithumbData import ScrapCurrBithum

if __name__ == '__main__':
    scb = ScrapCurrBithum()
    df = scb.get_data()
    print(df)
반응형
LIST

+ Recent posts