[Backtest] 자산배분 - Three Fund 전략 테스트


미국 주식 50%, 선진국 주식 30%, 미국 채권 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인 AGG를 사용합니다.

start = '2003-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")
agg = web.DataReader("AGG", 'yahoo', start, end)['Adj Close'].to_frame("agg_Close")
vti.head()
vti_Close
Date
2003-10-0135.008533
2003-10-0235.166664
2003-10-0335.551212
2003-10-0635.705772
2003-10-0735.867489

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

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

df_return.head()
vti_Closeefa_Closeagg_Close
Date
2003-10-01NaNNaNNaN
2003-10-020.004517-0.001572-0.001558
2003-10-030.0109350.014588-0.007221
2003-10-060.0043480.0104570.001475
2003-10-070.004529-0.001536-0.003435
df_return['ThreeFund_return'] = df_return['vti_Close']*0.5 + df_return['efa_Close']*0.3 + df_return['agg_Close']*0.2
df_return.head()
vti_Closeefa_Closeagg_CloseThreeFund_return
Date
2003-10-01NaNNaNNaNNaN
2003-10-020.004517-0.001572-0.0015580.001475
2003-10-030.0109350.014588-0.0072210.008400
2003-10-060.0043480.0104570.0014750.005606
2003-10-070.004529-0.001536-0.0034350.001117
quantstats.reports.plots(df_return['ThreeFund_return'], mode='basic')

output_7_0

output_7_1

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

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

Cumulative Return          317.39%
CAGR%                      8.52%
Sharpe                     0.6
Sortino                    0.84
Max Drawdown               -47.7%
Longest DD Days            1273
Volatility (ann.)          15.73%
Calmar                     0.18
Skew                       -0.12
Kurtosis                   16.22

Expected Daily %           0.03%
Expected Monthly %         0.68%
Expected Yearly %          7.81%
Kelly Criterion            6.28%
Risk of Ruin               0.0%
Daily Value-at-Risk        -1.59%
Expected Shortfall (cVaR)  -1.59%

Payoff Ratio               0.89
Profit Factor              1.13
Common Sense Ratio         1.05
CPC Index                  0.56
Tail Ratio                 0.94
Outlier Win Ratio          4.41
Outlier Loss Ratio         4.5

MTD                        1.99%
3M                         4.45%
6M                         14.56%
YTD                        3.42%
1Y                         57.88%
3Y (ann.)                  10.72%
5Y (ann.)                  11.88%
10Y (ann.)                 9.78%
All-time (ann.)            8.52%

Best Day                   11.95%
Worst Day                  -8.96%
Best Month                 10.35%
Worst Month                -15.19%
Best Year                  23.71%
Worst Year                 -29.98%

Avg. Drawdown              -1.54%
Avg. Drawdown Days         23
Recovery Factor            6.65
Ulcer Index                1.02

Avg. Up Month              2.63%
Avg. Down Month            -2.87%
Win Days %                 55.82%
Win Month %                65.71%
Win Quarter %              74.29%
Win Year %                 84.21%

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

vti = vti.rename({'vti_Close':'Close'}, axis='columns')
efa = efa.rename({'efa_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"]
    agg[column] = agg["Close"]
vti.head()
CloseOpenHighLow
Date
2003-10-0135.00853335.00853335.00853335.008533
2003-10-0235.16666435.16666435.16666435.166664
2003-10-0335.55121235.55121235.55121235.551212
2003-10-0635.70577235.70577235.70577235.705772
2003-10-0735.86748935.86748935.86748935.867489

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

class AssetAllocation_ThreeFund(bt.Strategy):
    params = (
        ('USequity',0.5),
        ('DEVequity', 0.3),
        ('USBond', 0.2),
    )
    def __init__(self):
        self.VTI = self.datas[0]
        self.EFA = self.datas[1]
        self.AGG = self.datas[2]
        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.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)
AGG = bt.feeds.PandasData(dataname = agg)

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

cerebro.addstrategy(AssetAllocation_ThreeFund)

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_16_0

output_16_1

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

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

Cumulative Return          301.5%
CAGR%                      8.28%
Sharpe                     0.6
Sortino                    0.84
Max Drawdown               -46.84%
Longest DD Days            1278
Volatility (ann.)          15.18%
Calmar                     0.18
Skew                       -0.21
Kurtosis                   14.5

Expected Daily %           0.03%
Expected Monthly %         0.66%
Expected Yearly %          7.59%
Kelly Criterion            6.19%
Risk of Ruin               0.0%
Daily Value-at-Risk        -1.54%
Expected Shortfall (cVaR)  -1.54%

Payoff Ratio               0.89
Profit Factor              1.12
Common Sense Ratio         1.04
CPC Index                  0.56
Tail Ratio                 0.93
Outlier Win Ratio          4.29
Outlier Loss Ratio         4.45

MTD                        2.02%
3M                         4.49%
6M                         14.54%
YTD                        3.47%
1Y                         56.12%
3Y (ann.)                  10.45%
5Y (ann.)                  11.69%
10Y (ann.)                 9.56%
All-time (ann.)            8.28%

Best Day                   11.33%
Worst Day                  -8.72%
Best Month                 10.35%
Worst Month                -15.0%
Best Year                  23.23%
Worst Year                 -29.87%

Avg. Drawdown              -1.55%
Avg. Drawdown Days         23
Recovery Factor            6.44
Ulcer Index                1.02

Avg. Up Month              2.61%
Avg. Down Month            -2.81%
Win Days %                 55.79%
Win Month %                65.24%
Win Quarter %              74.29%
Win Year %                 84.21%

월간 데이터를 사용하면 훨씬 더 과거의 결과도 테스트해 볼 수 있습니다. 가장 긴 시계열의 경우 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), 월간 선진국 주식, 월간 10년 만기 미국 국채 데이터입니다. 1970년 1월부터 2020년 12월까지 51년치 테스트입니다.

