<7. 시가 총액/PER/PBR 기준 변동성 돌파 전략 분석 - (1) >


6. 지표 혼합 수익률 백테스팅 프로그래밍 - (3)

해당 포스트를 읽기 전에 위 포스트를 읽길 추천한다.


이번 포스트는 이전 포스트들에서 다루었던 프로그램을 토대로 가치 투자 지표(시가총액, PER, PBR) 기준 변동성 돌파 전략을 분석해 보겠다. 분석하는 데 사용하는 데이터는 현재 코스닥/코스피 시장에 상장되어 있는 전 주식의 2007년 1월 1일부터 2018년 1월 1일까지의 일봉 데이터이다. 일봉 데이터 중 시가, 고가, 저가 데이터를 사용한다. 변동성 돌파 전략의 매수/매도 알고리즘은 아래와 같이 했다.



매수 : ( 전일 고가 - 전일 저가 ) * Scope + 시초가 <= 현재가   [ Scope : 0.2 ~ 1.4 (0.1 간격) ]

* ( 전일 고가 - 전일 저가 )가 0 이상이어야 함.

매도 :  다음 날 시초가



- 시가 총액, PER, PBR이 낮을 수록 변동성 돌파 전략에 효과가 있을까?


대부분의 사람들이 알다시피 PER, PBR, 시가 총액은 가치투자에 중요한 value이다. 보통 시가 총액이 작은 소형주, 낮은 PBR, 낮은 PER을 가진 주식이 가치 투자로써 중장기 투자에 적합하다. 그렇다면 변동성 돌파 전략에도 똑같이 적용될까?



위 차트는 시가총액을 기준으로 10등분한 코스피와 코스닥 종목의 변동성 돌파 전략 수익율이다. 보다 시피 코스피/코스닥 둘 다 시장 수익율보다 한참 못한다. 10년 후에 거의 0퍼가 된다. 코스닥의 경우 사총이 가장 큰 게 그나마 수익율이 높다. 나머지는 비등비등하다. 코스피의 경우 시총이 낮을 수록 수익율이 높은 게 뚜렷하다. 


위 차트는 pbr 기준으로 10등분한 코스피와 코스닥 종목의 변동성 돌파 전략 수익율이다. 코스피와 코스닥 모두 뚜렷한 방향성이 없다.

위 차트는 per 기준으로 10등분한 코스피와 코스닥 종목의 변동성 돌파 전략 수익율이다. 코스피와 코스닥 모두 뚜렷한 방향성이 없다.


시가 총액, pbr, per 각 지표에 대한 차트를 보았다. 어떠한 규칙성을 찾긴 힘들었다. 또한 모두 누적 수익율이 0~1사이에 있기에 서로 간의 차이를 확인하기도 힘들었다. 그래서 슬리피지를 줄어보았다. 위 차트들은 세금 및 기타 수수료를 생각해 각 매매에 0.4%의 슬리피지를 붙였다. 그렇다면 슬리피지를 극단적으로 0으로 하면 어떻게 될까? 




위 차트는 슬리피지 값을 0으로 해 시가총액 기준 10등분한 코스피와 코스닥 종목의 변동성 돌파 전략 수익율이다. 슬리피지 값이 0.4%밖에 차이가 안 나는 데 수익율에 있어 엄청 큰 차이를 보인다. 코스피는 시가총액이 낮으면 압도적으로 수익율이 높다. 하지만 코스닥은 시가총액이 커야 수익율이 높다.


위 차트는 슬리피지 값을 0으로 해 PBR 기준 10등분한 코스피와 코스닥 종목의 변동성 돌파 전략 수익율이다. 코스피의 경우 pbr이 상위와 하위 수익율이 비슷하다. 중간 정도 수치일 경우 수익율이 가장 높다. 코스닥은 pbr이 아예 높거나 pbr이 낮으면 수익율이 좋다.

위 차트는 슬리피지 값을 0으로 해 PER 기준 10등분한 코스피와 코스닥 종목의 변동성 돌파 전략 수익율이다. 코스피의 경우 PER이 높을 수록 수익율이 좋다. 코스닥은 상위 Rank7 만 뚜렷하게 높다. 나머지는 비등비등하다. 



