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


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

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



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


이전 포스트에 이어서 이번 포스트에는 총 수익률을 계산해서 차트로 보여주는 코드에 대해 알아 보겠다. 다음은 전체 코드이다.

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


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

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

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

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

async def req_divide_count(market, quarter, divide):
    log.debug("Requesting divide count list - quarter : {}, divide : {}".format(quarter,divide))

    global dB

    try:
        total_count = await dB.req_stock_index_count(market, quarter)
        count = int(total_count/divide)
        last_count = total_count-count*(divide-1)

    except Exception as e:
        log.warning("Requsting divide count list Error : {}".format(repr(e)))
        raise

    log.debug("About Count per search : {}".format(count))

    return (count, last_count)


async def req_backtesting_data(market, start, end, quarter, offset, limit, name):
    log.info("Requesting backtesting data by year, index")

    global dB

    try:
        data = await dB.req_backtesting_data(market, start, end, quarter, offset, limit)

        data['Date'] = data['Date'].astype('datetime64[ns]')
        name = 'Rank '+str(name)
        profit_by_date = pd.DataFrame(data.groupby(data['Date'])['Profit'].mean().rename(name))

    except Exception as e:
        log.warning("Requesting backtesting data Error : {}".format(repr(e)))
        raise

    return profit_by_date

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

    try:
        counts = await req_divide_count(market, quarter, divide)
        futures = [asy.ensure_future(req_backtesting_data(market, start, end, quarter,i*counts[0], counts[0], i+1)) for i in range(divide-1)]
        futures.append(asy.ensure_future(req_backtesting_data(market, start, end, quarter,counts[0]*(divide-1), counts[1], divide)))

        results = await asy.gather(*futures)
        result = reduce(lambda df1, df2: pd.merge(df1, df2, how='outer', left_index=True, right_index=True), results)
        result = result.fillna(0)

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

    return result

async def load_index_profit(market, start, end):
    log.info("Loading {} index profit".format(market))

    global dB

    try:
        result = await dB.req_index_data(market, start, end)
        criteria = result.iloc[0][0]
        result = (result/criteria).Close.astype(float).rename(market+' buy/hold')
    except Exception as e:
        log.warning("Loading {} index profit Error : {}".format(market, repr(e)))

    return pd.DataFrame(result)

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

    try:
        market_data = await  load_index_profit(market, start, end)
        data = pd.merge(data, market_data, how='outer', left_index=True, right_index=True)

        data = data.fillna(method='ffill').fillna(method='bfill')
        axes = 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)))


async def main(loop):
    log.info("Starting main function")

    during = "12"

    index = ConstVar.PBR
    divide = 5
    market = ConstVar.KOSDAQ
    start = ConstVar.start_2008
    end = ConstVar.start_2018
    slip = 0.003
    settings = ConstVar.get_settings(start,end, index)

    try:
        await db_init(loop, during, index)
        data = [await load_backtesting_data(market, setting[0], setting[1] , setting[2], divide) for setting in settings]
        profit = reduce(lambda df1, df2: df1.append(df2), data)
        cul_profit = (profit+1- slip).cumprod()
        await show_chart(cul_profit, index, market, start, end)

    except Exception as e:
        log.warning("Error : {}".format(repr(e)))
        raise
    finally:
        log.info("Finishing main function")

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


큰 부분부터 하나씩 세세하게 설명하겠다. 먼저 main() 함수이다. 우리는 "asyncio"(aiomysql 패키지가 asyncio 패키지를 사용한다.) 를 사용하기에 루프를 생성해 main() 메소드에 전달한다. "during"은 이전 포스트에 말한 DB에 전달되는 값이다. divide는 지표를 기준으로 종목을 몇 등분할지 정하는 변수이다. "5"라면 5등분한다. start와 end는 백테스팅할 기간이다. slip은 세금 및 기타 요인으로 인해 부과되는 슬리피지 값이다. 수익율을 구할 때 슬리피지를 고려할 것이다. settings는 이전 포스트에서 설명한 "get_settings()" 메서드를 통해 백테스팅할 기간은 연도별로 나눈다. 나누는 이유는 연마다 종목을 리밸런싱 해야하기 때문이다. "db_init()" 메서드는 이전 포스트에서 본 StockDB 클래스의 객체 인스턴스를 초기화한다. "load_bactesting_data"는 "setting[0]"에서 setting[1]"까지 기간동안 "index" 값의 "setting[2]" 분기를 divide로 나눴을 때 각 나뉜 범위의 수익율을 구한다. 예로 load_backtesting_data(ConstVar.KOSDAQ, "2010-01-01", "2011-01-01", ConstVar._09_3Q, 5) 가 호출됬다고 하자. 그러면 2009년 3분기 PBR 값을 기준으로 모든 종목을 5등분한다. 각 등분 내에 있는 코스닥 종목들의 2010년 1월 1일부터 2011년 1월 1일까지 매수 매도 할때의 수익율을 구하고 리턴한다. 5등분이기 때문에 2010년에서 2011년까지의 수익율 데이터은 5개가 된다.  그리고 for문을 통해 각 연도마다 이를 구하고 data에 저장한다. 그리고 reduce, append 메서드를 이용해 data에 저장된 연도별 수익율을 이어준다. 그리고 각 수익율에 1을 더한 후 슬리피지를 빼 "cumprod()"를 이용해 누적곱을 구한다. 그러면 누적 수익율을 계산할 수 있다. 이 누적 수익율을 show_char() 메서드를 이용해 표로 나타낸다.       

