자산분배 백테스트(주식+채권 vs 주식+현금)
본문 바로가기
파이썬(Python)/파이썬으로 투자실험

자산분배 백테스트(주식+채권 vs 주식+현금)

by Squat Lee 2024. 3. 25.

자산배분의 기본 개념은 장기적으로 우상향하는 서로 반대로 움직이는 자산에 투자하는 것입니다.

 

보통 주식+채권의 형태로 하는데, 상관성이 0인 현금으로 백테스트를 해 보았습니다.

 

SPY와 TLT 그리고 QQQ와 GLD를 기준으로 현금과 자산배분을 했을시 결과값을 비교했습니다.

 

배당수익, 수수료, 세금은 고려하지 않았습니다. 현금은 수익률을 4%로 설정하였습니다.(현재 외화 RP가 4~4.5%정도 됩니다.)

 

주식과 현금은 최초 동일한 비율로 투입이 되고 각 모멘텀 기간별로 아래와 같이 결과값이 나옵니다. 

구  분 3개월 6개월 9개월 12개월
CAGR MDD CAGR MDD CAGR MDD CAGR MDD
SPY 10.2 -51.5 10.2 -51.5 10.2 -51.5 10.2 -51.5
SPY+TLT 9.9 -39.1 10.1 -37.8 9.9 -36.0 9.9 -34.9
SPY+현금 7.9 -22.3 8.9 -17.7 8.9 -23.1 9 -25.8

Momentum 기간별로 CAGR과 MDD 비교(2005.1.2~2024.2.29)

 

 

 

구  분 3개월 6개월 9개월 12개월
CAGR MDD CAGR MDD CAGR MDD CAGR MDD
 QQQ 14.7 -51.2 14.7 -51.2 14.7 -51.2 14.7 -51.2
QQQ+GLD 15.9 -38.9 14.7 -36.0 14.3 -36.7 14.3 -39.4
QQQ+현금 11.8 -23.5 12.2 -23.4 12.3 -25.1 12.5 -26.0

Momentum 기간별로 CAGR과 MDD 비교(2005.1.2~2024.2.29)

 

 

QQQ로 했을시 CAGR이 12%넘고 MDD도 -23%까지 떨어집니다.

 

당연히 CAGR은 QQQ+GLD가 가장 높지만, 안정적으로 투자를 하시고 싶은 분은 현금과 자산을 분배해서 투자하는 방법도 괜찮은 것 같습니다.

 

백테스트를 한 전체 코드는 아래와 같습니다.

 

import pandas as pd
from pandas_datareader import data as pdr
import yfinance as yf
yf.pdr_override()
from datetime import datetime
import matplotlib.pyplot as plt

pd.options.display.float_format = '{:,.2f}'.format

p1 = 'QQQ'
p2 = 'GLD'
현금비율 = 0 #현금비율

#초기 투입자금
주식자금 = 500
채권자금 = 500
현금자금 = 0

#평균모멘텀스코어 기간
moment_months = 3
start = datetime(2000, 1, 1)
end = datetime(2024, 2, 29)

def get_etf_data(start, end, p):
    if p[-2:] == 'KS': #한국 ETF를 달러로 계산
        df_ex = pdr.get_data_yahoo('USDKRW=X', start, end)  # 환율데이터 가져오기
        df_ko = pdr.get_data_yahoo(p, start, end)
        df = pd.merge(df_ko['Adj Close'], df_ex['Adj Close'], left_index=True, right_index=True)
        df.columns = ['국내ETF', '환율']
        df['Adj Close'] = df['국내ETF'] / df['환율']
    else:
        df = pdr.get_data_yahoo(p, start, end)
    return df

df1 = get_etf_data(start, end, p1)
print(df1)
df2 = get_etf_data(start, end, p2)
print(df2)
df_o = pd.merge(df1['Adj Close'], df2['Adj Close'], left_index=True, right_index=True)
df_o.columns = ['p1', 'p2']
df_o.dropna(inplace=True)

# 월말 날짜만 가져오기
def get_monthly_end(df_d):
    df_d['Odate'] = df_d.index
    df = df_d.groupby(by=[df_d.index.year, df_d.index.month]).last()
    df.set_index('Odate', inplace=True)
    return df

#평균모멘텀스코어 구하기
def score(df_d, moment_months):
    mm = int(moment_months)
    df = get_monthly_end(df_d)
    li_score = []
    for i in range(len(df)):
        if i < 11:
            li_score.append(0)
        else:
            val = 0
            for m in range(1, mm+1):
                if df['p1'].iloc[i] - df['p1'].iloc[i - m] > 0:
                    val += 1
            li_score.append(val / mm)

    df['평균모멘텀스코어'] = li_score
    return df

#MDD 구하기
def get_mdd(col):
    window = 252 #1년간 영업일을 252일로 가정
    peak = col.rolling(window, min_periods=1).max() #기간 동안에 최고 주가(결과값은 시리즈로 나옴)
    drawdown = col/peak - 1 #최고치 대비 현재 주가가 얼마나 하락했는지 구함
    연도별mdd = drawdown.rolling(window, min_periods=1).min()#"최고치-현재주가" 중 가장 작은값을 구함
    mdd = 연도별mdd.min()
    return mdd

