GIL's LAB

실험 10. 상승 반전형 단일 캔들 패턴에 따른 수익률 비교 본문

퀀트 투자/실험 일지

실험 10. 상승 반전형 단일 캔들 패턴에 따른 수익률 비교

GIL~ 2021. 12. 29. 20:44

안녕하세요. 이번 포스팅에서는 상승 반전형 단일 캔들 패턴이 발생한 다음 날 매수하여 n (n = 10, 20, 60) 영업일 동안 보유했다가 매도했을 때의 수익률을 비교해보겠습니다. 

 

대상 캔들 패턴

여기서 실험하고자 하는 캔들 패턴은 망치형, 역망치형, 잠자리형 세 가지 패턴입니다.

 

망치형

매수 신호 패턴으로, 다음 그림과 같이 캔들 몸통 아래에만 꼬리가 생기는 패턴을 말합니다. 

규칙

  • 양봉 생성: 당일 종가 > 당일 시가
  • 고가와 종가가 같음: 당일 고가 == 당일 종가.

 

파라미터

  • 꼬리 길이: (당일 시가 - 당일 저가) / 당일 저가 * 100
  • 몸통 길이: (당일 종가 - 당일 시가) / 당일 시가 * 100

 

역망치형

대표적인 매수 신호 패턴으로, 다음 그림과 같이 캔들 몸통 위에만 꼬리가 생기는 패턴

규칙

  • 양봉 생성: 당일 종가 > 당일 시가
  • 저가와 시가가 같음: 당일 저가 == 당일 시가.

 

파라미터

  • 꼬리 길이: (당일 고가 - 당일 종가) / 당일 종가 * 100
  • 몸통 길이: (당일 종가 - 당일 시가) / 당일 시가 * 100

 

잠자리형

잠자리형: 매수 신호 패턴으로, 다음 그림과 같이 아래 꼬리만 생기는 패턴

규칙

  • 고가, 시가, 종가가 같음: 당일 고가 == 당일 종가 == 당일 시가

 

파라미터

  • 꼬리 길이: (당일 시가 - 당일 저가 / 당일 저가) * 100

 

대상 종목

실험에 사용할 주가 데이터는 2018년 1월 1일 이전에 코스피에 상장된 모든 기업의 2018년 1월 1일부터 2021년 1월 1일까지 3개년 데이터로 한정하겠습니다.

 

 

실험 코드

1. 모듈 설치

anaconda prompt에서 pip을 사용해 qspy 패키지를 설치해주겠습니다. 이 패키지에는 다양한 데이터가 포함되어 있기 때문에, 설치에 제법 오랜 시간이 걸립니다. 이미 설치한 분이라면 이 단계를 스킵해도 좋습니다.

pip install git+https://github.com/GilseungAhn/qspy.git

 

2. 데이터 준비

ListingDate 컬럼이 2018년 1월 1일 이전인 데이터를 stock_list에 다음과 같이 저장하겠습니다. 

from qspy import datasets
import pandas as pd
stock_list = datasets.load_stock_list(market = "KOSPI")
stock_list = stock_list.loc[stock_list['ListingDate'] < pd.to_datetime("2018-01-01")]

이제 stock_list에 속한 종목 코드를 기준으로 주가 데이터를 가져오겠습니다. 2018년 1월 1일부터 2020년 1월 1일까지의 데이터는 이미 패키지에 포함되어 있으므로, download를 False로 설정하겠습니다.

data_list = datasets.load_stock_data_list(stock_list["Symbol"],
                                          start_date = "2018-01-01",
                                          end_date = "2021-01-01",
                                          download = False)

데이터에 저가와 고가 등이 0인 잘못된 레코드가 종종 있기에, 이러한 데이터는 제거해주겠습니다 (추후 버전에서 이러한 데이터를 제거한 결과를 리턴하도록 수정하겠습니다).

data_list = [data.loc[data['Low'] != 0].reset_index(drop = True) for data in data_list]

 

3. 실험용 함수 생성

실험에 필요한 모듈을 추가로 불러오겠습니다.

from qspy.analysis.candle_pattern import *
from qspy.validation import ror_buy_and_hold
import numpy as np

qspy.analysis.candle_pattern은 캔들 패턴이 등장하면 True를 그렇지 않으면 False를 반환하는 함수로 구성되며, validation.ror_buy_and_hold는 매수 시점과 보유 기간을 주면 거기에 맞는 수익률을 반환하는 함수로 구성됩니다. 참고로 매매시에 발생하는 세금과 수수료도 포함합니다.

 

이제 패턴이 발생했을 때 매수해서 n 영업일 후에 매도하는 함수를 작성하겠습니다.

def calc_ror_buy_and_hold_after_patt(data, pattern_func, period):
    patt_arr = pattern_func(data)
    buy_arr = np.insert(patt_arr[1:], -1, False)
    result = ror_buy_and_hold(data = data, period = period, buy_arr = buy_arr)
    return result

data에 pattern_func 함수를 적용한 결과를 patt_arr에 저장합니다. pattern_func은 패턴의 발생 여부를 탐지하는 함수이므로, patt_arr에서 True인 점에서 해당 패턴이 발생했다고 할 수 있습니다.