async def main(loop):
    log.info("Starting main function")

    during = "12"

    index = ConstVar.PBR
    divide = 5
    market = ConstVar.KOSDAQ
    start = ConstVar.start_2008
    end = ConstVar.start_2018
    slip = 0.003
    settings = ConstVar.get_settings(start,end, index)

    try:
        await db_init(loop, during, index)
        data = [await load_backtesting_data(market, setting[0], setting[1] , setting[2], divide) for setting in settings]
        profit = reduce(lambda df1, df2: df1.append(df2), data)
        cul_profit = (profit+1- slip).cumprod()
        await show_chart(cul_profit, index, market, start, end)

    except Exception as e:
        log.warning("Error : {}".format(repr(e)))
        raise
    finally:
        log.info("Finishing main function")

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


다른 메서드에 대해 알아보자. "db_init" 메서드는 간단해 생략한다. 다음은 load_backtesting_data() 메서드이다. 해당 메서드의 기능은 위에서 설명했다. "req_divide_count()" 메서드는 quarter 분기 지표 값으로 divide 등분할 때 각 등분에 몇 개의 종목이 포함되는 지를 리턴한다. 예로 시가총액 기준으로 10등분했다고 하자. 시가총액이 0이 아닌 총 주식이 1005개이라면 10등분할 때 100개씩 9개, 105 한 개로 나눌 수 있다. 'req_divide_count()" 메서드는 앞에서와 같이 계산해 (100, 105) 를 리턴한다. 

그 다음 "req_backtesting_data()"를 비동기적으로 호출한다. 이 메서드는 start부터 end 까지의 각 등분의 매수/매도 수익율을 가져온다. 추가로 append를 한 이유는 위 예에서 본 것 같이 주식을 지표로 나눌 때 마지막 등분은 105로 똑같이 나눈 후의 나머지 부분도 포함해야 하기 때문이다. 이 result의 최종 결과는 각 등분의 매수/매도 수익율 배열이다. 이를 하나의 데이터 프레임으로 만들기 위해 merge를 호출하고 NA 부분을 0으로 해준다.  

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

    try:
        counts = await req_divide_count(market, quarter, divide)
        futures = [asy.ensure_future(req_backtesting_data(market, start, end, quarter,i*counts[0], counts[0], i+1)) for i in range(divide-1)]
        futures.append(asy.ensure_future(req_backtesting_data(market, start, end, quarter,counts[0]*(divide-1), counts[1], divide)))

        results = await asy.gather(*futures)
        result = reduce(lambda df1, df2: pd.merge(df1, df2, how='outer', left_index=True, right_index=True), results)
        result = result.fillna(0)

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

    return result


req_divide_count() 메서드는 위에서 설명한 내용을 이해했다면 코드가 바로 이해될 것이다. "req_backtesting_data()" 메서드를 보자. 먼저 db에서 기간동안 수익율 데이터를 불러온다. 그 다음 groupby를 통해 Date 별로 수익율의 평균을 구한다. db에서 불러온 데이터는 지표에 의해 n등분된 모든 주식들의 수익율을 나타낸다. 따라서 일자별 수익율로 나타내기 위해 groupby를 이용하고 평균을 구한 다음 이를 리턴한다. 

async def req_backtesting_data(market, start, end, quarter, offset, limit, name):
    log.info("Requesting backtesting data by year, index")

    global dB

    try:
        data = await dB.req_backtesting_data(market, start, end, quarter, offset, limit)

        data['Date'] = data['Date'].astype('datetime64[ns]')
        name = 'Rank '+str(name)
        profit_by_date = pd.DataFrame(data.groupby(data['Date'])['Profit'].mean().rename(name))

    except Exception as e:
        log.warning("Requesting backtesting data Error : {}".format(repr(e)))
        raise

    return profit_by_date


마지막으로 show_chart() 메서드이다. load_index_profit 메서드로 코스피 또는 코스닥 지수 buy_and_hold 수익율을 구한다. 그리고 인자로 받은 data와 merge를 해 하나로 만든 후 plot 메서드를 이용해 해당 데이터를 차트로 보여준다. datacursor 메서드는 차트의 라인을 클릭할 때 클릭한 곳의 value를 보여준다.

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

    try:
        market_data = await  load_index_profit(market, start, end)
        data = pd.merge(data, market_data, how='outer', left_index=True, right_index=True)

        data = data.fillna(method='ffill').fillna(method='bfill')
        axes = 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)))


다음은 시가총액 기준으로 5등분해 2008년부터 2018년까지 백테스팅한 결과이다. 결과는 처참한 걸 알 수 있다. 하지만 PBR이랑 시가총액을 합쳐서 수익율을 구한다면 어떻게 될까? 기대 이상이다. 이에 대한 포스팅은 다음부터 이어갈 예정이다. 또한 전체적인 분석은 해당 시리즈의 마지막 포스트에 올릴 예정이다. 코드에 대한 첨부파일은 아래에 있다.


여기까지 "시가 총액/PER/PBR 기준 변동성 돌파 전략 분석 프로그래밍" 시리즈의 첫 번째 부분을 마쳤다. 다음 포스트부터는 각 지표 하나씩이 아닌 몇 개 합쳐서 변동성 돌파 전략을 하는 프로그램에 대해 설명할 예정이다. 시리즈 마지막 포스트에서는 구현한 프로그램들을 통해 최종적으로 변동성 돌파 전략을 분석할 예정이다. 



ConstVar.py

MainFunctions.py

StockDB.py




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


+ Recent posts