#백테스트
def back_test(df_d, moment_months, 현금비율, p1자금, p2자금, 현금자금):
    df = score(df_d, moment_months)
    li_p1 = []
    li_p1c = [] #자산p1의 개수
    li_p2 = []
    li_p2c = [] #자산p2의 개수
    li_cash = []
    li_sum = []
    for i in range(len(df)):
        if i == 0:
            li_p1.append(p1자금)
            li_p1c.append(p1자금/df['p1'].iloc[i])
            li_p2.append(p2자금)
            li_p2c.append(p2자금/df['p2'].iloc[i])
            li_cash.append(현금자금)
            li_sum.append(li_p1[i]+li_p2[i]+li_cash[i])
        else:
            li_sum.append(li_p1c[i-1]*df['p1'].iloc[i] + li_p2c[i-1]*df['p2'].iloc[i] + li_cash[i-1]*(1+0.03/12))
            li_p1.append(li_sum[i]*(1-현금비율)*df['평균모멘텀스코어'].iloc[i])
            li_cash.append(li_sum[i]*현금비율)
            li_p2.append(li_sum[i]-li_p1[i]-li_cash[i])
            li_p1c.append(li_p1[i]/df['p1'].iloc[i])
            li_p2c.append(li_p2[i]/df['p2'].iloc[i])

    df['자산1'] = li_p1
    df['자산2'] = li_p2
    df['현금'] = li_cash
    df['합계'] = li_sum
    df['자산1 개수'] = li_p1c
    df['자산2 개수'] = li_p2c

    # 월말 데이터를 일별 데이터로 만들기
    df = pd.merge(df_o[['p1', 'p2']], df[['평균모멘텀스코어', '자산1', '자산2', '현금', '합계', '자산1 개수', '자산2 개수']],
                  left_index=True, right_index=True, how='outer')
    df.fillna(0, inplace=True)

    for s in range(1, len(df)):
        if df['합계'].iloc[s] == 0:
            df['자산1 개수'].iloc[s] = df['자산1 개수'].iloc[s-1]
            df['자산2 개수'].iloc[s] = df['자산2 개수'].iloc[s-1]
            df['자산1'].iloc[s] = df['p1'].iloc[s] * df['자산1 개수'].iloc[s]
            df['자산2'].iloc[s] = df['p2'].iloc[s] * df['자산2 개수'].iloc[s]
            df['현금'].iloc[s] = df['현금'].iloc[s-1]
            df['합계'].iloc[s] = df['자산1'].iloc[s] + df['자산2'].iloc[s] + df['현금'].iloc[s]
            df['평균모멘텀스코어'].iloc[s] = df['평균모멘텀스코어'].iloc[s-1]

    # 합계열에서 0이 아닌 숫자가 나오면 행삭제
    df.drop(df[df['합계']==0].index, inplace=True)

    # 2가지 자산의 누적수익률을 비교하기 위해 백분율로 표현
    df['주가백분율'] = df['p1'] / df['p1'].iloc[0]
    df['전략백분율'] = df['합계'] / df['합계'].iloc[0]

    #CAGR
    diff = df.index[-1].year - df.index[0].year
    cagr_etf = (df['주가백분율'].iloc[-1] / df['주가백분율'].iloc[0]) ** (1 / diff) - 1
    cagr_전략 = (df['전략백분율'].iloc[-1] / df['전략백분율'].iloc[0]) ** (1 / diff) - 1
    cagr = f'CAGR(ETF) : {round(cagr_etf, 3)} / CAGR(전략) : {round(cagr_전략, 3)}'
    print(cagr)

    #MDD구하기
    mdd_etf = get_mdd(df['p1'])
    mdd_전략 = get_mdd(df['합계'])
    mdd_t = f'MDD(ETF) : {round(mdd_etf,3)} / MDD(전략) : {round(mdd_전략,3)}'
    print(mdd_t)

    #엑셀파일로 만들기
    df.to_excel(f'평균모멘텀스코어 백테스트({p1}, {p2}, CAGR {round(cagr_전략*100,1)} '
                f'MDD {round(mdd_전략*100,1)}).xlsx')

    #그래프로 표현하기
    plt.rcParams['figure.figsize'] = (16, 9)

    plt.plot(df.index, df['주가백분율'], color='blue', label=p1)
    plt.plot(df.index, df['전략백분율'], color='red', label='Ave_momentum')
    plt.grid(True)
    plt.legend(loc='best')

    plt.title(f'{p1} vs Average_Momentum({p1}, {p2}, CASH {int(현금비율*100)}%)_{moment_months} Months')

    plt.show()

    return df

back_test(df_o, moment_months, 현금비율, 주식자금, 채권자금, 현금자금)
728x90
반응형

댓글