일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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
- sklearn
- 주가데이터
- 사이킷런
- 랜덤포레스트
- 경력기술서 첨삭
- 퀀트 투자 책
- 이력서 첨삭
- 하이퍼 파라미터 튜닝
- 주식데이터
- 베이지안 최적화
- 대학원
- 데이터 사이언티스트
- 데이터사이언티스트
- 데이터사이언스학과
- 데이터 사이언스
- 데이터사이언스
- 주요 파라미터
- 파라미터 튜닝
- 자기소개서
- 하이퍼 파라미터
- 퀀트
- 파이썬
- pandas
- Today
- Total
GIL's LAB
실험 4. 투자 지표를 가지고만 3개월씩 투자해보기 본문
* 이번 실험부터는 실험 설계, 실험 코드, 실험 결과를 하나의 포스팅에 정리합니다.
따로따로 하다보니 가독성만 안 좋아지는 듯 하네요.
개요
일반적으로, EPS, ROE, 순이익률, 영업이익률은 높을수록 투자할 가치가 있고, PER, PBR은 낮을수록 투자할 가치가 있다고 알려져 있다. 그래서 이번 실험에서는 이전 포스팅에서 수집한 투자지표를 가지고 투표했을 때의 효과에 대해 분석한다. 단, 주의해야할 사항으로는 각 지표는 현재 분기가 끝난 다음에나 알 수 있다는 것이다. 예를 들어, 글을 쓰고 있는 시점이 2021년 4분기지만, 4분기 실적은 당연히도 4분기가 끝난 다음에 나온다. 그것도 바로 나오는 것이 아니고, 1-3분기 보고서는 +45일, 4분기는 +90일까지 제출을 하면 된다.
따라서 이번 실험에서는 실적을 발표해야 하는 마감기한에 실적이 발표되었다고 가정하고 실험을 진행한다.
분기별 실적보고서 발표일의 마감기한은 다음과 같다.
- 1분기 실적보고서 마감일: 5월 15일
- 2분기 실적보고서 마감일: 9월 14일
- 3분기 실적보고서 마감일: 11월 14일
- 4분기 실적보고서 마감일: 내년 3월 30일
준비물
실험 내용
모든 종목에 대해, 각 분기 실적보고서 마감일날 주가 (마감일날에 장이 열리지 않으면, 가장 가까운 미래의 날짜의 주가를 활용)와 다음 분기 실적 보고서 마감일날 주가의 차이와 실적보고서에 있는 투자지표들 간 상관관계를 분석한다.
예를 들어 설명하면 다음과 같다. 모든 종목에 대해, 2016년 11월 14일의 주가와 2016년 9월 14일 주가의 퍼센트 차이 (=이익 혹은 손실 비율)와 2016년 9월 14일에 발표된 2분기 실적의 투자 지표와의 관계를 확인한다. 이때, 2016년 11월 14일의 이익 혹은 손실 비율은 2016년 2분기 실적을 보고 투자한 결과라고 간주한다. 이 실험에서는 (1) 일반적으로 투자지표를 보고 투자했을 때의 효과가 있는지, (2) 투자지표를 이용한 적절한 투자 전략이 무엇인지를 파악한다.
실험 과정 및 소스코드
먼저 필요한 모듈을 불러오고 데이터가 있는 경로로 설정하는 등 기본적인 설정을 한다.
import os
import pandas as pd
import numpy as np
import warnings
import matplotlib.pyplot as plt
from scipy import stats
import seaborn as sns
plt.rcParams["figure.figsize"] = (20,10)
warnings.filterwarnings("ignore")
sns.set()
os.chdir(r"C:\Users\Gilseung\Desktop\Jupyter\GILLAB\QUANT_DATA\201609~202108")
그리고나서 모든 종목에 대해 주가 데이터와 투자 지표 데이터를 병합한 데이터를 저장할 total_df를 빈 데이터프레임으로 생성한다.
또, 투자지표 폴더 내에 있는 모든 파일명이 종목명이어서 os.listdir 함수를 이용하여 파일명 목록을 가져온다.
total_df = pd.DataFrame()
stock_file_name_list = os.listdir("투자지표") # 투자지표 폴더에 있는 모든 파일이 종목명으로 되어 있음
투자지표에 "10,234,567"과 같이 콤마가 섞여 있는 숫자는 int나 float 형으로 변환이 불가능하다.
따라서 이러한 숫자들을 전처리하기 위한 함수를 정의한다.
# 숫자에 콤마가 껴있어서 문자로 인식되는 경우를 처리하는 함수
def clean_str_num(value):
if type(value) == str:
return float(value.replace(',', ''))
else:
return value
이제 투자 지표 데이터와 주가 데이터를 불러와서 df로 병합한 뒤, 이 df를 total_df에 추가한다.
구체적인 과정은 다음과 같다. 먼저, 위에서 설명한 바와 같이, 2021년 2분기 실적은 9월 14일에 발표난다고 가정했으므로 해당 주가 데이터가 없다 (글을 쓰는 시점은 9월 9일이다). 그래서 index_df에서 2021년 2분기 실적 데이터를 삭제한다. 그리고 주가 데이터와 손쉽게 병합하기 위해, 분기별 실적발표날을 직접 정의해준다. 그 다음으로 주가 데이터와 인덱스 데이터를 각각 날짜와 실적발표날 날짜를 기준으로 병합한다. 가령, 2021년 1분기에는 2021년 5월 17일 주가가 매칭된다. 마지막으로 해당 시점에 투자를 했을 때 예상되는 기대수익을 계산한다. 예를 들어, 2021년 3월 30일에 주가가 1만원이었고 5월 17일 주가가 1만 5천원이라 하면, 2020년 4분기 투자지표를 보고 투자하면 50%의 기대수익이 발생하게 된다.
for stock_file_name in stock_file_name_list:
# 데이터 불러오기
index_df = pd.read_csv("투자지표/{}".format(stock_file_name), encoding = "cp949") # 투자 지표 데이터
index_df = index_df.iloc[1:] # 2021년 2분기 삭제
for col in index_df.columns[:-1]:
index_df[col] = index_df[col].apply(clean_str_num)
index_df['실적발표날_날짜'] = [20210517, # 2021년 1분기
20210330, 20201116, 20200914, 20200515, # 2020년 4분기, 3분기, 2분기, 1분기
20200330, 20191114, 20190916, 20190515, # 2019년 4분기, 3분기, 2분기, 1분기
20190401, 20181114]
if stock_file_name in os.listdir("주가/KOSPI"):
price_df = pd.read_csv("주가/KOSPI/{}".format(stock_file_name), encoding = "cp949") # 주가 데이터
elif stock_file_name in os.listdir("주가/KOSDAQ"):
price_df = pd.read_csv("주가/KOSDAQ/{}".format(stock_file_name), encoding = "cp949")
df = pd.merge(price_df, index_df, left_on = "날짜", right_on = "실적발표날_날짜")
# 현재 투자 지표를 보고 투자했을 때의 기대 이익 (손실)
profit_ratio = (df['종가'].values[:-1] - df['종가'].values[1:]) / df['종가'].values[1:]
profit_ratio = np.insert(profit_ratio, 0, np.nan)
df['기대이익'] = profit_ratio
total_df = pd.concat([total_df, df], axis = 0, ignore_index = True)
마지막으로 필요한 컬럼만 선정하여, 분석을 위한 준비를 마무리한다.
X_cols = ['주당순이익(EPS,연결지배)', '주당순이익(EPS,개별)', 'PER (배)', '주당순자산(지분법)', 'PBR (배)', 'ROE (%)', '순이익률 (%)', '영업이익률 (%)']
total_df = total_df[X_cols + ["기대이익", "분기"]]
상관관계 분석
먼저 피어슨 상관계수를 이용하여, 각 지표와 기대이익 간 선형상관관계를 파악한다.
# 상관관계 분석: 대부분 유효하지 않음 -- 일단 선형 관계는 거의 없다고 봐도 무방
for col in X_cols:
temp = total_df[[col, "기대이익"]].dropna() # x와 y 모두 결측이 아닌 경우만 걸러냄
print(col, stats.pearsonr(temp[col], temp["기대이익"]))
순서대로 상관계수와 p-value인데, 대부분 상관계수가 유의하지 않거나 유의하더라도 0에 가깝다.
즉, 예상한대로 지표와 기대이익 간 선형관계는 존재하지 않는다.
산점도로 시각화하여 지표와 기대이익 간 분포가 어떻게 되어 있는지를 확인하자.
for col in X_cols:
print(col)
temp = total_df[[col, "기대이익"]].dropna() # x와 y 모두 결측이 아닌 경우만 걸러냄
plt.scatter(temp[col], temp['기대이익'], alpha = 0.5)
plt.show()
아래는 PER과 기대이익 간 산점도이다.
PER뿐만 아니라, 다른 지표에서도 이상치가 있음을 알 수 있다 (x축의 범위를 설정하지 않았는데, PER의 최대값때문에 10만이 넘게 설정되어 있다).
그래서 이상치를 제거하기 위한 함수를 작성하여 활용한다. 여기서는 IQR 기반의 이상치 탐지를 사용하도록 하자.
def find_inlier(df, col):
Q1 = np.quantile(df[col], 0.25)
Q3 = np.quantile(df[col], 0.75)
IQR = Q3 - Q1
# outlier면 False, inlier면 True
return (df[col] < Q3 + 1.5 * IQR) & (df[col] > Q1 - 1.5 * IQR)
이제 이 함수를 적용한 다음에 다시 산점도를 찍어보도록 하자.
for col in X_cols:
print(col)
temp = total_df[[col, "기대이익"]].dropna()
temp = temp.loc[find_inlier(temp, col)]
plt.scatter(temp[col], temp['기대이익'], alpha = 0.5)
plt.show()
마찬가지로 x축이 PER이고 y축이 기대이익인데, 특별한 선형관계는 보이지 않는다.
소결론: 어떤 지표도 기대이익과 선형 관계가 있지는 않다.
독립표본 t-검정
이제는 기대이익이 양수인 그룹과 음수인 그룹의 투자지표가 어떻게 다른지를 분석해보자.
각 지표에 대해, 기대이익이 양수인 컬럼의 값들을 good에, 음수인 컬럼의 값들을 bad에 저장하였다.
그리고 나서 두 그룹의 평균 차이가 통계적으로 유의한지 확인하기 위해, 독립표본 t-검정을 수행한다.
사실 많은 통계분석 기법이 그렇지만, t-검정을 하려면 몇몇 조건을 확인해야 한다.
그러나 여기서는 t-검정 자체가 목적이 아니기에 이러한 조건 확인은 생략하였다.
for col in X_cols:
# 이상치 제거
temp = total_df[[col, "기대이익"]].dropna()
temp = temp.loc[find_inlier(temp, col)]
good = temp.loc[total_df['기대이익'] > 0, col].values
bad = temp.loc[total_df['기대이익'] < 0, col].values
print(col)
print("수익 그룹 평균:{}, 손실 그룹 평균: {}, t-test 결과 (p-value): {}".format(good.mean(), bad.mean(), stats.ttest_ind(good, bad)[1]))
sns.distplot(good, label = "profit", hist = False)
sns.distplot(bad, label = "loss", hist = False)
plt.legend()
plt.show()
순이익률과 영업이익률을 제외한 모두가 그룹 간 평균 차이가 있음을 확인했다. 즉, 순이익률과 영업이익률은 기대이익과는 관련이 없다고 결론내릴 수 있다.
PBR의 결과를 예를 들어 결과를 해석해보자.
수익 그룹 평균:1.2846524550414224, 손실 그룹 평균: 1.4345503805960522, t-test 결과 (p-value): 1.522704457412984e-25
먼저, 수익이 난 종목들의 PBR의 평균은 1.28, 손실이 난 종목들의 PBR의 평균은 1.43이었으며, 이는 통계적으로 유의한 차이이다. 그러면 저 그룹 간 평균의 중심을 기준으로 백테스트를 해볼 수 있지 않을까? 다시 얘기해서, PBR이 1.28과 1.43의 중심인 1.355보다 큰 종목의 기대수익과 그렇지 않은 종목의 기대수익을 비교하면 PBR을 기준으로 투자하는 것이 적절한지를 판단할 수 있을 것이다.
그러면 백테스트를 하기 위한 함수를 만들어보자.
이 함수는 동일한 시드머니로 A와 B그룹에 속한 종목들을 동일가중평균으로 구매했을 때, A그룹에 투자하는 것이 B그룹에 투자하는 것에 비해 어느정도 이익이 나는지를 비교한다.
다시 얘기해서, 시드머니가 1억이고 A 그룹에 10개의 종목이 있다면 각 종목을 1천만원어치씩, B그룹에 20개의 종목이 있다면 각 종목을 5백만원어치씩 구매한다는 이야기이다. 그 다음에 기대 수익에 따라 얻게 되는 수익을 계산한다. 가령 500만원에 구매한 종목의 기대이익이 0.2이면, 이 종목은 600만원이 되어 100만원의 수익이 생기는 것이고, 기대이익이 -0.2라면 이 종목은 400만원이 되어 100만원의 손실이 생기는 것이다.
def backtest(seedmoney, A, B):
# 동일한 금액으로 각 종목에 투자
A_money = np.array([seedmoney / len(A)] * len(A))
B_money = np.array([seedmoney / len(B)] * len(B))
A_money = A_money * (A + 1)
B_money = B_money * (B + 1)
return int(A_money.sum() - B_money.sum()) # A에 투자했을 때 이익 - B에 투자했을 때 이익
모든 지표에 대해 위 함수를 적용해보자.
여기서 백테스트는 1억원을 가지고 good_group과 bad_group에 대해 수행하였다.
good_group과 bad_group은 수익이 나는 그룹의 지표의 평균과 그렇지 않은 그룹의 지표의 평균을 바탕으로 설정한다.
# PER과 PBR만 이익이 나는 것을 확인
for col in X_cols:
# 이상치 제거
temp = total_df[[col, "기대이익"]].dropna()
temp = temp.loc[find_inlier(temp, col)]
good = temp.loc[total_df['기대이익'] > 0, col].values.mean() # 수익이 나는 그룹의 평균
bad = temp.loc[total_df['기대이익'] < 0, col].values.mean() # 손실이 나는 그룹의 평균
threshold = (good + bad) / 2 # good과 bad의 중간으로 설정
if good > bad:
good_group = temp.loc[total_df[col] > threshold, '기대이익'].values
bad_group = temp.loc[total_df[col] < threshold, '기대이익'].values
elif bad > good:
good_group = temp.loc[total_df[col] < threshold, '기대이익'].values
bad_group = temp.loc[total_df[col] > threshold, '기대이익'].values
print(col, good, bad, backtest(10e8, good_group, bad_group))
결과는 아래와 같이, PER과 PBR을 가지고 투자하면 이익을 보지만, 나머지를 가지고 투자하면 손해를 본다는 것을 확인했다.
이 결과가 매분기 유효한지 확인하기 위해, 분기별 분석을 추가로 진행하자.
먼저, 위의 결과를 바탕으로 threshold를 설정한다.
PER_threshold = (17.727183327906218 + 19.02762184508268) / 2
PBR_threshold = (1.2846524550414224 + 1.4345503805960522) / 2
그리고 각 분기에 대해서만 백테스트를 진행한 결과를 저장하여 시각화한다.
# PER과 PBR만 이익이 나는 것을 확인
# 분기별로 확인
Q_list = ['2020-4Q', '2020-3Q', '2020-2Q', '2020-1Q', '2019-4Q', '2019-3Q',
'2019-2Q', '2019-1Q', '2018-4Q', '2018-3Q']
Q_list = sorted(Q_list, reverse = False)
for col, threshold in zip(["PER (배)", "PBR (배)"], [PER_threshold, PBR_threshold]):
backtest_result = []
for Q in Q_list:
temp = total_df.loc[total_df["분기"] == Q, [col, "기대이익"]].dropna()
good_group = temp.loc[temp[col] < threshold, '기대이익'].values
bad_group = temp.loc[temp[col] >= threshold, '기대이익'].values
backtest_result.append(backtest(10e8, good_group, bad_group))
plt.scatter(range(len(backtest_result)), backtest_result, s = 100, label = col.split(' ')[0])
plt.xticks(range(len(Q_list)), Q_list)
plt.axhline(y=0, color='r', linestyle='-')
plt.legend()
그 결과는 아래와 같다.
먼저 알 수 있는 것은 2019년 2분기부터 2020년 1분기까지는 PER과 PBR 어느 지표를 보고 투자하더라도 평균적으로 손실이 났지만, 그 외에서는 모두 이익이 났다. 그리고 2018년 4분기를 제외하면 PBR을 기준으로 투자하는 것이 더 리스키하다. 즉, 수익을 낼때는 더 수익을 내지만, 손해를 볼때도 더 큰 손해를 본다.
실험 결과 요약
- 무작정 지표가 낮거나 높다고 투자하는 것은 아무런 의미가 없음 (상관관계 분석 결과 참고)
- EPS, ROE, PER, PBR 가운데 그나마 참고할만한 지표는 PER과 PBR임을 확인하였음
- PER이 18미만인 종목 그룹과 PBR이 1.35미만인 종목 그룹에 대해 동일 가중 평균 구매를 하면, 평균적으로 이익을 냄을 확인하였음
- PER보다 PBR 기준 투자의 베타값이 더 큼을 확인함
실험에 사용한 전체 소스코드는 아래와 같습니다.
수집하고 싶은 금융 데이터나 실험하고 싶은 퀀트 관련 아이디어가 있으면 댓글로 남겨주세요!
관련 포스팅을 준비하도록 하겠습니다!
'퀀트 투자 > 실험 일지' 카테고리의 다른 글
실험 6. 증권사의 의견에 따라 투자해보기 (1) | 2021.10.03 |
---|---|
실험 5. 주가가 상승하기 전의 시계열 패턴 찾기 (feat. 쉐이플릿) (0) | 2021.09.14 |
실험 3. 이전 종가의 등락폭을 가지고 미래 종가의 등락폭 예측하기 (3) 실험 결과 해석 및 백테스팅 (0) | 2021.09.08 |
실험 3. 이전 종가의 등락폭을 가지고 미래 종가의 등락폭 예측하기 (2) 실험 코드 (0) | 2021.09.07 |
실험 2. 종목 간 선후행 관계 파악 (2) 실험 코드 (0) | 2021.09.07 |