각 지표 별로 변동성 돌파 전략을 백테스팅해봤다. 결과, 지표에 의한 공통적인 뚜렷한 방향성이 그다지 나타나지 않았다. 코스피의 경우 시가총액이 낮고 PER이 높을 수록 수익율이 좋다. 코스닥의 경우 시가총액이 높고 PBR이 아주 높거나 낮어야 수익율이 좋다. 단, 각 지표를 분리하여 수익율을 계산했기에 이들 지표들을 합쳐서 백테스팅할 경우 다른 결과가 나올 수 있다. 이에 대한 분석은 다음 포스트에 설명할 예정이다. 


이번 분석에서 뚜렷한 결과를 얻기 힘들었다. 단, 한 가지 확실한 건 있다. 슬리피지 값이다. 슬리피지를 0.4%로 한 것과 0%로 한 것에 엄청난 수익율 차이가 있었다. 변동성 돌파 전략은 단타를 하기에 슬리피지 요소가 많이 반영되는 것 같다. 그렇다면 슬리피지 값이 거의 0이라고 할 수 있는 ETF에 변동성 돌파 전략을 적용하면 어떻게 될까? 이에 대한 연구는 차차 진행할 예정이다.  




참고로 필자는 컴퓨터 공학과를 재학 중인 대학생입니다. 따라서 코드가 완벽할 수 없습니다. 알고리즘이나 코드가 비효율적이거나 오류가 있다면 댓글 달아주세요..

<6. 지표 혼합 수익률 백테스팅 프로그래밍 - (3) >


1. 시가 총액/PER/PBR 기준 변동성 돌파 전략 분석 프로그래밍 - 개요

2. 각 지표 순위별 수익률 백테스팅 프로그래밍 - (1)

3. 각 지표 순위별 수익률 백테스팅 프로그래밍 - (2)

5. 지표 혼합 수익률 백테스팅 프로그래밍 - (2)

해당 포스트를 읽기 전에 위 포스트를 읽길 추천한다.


이번 포스트는 지난 포스트에 봤던 StockDB.py 코드를 토대로 수익율을 계산하고 차트로 보여주는 코드에 대해 볼 것이다. 이번 포스트는 위에 링크된 "각 지표 순위별 수익률 백테스팅 프로그래밍"에서 본 수익율 계산 코드의 일부분을 사용한다. 즉, 해당 포스트를 이해한다면 이번 포스트는 무지 쉽다. 링크된 포스트에 다 설명되있기 때문이다. 따라서 이번 포스트에서는 자세하게 설명하진 않는다. 이해가 어렵다면 링크된 포스트를 다시 보길 바란다.


전체 소스는 다음과 같다.

import pandas as pd
import logging as log
import StockDB as db
import asyncio as asy
import sys
import Utility
from mpldatacursor import datacursor
from matplotlib.dates import DateFormatter
import matplotlib.pyplot as plt
from functools import reduce

log.basicConfig(stream=sys.stdout, level=log.DEBUG)


async def db_init(loop, during, type,divide):
    log.info("initializing Database")

    global dB
    try:
        dB= db.StockDB( during , type,divide)
        await dB.init_pool(loop)

    except Exception as e:
        log.warning("Database init Error : {}".format(repr(e)))


async def load_kospi_kosdaq_profit():
    log.info("Loading {} kospi/kosdaq profit")

    global dB
    global start
    global end


    try:
        result = await dB.req_kospi_kosdaq_data(start, end)

        criteria = result[0].iloc[0][0]
        kospi_profit = pd.DataFrame((result[0]/criteria).Close.astype(float).rename("kospi buy/hold"))

        criteria = result[1].iloc[0][0]
        kosdaq_profit = pd.DataFrame((result[1] / criteria).Close.astype(float).rename("kosdaq buy/hold"))

    except Exception as e:
        log.warning("Loading kospi/kosdaq profit Error : {}".format(repr(e)))

    return pd.merge(kospi_profit, kosdaq_profit, how='outer', left_index=True, right_index=True)



async def show_chart(data,title):
    log.info("Preparing chart")

    global start
    global end

    try:
        index_profit = await load_kospi_kosdaq_profit()
        final_data = pd.merge(data, index_profit, how='outer', left_index=True, right_index=True)

        final_data = final_data.fillna(method='ffill').fillna(method='bfill')
        axes = final_data.plot(grid=True, title=title)
        lines = axes.get_lines()
        fmt = DateFormatter('%Y-%m-%d')
        datacursor(lines,
                   formatter=lambda **kwargs: 'Return : {y:.4f}'.format(**kwargs) + '\ndate: ' + fmt(kwargs.get('x')))
        plt.show()
    except Exception as e:
        log.warning("Preparing chart Error : {}".format(repr(e)))
        raise

