Scribbling

<1> PBR과 주식 수익률 본문

Knowledge Repo/Quant

<1> PBR과 주식 수익률

focalpoint 2021. 10. 13. 21:42

 

2021년 2분기즈음 한국 주식시장에 훈풍이 불면서, 나는 퀀트와 자동매매에 관심을 갖게 되었다.

"Quant 이모저모" 게시판에는, 당시 내가 해보았던 작업을 기록해두고자 한다.

이는 다양한 Backtesting과 자동매매에 대한 내용을 포함할 것이다.

 

첫 글에서 다루고 싶은 내용은 PBR과 주식 수익률의 상관관계이다.

이는 퀀트에 관심을 갖고 찾아보다보면 매우 흔히 접하게 되는 주제이기도 하다.

 

짬이 조금이라도 있는 주식 투자자라면, 'PBR' 지표는 들어보았을 것이다.

PBR(Price-to-Book Ratio)은 말 그대로 장부가 대비 주가를 의미한다.

PBR이 높다는 의미는 장부가 대비 주가가 높다는 것이며,

반대로 PBR이 낮다는 것은 장부가 대비 주가가 낮다는 것이다.

 

PBR이 낮은 주식은 퀀트적 관점에서는 '저평가 주식'을 의미하므로,

PBR이 낮은 주식을 매수하여 일정 기간 후에 매도하는 방식으로 수익을 올릴 것으로 기대할 수 있다.

 

아래의 소스 코드는 이를 검증한다.

코드의 마지막 줄에 있는 함수의 매개 변수를 바꾸면서 직접 테스트가 가능하다.

* 매개변수 * 

- market: "KOSPI", "KOSDAQ", "MIX" 중 선택 가능하다. "KOSPI"을 선택할 경우, 코스피에 상장된 종목만을 대상으로 백테스트한다. "MIX"는 KOSPI와 KOSDAQ 시장을 섞어서 테스트한다.

- start_year: 투자 시작 연도

- start_month: 투작 시작 월

- period: 종목 보유 기간으로, 'year', 'half year', 'quarter', 'month' 중 선택 가능하다.

 

선택한 시장에서 PBR Value 기준으로 하위 50개 종목을 선택하되, 이 중 PBR 값이 0.2보다 작은 종목은 제외한다. 이는 PBR 값이 너무 작은 경우, 저평가된 주식이라기 보다는 '어서 둠황쳐' 쪽에 가깝다고 할 수 있다. 

 

아래 표는 코스닥 시장으로 테스트한 결과이다.

2010년 11월부터 6개월간 보유하는 조건이며,

수익률은 1.0이 본전, 1.2292면 +22.92%라고 해석하면 된다.

수익률 수치만 놓고 보면 '별거 없네'하기 쉽지만, 2010년 11월부터 2021년 5월까지의 누적 수익률은 9.0 (+800%)가 넘는다.

 

코스피 시장으로 테스트한 결과는 아래와 같다. 코스닥에는 못미치지지만, 누적 수익률이 4.6 (+360%)에 달한다.

 

백테스트 결과는 PBR 값이 낮은 주식이 대체로 높은 수익률을 낸다고 할 수 있다.

그러나 단순히 PBR 값이 작은 주식을 산다면 위와 같은 수익률을 절대로 달성할 순 없을 것이다.

이는 백테스트의 한계 때문이다.

백테스트 결과가 현실의 매매 결과를 완벽히 뒷받침하지 못하는 데에는 여러가지 요인이 있는데,

이 글에서 언급하기에는 너무 머리아플 듯하여 생략한다.

다만 핵심만 요약하자면 이렇다: Low PBR 주식은 대부분 소형주인데, 거래량이 충분하지 않다. 때문에 백테스트와 같은 가격에 매수할 가능성이 거의 없다. 이는 매도하는 경우에도 마찬가지이다.

 

from pykrx import stock
import pandas as pd
from datetime import datetime, timedelta
import numpy as np

