GIL's LAB

실험 4. 투자 지표를 가지고만 3개월씩 투자해보기 본문

퀀트 투자/실험 일지

실험 4. 투자 지표를 가지고만 3개월씩 투자해보기

GIL~ 2021. 9. 9. 18:50

* 이번 실험부터는 실험 설계, 실험 코드, 실험 결과를 하나의 포스팅에 정리합니다. 

따로따로 하다보니 가독성만 안 좋아지는 듯 하네요. 

 

개요 

일반적으로, 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 기준 투자의 베타값이 더 큼을 확인함

 

실험에 사용한 전체 소스코드는 아래와 같습니다.

실험4. 투자 지표만 가지고 3개월 투자해보기.ipynb
1.75MB

 

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

 

Comments