ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [비트코인] 가상화폐 자동투자 프로그램 개발하기(2) - 변동성 돌파 전략 및 백테스팅
    프로젝트/가상화폐 자동투자 2021. 4. 24. 03:15
    반응형

    이번 시간에는 가상화폐 자동투자를 실행할 '전략'에 대해 포스팅한다. 사람이 직접 하는 투자가 아니라 프로그램이 자동으로 진행하는 투자이기 때문에, 최대한 체계적이고 일률적인 투자전략을 세워야 한다. 또한, 잦은 거래는 많은 수수료를 발생시키므로 거래 횟수도 너무 많으면 안 된다. 이러한 점들을 고려하여 채택한 전략은 '변동성 돌파 전략'이다. 그럼 '변동성 돌파 전략'이 무엇인지 알아보자.

     

    변동성 돌파 전략

    투자에서 사용할 수 있는 분석 기법은 크게 투자 대상의 내재적 가치(기업가치, 재무재표 등)를 분석하는 '기본적 분석(Fundamental Analysis)'과, 투자 대상의 가격 변동(차트)를 분석하는 '기술적 분석(Technical Analysis)' 두 가지로 나누어진다. 이 중에서 우리는 자동매매라는 특성상 기본적 분석은 애초에 불가하기 때문에 기술적 분석, 즉 차트분석에 모든 것을 맡겨야만 한다. 변동성 돌파 전략이란 이러한 기술적 투자의 대가로 불리는 '래리 윌리엄스(Larry R. Williams)' 가 실제로 사용한 추세 추종의 방식을 따르는 전략으로 하루 단위로 일정 수준 이상의 강한 상승세가 나타나면 이를 돌파 신호로 보고 상승하는 추세를 따라가며 일 단위로 빠르게 수익을 실현하는 전략이다. 이 전략은 가격이 한 방향으로 일정량 이상의 추세가 형성되면 한동안 계속 그 방향으로 움직일 가능성이 크다는 시장의 특성을 이용함과 동시에, 일 단위 청산 과정을 통하여 급작스러운 낙폭에 대한 리스크를 최소화하여 안정적인 우상향을 목표로 한다. 자세한 방법은 아래와 같다.

     

    1. 전일의 고가와 저가의 차이인 Range 를 구한다. (Range = 전일고가- 전일저가)
    2. 당일 "장중 가격 > 당일시가 + Range * K" 를 만족하는 시점에 매수한다. (K는 0~1 사이의 값으로 가장 효율적인 값을 찾아야 한다.)
    3. 익일 시가에 매도한다.

    생각보다 간단하다! 전일의 가격 변동폭을 기준으로 당일 시가에서 일정분을 상승하면 매수하고 다음날 시가에 팔아버리면 된다. 이제 전략을 정했으니 이 전략이 실제로 가상화폐 시장에서 잘 통하는지 과거 데이터를 통해 검증해보자.

     

     

    백테스팅

    투자전략을 바로 실전에 투입하기 전에 과거 데이터들로 검증을 해 보는 과정을 백테스팅이라고 부른다. 백테스팅을 위하여 업비트 API 의 차트 데이터를 이용한다.

    차트 불러오기

    업비트 API 에서 차트를 불러올 때는 get_ohlcv() 를 사용하면 된다. 자세한 사용법은 pyupbit github (github.com/sharebook-kr/pyupbit) 에서 확인할 수 있다.

    import pyupbit
    import pandas as pd
    import time
    
    coin = "KRW-BTC"		# 코인명
    interval = "day"		# 차트의 종류
    fees = 0.0005			# 수수료
    day_count = 1300		# 데이터 수
    
    date = None
    dfs = [ ]
    
    for i in range(day_count // 200 + 1):
        if i < day_count // 200 :
            df = pyupbit.get_ohlcv(coin, to = date, interval = interval)
            date = df.index[0]
        elif day_count % 200 != 0 :
            df = pyupbit.get_ohlcv(coin, to = date, interval = interval, count = day_count % 200)
        else :
            break
        dfs.append(df)
        time.sleep(0.1)
    
    df = pd.concat(dfs).sort_index()
    • coin, interval, fees, day_count 는 각각 데이터를 불러오기 위한 변수이다. coin 은 불러올 가상화폐명을, interval 은 일봉, 주봉, 분봉 등의 구분을, fees 는 수수료, day_count 는 불러올 데이터의 수를 뜻한다.
    • get_ohlcv() 함수에서 한 번에 불러올 수 있는 최대 데이터 수는 200 개이므로 for 반복문을 사용하여 200 개 이상의 데이터를 받아올 수 있게 하였다.
    • 업비트 API 에서 차트의 요청은 초당 10회의 제한이 있기 때문에 time.sleep(0.1) 로 0.1초씩 지연시켜주어야 오류가 발생하지 않는다.

     

    불러온 차트는 df 에 DataFrame 형태로 저장된다. 이를 출력하여 보면 아래와 같다.

    반응형

    전략 구현

    현재 차트에서 불러온 데이터는 open(시가), high(고가), low(저가), close(종가), volume(거래량) 이다. 이 값들을 이용하여 변동성돌파전략을 실행하고 수익률을 구하여 데이터에 추가한다.

    df['range'] = df['high'].shift(1) - df['low'].shift(1)
    

    우선 range 를 구한다. shift(1) 은 데이터를 1 만큼 아래로 당기는 명령어로 range 는 전일 고가와 전일 저가를 이용하므로 전일 데이터를 당겨서 쓰기 위함이다. 위와 같이 DataFrame 에서 column 끼리의 연산을 하면 DataFrame의 모든 데이터(row)에 적용된다.

    df['targetPrice'] = df['open'] + df['range'] * K

    targetPrice 는 매수목표가를 의미한다. 당일 시가 + 전일 range * K 값을 매수목표가로 정해두고 이 값을 돌파하면 매수한다.

    df['drr'] = np.where(df['high'] > df['targetPrice'], (df['close'] / (1 + fees)) / (df['targetPrice'] * (1 + fees)) - 1, 0)
    

    drrDaily Rate of Return 으로 하루동안의 수익률을 나타낸다. np 는 numpy 모듈로, 사용하기 위해 import numpy as np 을 포함하여야 한다. np.where 은 if 문과 유사한 역할을 한다. np.where(condition, A, B) 는 condition 이 true 이면 A 를, false 이면 B 를 반환한다.

    • df['high] > df['targetPrice'] : 당일의 고가가 매수목표가보다 크므로 매수목표가에서 매수가 이루어진다.
    • df['high] < df['targetPrice'] : 매수가 이루어지지 않으므로 drr 은 0 이다.

     

    매수가 이루어진 경우 drr 을 계산하기 위한 식을 보면 수수료를 포함하기 때문에 다소 복잡해 보인다.

    • 수익률의 계산은 기본적으로 (매도가 / 매수가) - 1 로 계산된다. 하지만 이는 수수료가 포함되지 않은 것으로 수수료를 포함하여서 (실매도가 / 실매수가) - 1 의 계산이 이루어져야 한다.
    • '실매도가 * (1 + 수수료) = 매도가' 이므로, '실매도가 = 매도가 / (1 + 수수료)' 이다.
    • '매수가 * (1 + 수수료) = 실매수가' 이므로, '실매수가 = 매수가 * (1 + 수수료)' 이다.

     

    이를 토대로 '수익률 = (매도가 / (1 + 수수료)) / (매수가 * (1+ 수수료)) - 1' 임을 알 수 있다. 위의 전략 설명에서 매도는 익일 시가에 한다고 하였는데, 암호화폐 시장의 특성상 장 마감시간이 존재하지 않으므로 '익일 시가 = 전일 종가' 이기 때문에 매도가로 df['close'] 를 사용하였다. (업비트상 일봉이 넘어가는 기준 시간은 am 9:00 이다.)

     

    이제 하루하루의 수익률을 구했으므로, 이를 활용하여 전체 기간동안의 누적 수익률을 구한다.

    df['crr'] = (df['drr'] + 1).cumprod() - 1

    crrCumulative Rate of Return 으로 누적 수익률을 나타낸다. 누적 수익률은 시작하는 날부터 모든 날의 수익률을 곱하여서 계산할 수 있다. cumprod() 는 where() 과 마찬가지로 numpy 모듈에 포함된 함수로 원소들의 누적 곱을 구하여주는 함수이다. drr 은 단순 수익률을 나타내기 위하여 0 ~ 1 (0% ~ 100%) 의 값을 가지도록 설계하였기 때문에 이 값들의 누적 곱은 0 으로 점점 가까워 질 수 밖에 없다. 이를 해결하기 위해 drr 값에 1 을 더한 값들의 누적 곱을 더한 후 기초값인 1을 빼줘서 마무리한다. 아래 예를 보면 이해하기 쉬울 것 같다.

    • 첫 날 20%, 둘째 날 10% 의 수익률을 올린 경우 drr1 = 0.2, drr2 = 0.1 이다.
    • 이를 단순히 곱하면 0.02, 즉 2% 라는 누적 수익률이 나온다.
    • (1 + 0.2) * (1 + 0.1) - 1 = 1.2 * 1.1 - 1 = 0.32, 즉 32% 라는 올바른 누적 수익률을 얻을 수 있다.

     

    투자에 있어서 수익률만큼 중요한 것이 최대 손실률이다. 다음은 최대 손실률을 구해보자.

    df['dd'] = -(((df['crr'] + 1).cummax() - (df['crr'] + 1)) / (df['crr'] + 1).cummax())
    

    dd Draw Down 으로 최대 누적 수익률 대비 당일 누적 수익률의 손실률을 나타낸다. cummax() 는 마찬가지로 numpy 모듈의 함수로 누적 최대 값을 구하여 준다. 최대 누적수익률에서 당일까지의 누적 수익률을 빼서 최대 누적 수익률에서 잃은 수익률을 구한 후 이를 최대 누적 수익률로 나누어 주면 손실률이 나온다. 손실률이므로 앞에 - 를 붙여준다. 이렇게 각각의 dd 를 구하면, 그 중 최소값( = df['dd'].min() )이 최대 손실률이다.

    여기까지 한 후 데이터를 출력하여 보면 아래와 같이 column들이 새로 추가된 것을 볼 수 있다.

    엑셀로 정리

    이렇게 터미널상으로 확인하는 것은 눈에 잘 들어오지 않는다. 이 데이터들을 엑셀파일로 저장하여 보도록 하자. 파이썬에서 엑셀파일을 다루기 위한 모듈로는 openpyxl 이 있다. 일단 pip install openpyxl 명령어로 모듈을 설치하여 준다. 그 후 DataFrame 을 엑셀 파일로 저장하는 것은 to_excel(file_name) 명령어 하나만 있으면 된다. 프로그램의 맨 아래에 이 코드를 추가하여 준다.

    df.to_excel("crypto_history.xlsx")

    이제 프로그램을 실행시켜 보면 crypto_hitory.xlsx 라는 엑셀 파일이 같은 폴더에 생성되었을 것이다.

    이런 모양을 하고 있다. 파일의 맨 아래까지 내렸을 때, 마지막 데이터의 crr 에서 모든 기간의 누적수익률을 볼 수 있다. drr, crr, dd 는 아무래도 % 로 나타나 있는 것이 보기 편할 것 같다.

    언급한 데이터들을 선택한 후 오른쪽 마우스를 클릭하여 셀 서식에 들어간 후,

    표시 형식에서 백분율을 선택하고 소수 자리수를 2로 설정한 후 확인을 누르면 데이터가 백분율로 표시된다.

     

    결과

    실제 백테스팅은 비트코인(KRW-BTC), 이더리움(KRW-ETH), 리플(KRW-XRP) 세 가지 코인으로 진행하였고, 수수료는 현재 업비트 수수료 기준인 0.05%, 데이터 갯수는 업비트 API 가 최대로 가지고 있는 데이터 수가 1300 정도인 이유로 1300 일간의 데이터로 진행하였다.

     

    비트코인(KRW-BTC)

      기간 누적 수익률 최대 손실률 알고리즘 적용 없이 보유 수익률
    K = 0.1 430.92% -80.03% 1097.44%
    K = 0.2 563.27% -67.23% 1097.44%
    K = 0.3 1229.17% -48.28% 1097.44%
    K = 0.4 2005.12% -25.47% 1097.44%
    K = 0.5 2010.45% -20.24% 1097.44%
    K = 0.6 1256.80% -20.81% 1097.44%
    K = 0.7 1010.61% -23.01% 1097.44%
    K = 0.8 749.33% -20.62% 1097.44%
    K = 0.9 572.70% -21.98% 1097.44%

    시작일인 2017-10-13 의 시가에 비트코인을 매수하여 종료일인 2021-04-24 종가에 매도하였다고 할 때 수익률은 1097.44% 였다. 여기에 변동성 돌파 전략을 적용하면, K 의 값에 따라 최대 2000% 에 달하는 수익률을 얻을 수 있었다. K 의 값이 작을수록 작은 상승세에도 금방 돌파신호로 인식하기 때문에 최대 손실률이 커지는 경향이 있지만, K = 0.4 이상의 값에서는 유의미한 차이가 없음을 볼 수 있다. K 의 값이 일정 수준 이상에서는 커질수록 시가와 매수목표가의 차이가 커져서 그 상승분만큼의 이익을 보지 못하므로 누적 수익률이 떨어진다. K 값에 따른 이러한 성향으로 인하여 변동성 돌파 전략의 사용자들은 K 값을 주로 0.5 로 사용한다. 실제로 이번 백테스팅 결과에서도 K = 0.5 일때 가장 높은 수익률을 보인다.

     

    이더리움(KRW-ETH)

      기간 누적 수익률 최대 손실률 알고리즘 적용 없이 보유 수익률
    K = 0.1 48.63% -96.25% 744.34%
    K = 0.2 237.38% -92.92% 744.34%
    K = 0.3 1724.84% -42.07% 744.34%
    K = 0.4 1954.64% -33.94% 744.34%
    K = 0.5 1152.77% -34.53% 744.34%
    K = 0.6 1978.92% -25.64% 744.34%
    K = 0.7 1351.71% -35.32% 744.34%
    K = 0.8 1175.63% -27.55% 744.34%
    K = 0.9 932.97% -31.96% 744.34%

    비트코인과 다르게 이더리움에서는 K = 0.6 일 때 가장 높은 수익률을 보인다.

     

    리플(KRW-XRP)

      기간 누적 수익률 최대 손실률 알고리즘 적용 없이 보유 수익률
    K = 0.1 85.65% -93.17 522.10%
    K = 0.2 503.18% -76.58% 522.10%
    K = 0.3 958.38% -51.64% 522.10%
    K = 0.4 2046.60% -48.52% 522.10%
    K = 0.5 4412.22% -35.62% 522.10%
    K = 0.6 4412.22% -35.62% 522.10%
    K = 0.7 2485.53% -37.91% 522.10%
    K = 0.8 1494.27% -37.36% 522.10%
    K = 0.9 546.61% -38.86% 522.10%

    리플은 위의 두 코인보다 등락폭이 커서 전략을 통한 수익률이 훨씬 높게 나오는 것 같다. 리플도 마찬가지로 K = 0.5, 0.6 일 때 가장 높은 수익률을 갖는다.

     

    이 데이터를 통해 볼 수 있는 변동성 돌파 전략의 가장 큰 장점 중 하나는 최대 손실률(MDD) 이 낮다는 것이다. 가상화폐는 기본적으로 변동폭이 커서 같은 기간 동안 최대 손실률은 -90% 수준에 달한다. 이는 기간 중 나의 자산이 1/10 이 되는 기간이 있다는 것을 의미하고 일반적인 투자자가 이러한 상황을 버티기란 쉽지 않다. 변동성 돌파 전략은 MDD 를 -20% ~ -30% 수준으로 낮추어 주어서 투자를 유지하기 쉽게 만들고 전략의 신뢰도를 더한다.

     

    완성된 코드는 github.com/poArlim/crypto-auto 에 backtesting.py 라는 이름으로 올려놓았다.

     

    오늘은 가상화폐 자동매매 프로그램을 만들기 위한 변동성 돌파 전략과 백테스팅을 진행해보았다. 다음 시간에는 실제로 이를 구현하여 자동매매 프로그램을 만들어 볼 것이다. 기다려주세요~!

    반응형

    댓글 7

    • firenews 2021.05.06 14:54

      이용약관위배로 관리자 삭제된 댓글입니다.

    • firenews 2021.05.25 22:03

      이용약관위배로 관리자 삭제된 댓글입니다.

    • waterfront9 2021.08.30 00:35 신고

      안녕하세요, 깃헙과 블로그 잘 봤습니다! 한가지 질문이 있는데, 수수료 계산식에서 매수와 매도 둘 다 (1+fee)가 아닌 (1-fee)로 보정해야하지 않을까요? 왜냐하면 (수수료 미고려시) Seed/Price = Tot_BTC라고 한다면 업비트 기준으로 살 때는 코인 구매량 대비 0.05%를 떼가므로 Seed/Actual_Price = Tot_BTC*0.9995 라서 Actual_Price=실매수가=Price/0.9995입니다. 반면 매도시에는 최종 정산량의 0.05%를 떼가므로 (수수료 미고려시) Seed = Price*Tot_BTC일 때 Actual_Seed=Seed*0.9995=(Price*0.9995)*Tot_BTC이고 괄호안의 숫자가 실매도가이기 때문입니다. 혹시 제가 잘못 생각하고 있다면 피드백 부탁드립니다. 저는 코인 퀀트 자동매매 시작한지 1주일 밖에 되지 않았는데 요즘 동지를 만날 때마다 반갑습니다.... ㅎㅎ

      • aa_rong_blog 2021.08.30 18:07 신고

        안녕하세요. 긴 글 읽어주셔서 감사합니다!
        우선 제 생각을 말씀드리겠습니다.
        1. 제 포스팅에서의 실 매수가의 계산식은
        실 매수가 = 매수가 * (1+fee)
        이고 댓글로 적어주신 거는
        실 매수가 = 매수가 / (1-fee)
        여서 단순히 (1+fee) 와 (1-fee) 의 차이가 아니라 곱하기와 나누기의 차이도 있어서 사실상 비슷한 값이 나올 것 같네요! 실제로 0.9995로 나누는 것은 1.000502... 을 곱하는 것쯤의 값이 나오네요!
        2. 동일하게 실매도가도 저의 계산식은 (1+fee) 로 나누고, 적어주신 계산은 (1-fee) 을 곱하는 식인 것 같습니다!
        수수료 계산을 위해 세운 식에 차이가 있지만 결과는 같다고 생각해요. 감사합니다 :)

    • waterfront9 2021.08.30 01:31 신고

      1줄 요약하자면... (df['close'] / (1 + fees)) / (df['targetPrice'] * (1 + fees)) 이 값에서 (수수료에 의해) 분모에서 비싸게 사고 분자에서 싸게파는 건 맞아서 방향성은 동일할 것 같은데 (df['close'] * (1 - fees)) / (df['targetPrice'] / (1 - fees)) 아닌가라는 의미였습니다.

    • 타이거웅스 2021.12.03 15:02 신고

      많은 도움이 되었습니다!! 구독 박습니다!

    • 배효성 2022.02.20 17:30

      안녕하세요 한가지 여쭤볼게 있어서 문의드립니다
      해당 소스에서
      df['drr'] = np.where(df['high'] > df['targetPrice'], (df['close'] / (1 + fees)) / (df['targetPrice'] * (1 + fees)) , 1)
      return df['drr'].cumprod()[-2]
      누적 수익률 구하면 보통 몇나오나요?
      k = 0.1 crr ========== 0.9833693097420789
      k = 0.2 crr ========== 0.9910064574119131
      k = 0.3 crr ========== 0.99251372291732
      k = 0.4 crr ========== 0.9900431438178441
      k = 0.5 crr ========== 0.9875848337784414
      k = 0.6 crr ========== 0.985138701632031
      k = 0.7 crr ========== 0.9827046571125407
      k = 0.8 crr ========== 0.980282610843804
      k = 0.9 crr ========== 0.9778724743286202
      Best K ========== 0.3
      이렇게 나오면 틀린건가요?

Designed by Tistory.