"""
market: "KOSPI" "KOSDAQ" "MIX"
start_year: 투자 시작 연도 ('2014' ;str)
start_month: 투자 시작 월 ('11', ;str)
period: 'year', 'half year', 'quarter', 'month' ;str
"""
def lowPBR_backTesting(market: str='KOSPI', start_year: str='2014', start_month: str='11', period: str='half year'):
    fromDate = start_year + start_month + "01"
    if period == 'year':
        duration = 365
        counter = 1
    elif period == 'half year':
        duration = 182
        counter = 2
    elif period == 'quarter':
        duration = 91
        counter = 4
    elif period == 'month':
        duration = 30
        counter = 12
    else:
        print(period + '는 지원되지 않습니다.')
        return

    this_fromDate = fromDate
    this_toDate = datetime.strftime(datetime.strptime(this_fromDate, "%Y%m%d")
                                    + timedelta(days=duration), "%Y%m%d")
    count = 0

    term_list = []
    profit_list = []

    while True:
        """ 1년 단위 날짜 보정 """
        if count == counter:
            this_fromDate = this_fromDate[:4] + start_month + "01"
            count = 0

        open_fromDate = stock.get_nearest_business_day_in_a_week(this_fromDate, prev=False)
        open_toDate = stock.get_nearest_business_day_in_a_week(this_toDate, prev=True)
        if market == "KOSPI" or market == "KOSDAQ":
            df = stock.get_market_fundamental_by_ticker(open_fromDate, market=market)
            df = df.sort_values(by='PBR', ascending=True)
            df = df[df['PBR'] > 0.2]
            lowPBR_list = df.index[:50]
        elif market == "MIX":
            df = stock.get_market_fundamental_by_ticker(open_fromDate, market="KOSPI")
            df = df.sort_values(by='PBR', ascending=True)
            df = df[df['PBR'] > 0.2]
            temp1 = df.index[:25].to_list()
            df = stock.get_market_fundamental_by_ticker(open_fromDate, market="KOSDAQ")
            df = df.sort_values(by='PBR', ascending=True)
            df = df[df['PBR'] > 0.2]
            temp2 = df.index[:25].to_list()
            lowPBR_list = temp1 + temp2
        else:
            print("INVALID MARKET")
            return

        code_list = []
        buy_price = []
        sell_price = []
        yield_list = []

        for i in range(len(lowPBR_list)):
            # 거래 시작일에 거래 정지였던 종목 배제
            db = stock.get_market_ohlcv_by_date(open_fromDate, open_fromDate, lowPBR_list[i])
            if db.iloc[0]['시가'] == 0:
                continue

            code_list.append(lowPBR_list[i])
            buy_price.append(db.iloc[0]['종가'])

            ds = stock.get_market_ohlcv_by_date(open_toDate, open_toDate, lowPBR_list[i])
            # print(lowPBR_list[i])
            # print(ds)
            if ds.empty:
                with pd.option_context('display.max_rows', None, 'display.max_columns', None):
                    # print(stock.get_market_ohlcv_by_date(open_fromDate, open_toDate, lowPBR_list[i]))
                    dx = stock.get_market_ohlcv_by_date(open_fromDate, open_toDate, lowPBR_list[i])
                    sell_price.append(dx[dx['시가'] > 0].iloc[-1]['종가'])
            else:
                sell_price.append(ds.iloc[0]['종가'])

        for i in range(len(buy_price)):
            yield_list.append((sell_price[i] - buy_price[i]) / buy_price[i] + 1)

        term_list.append(open_fromDate + '~' + open_toDate)
        profit_list.append(round(np.mean(yield_list), 4))
        print('기간: ' + open_fromDate + '~' + open_toDate)
        print(round(np.mean(yield_list), 4))

        this_fromDate = this_toDate
        this_toDate = datetime.strftime(datetime.strptime(this_fromDate, "%Y%m%d")
                                    + timedelta(days=duration), "%Y%m%d")
        count = count + 1

        if datetime.strptime(this_toDate, "%Y%m%d").date() > datetime.now().date():
            break

    data = {'기간': term_list, '수익률': profit_list}
    df = pd.DataFrame(data=data)
    file_name = './lowPBR_backTesting_' + market + '.xlsx'
    writer = pd.ExcelWriter(file_name, engine='xlsxwriter')
    df.to_excel(writer)
    writer.close()

lowPBR_backTesting(market="KOSPI", start_year="2011", start_month="01", period="half year")