[Backtest] 자산배분 - Core Four 전략 테스트


미국 주식 48%, 선진국 주식 24%, 미국 부동산 8%, 미국 채권 20% 비중으로 가져가는 전략입니다.

import pandas as pd
import pandas_datareader.data as web
import datetime
import numpy as np
%matplotlib inline
import backtrader as bt
import matplotlib.pyplot as plt
import pyfolio as pf
import quantstats
import math
plt.rcParams["figure.figsize"] = (10, 6) # (w, h)

ETF로 매수하는 것이 간편하니 적절한 ETF 데이터를 다운받습니다. 미국 전체 주식 ETF인 VTI, 선진국 주식 ETF인 EFA, 미국 부동산 ETF VNQ, 미국 채권 ETF인 AGG를 사용합니다.

start = '2004-10-02'
end = '2021-03-19'
vti = web.DataReader("VTI", 'yahoo', start, end)['Adj Close'].to_frame("vti_Close")
efa = web.DataReader("EFA", 'yahoo', start, end)['Adj Close'].to_frame("efa_Close")
vnq = web.DataReader("VNQ", 'yahoo', start, end)['Adj Close'].to_frame("vnq_Close")
agg = web.DataReader("AGG", 'yahoo', start, end)['Adj Close'].to_frame("agg_Close")
vti.head()
vti_Close
Date
2004-10-0139.929573
2004-10-0440.060860
2004-10-0540.049908
2004-10-0640.294239
2004-10-0739.929573

일단 모델 포트폴리오로, 매일 48:24:8:20 비중을 맞추는 것으로 생각하고 만듭니다. 거래비용은 생략합니다.

vti_return = vti.pct_change(periods=1)
efa_return = efa.pct_change(periods=1)
vnq_return = vnq.pct_change(periods=1)
agg_return = agg.pct_change(periods=1)
df_return = pd.concat([vti_return, efa_return, vnq_return, agg_return], axis=1)

df_return.head()
vti_Closeefa_Closevnq_Closeagg_Close
Date
2004-10-01NaNNaNNaNNaN
2004-10-040.0032880.0015320.0042990.000294
2004-10-05-0.0002730.000557-0.000195-0.000098
2004-10-060.0061010.0037520.004670-0.001859
2004-10-07-0.009050-0.006854-0.0102660.000000
df_return['CoreFour_return'] = df_return['vti_Close']*0.48 + df_return['efa_Close']*0.24 + df_return['vnq_Close']*0.08 + df_return['agg_Close']*0.2
df_return.head()
vti_Closeefa_Closevnq_Closeagg_CloseCoreFour_return
Date
2004-10-01NaNNaNNaNNaNNaN
2004-10-040.0032880.0015320.0042990.0002940.002349
2004-10-05-0.0002730.000557-0.000195-0.000098-0.000033
2004-10-060.0061010.0037520.004670-0.0018590.003831
2004-10-07-0.009050-0.006854-0.0102660.000000-0.006810
quantstats.reports.plots(df_return['CoreFour_return'], mode='basic')

output_8_0

output_8_1

매일 비중을 맞춘 결과 연 복리 수익률 8.46%, 샤프 비율 0.58, MDD -48% 정도입니다. MDD가 너무 높아 그리 좋은 전략은 아닙니다.

quantstats.reports.metrics(df_return['CoreFour_return'], mode='full')
                           Strategy
-------------------------  ----------
Start Period               2004-10-01
End Period                 2021-03-19
Risk-Free Rate             0.0%
Time in Market             100.0%

Cumulative Return          281.28%
CAGR%                      8.46%
Sharpe                     0.58
Sortino                    0.81
Max Drawdown               -48.12%
Longest DD Days            1217
Volatility (ann.)          16.31%
Calmar                     0.18
Skew                       -0.16
Kurtosis                   15.46

Expected Daily %           0.03%
Expected Monthly %         0.68%
Expected Yearly %          7.72%
Kelly Criterion            6.21%
Risk of Ruin               0.0%
Daily Value-at-Risk        -1.65%
Expected Shortfall (cVaR)  -1.65%

Payoff Ratio               0.88
Profit Factor              1.12
Common Sense Ratio         1.05
CPC Index                  0.56
Tail Ratio                 0.93
Outlier Win Ratio          4.79
Outlier Loss Ratio         4.55