async def load_backtesting_data(start, end, quarter):
    log.info("Loading backtesting data")

    global market
    global dB

    try:
        if market == Utility.ALL:
            try:
                data = await dB.req_backtesting_data(Utility.KOSPI, start, end, quarter)
            except ResourceWarning as re:
                data = pd.DataFrame()
            try:
                data = data.append(await dB.req_backtesting_data(Utility.KOSDAQ, start, end, quarter))
            except ResourceWarning as re:
                if data.empty is True:
                    return

        else:
            data = await dB.req_backtesting_data(market, start, end, quarter)

        data['Date'] = data['Date'].astype('datetime64[ns]')
        profit_by_date = pd.DataFrame(data.groupby(data['Date'])['Profit'].mean())

    except ResourceWarning as re:
        log.warning("No Data")
        return
    except Exception as e:
        log.warning("Loading backtesting data  Error : {}".format(repr(e)))
        raise

    return  profit_by_date


def sorted_data(datas):
    dic = {}
    sorted_data = []
    for i, data in enumerate(datas):
        if data is None:
            continue
        dic[data.index[0]] = i

    dic = sorted(dic.items())
    for i in dic:
        sorted_data.append(datas[i[1]])

    return sorted_data

async  def backtesting(type):
    log.info("Starting backtesting")

    global start
    global end

    try:
        date_settings = Utility.get_date_ranges(start, end)
        quarter_settings = Utility.get_quarter_ranges(start, end)

        futures = [asy.ensure_future(load_backtesting_data(date_setting[0], date_setting[1], quarter_settings[i]))
                   for i, date_setting in enumerate(date_settings)]
        results = await asy.gather(*futures)

        sort_results = sorted_data(results)

        profit = reduce(lambda df1, df2: df1.append(df2), sort_results)
        cul_profit = (profit + 1 - 0.004).cumprod()

        await show_chart(cul_profit,type)
    except Exception as e:
        log.info("Backtesting Error : {}".format(repr(e)))
        raise

async def main(loop):

    global market
    global start
    global end

    market = Utility.KOSDAQ
    during = "6"
    divide = 20
    start="2008-01-01"
    end="2018-01-01"
    type = Utility.TMV_PBR

    await db_init(loop, during, type,divide)
    await backtesting(type)

loop = asy.get_event_loop()
loop.run_until_complete(main(loop))

큰 부분부터 하나씩 세세하게 설명하겠다. 먼저 main() 함수를 보자. market은 코스닥/코스피 종목을 대상으로 할 지 아니면 전체 시장을 대상으로 할지 선택한다. during은 앞에서 많이 언급했다. Scope 값을 결정하는 변수이다. divide는 지표 기준으로 몇 등분할지 결정하는 변수이다. 20을 대입하면 지표 기준 상위 또는 하위 5% 주식을 가져온다. start와 end는 백테스팅 기간이다. type은 어떤 지표들을 가지고 백테스팅할 지 결정하는 변수이다. type에 들어갈 변수들은 이전에 설명했던 Utility.py 에 있다. 아래 코드는 시가 총액과 PBR 이 하위 5%인 주식들의 2008년 1월 1일부터 2018년 1월 1일까지의 수익율 구해 백테스팅한다. 

async def main(loop): global market global start global end market = Utility.ALL # Utility.KOSPI, Utility.KOSDAQ during = "6" divide = 20 start="2008-01-01" end="2018-01-01" type = Utility.TMV_PBR await db_init(loop, during, type,divide) await backtesting(type) loop = asy.get_event_loop() loop.run_until_complete(main(loop))


db_init() 메서드는 코드가 쉽기에 생략한다. backtesting() 메서드를 보자. date_settings와 quarter_settings 를 구한다. 구하는 메서드는 이전 포스트에서 설명했다. load_backtesting_data()는 인자로 받은 기간과 분기 정보를 토대로 매수/매도 수익율을 비동기적으로 반환한다. 비동기적으로 실행되기 때문에 각 load_backtesting_data()의 실행 순서와 리턴 순서가 달라진다. 따라서 리턴된 데이터들의 시간적 순서를 맞춰야 한다. 누적 수익율을 구하기 위해서는 시간적 연속성이 필요하다. 2010년 1월부터 2012년 1월 누적 수익율을 구한다고 가정하자. 그러면 results는 [2011년부터 2012까지 수익율, 2010년부터 2011년까지의 수익율]이 결과로 된다. 이들 순서는 무작위로 된다. 그러면 무엇이 시간적으로 앞에 있는 지 확인할 수 없다. 따라서 정렬을 해주기 위해 sorted_data 메서드를 이용한다. 시간적으로 정렬됬다면 reduce와 append를 이용해 각 연도 수익율 데이터를 합치고 cumprod()를 이용해 누적곱을 구한다. 0.004는 슬리피지 값이다. 마지막으로 show_chart()를 호출해 차트를 보여준다.

