<2. 각 지표 순위별 수익률 백테스팅 프로그래밍 - (1) >
1. 시가 총액/PER/PBR 기준 변동성 돌파 전략 분석 프로그래밍 - 개요
해당 포스트를 읽기 전에 위 포스트를 읽길 추천한다.
이번 포스트는 각 지표(시가총액, PBR, PER)을 기준으로 모든 주식을 10등분 해 변동성 돌파 전략을 적용한 수익률을 계산하는 프로그램을 구현할 것이다. 이 프로그램을 통해 각 지표와 수익률의 관계를 파악할 수 있다. 아래는 프로그램 결과 화면이다. PBR 기준으로 코스닥 주식을 10등분해 2008년부터 2018년까지의 수익률을 계산했다. Rank 10이 pbr 하위 10%이다.
프로그램 설명
프로그램 코드
먼저 DB관련 코드, 실제 계산 코드 등 공통적으로 사용할 상수나 함수를 정의한 코드에 대해 알아보자. 먼저, 전체 코드를 보고 세부 사항을 설명하겠다.
import logging as log KOSPI = "kospi" KOSDAQ = "kosdaq" start_2008 = "2008-01-01" start_2009 = "2009-01-01" start_2010 = "2010-01-01" start_2011 = "2011-01-01" start_2012 = "2012-01-01" start_2013 = "2013-01-01" start_2014 = "2014-01-01" start_2015 = "2015-01-01" start_2016 = "2016-01-01" start_2017 = "2017-01-01" start_2018 = "2018-01-01" TMVALUE = "tmvalue" PER = "per" PBR = "pbr" _07_3Q = "07_3Q" _07_4Q = "07_4Q" _08_1Q = "08_1Q" _08_2Q = "08_2Q" _08_3Q = "08_3Q" _08_4Q = "08_4Q" _09_1Q = "09_1Q" _09_2Q = "09_2Q" _09_3Q = "09_3Q" _09_4Q = "09_4Q" _10_1Q = "10_1Q" _10_2Q = "10_2Q" _10_3Q = "10_3Q" _10_4Q = "10_4Q" _11_1Q = "11_1Q" _11_2Q = "11_2Q" _11_3Q = "11_3Q" _11_4Q = "11_4Q" _12_1Q = "12_1Q" _12_2Q = "12_2Q" _12_3Q = "12_3Q" _12_4Q = "12_4Q" _13_1Q = "13_1Q" _13_2Q = "13_2Q" _13_3Q = "13_3Q" _13_4Q = "13_4Q" _14_1Q = "14_1Q" _14_2Q = "14_2Q" _14_3Q = "14_3Q" _14_4Q = "14_4Q" _15_1Q = "15_1Q" _15_2Q = "15_2Q" _15_3Q = "15_3Q" _15_4Q = "15_4Q" _16_1Q = "16_1Q" _16_2Q = "16_2Q" _16_3Q = "16_3Q" _16_4Q = "16_4Q" _17_1Q = "17_1Q" _17_2Q = "17_2Q" _17_3Q = "17_3Q" yearly_days = [start_2008, start_2009, start_2010, start_2011, start_2012, start_2013, start_2014, start_2015, start_2016, start_2017, start_2018] _1quarters = [_08_1Q, _09_1Q, _10_1Q, _11_1Q, _12_1Q, _13_1Q, _14_1Q, _15_1Q, _16_1Q, _17_1Q] _3quarters = [_07_3Q, _08_3Q, _09_3Q, _10_3Q, _11_3Q, _12_3Q, _13_3Q, _14_3Q, _15_3Q, _16_3Q] def get_settings(start, end, index): try: start_index = yearly_days.index(start) end_index = yearly_days.index(end) start_days = yearly_days[start_index : end_index] end_days = yearly_days[start_index+1: end_index+1] if index==TMVALUE: indexs = _1quarters[start_index: end_index] else: indexs = _3quarters[start_index: end_index] except IndexError as e: log.warning("Settings index Error : {}".format(repr(e))) raise return zip(start_days, end_days, indexs)
_09_1Q 는 2009년 1분기를 일컫는다. _YY_XQ는 20YY년 X분기를 말한다. 이 상수가 필요한 이유는 시가총액을 기준으로 주식을 나눌 때 해당 년도 1분기를 기준으로 나누고 PER, PBR 기준으로 나눌 때는 작년도 3분기를 기준으로 나누기 때문이다. "_1quarters"와 "_3quarters"는 모든 년도 1분기 상수, 모든 년도 3분기 상수 데이터가 들어있다. "get_settings" 메서드는 계산할 날짜 범위와 분기를 반환한다. 예로 2008년 1월 1일부터 2010년 1월 1일까지 pbr을 기준으로 백테스팅한다고 가정하자. 그러면 "get_settings("2008-01-01","2010-01-01",PBR)을 호출한다. 결과로 zip(["2008-01-01", "2009-01-01"] , ["2009-01-01", "2010-01-01"], ["_07_3Q" , _08_3Q])가 리턴된다. 따라서 결과물에서 순차적으로 ("2008-01-01", "2009-01-01", "_07_3Q") , ("2009-01-01", "2010-01-01", "_08_3Q") 를 접근할 수 있다. 즉, 백테스팅 시작과 끝 기간을 연도별로 쪼개고 그에 맞는 분기 변수를 얻을 수 있다. 연도별로 쪼개야 되는 이유는 연도가 리밸런싱 기준이기 때문이다.
import aiomysql as aio import logging as log import pandas as pd import ConstVar class StockDB(): def __init__(self, during, index): log.info("Connecting to database") self.during = during self.index = index async def init_pool(self,loop): log.info("Connection to Connection Pool") try: self.__pool = await aio.create_pool(host='127.0.0.1', port=3306, user='root', password='qhdks12#$', db='stock',loop=loop) except: log.warning("Connecting Pool Error {}".format(repr(0))) raise async def req_stock_index_count(self,market, quarter): log.debug("Selecting {0} market {1} index {2} row count".format(market, self.index, quarter)) if market == ConstVar.KOSDAQ: sql = "select count(kos.Code) from (select distinct Code from kosdaq_profit_" + self.during + ") as kos join (select Code from " elif market == ConstVar.KOSPI: sql = "select count(kos.Code) from (select distinct Code from kospi_profit_" + self.during + ") as kos join (select Code from " if self.index == ConstVar.TMVALUE: sql = sql + self.index+" where "+quarter+" != 0 ) as tmv on kos.Code=tmv.Code" elif self.index == ConstVar.PBR: sql = sql + self.index + " where " + quarter + " >= 0.2 ) as pb on kos.Code=pb.Code" elif self.index == ConstVar.PER: sql = sql + self.index + " where " + quarter + " >= 2 ) as pe on kos.Code=pe.Code" try: async with self.__pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute(sql) result = await cur.fetchone() except aio.Error as e: log.warning("Selecting stock index Error : {}".format(repr(e))) raise log.debug("Stock index count : {}".format(result[0])) return result[0] async def req_backtesting_data(self,market, start, end, quarter, offset, limit): log.info("Selecting backtesting data") sql = "select kos.Code, kos.Date, kos.Profit from " \ "(select Code, Date, Profit from "+market+"_profit_"+self.during+" where Date>\'"+start+"\' and Date<\'"+end+"\') " \ "as kos join (select Code from " if self.index == ConstVar.TMVALUE: sql = sql + self.index+" where "+quarter+" != 0 order by "+quarter+" desc limit "+str(offset)+", "+str(limit)+") as tmv on kos.Code=tmv.Code;" elif self.index == ConstVar.PBR: sql = sql + self.index + " where "+quarter+" >= 0.2 order by "+quarter+" desc limit "+str(offset)+", "+str(limit)+") as tmv on kos.Code=tmv.Code;" elif self.index == ConstVar.PER: sql = sql + self.index + " where "+quarter+" >= 2 order by "+quarter+" desc limit "+str(offset)+", "+str(limit)+") as tmv on kos.Code=tmv.Code;" log.info("sql {} ".format(sql)) try: async with self.__pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute(sql) rows = await cur.fetchall() result = pd.DataFrame.from_records(list(rows)) result.columns = ['Code','Date', 'Profit'] except aio.Error as e: log.warning("Selecting backtesting data Error : {}".format(repr(e))) raise return result async def req_index_data(self,market, start, end): log.info("Selecting {} index data".format(market)) sql = "select Date, Close from "+market+" where Date<\'"+end+"\' and Date >=\'"+start+"\';" try: async with self.__pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute(sql) rows = await cur.fetchall() result = pd.DataFrame.from_records(list(rows)) result.columns = ['Date', 'Close'] result = result.set_index('Date') except aio.Error as e: log.warning("Selecting backtesting data Error : {}".format(repr(e))) raise return result
세부적으로 살펴보자. 초기화 부분이다. "init_pool()" 로 DB와 커낵션을 생성할 풀을 초기화한다. "index"는 백테스팅할 지표를 나타낸다. 위에서 본 ConstVar 클래스의 TMVALUE(시가총액), PER, PBR 변수가 들어간다. "during"은 scope 값을 결정하는 변수이다. 이전 포스트에서 말했듯이 scope 값을 어떻게 결정할 지가 중요하다. 우리는 scope 값을 과거 몇 개월 동안 가장 수익율을 많이 주는 값으로 정하거나 상수로 정할 수 있다. during이 이 역할을 한다. during이 "6"이면 과거 6개월동안 종목마다 최적화된 Scope, "12"이면 과거 12개월동안 최적화된 Scope다. 이 Scope 값은 매번 계산하면 매우 시간이 오래 걸리기 때문에 DB에 저장되어 있고 저장된 테이블 형식은 "kospi_profit_during", "kosdaq_profit_during" 이 된다. 즉 during이 "6" 이면 "kospi_profit_6" 테이블을 참조할 수 있다.
def __init__(self, during, index): log.info("Connecting to database") self.during = during self.index = index async def init_pool(self,loop): log.info("Connection to Connection Pool") try: self.__pool = await aio.create_pool(host='127.0.0.1', port=3306, user='root', password='qhdks12#$', db='stock',loop=loop) except: log.warning("Connecting Pool Error {}".format(repr(0))) raise
다음 메서드는 지표를 등분하기 위해서 특정 범위에 해당하는 종목 수를 리턴한다. 만약 per 기준으로 등분한다면 per이 3이상인 모든 주식 수를 리턴한다. 시가총액 기준으로 분등한다면 시가총액이 0이 아닌 모든 종목 수를 리턴한다. 이 값을 구하는 이유는 10등분한다면 (전체 종목 수)/10을 구해서 0에서 10% , 10%에서 20% ...의 구체적 수를 정해야 하기 때문이다.
async def req_stock_index_count(self,market, quarter): log.debug("Selecting {0} market {1} index {2} row count".format(market, self.index, quarter)) if market == ConstVar.KOSDAQ: sql = "select count(kos.Code) from (select distinct Code from kosdaq_profit_" + self.during + ") as kos join (select Code from " elif market == ConstVar.KOSPI: sql = "select count(kos.Code) from (select distinct Code from kospi_profit_" + self.during + ") as kos join (select Code from " if self.index == ConstVar.TMVALUE: sql = sql + self.index+" where "+quarter+" != 0 ) as tmv on kos.Code=tmv.Code" elif self.index == ConstVar.PBR: sql = sql + self.index + " where " + quarter + " >= 0.2 ) as pb on kos.Code=pb.Code" elif self.index == ConstVar.PER: sql = sql + self.index + " where " + quarter + " >= 3 ) as pe on kos.Code=pe.Code" try: async with self.__pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute(sql) result = await cur.fetchone() except aio.Error as e: log.warning("Selecting stock index Error : {}".format(repr(e))) raise log.debug("Stock index count : {}".format(result[0])) return result[0]
async def req_index_data(self,market, start, end): log.info("Selecting {} index data".format(market)) sql = "select Date, Close from "+market+" where Date<\'"+end+"\' and Date >=\'"+start+"\';" try: async with self.__pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute(sql) rows = await cur.fetchall() result = pd.DataFrame.from_records(list(rows)) result.columns = ['Date', 'Close'] result = result.set_index('Date') except aio.Error as e: log.warning("Selecting backtesting data Error : {}".format(repr(e))) raise return result
아래 메서드는 market(코스피, 코스닥) 시장 내 self.index의 quarter 분기 지표를 기준으로 내림차순으로 정렬했을 때 offset 순위부터 offset+limit 순위까지 주식의 during 범위 수익율 데이터를 가져온다. 예로 req_backtesting_data(ConstVar.KOSPI, ConstVar.TMVALUE, "2008-01-01", "2009-01-01", ConstVar._08_01, 0 ,100) 을 호출한다 하자. 그러면 시가총액 2008년 1분기 값 상위 1위부터 100위까지의 종목 중 코스닥 시장에 해당하는 종목들의 2008년 1월 1일부터 2009년 1월 1일까지의 수익률 데이터를 리턴한다. 이 수익률 데이터는 self.during 값에 의해서 kosdaq_profit_self.during 테이블에 있는 값을 가져온다.
async def req_backtesting_data(self,market, start, end, quarter, offset, limit): log.info("Selecting backtesting data") sql = "select kos.Code, kos.Date, kos.Profit from " \ "(select Code, Date, Profit from "+market+"_profit_"+self.during+" where Date>\'"+start+"\' and Date<\'"+end+"\') " \ "as kos join (select Code from " if self.index == ConstVar.TMVALUE: sql = sql + self.index+" where "+quarter+" != 0 order by "+quarter+" desc limit "+str(offset)+", "+str(limit)+") as tmv on kos.Code=tmv.Code;" elif self.index == ConstVar.PBR: sql = sql + self.index + " where "+quarter+" >= 0.2 order by "+quarter+" desc limit "+str(offset)+", "+str(limit)+") as tmv on kos.Code=tmv.Code;" elif self.index == ConstVar.PER: sql = sql + self.index + " where "+quarter+" >= 2 order by "+quarter+" desc limit "+str(offset)+", "+str(limit)+") as tmv on kos.Code=tmv.Code;" log.info("sql {} ".format(sql)) try: async with self.__pool.acquire() as conn: async with conn.cursor() as cur: await cur.execute(sql) rows = await cur.fetchall() result = pd.DataFrame.from_records(list(rows)) result.columns = ['Code','Date', 'Profit'] except aio.Error as e: log.warning("Selecting backtesting data Error : {}".format(repr(e))) raise return result
이번 포스트는 여기까지 마무리하고 다음 포스트는 DB에서 불러온 데이터를 가지고 계산해 차트로 보여주는 부분을 구현할 것이다.
참고로 필자는 컴퓨터 공학과를 재학 중인 대학생입니다. 따라서 코드가 완벽할 수 없습니다. 알고리즘이나 코드가 비효율적이거나 오류가 있다면 댓글 달아주세요..
'주식 프로그래밍(시스템 트레이딩)' 카테고리의 다른 글
4. 지표 혼합 수익률 백테스팅 프로그래밍(파이썬) - (1) (0) | 2018.01.24 |
---|---|
3. 각 지표 순위별 수익률 백테스팅 프로그래밍(파이썬) - (2) (0) | 2018.01.23 |
1. 시가 총액/PER/PBR 기준 변동성 돌파 전략 분석 프로그래밍(파이썬) (0) | 2018.01.23 |
파이썬을 이용한 주식 변동성 돌파 전략 분석을 위한 최적화 변수 계산(2) (1) | 2018.01.19 |
파이썬을 이용한 주식 변동성 돌파 전략 분석을 위한 최적화 변수 계산(1) (0) | 2018.01.19 |