Monthly_ThreeFund = MonthlyReturn.loc[MonthlyReturn.index >= '1970-01-31', ['S&P 500 Total return', 'MSCI EAFE', 'US 10 YR']]
Monthly_ThreeFund.head()
S&P 500 Total returnMSCI EAFEUS 10 YR
Data Index
1970-01-31-0.073601-0.0108000.015914
1970-02-280.055799-0.0226240.069766
1970-03-310.0044070.016053-0.007240
1970-04-30-0.087527-0.082823-0.046007
1970-05-31-0.057719-0.049280-0.002731
Monthly_ThreeFund['Monthly_ThreeFund'] = Monthly_ThreeFund['S&P 500 Total return'] * 0.5 + Monthly_ThreeFund['MSCI EAFE'] * 0.3 + Monthly_ThreeFund['US 10 YR'] * 0.2
Monthly_ThreeFund.head()
S&P 500 Total returnMSCI EAFEUS 10 YRMonthly_ThreeFund
Data Index
1970-01-31-0.073601-0.0108000.015914-0.036858
1970-02-280.055799-0.0226240.0697660.035065
1970-03-310.0044070.016053-0.0072400.005571
1970-04-30-0.087527-0.082823-0.046007-0.077812
1970-05-31-0.057719-0.049280-0.002731-0.044190

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

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

output_30_0

output_30_1

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

연 복리 수익률 10.07%, 샤프 비율은 위에서 계산한대로 0.85, MDD는 -43%입니다. 대공황 시기가 아닌데도 40%대 MDD는 큽니다.

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

Cumulative Return          13,175.09%
CAGR%                      10.07%
Sharpe                     4.01
Sortino                    6.39
Max Drawdown               -42.88%
Longest DD Days            1706
Volatility (ann.)          54.02%
Calmar                     0.23
Skew                       -0.48
Kurtosis                   1.62

Expected Daily %           0.8%
Expected Monthly %         0.8%
Expected Yearly %          10.06%
Kelly Criterion            31.08%
Risk of Ruin               0.0%
Daily Value-at-Risk        -4.74%
Expected Shortfall (cVaR)  -4.74%

Payoff Ratio               1.08
Profit Factor              1.94
Common Sense Ratio         2.43
CPC Index                  1.34
Tail Ratio                 1.25
Outlier Win Ratio          3.07
Outlier Loss Ratio         3.33

MTD                        3.17%
3M                         7.59%
6M                         17.01%
YTD                        14.51%
1Y                         14.51%
3Y (ann.)                  10.63%
5Y (ann.)                  10.87%
10Y (ann.)                 10.37%
All-time (ann.)            10.07%

Best Day                   11.67%
Worst Day                  -14.64%
Best Month                 11.67%
Worst Month                -14.64%
Best Year                  38.64%
Worst Year                 -29.74%

Avg. Drawdown              -4.66%
Avg. Drawdown Days         137
Recovery Factor            307.24
Ulcer Index                0.98

Avg. Up Month              2.77%
Avg. Down Month            -2.56%
Win Days %                 64.22%
Win Month %                64.22%
Win Quarter %              74.51%
Win Year %                 82.35%





© 2021.03. by JacobJinwonLee

Powered by theorydb