MTD                        1.99%
3M                         4.57%
6M                         13.99%
YTD                        3.56%
1Y                         56.51%
3Y (ann.)                  10.98%
5Y (ann.)                  11.57%
10Y (ann.)                 9.92%
All-time (ann.)            8.46%

Best Day                   11.25%
Worst Day                  -9.02%
Best Month                 10.67%
Worst Month                -16.14%
Best Year                  24.89%
Worst Year                 -29.06%

Avg. Drawdown              -1.56%
Avg. Drawdown Days         22
Recovery Factor            5.85
Ulcer Index                1.02

Avg. Up Month              2.64%
Avg. Down Month            -2.97%
Win Days %                 56.01%
Win Month %                66.16%
Win Quarter %              74.24%
Win Year %                 88.89%

위에서 한 것처럼 그냥 만들어도 되지만, 백테스트에 많이 쓰이는 Backtrader 패키지를 한번 사용해 보겠습니다. Input 형식을 맞추어야 합니다.

vti = vti.rename({'vti_Close':'Close'}, axis='columns')
efa = efa.rename({'efa_Close':'Close'}, axis='columns')
vnq = vnq.rename({'vnq_Close':'Close'}, axis='columns')
agg = agg.rename({'agg_Close':'Close'}, axis='columns')

for column in ['Open', 'High', "Low"]:
    vti[column] = vti["Close"]
    efa[column] = efa["Close"]
    vnq[column] = vnq["Close"]
    agg[column] = agg["Close"]
vti.head()
CloseOpenHighLow
Date
2004-10-0139.92957339.92957339.92957339.929573
2004-10-0440.06086040.06086040.06086040.060860
2004-10-0540.04990840.04990840.04990840.049908
2004-10-0640.29423940.29423940.29423940.294239
2004-10-0739.92957339.92957339.92957339.929573

48:24:8:20 비율로 매수하고 20 거래일마다 리밸런싱하는 전략입니다.

class AssetAllocation_CoreFour(bt.Strategy):
    params = (
        ('USequity',0.48),
        ('DEVequity', 0.24),
        ('USREITs', 0.08),
        ('USBond', 0.2),
    )
    def __init__(self):
        self.VTI = self.datas[0]
        self.EFA = self.datas[1]
        self.VNQ = self.datas[2]
        self.AGG = self.datas[3]
        self.counter = 0
        
    def next(self):
        if  self.counter % 20 == 0:
            self.order_target_percent(self.VTI, target=self.params.USequity)
            self.order_target_percent(self.EFA, target=self.params.DEVequity)
            self.order_target_percent(self.VNQ, target=self.params.USREITs)
            self.order_target_percent(self.AGG, target=self.params.USBond)
        self.counter += 1
cerebro = bt.Cerebro()

cerebro.broker.setcash(1000000)

VTI = bt.feeds.PandasData(dataname = vti)
EFA = bt.feeds.PandasData(dataname = efa)
VNQ = bt.feeds.PandasData(dataname = vnq)
AGG = bt.feeds.PandasData(dataname = agg)

cerebro.adddata(VTI)
cerebro.adddata(EFA)
cerebro.adddata(VNQ)
cerebro.adddata(AGG)

cerebro.addstrategy(AssetAllocation_CoreFour)

cerebro.addanalyzer(bt.analyzers.PyFolio, _name = 'PyFolio')

results = cerebro.run()
strat = results[0]

portfolio_stats = strat.analyzers.getbyname('PyFolio')
returns, positions, transactions, gross_lev = portfolio_stats.get_pf_items()
returns.index = returns.index.tz_convert(None)

#quantstats.reports.html(returns, output = 'Report_AssetAllocation_6040.html', title='AssetAllocation_6040')
quantstats.reports.plots(returns, mode='basic')

output_17_0

output_17_1

20 거래일마다 리밸런싱하는 것으로 바꾸니 연 복리 수익률 8.11%, 샤프 비율 0.58, MDD -48% 정도로 나옵니다.

quantstats.reports.metrics(returns, mode='full')
                           Strategy
-------------------------  ----------
Start Period               2004-10-01
End Period                 2021-03-19
Risk-Free Rate             0.0%
Time in Market             100.0%

Cumulative Return          261.5%
CAGR%                      8.11%
Sharpe                     0.58
Sortino                    0.8
Max Drawdown               -48.33%
Longest DD Days            1294
Volatility (ann.)          15.71%
Calmar                     0.17
Skew                       -0.28
Kurtosis                   13.53