그러나 패턴이 발생하는 것을 아는 것은 그날 장이 마감된 이후이므로, 실제 매수는 그 다음날 할 수 있습니다. 

따라서 patt_arr[1:]을 buy_arr로 하되, 맨 마지막에는 그 다음 날 데이터가 없으므로 False를 추가합니다.

마지막으로 ror_buy_and_hold 함수를 이용하여 수익률 목록을 계산하고 반환합니다.

 

4-1. 10 영업일 동안 보유

패턴이 발생한 다음날 매수해서 10영업일 동안 보유했다가 매도했을 때의 수익률을 계산하겠습니다.

먼저, 수익률을 저장할 세 개의 리스트를 초기화합니다.

hammer_ror_list = []
inverted_hammer_ror_list = []
dragon_fly_doji_ror_list = []

다음으로 data_list를 data로 순회하면서, calc_ror_buy_and_hold_after_patt 함수를 사용하여 hammer, inverted_hammer, dragon_fly_doji 함수를 각각 적용한 결과를 초기화한 리스트에 추가하겠습니다.

period = 10
for data in data_list:
    hammer_ror_list += calc_ror_buy_and_hold_after_patt(data, hammer, period)
    inverted_hammer_ror_list += calc_ror_buy_and_hold_after_patt(data, inverted_hammer, period)
    dragon_fly_doji_ror_list += calc_ror_buy_and_hold_after_patt(data, dragon_fly_doji, period)

이제 각 리스트를 시리즈로 변환해서 통계량을 계산한 뒤, 각 통계량을 열별로 병합한 결과를 확인하겠습니다.

result = pd.concat([pd.Series(hammer_ror_list).describe(),
                    pd.Series(inverted_hammer_ror_list).describe(),
                    pd.Series(dragon_fly_doji_ror_list).describe()], axis = 1)

result.columns = ["망치형", "역망치형", "잠자리형"]
display(result.round(3))

 

망치형과 역망치형은 10 영업일 간 평균 수익률이 2%이고 중위수도 모두 양수로 나름 참고할만한 패턴임을 알 수 있습니다. 그러나 잠자리형은 나머지 두 패턴에 비해 수익률도 낮고 발생 빈도도 낮은 것을 알 수 있습니다. 또한 중위수가 음수로 과반은 손해를 봄을 알 수 있습니다. 

물론 꼬리의 길이와 몸통의 길이 등 다양한 파라미터를 조정하거나 다른 데이터를 사용하면 다른 결과가 나올 수도 있습니다. 

 

 

4-2. 20 영업일 동안 보유

위 코드에서 period 변수만 20으로 바꾼 결과를 확인해보겠습니다.

hammer_ror_list = []
inverted_hammer_ror_list = []
dragon_fly_doji_ror_list = []
period = 20
for data in data_list:
    hammer_ror_list += calc_ror_buy_and_hold_after_patt(data, hammer, period)
    inverted_hammer_ror_list += calc_ror_buy_and_hold_after_patt(data, inverted_hammer, period)
    dragon_fly_doji_ror_list += calc_ror_buy_and_hold_after_patt(data, dragon_fly_doji, period)

 

result = pd.concat([pd.Series(hammer_ror_list).describe(),
                    pd.Series(inverted_hammer_ror_list).describe(),
                    pd.Series(dragon_fly_doji_ror_list).describe()], axis = 1)

result.columns = ["망치형", "역망치형", "잠자리형"]
display(result.round(3))

역시 영업일을 늘리더라도 망치형과 역망치형에 비해 잠자리형은 좋은 수익을 기대하긴 어려움을 알 수 있습니다.

 

4-3. 60 영업일 동안 보유

마지막으로 60 영업일 동안 보유했을 때의 수익을 확인해보겠습니다.

hammer_ror_list = []
inverted_hammer_ror_list = []
dragon_fly_doji_ror_list = []
period = 60
for data in data_list:
    hammer_ror_list += calc_ror_buy_and_hold_after_patt(data, hammer, period)
    inverted_hammer_ror_list += calc_ror_buy_and_hold_after_patt(data, inverted_hammer, period)
    dragon_fly_doji_ror_list += calc_ror_buy_and_hold_after_patt(data, dragon_fly_doji, period)
result = pd.concat([pd.Series(hammer_ror_list).describe(),
                    pd.Series(inverted_hammer_ror_list).describe(),
                    pd.Series(dragon_fly_doji_ror_list).describe()], axis = 1)

result.columns = ["망치형", "역망치형", "잠자리형"]
display(result.round(3))

60 영업일, 즉 패턴이 발생한 다음 약 3개월을 보유하면 최댓값은 당연히 커지지만 평균적으로 수익은 더 감소함을 알 수 있습니다. 다시 말해, 위에서 확인한 세 개의 패턴은 단기적으로만 참고할만한 패턴이라고 할 수 있습니다.

 

전체 소스 코드는 아래에서 다운로드받을 수 있습니다.

단일 패턴 등장에 따른 수익률 비교.ipynb
0.01MB

 

수집하고 싶은 금융 데이터나 실험하고 싶은 퀀트 관련 아이디어가 있으면 댓글로 남겨주세요! 
관련 포스팅을 준비하도록 하겠습니다!



출처: https://gils-lab.tistory.com/41 [GIL's LAB]

Comments