async  def backtesting(type):
    log.info("Starting backtesting")

    global start
    global end

    try:
        date_settings = Utility.get_date_ranges(start, end)
        quarter_settings = Utility.get_quarter_ranges(start, end)

        futures = [asy.ensure_future(load_backtesting_data(date_setting[0], date_setting[1], quarter_settings[i]))
                   for i, date_setting in enumerate(date_settings)]
        results = await asy.gather(*futures)

        sort_results = sorted_data(results)

        profit = reduce(lambda df1, df2: df1.append(df2), sort_results)
        cul_profit = (profit + 1 - 0.004).cumprod()

        await show_chart(cul_profit,type)
    except Exception as e:
        log.info("Backtesting Error : {}".format(repr(e)))
        raise

load_backtesting_data() 메서드는 위에서 말한  "각 지표 순위별 수익률 백테스팅 프로그래밍"에서 나오는 부분이기에 생략한다. 

sorted_data는 DB에서 불러온 수익율 데이터들을 시간적으로 정렬한다. 정렬하는 데 각 수익율 데이터에 있는 일자 데이터를 이용한다. 각 수익율 데이터는 데이터 프레임으로 'Date' 인덱스와 "Profit" 칼럼으로 구성되 있다. 이들은 연도별로 구분되기에 겹치는 날짜가 없다. 따라서 Date 인덱스의 첫 번째 값을 기준으로 정렬한다. dic[data.index[0]] = i 는 각 수익율 데이터 프레임의 날짜 데이터를 키로하고 enumerate()로 반환된 i 값을 값으로 하는 딕셔내리를 만든다. 이 i는 i의 키 값을 가지는 datas의 데이터 프레임 인덱스를 가리킨다. 이 딕셔내리를 키 값을 기준으로 정렬한다면 딕셔내리는 날짜 순으로 배열될 것이다. 날짜 순으로 정렬된 딕셔내리의 값은 해당 날짜를 가진 datas의 위치 인덱스이기에 위치 인덱스를 토대로 정렬된 순서대로 새로운 리스트에 데이터를 추가하면 된다. 다음은 이에 대한 로직이다. sorted(dic.items())를 호출하면 키 값을 기준으로 정렬된다. 다만 이 때 데이터 형이 딕셔내리에서 리스트 형태로 바뀐다. 그 다음 for문을 통해 정렬된 순서대로 접근해 datas[i[1]]을 순서대로 리스트에 넣으면 된다. i[1]이 datas의 위치 인덱스가 된다. 

def sorted_data(datas):
    dic = {}
    sorted_data = []
    for i, data in enumerate(datas):
        if data is None:
            continue
        dic[data.index[0]] = i

    dic = sorted(dic.items())
    for i in dic:
        sorted_data.append(datas[i[1]])

    return sorted_data


show_chart() 메서드와 load_kospi_kosdaq_profit() 메서드는 "각 지표 순위별 수익률 백테스팅 프로그래밍"에서 나오는 부분이기에 생략한다.  



아래 사진은 해당 프로그램의 결과 화면이다. 위 전체 소스에 나와있는 설정으로 백테스팅했다. 최대 41배다. 하지만 2018년도 까지 반토막 이상이 났다. 물론 10년간 11배긴 하지만.....


여기까지 해서 시가 총액/PER/PBR 기준 변동성 돌파 전략 분석 프로그래밍 시리즈의 프로그래밍 부분을 마쳤다. 다음 포스트에서는 지금까지 구현한 프로그램들을 토대로 시가 총액/PER/PBR 기준  변동성 돌파 전략이 유용한지 분석해볼 예정이다. 



참고로 필자는 컴퓨터 공학과를 재학 중인 대학생입니다. 따라서 코드가 완벽할 수 없습니다. 알고리즘이나 코드가 비효율적이거나 오류가 있다면 댓글 달아주세요..


+ Recent posts