한국을 포함하여 미국, 일본 등 여러 국가의 자산에 대한 퀀트 투자를 지원하는 퀀트킹(Quant King) 툴의 백테스트를 결과를 파이썬(Python)으로 읽어 처리하는 방법을 연재합니다. 파이썬으로 변환한 퀀트킹 백테스트 데이터를 이용하여 몇 가지 기초적인 분석을 하는 방법을 구체적인 사례와 함께 소개합니다.
참고: 저자는 퀀트킹과 특별한 이해관계가 있지 않습니다. 동일한 목적으로 구글 시트와 같은 스프레드시트를 사용하는 방법은 [데이터 분석 부록 B1] 투자 전략의 벤치마크 대비 누적 수익률 변화를 살펴보자 (구글 시트 편, feat. 퀀트 투자)부터 몇 편에 걸쳐 연재한 바 있으며, 책 <구글 시트로 시작하는 투자 포트폴리오 분석: 부록 A 퀀트 투자 전략 분석 기초 - 오렌지사과의 불친절한 워크북>으로도 출간되어 있습니다.
참고: 이 연재는 파이썬을 사용하는 기초적인 방법을 설명하지 않습니다. 파이썬에 대한 간단한 소개와 파이썬을 활용하여 계량적 분석을 하는 방법과 사례를 기초 투자 이론과 함께 해설한 책 <파이썬으로 그려보는 투자 포트폴리오 분석 - 정량적 투자 분석을 위한 입문서>을 참고하기 바랍니다. 이 연재는 해당 책의 부록 성격을 가집니다.
주의: 이 글은 특정 상품 또는 특정 전략에 대한 추천의 의도가 없습니다. 이 글에서 제시하는 수치는 과거에 그랬다는 기록이지, 앞으로도 그럴 거라는 예상이 아닙니다. 분석 대상, 기간, 방법에 따라 전혀 다른 결과가 나올 수 있습니다. 데이터 수집, 가공, 해석 단계에서 의도하지 않은 오류가 있을 수 있습니다. 일부 설명은 편의상 현재형으로 기술하지만, 데이터 분석에 대한 설명은 모두 과거형으로 이해해야 합니다.
퀀트킹 백테스트 결과 파일의 생성과 구조
퀀트킹으로 백테스트를 수행하면 다음과 같은 결과 화면이 나옵니다.
해당 화면은 퀀트킹이 제공하는 추천 로직 중에서 1. 마법공식 (저PER + 고ROE)를 선택하여 백테스트한 결과입니다. 상단에 "파일로 내려받기"라는 버튼이 있습니다. 이 버튼을 누르면 백테스트에 대해 정리된 데이터를 CSV 파일로 다운로드할 수 있습니다. 다운로드한 파일은 마이크로소프트 엑셀이나 구글 시트로 열어 볼 수 있습니다.
다음은 다운로드 한 파일을 엑셀에서 열어 본 화면입니다.
C2 셀에 "1. 마법공식 (저PER + 고ROE)"라고 백테스트에 사용한 로직의 이름이 들어 있습니다. 이 셀을 참조하여 데이터에 이름을 붙일 것이기에, 로직 이름이 제대로 들어가 있는지 확인할 필요가 있습니다. 방금 백테스트한 로직 이름이 아니라, 이전 로직 이름이 나타날 수 있습니다.
참고: 이 글을 쓰는 시점 기준으로 제가 파악하기로는 결과는 방금 실행한 백테스트인데 로직 이름은 이전 것이 들어갑니다. 간단한 편법의 하나는 백테스트를 실행하자마자 바로 취소하고 다시 실행하면 제대로 된 이름이 붙은 파일을 만들 수 있습니다.
이 연재에서는 수익률 데이터만 사용하며 다른 정보는 추출하지 않습니다. 소개한 방법을 이용하면, 다른 정보도 어렵지 않게 추출할 수 있을 것입니다.
수익률 표는 다음 왼쪽 화면과 같이 I6 셀부터 시작합니다.
퀀트킹은 월단위로 분석한 결과를 제시합니다. 수익률 표의 시작 위치에서 오른쪽으로는 연간 수익률이 나오고 이어서 1월부터 12월까지 월별로 수익률이 나열되어 있습니다. 아래로는 연도별로 정리되어 있으며, 마지막은 오른쪽 그림과 같이 월평균수익률이 정리되어 있습니다.
파이썬으로 수익률 데이터를 추출하여 정리하기
파이썬에서는 판다스(Pandas)의 pd.read_csv() 함수를 이용하면 CSV 파일을 읽을 수 있습니다.
qk_df = pd.read_csv('data/qk01.csv')
qk_df.columns = range(len(qk_df.columns))
qk_df
CSV 파일을 읽으면 기본으로 행은 0부터 번호가 붙으며, 열은 Unnamed: x와 같이 문자열로 붙습니다. 이를 숫자로 바꿔주기 위해 qk_df.columns = range(len(qk_df.columns))를 사용했습니다. 다음은 실행 결과입니다.
CSV 파일의 첫 줄은 데이터가 비어 있어 제거되어 로직 이름이 (2, 1)가 아닌 (2, 0) 위치에 있습니다. 데이터프레임에서 NaN은 해당 셀 값이 비어있다는 의미입니다.
특정 값이 담긴 셀의 위치를 찾기 위해 다음과 같이 find_qk_xy() 함수를 정의합니다.
def find_qk_xy(_qk_df, _value):
_x = _qk_df.isin([_value]).any(axis = 0).idxmax()
_y = _qk_df.isin([_value]).any(axis = 1).idxmax()
return _x, _y
데이터프레임으로 넘긴 _qk_df에서 지정한 _value가 처음으로 나타나는 위치를 x, y 좌표로 돌려주는 함수입니다. 동일한 값을 가진 셀이 여럿 있다면, 해당 값이 가장 먼저 나타나는 열과 행 번호를 찾아줍니다.
열로 봤을 때 맨 처음 나타나는 위치와 행으로 봤을 때 맨 처음 나타나는 위치를 각각 따로 파악하므로, 동일한 값이 여러 군데 흩어져 있을 경우 의도와 다른 결과가 나올 수 있습니다. 일단 제가 테스트해 본 바로는 별다른 문제가 없었습니다.
cellx, celly = find_qk_xy(qk_df, '포트네임:')
logic_name = qk_df.iloc[celly, cellx+1]
cellx, celly, logic_name
'포트네임:'이라 적힌 셀의 위치를 찾아 그 오른쪽 셀에서 로직 이름을 꺼냅니다. iloc[]은 [행 번호, 열 번호]를 찾기에 [y, x]로 값을 꺼내야 합니다. '행렬'로 기억하면 됩니다. 다음과 같이 제대로 나옵니다.
(1, 0, '1. 마법공식 (저PER +고ROE)')
동일한 방법으로 수익률 표의 시작과 끝을 찾아봅니다.
cellx, celly = find_qk_xy(qk_df, '나의로직')
cellx2, celly2 = find_qk_xy(qk_df, '월평균수익률')
cellx, celly, cellx2, celly2
(8, 4, 8, 26)
찾은 좌표를 이용하여 수익률 표를 꺼내 봅니다.
mr_df = qk_df.iloc[celly:celly2, cellx:cellx+14]
mr_df
시작 위치에서 오른쪽으로는 14개 (년도 + 연간수익률 + 12개 월간 수익률) 열을 꺼내고, 아래로는 '월평균수익률' 직전 행까지 꺼냅니다.
제대로 추출되었습니다. 다음 코드를 이용하여 년과 월에서 숫자만 꺼내 정리합니다.
col_names = mr_df.iloc[0][2:].values
col_names = [int(name.strip('월')) for name in col_names]
row_names = mr_df.iloc[1:, 0].values
row_names = [int(name.strip('년')) for name in row_names]
mr_df = mr_df.iloc[1:, 2:]
mr_df.columns = col_names
mr_df['Year'] = row_names
mr_df = mr_df.set_index('Year')
mr_df
월 이름을 꺼내 col_names에 담고, 년 이름을 꺼내 row_names에 담았습니다. strip() 함수와 int() 함수를 이용하여 col_names와 row_names에서 숫자만 추출합니다. 월간 수익률만 있는 데이터 범위에 대해 열과 행 이름으로 지정하고, 연도를 색인으로 지정했습니다.
행으로는 년, 열로는 월단위로 깔끔하게 정리되었습니다. 이러한 2차원 배열은 사람이 보기에는 좋지만, 기계적으로 처리하기에는 불편합니다. 이 데이터를 매월마다 한 줄에 하나씩 정리하기 위해 다음과 같이 melt() 함수를 사용합니다.
mr_df = mr_df.melt(ignore_index = False, var_name = 'Month', value_name = logic_name).dropna().reset_index()
mr_df
melt() 함수를 이용하면, 각 칼럼의 이름을 새로운 칼럼의 값으로 하는 데이터를 만들 수 있습니다. 칼럼의 이름은 Month로 두고, 수익률은 앞에서 찾은 로직 이름으로 설정하였습니다. 값이 없는 달은 지우기 위해 dropna()를 사용했고, 색인은 이제 유니크(unique) 하지 않기에 reset_index()로 풀었습니다.
순서가 월이 우선이라는 점을 제외하면, 12개월 × 20년 = 240개 데이터로 제대로 정리되었습니다. 참고: 현시점 퀀트킹의 최대 백테스트 기간은 20년입니다.
mr_df['YMonth'] = pd.to_datetime(dict(year = mr_df.Year, month = mr_df.Month, day = 1)).dt.strftime('%Y-%m')
mr_df = mr_df.set_index('YMonth')
mr_df = mr_df[[logic_name]]
mr_df = mr_df.astype(float) / 100
mr_df = mr_df.sort_index()
mr_df
연도와 월을 합해 YYYY-MM 형식의 문자열을 만들어서 YMonth라는 이름의 색인으로 지정하고. 월별 수익률이 든 로직 이름의 열만 꺼냈습니다. 수익률은 100% 단위로 된 문자열이기에 숫자로 변환하고 단위를 조정했습니다. 최종적으로 정렬하면 다음과 같습니다.
투자 성과 분석에서는 수익률보다 자산비가 편리합니다. 2005년 7월부터 데이터가 있으니 2006년 6월에는 자산비로 1일 것입니다. 이를 위해 다음과 같이 가장 오래된 달의 전달을 구해 데이터를 채워 넣고 자산비로 변환합니다.
prev_day = pd.to_datetime(mr_df.index[0]) - pd.Timedelta(1, unit = 'D')
mr_df.loc[prev_day.strftime('%Y-%m')] = 0
mr_df = mr_df.sort_index()
mr_df = (mr_df + 1).cumprod()
mr_df
mr_df의 가장 오래된 년월을 구해 pd.to_datetime() 함수를 이용하여 날짜로 변환하면, 일(day)이 생략되어 있기에 2005년 7월 1일이 됩니다. 하루 전은 전달인 2005년 6월 30이 됩니다. 여기에서 년월을 찾아 데이터에 추가하고 수익률을 0으로 지정합니다. 최종적으로 수익률에 1을 더해 누적 곱을 계산하는 cumprod() 함수로 자산비로 변환합니다. 다음과 같이 월별 자산비로 정리된 데이터가 만들어집니다.
최종 정리한 함수와 실행 결과
설명과 스크린숏이 많아 길어 보이지만, 코드를 모두 묶어 정리하면 다음과 같이 그리 복잡하지 않습니다.
def find_qk_xy(_qk_df, _value):
_x = _qk_df.isin([_value]).any(axis = 0).idxmax()
_y = _qk_df.isin([_value]).any(axis = 1).idxmax()
return _x, _y
def read_qk_csv(_qk_csv_fname):
_qk_df = pd.read_csv(_qk_csv_fname)
_qk_df.columns = range(len(_qk_df.columns))
_cellx, _celly = find_qk_xy(_qk_df, '포트네임:')
_logic_name = _qk_df.iloc[_celly, _cellx+1]
_cellx, _celly = find_qk_xy(_qk_df, '나의로직')
_cellx2, _celly2 = find_qk_xy(_qk_df, '월평균수익률')
_mr_df = _qk_df.iloc[_celly:_celly2, _cellx:_cellx+14]
_col_names = _mr_df.iloc[0][2:].values
_col_names = [int(name.strip('월')) for name in _col_names]
_row_names = _mr_df.iloc[1:, 0].values
_row_names = [int(name.strip('년')) for name in _row_names]
_mr_df = _mr_df.iloc[1:, 2:]
_mr_df.columns = _col_names
_mr_df['Year'] = _row_names
_mr_df = _mr_df.set_index('Year')
_mr_df = _mr_df.melt(ignore_index = False, var_name = 'Month', value_name = _logic_name).dropna().reset_index()
_mr_df['YMonth'] = pd.to_datetime(dict(year = _mr_df.Year, month = _mr_df.Month, day = 1)).dt.strftime('%Y-%m')
_mr_df = _mr_df.set_index('YMonth')
_mr_df = _mr_df[[_logic_name]]
_mr_df = _mr_df.astype(float) / 100
_mr_df = _mr_df.sort_index()
_prev_day = pd.to_datetime(_mr_df.index[0]) - pd.Timedelta(1, unit = 'D')
_mr_df.loc[_prev_day.strftime('%Y-%m')] = 0
_mr_df = _mr_df.sort_index()
_mr_df = (_mr_df + 1).cumprod()
return _mr_df
코드에서 언더바(_)는 함수 내에서 사용하는 변수임을 명확하게 하기 위해 붙인 것입니다. 대부분의 파이썬 개발자는 언더바를 붙여 쓰지 않습니다. 개인적인 취항입니다.
퀀트킹의 추천 로직 4개로 만든 백테스트 결과 파일을 읽어 그래프로 그려보는 코드는 다음과 같습니다.
mr_df1 = read_qk_csv('data/qk01.csv')
mr_df2 = read_qk_csv('data/qk02.csv')
mr_df3 = read_qk_csv('data/qk03.csv')
mr_df4 = read_qk_csv('data/qk04.csv')
mr_df = pd.concat([mr_df1, mr_df2, mr_df3, mr_df4], axis = 1)
mr_df.plot()
plt.yscale('log', base = 1.1)
_, _, ymin, ymax = plt.gca().axis()
step = 20
yticks = np.arange(int((ymin - 1) / step) * step, (int((ymax - 1) / step) + 1) * step, step).round(1)
yticks = np.array([* yticks] + [1, 5, 10])
plt.yticks(yticks + 1, [f'{y * 100:.0f}%' for y in yticks])
plt.xlabel('')
plt.ylabel('누적 수익률')
plt.show()
로그 스케일로 누적 수익률을 그리고 눈금은 퍼센트로 붙이는 방법은 누적 수익률로 그래프로 그려 보자 (로그 스케일에 수익률을 표현하는 방법) [파이썬 분석 3]을 참고하기 바랍니다.
다음과 같은 그래프가 그려집니다.
올해 들어 4개의 추천 로직 모두 크게 성장했음을 알 수 있습니다.
정리하며
퀀트킹으로 실행한 백테스트 결과가 담긴 CSV 파일을 파이썬으로 읽어 이후 분석에 사용할 데이터로 변환하는 방법을 살펴보았습니다. 이 예에서는 연월로 된 문자열을 날짜처럼 사용했는데, 다른 데이터와 함께 분석하기 위해서는 일반 날짜 형식으로 먼저 바꾸어야 합니다. 이에 대해서는 이어지는 글에서 살펴봅니다.
이어지는 글: [파이썬 부록 K2] 기초 자산과 투자 데이터를 정리해 보자 (+퀀트킹을 이용한 퀀트 투자) [모바일]
참고: 질문이나 요청 사항이 있다면 댓글로 남겨 주시기 바랍니다.
참고 도서:
'주식투자' 카테고리의 다른 글
[파이썬 부록 K14 - K15] 시간의 흐름에 따른 성과 변화를 살펴보자 (퀀트킹) (0) | 2025.08.02 |
---|---|
[파이썬 부록 K9 - K12] CVaR 위험 지표를 이용한 기초자산과 퀀트 전략의 분산 투자 분석 (퀀트킹) (0) | 2025.07.27 |
[파이썬 부록 K7 - K8] 분산 투자와 효율적 투자선 그리고 후보 자산들 (퀀트킹) (0) | 2025.07.27 |
[파이썬 부록 K2 - K6] 기초자산과 퀀트 전략의 분산 투자 (퀀트킹) (0) | 2025.07.26 |
[중급 부록 A3] 이 커버드콜 ETF의 기초자산은 무엇일까? (피어슨 상관 계수를 이용한 닮은 ETF 찾기 2) (0) | 2025.07.12 |
[중급 부록 A2] 이 자산은 무엇과 비슷했을까? (피어슨 상관 계수를 이용한 닮은 ETF 찾기) (0) | 2025.07.11 |
[중급 부록 A1] 피어슨 상관 계수의 기하학적 해석 (표준화한 두 자산 간의 선형 상관성) (0) | 2025.07.11 |
해외증권세전합병입금 (상장 폐지로 인한 매도 대금, 한국투자증권 미니스탁) (0) | 2025.07.01 |