Expected Daily %           0.03%
Expected Monthly %         0.65%
Expected Yearly %          7.4%
Kelly Criterion            6.05%
Risk of Ruin               0.0%
Daily Value-at-Risk        -1.59%
Expected Shortfall (cVaR)  -1.59%

Payoff Ratio               0.88
Profit Factor              1.12
Common Sense Ratio         1.05
CPC Index                  0.55
Tail Ratio                 0.94
Outlier Win Ratio          4.69
Outlier Loss Ratio         4.55

MTD                        1.98%
3M                         4.57%
6M                         13.93%
YTD                        3.57%
1Y                         54.99%
3Y (ann.)                  10.59%
5Y (ann.)                  11.29%
10Y (ann.)                 9.67%
All-time (ann.)            8.11%

Best Day                   10.49%
Worst Day                  -8.72%
Best Month                 10.19%
Worst Month                -15.93%
Best Year                  23.63%
Worst Year                 -29.64%

Avg. Drawdown              -1.56%
Avg. Drawdown Days         23
Recovery Factor            5.41
Ulcer Index                inf

Avg. Up Month              2.59%
Avg. Down Month            -2.96%
Win Days %                 56.02%
Win Month %                66.16%
Win Quarter %              74.24%
Win Year %                 83.33%

월간 데이터를 사용하면 훨씬 더 과거의 결과도 테스트해 볼 수 있습니다. 가장 긴 시계열의 경우 1900년 1월부터 2020년 12월까지의 데이터가 있습니다.

MonthlyReturn = pd.read_excel('MonthlyAssetClassReturn.xlsx')
MonthlyReturn.head()
Data IndexBroker Call RateCPIT-BillsS&P 500 Total returnSmall Cap StocksMSCI EAFEEEMUS 10 YRUS Corp Bond Return Index...International Small Cap Value (Global B/M Small Low)International Large Cap Value (Global B/M Big Low)International Small High Mom (Global mom Small High)International Large High Mom (Global mom Small High)Merrill High YieldWorld StocksWorld ex USABuyWritePutWriteBitcoin
01900-01-31NaN0.0133330.00250.016413NaNNaNNaN0.000000NaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
11900-02-28NaN0.0000000.00250.021138NaNNaNNaN0.011278NaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
21900-03-31NaN0.0000000.00250.011084NaNNaNNaN0.009758NaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
31900-04-30NaN0.0000000.00250.015894NaNNaNNaN-0.016107NaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
41900-05-31NaN0.0000000.0025-0.044246NaNNaNNaN0.016023NaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN

5 rows × 50 columns

시계열로 바꾸어 주는 것이 사용하기 편합니다. 1열인 Data Index가 월말 날짜이므로, 이 열을 인덱스로 잡습니다.

MonthlyReturn = MonthlyReturn.set_index('Data Index')
MonthlyReturn.head()
Broker Call RateCPIT-BillsS&P 500 Total returnSmall Cap StocksMSCI EAFEEEMUS 10 YRUS Corp Bond Return IndexGSCI...International Small Cap Value (Global B/M Small Low)International Large Cap Value (Global B/M Big Low)International Small High Mom (Global mom Small High)International Large High Mom (Global mom Small High)Merrill High YieldWorld StocksWorld ex USABuyWritePutWriteBitcoin
Data Index
1900-01-31NaN0.0133330.00250.016413NaNNaNNaN0.000000NaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
1900-02-28NaN0.0000000.00250.021138NaNNaNNaN0.011278NaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
1900-03-31NaN0.0000000.00250.011084NaNNaNNaN0.009758NaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
1900-04-30NaN0.0000000.00250.015894NaNNaNNaN-0.016107NaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
1900-05-31NaN0.0000000.0025-0.044246NaNNaNNaN0.016023NaNNaN...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN

5 rows × 49 columns

필요한 것만 뽑아옵니다. 미국 전체 채권 데이터가 없으니 10년 만기 국채로 대체합니다. 월간 미국 주식(S&P 500), 월간 선진국 주식, 월간 REITs, 월간 10년 만기 미국 국채 데이터입니다. 1972년 1월부터 2020년 12월까지 49년 테스트입니다.

