일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- AutoML
- 데이터사이언티스트
- 파라미터 튜닝
- 경력기술서 첨삭
- 판다스
- 데이터 사이언티스트
- 머신러닝
- 코딩테스트
- 이력서 첨삭
- 파이썬
- 주요 파라미터
- 베이지안 최적화
- 자기소개서
- 데이터사이언스
- 데이터분석
- 퀀트
- 하이퍼 파라미터
- pandas
- 경력 기술서
- 대학원
- 주가데이터
- sklearn
- 하이퍼 파라미터 튜닝
- 사이킷런
- 데이터 사이언스
- 퀀트 투자 책
- 커리어전환
- 랜덤포레스트
- 데이터사이언스학과
- 주식데이터
- Today
- Total
GIL's LAB
실험 8. 캔들 패턴 분석: (1) 상승장악형, 하락장악형 본문
개요
이번 실험에서는 캔들 패턴을 분석하여 현재 시장에서 매수와 매도 세력 중 어느 세력이 더 강한지를 판단하여 투자하는 전략을 검증해보겠습니다. 굉장히 많은 종류의 캔들 패턴이 있지만, 상승장악형, 하락장악형, 적삼병, 흑삼병, 샛별형이라는 다섯 개의 캔들 패턴을 검증하고, 이번 실험에서는 상승장악형과 하락장악형만 검증해보겠습니다. 구체적으로 각 패턴이 등장했는지 여부를 확인하고, 등장일의 주가와 5, 20, 60, 120 영업일 이후 주가를 비교해보겠습니다.
데이터 준비
이번 실험에는 주가 데이터만 필요합니다. FinanceDataReader를 이용하여 2011년부터 2021년까지의 전 종목의 주가 데이터를 수집했습니다. 주가 데이터를 올리려고 했더니 파일 크기 제한이 있네요.
이제 데이터를 불러옵니다. 패턴이 드러나려면 어느 정도 데이터가 있어야 하므로, 길이가 300이상인 데이터만 필터링하여 sp_data_dict에 추가하겠습니다.
import os
os.chdir("../../데이터")
import pandas as pd
sp_data_dict = dict()
for file_name in os.listdir("주가데이터"):
stock_price_data = pd.read_csv("주가데이터/" + file_name, parse_dates = ['Date'])
if len(stock_price_data) >= 300:
sp_data_dict[file_name[:-4]] = stock_price_data
상승 장악형
상승장악형은 대표적인 매수 신호 패턴으로 다음 그림과 같이 음봉을 완전히 감싸는 양봉이 출현한 패턴을 뜻합니다.
위 그림을 보면 왼쪽에 있는 음봉이 오른쪽에 있는 양봉에 완전히 속함을 알 수 있습니다. 이 패턴을 규칙으로 작성하면 다음과 같습니다. 이처럼 규칙으로 작성해야 프로그래밍하기 좋습니다.
- 전일에 음봉 생성: 전일 시가 > 전일 종가
- 당일에 양봉 생성: 당일 시가 < 당일 종가
- 전일 음봉이 당일 양봉에 포함: 전일 저가 >당일 시가 & 전일 고가 < 당일 종가.
위에서 작성한 규칙을 바탕으로 상승장악형 패턴이 발생했는지 여부를 나타내는 컬럼을 생성해보겠습니다.
for stock_name in sp_data_dict.keys():
stock_data = sp_data_dict[stock_name]
cond_1 = (stock_data['Open'] > stock_data['Close']).values[:-1]
cond_2 = (stock_data['Open'] < stock_data['Close']).values[1:]
cond_3 = (stock_data['Low'].values[:-1] > stock_data['Open'].values[1:])
cond_4 = (stock_data['High'].values[:-1] < stock_data['Close'].values[1:])
cond = cond_1 & cond_2 & cond_3 & cond_4
cond = np.insert(cond, 0, False)
stock_data["상승장악형"] = cond
여기서 values 속성을 사용하여 데이터만 취한 이유는 시리즈 간 연산을 했을 때 인덱스가 달라 계산 결과가 꼬이는 것을 방지하기 위함입니다. cond_1은 전일 시가 > 전일 종가, cond_2는 당일 시가 < 당일 종가, cond_3은 전일 저가 > 당일 시가, cond_4는 전일 고가 < 당일 종가를 나타내며, 모든 조건이 True인 경우에만 상승장악형이라 볼 수 있으므로 교집합을 수행하여 cond에 저장합니다. 단, 0번째 요소는 이전 주가 정보가 없으므로 np.insert 함수를 이용하여 False를 추가해줍니다.
이제 상승장악형이 발생한 다음 날 매수하고 n 영업일 후에 매도했을 때의 수익과 그렇지 않을 때의 수익을 비교하는 함수를 작성하겠습니다.
1 def BE_profit(stock_data, n):
2 pat_idx_list = stock_data.loc[stock_data["상승장악형"]].index
3 no_pat_idx_list = stock_data.index.difference(pat_idx_list)
4 max_idx = max(stock_data.index)
5
6 pat_buy_idx_list = pat_idx_list[pat_idx_list + n < max_idx] + 1
7 pat_sell_idx_list = pat_buy_idx_list + n
8 no_pat_buy_idx_list = no_pat_idx_list[no_pat_idx_list + n < max_idx] + 1
9 no_pat_sell_idx_list = no_pat_buy_idx_list + n
10
11 pat_buy_price_list = stock_data.loc[pat_buy_idx_list, 'Close'].values
12 pat_sell_price_list = stock_data.loc[pat_sell_idx_list, 'Close'].values
13 pat_profit_list = (pat_sell_price_list - pat_buy_price_list) / pat_buy_price_list * 100
14
15 no_pat_buy_price_list = stock_data.loc[no_pat_buy_idx_list, 'Close'].values
16 no_pat_sell_price_list = stock_data.loc[no_pat_sell_idx_list, 'Close'].values
17 no_pat_profit_list = (no_pat_sell_price_list - no_pat_buy_price_list) / no_pat_buy_price_list * 100
18
19 return pat_profit_list.tolist(), no_pat_profit_list.tolist()
- 라인 2: 상승장악형이 True인 행의 인덱스를 정의합니다.
- 라인 3: 상승장악형이 False인 행의 인덱스를 정의합니다.
- 라인 4: 인덱스의 최댓값은 한 인덱스에서 n을 더했을 때 최대 인덱스를 넘어서버리면 오류가 발생하기 때문에 일종의 기준점으로 사용하기 위해 정의합니다.
- 라인 6: pat_idx_list에 n을 더했을 때 max_idx보다 작거나 같은 인덱스만 필터링했습니다. 이렇게 하면 라인 7에서 pat_buy_idx_list에 n을 더하더라도 원 데이터의 인덱스를 넘어서는 일이 발생하지 않습니다.
- 라인 11 - 17: 상승장악형에서의 수익률과 그렇지 않았을 때의 수익률을 각각 계산합니다.
- 라인 19: ndarray 자료형인 pat_profit_list와 no_pat_profit_list를 리스트로 변환합니다. 이는 다른 결과와 손쉽게 합치기 위함입니다.
이제 전 종목과 n = 5, 20, 60, 120에 대해 위 함수를 적용한 뒤, 상승장악형이 발생했을 때 매수하는 전략을 검증해보겠습니다. 먼저, 각 n에 대해서 모든 종목을 순회하면서 BE_profit 함수를 적용한 결과를 각각 total_pat_profit_list와 total_no_pat_profit_list에 추가한 뒤, 평균을 구하겠습니다.
1 pat_mean_list = []
2 no_pat_mean_list = []
3 for n in [5, 20, 60, 120]:
4 total_pat_profit_list = []
5 total_no_pat_profit_list = []
6 for stock_name in sp_data_dict.keys():
7 stock_data = sp_data_dict[stock_name]
8 pat_profit_list, no_pat_profit_list = BE_profit(stock_data, n = n)
9 total_pat_profit_list += pat_profit_list
10 total_no_pat_profit_list += no_pat_profit_list
11
12 pat_mean = np.mean(total_pat_profit_list)
13 no_pat_mean = np.mean(total_no_pat_profit_list)
14
15 pat_mean_list.append(pat_mean)
16 no_pat_mean_list.append(no_pat_mean)
이 결과를 막대 그래프로 시각화해보겠습니다.
1 plt.figure(figsize = (10, 6))
2 plt.bar([0, 2, 4, 6], pat_mean_list, color = "blue", label = "패턴고려O")
3 plt.bar([1, 3, 5, 7], no_pat_mean_list, color = "orange", label = "패턴고려X")
4 plt.ylabel("평균수익률")
5 plt.xticks(range(8), [5, 5, 20, 20, 60, 60, 120, 120])
6 plt.legend(loc = "upper right")
패턴이 발생했을 때 매수하는 것이 그렇지 않았을 때보다 오히려 기대 수익은 더 낮음을 알 수 있습니다.
이번엔 중위수를 확인해보겠습니다.
pat_median_list = []
no_pat_median_list = []
for n in [5, 20, 60, 120]:
total_pat_profit_list = []
total_no_pat_profit_list = []
for stock_name in sp_data_dict.keys():
stock_data = sp_data_dict[stock_name]
pat_profit_list, no_pat_profit_list = BE_profit(stock_data, n = n)
total_pat_profit_list += pat_profit_list
total_no_pat_profit_list += no_pat_profit_list
pat_median = np.quantile(total_pat_profit_list, 0.5)
no_pat_median = np.quantile(total_no_pat_profit_list, 0.5)
pat_median_list.append(pat_median)
no_pat_median_list.append(no_pat_median)
plt.figure(figsize = (10, 6))
plt.bar([0, 2, 4, 6], pat_median_list, color = "blue", label = "패턴고려O")
plt.bar([1, 3, 5, 7], no_pat_median_list, color = "orange", label = "패턴고려X")
plt.ylabel("중위수")
plt.xticks(range(8), [5, 5, 20, 20, 60, 60, 120, 120])
plt.legend(loc = "upper right")
중위수 역시 상승장악형이 나왔을 때 투자하는 것이 부적절함을 보여줍니다.
하락 장악형
하락장악형은 대표적인 매도 신호 패턴으로 다음 그림과 같이 양봉을 완전히 감싸는 음봉이 출현한 패턴을 뜻합니다.
위 그림을 보면 왼쪽에 있는 음봉이 오른쪽에 있는 양봉에 완전히 속함을 알 수 있습니다. 이 패턴을 규칙으로 작성하면 다음과 같습니다.
- 전일에 양봉 생성: 전일 시가 < 전일 종가
- 전일 양봉이 당일 음봉에 포함: 전일 고가 < 당일 시가 & 전일 저가 > 당일 종가.
- 당일에 음봉 생성: 당일 시가 > 당일 종가
위에서 정의한 규칙을 바탕으로 다음과 같이 하락장악형 컬럼을 생성하겠습니다.
1 for stock_name in sp_data_dict.keys():
2 stock_data = sp_data_dict[stock_name]
3 cond_1 = (stock_data['Open'] < stock_data['Close']).values[:-1]
4 cond_2 = (stock_data['Open'] > stock_data['Close']).values[1:]
5 cond_3 = (stock_data['High'].values[:-1] < stock_data['Open'].values[1:])
6 cond_4 = (stock_data['Low'].values[:-1] > stock_data['Close'].values[1:])
7
8 cond = cond_1 & cond_2 & cond_3 & cond_4
9 cond = np.insert(cond, 0, False)
10 stock_data["하락장악형"] = cond
이번에는 이전에 작성한 BE_profit 함수를 조금 더 범용적으로 사용할 수 있도록 패턴의 등장 여부를 나타내는 컬럼을 추가로 입력받도록 수정하겠습니다.
1 def calculate_profit(stock_data, col, n):
2 pat_idx_list = stock_data.loc[stock_data[col]].index
3 no_pat_idx_list = stock_data.index.difference(pat_idx_list)
4 max_idx = max(stock_data.index)
5
6 pat_buy_idx_list = pat_idx_list[pat_idx_list + n < max_idx] + 1
7 pat_sell_idx_list = pat_buy_idx_list + n
8 no_pat_buy_idx_list = no_pat_idx_list[no_pat_idx_list + n < max_idx] + 1
9 no_pat_sell_idx_list = no_pat_buy_idx_list + n
10
11 pat_buy_price_list = stock_data.loc[pat_buy_idx_list, 'Close'].values
12 pat_sell_price_list = stock_data.loc[pat_sell_idx_list, 'Close'].values
13 pat_profit_list = (pat_sell_price_list - pat_buy_price_list) / pat_buy_price_list * 100
14
15 no_pat_buy_price_list = stock_data.loc[no_pat_buy_idx_list, 'Close'].values
16 no_pat_sell_price_list = stock_data.loc[no_pat_sell_idx_list, 'Close'].values
17 no_pat_profit_list = (no_pat_sell_price_list - no_pat_buy_price_list) / no_pat_buy_price_list * 100
18
19 return pat_profit_list.tolist(), no_pat_profit_list.tolist()
라인 1에서 col을 추가로 입력받고, 라인 2에서 stock_data[col]을 사용하는 것을 제외하면 이전에 정의한 BE_profit 함수와 같은 함수입니다.
이전과 같은 방식으로 하락장악형이 등장했을 때 기대 수익과 그렇지 않았을 때의 기대 수익을 비교해보겠습니다.
1 pat_mean_list = []
2 no_pat_mean_list = []
3 for n in [5, 20, 60, 120]:
4 total_pat_profit_list = []
5 total_no_pat_profit_list = []
6 for stock_name in sp_data_dict.keys():
7 stock_data = sp_data_dict[stock_name]
8 pat_profit_list, no_pat_profit_list = calculate_profit(stock_data, col = "하락장악형", n = n)
9 total_pat_profit_list += pat_profit_list
10 total_no_pat_profit_list += no_pat_profit_list
11
12 pat_mean = np.mean(total_pat_profit_list)
13 no_pat_mean = np.mean(total_no_pat_profit_list)
14
15 pat_mean_list.append(pat_mean)
16 no_pat_mean_list.append(no_pat_mean)
17
18 plt.figure(figsize = (10, 6))
19 plt.bar([0, 2, 4, 6], pat_mean_list, color = "blue", label = "패턴고려O")
20 plt.bar([1, 3, 5, 7], no_pat_mean_list, color = "orange", label = "패턴고려X")
21 plt.ylabel("평균")
22 plt.xticks(range(8), [5, 5, 20, 20, 60, 60, 120, 120])
23 plt.legend(loc = "upper right")
하락장악형은 대표적인 매도신호이므로 수익률이 음수일 가능성이 나올 것이라 예상했지만, 이러한 캔들패턴이 등장했음에도 수익이 큰 경우가 있음을 알 수 있습니다.
정확히 알아보기 위해, 통계량을 계산하여 비교해보겠습니다.
1 result = pd.DataFrame()
2 column_names = []
3 for n in [5, 20, 60, 120]:
4 total_pat_profit_list = []
5 total_no_pat_profit_list = []
6 for stock_name in sp_data_dict.keys():
7 stock_data = sp_data_dict[stock_name]
8 pat_profit_list, no_pat_profit_list = calculate_profit(stock_data, col = "하락장악형", n = n)
9 total_pat_profit_list += pat_profit_list
10 total_no_pat_profit_list += no_pat_profit_list
11
12 result = pd.concat([result, pd.Series(total_pat_profit_list).describe()], axis = 1)
13 result = pd.concat([result, pd.Series(total_no_pat_profit_list).describe()], axis = 1)
14 column_names += ["패턴O_{}".format(n), "패턴X_{}".format(n)]
15
16 result.columns = column_names
17 display(result.round(2))
- 라인 1: 결과를 데이터프레임으로 초기화합니다.
- 라인 2: result의 컬럼명을 초기화합니다.
- 라인 12: total_pat_profit_list를 시리즈로 변환한 뒤 describe 메서드를 사용한 결과를 concat 함수를 사용하여 result의 열로 부착합니다.
- 라인 14: 라인 12 – 13에서 컬럼 두 개를 부착했으므로 그때의 컬럼명을 추가합니다.
- 라인 16: result의 컬럼명을 업데이트합니다. 참고로 concat을 사용하여 열 방향으로 시리즈를 부착하게 되면 컬럼명이 0, 1, 2, …로 자동 설정됩니다.
실행 결과를 전반적으로 살펴보면, 패턴이 발생했을 때 매수하는 것이 패턴이 발생하지 않았을 때 매수하는 것보다 낮은 수익을 보임을 알 수 있습니다. 매도 신호이기에 예상한 결과라고 생각할 수도 있지만, 3사분위수나 최댓값은 대부분 양수이고 최댓값이 지나치게 큰 경우도 종종 있습니다.
수집하고 싶은 금융 데이터나 실험하고 싶은 퀀트 관련 아이디어가 있으면 댓글로 남겨주세요!
관련 포스팅을 준비하도록 하겠습니다!
'퀀트 투자 > 실험 일지' 카테고리의 다른 글
실험 10. 상승 반전형 단일 캔들 패턴에 따른 수익률 비교 (0) | 2021.12.29 |
---|---|
실험 9. 평균 회귀 전략 검증 (feat. 첫 성공?) (0) | 2021.11.18 |
실험 7. 골든크로스와 데드크로스의 효과 분석 (2) | 2021.10.05 |
실험 6. 증권사의 의견에 따라 투자해보기 (1) | 2021.10.03 |
실험 5. 주가가 상승하기 전의 시계열 패턴 찾기 (feat. 쉐이플릿) (0) | 2021.09.14 |