Monthly_CoreFour = MonthlyReturn.loc[MonthlyReturn.index >= '1972-01-31', ['S&P 500 Total return', 'MSCI EAFE', 'US 10 YR', 'NAREIT']]
Monthly_CoreFour.head()
S&P 500 Total returnMSCI EAFEUS 10 YRNAREIT
Data Index
1972-01-310.0206240.058238-0.0097700.012204
1972-02-290.0240990.0656400.0087740.009497
1972-03-310.0119170.032981-0.0008550.002524
1972-04-300.0067700.0240030.0036260.002549
1972-05-310.0179730.0325290.011781-0.015069
Monthly_CoreFour['Monthly_CoreFour'] = Monthly_CoreFour['S&P 500 Total return'] * 0.48 + Monthly_CoreFour['MSCI EAFE'] * 0.24 + Monthly_CoreFour['NAREIT'] * 0.08 + Monthly_CoreFour['US 10 YR'] * 0.2
Monthly_CoreFour.head()
S&P 500 Total returnMSCI EAFEUS 10 YRNAREITMonthly_CoreFour
Data Index
1972-01-310.0206240.058238-0.0097700.0122040.022899
1972-02-290.0240990.0656400.0087740.0094970.029836
1972-03-310.0119170.032981-0.0008550.0025240.013667
1972-04-300.0067700.0240030.0036260.0025490.009939
1972-05-310.0179730.0325290.011781-0.0150690.017585

월간 데이터이므로, 일간 데이터 기준인 패키지가 주는 값을 적절히 조정해야 합니다. 1년 12개월 252거래일을 가정합니다. 1972년 1월부터 49년 동안 샤프 비율은 0.89로 나옵니다. 아래 그림의 제목 하단에 있는 샤프 비율은 무시하고, 직접 계산한 값을 보아야 합니다.

quantstats.stats.sharpe(Monthly_CoreFour['Monthly_CoreFour'])/math.sqrt(252/12)
0.8913072853311025
quantstats.reports.plots(Monthly_CoreFour['Monthly_CoreFour'], mode='basic')

output_31_0

output_31_1

** 주의: quantstats 라이브러리가 월간 데이터일 경우는 일부 지표를 이상한 값으로 돌려줍니다. 연간 기준으로 보정해서 생각해야 합니다.

연 복리 수익률 10.08%, 샤프 비율은 위에서 계산한대로 0.89, MDD는 -43%입니다. 매년 10%를 넘는 전략이지만 대공황 시기가 아닌데도 40%대 MDD는 큽니다.

quantstats.reports.metrics(Monthly_CoreFour['Monthly_CoreFour'], mode='full')
                           Strategy
-------------------------  ----------
Start Period               1972-01-31
End Period                 2020-12-31
Risk-Free Rate             0.0%
Time in Market             100.0%

Cumulative Return          10,906.82%
CAGR%                      10.08%
Sharpe                     4.08
Sortino                    6.48
Max Drawdown               -43.52%
Longest DD Days            1492
Volatility (ann.)          52.97%
Calmar                     0.23
Skew                       -0.53
Kurtosis                   2.11

Expected Daily %           0.8%
Expected Monthly %         0.8%
Expected Yearly %          10.07%
Kelly Criterion            32.48%
Risk of Ruin               0.0%
Daily Value-at-Risk        -4.63%
Expected Shortfall (cVaR)  -4.63%

Payoff Ratio               1.02
Profit Factor              1.97
Common Sense Ratio         2.46
CPC Index                  1.32
Tail Ratio                 1.25
Outlier Win Ratio          3.22
Outlier Loss Ratio         3.37

MTD                        3.03%
3M                         7.15%
6M                         16.12%
YTD                        13.1%
1Y                         13.1%
3Y (ann.)                  10.45%
5Y (ann.)                  10.7%
10Y (ann.)                 10.47%
All-time (ann.)            10.08%

Best Day                   12.81%
Worst Day                  -15.52%
Best Month                 12.81%
Worst Month                -15.52%
Best Year                  34.86%
Worst Year                 -29.09%

Avg. Drawdown              -4.81%
Avg. Drawdown Days         136
Recovery Factor            250.64
Ulcer Index                0.99

Avg. Up Month              2.64%
Avg. Down Month            -2.6%
Win Days %                 65.99%
Win Month %                65.99%
Win Quarter %              72.96%
Win Year %                 81.63%





© 2021.03. by JacobJinwonLee

Powered by theorydb