[Strategy] Sector Rotation with Dual Momentum


개요

미국 섹터 ETF로 듀얼 모멘텀 전략을 구현해 봅니다.

미국 섹터 ETF로 듀얼 모멘텀 전략을 구현해 볼 것입니다. 듀얼 모멘텀은 상대적으로 모멘텀이 좋은 자산을 편입하는 상대 모멘텀과 현금보다 수익이 나쁘면 현금으로 대체하는 절대 모멘텀을 혼합한 것입니다. 우선 필요한 라이브러리를 가져옵니다.

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

섹터는 StateStreet의 ETF 중 상장 기간이 긴 9개 섹터로 합니다. Consumer Discretionary, Consumer Staples, Financial, Energy, Health Care, Industrial, Materials, Technology, Utilities가 있습니다. 절대 모멘텀 판정을 위해 단기 국채 ETF인 SHY를 사용합니다.

# XLY (Consumer Discretionary), XLP (Consumer Staples), XLF (Financial), XLE (Energy), XLV (Health Care), XLI (Industrial)
# XLB (Materials), XLK (Technology), XLU (Utilities)
# SHY (1-3 year treasury: cash)

# shy starts: 2002-07-30
start = datetime(2002,8,1)
end = datetime(2021,5,31)

tickers = ['XLB','XLE','XLF','XLI','XLK','XLP','XLU','XLV','XLY','SHY']

def get_price_data(tickers):
    df_sector = pd.DataFrame(columns=tickers)
    
    for ticker in tickers:
        df_sector[ticker] = pdr.get_data_yahoo(ticker, start, end)['Adj Close']  
         
    return df_sector

SHY의 데이터가 2002년 7월 30일부터 시작되므로 2002년 8월부터 2021년 5월까지로 해 봅니다. Yahoo Finance에서 가져옵니다. tickers 리스트로 열을 설정한 데이터프레임을 만들고 각 열에 섹터 ETF 가격 데이터 다운받은 것을 넣습니다.

df_sector = get_price_data(tickers)
df_sector
XLBXLEXLFXLIXLKXLPXLUXLVXLYSHY
Date
2002-07-3113.53838614.19631112.65717915.61010411.06343513.37126310.52612819.72809819.04136859.007946
2002-08-0113.45279213.35975812.34417815.26320710.57670713.24571210.37342519.17766018.45886459.124153
2002-08-0212.88649613.20879612.03118214.70818510.35265213.22059710.16982719.02353317.64022359.240341
2002-08-0512.44531312.71818411.55893814.2225399.90455313.0385509.99167318.20886217.23090459.334690
2002-08-0612.97209813.17735611.93783014.70818510.32175313.05110910.40396618.65656117.74255859.189507
.................................
2021-05-2487.37000352.68000037.759998103.680000138.61000171.23000366.040001124.480003170.22000186.288010
2021-05-2586.62999751.61000137.380001103.370003138.64999471.25000065.239998124.110001170.75999586.288010
2021-05-2686.66000452.07000037.490002103.699997138.63999971.09999865.250000123.389999172.38999986.307999
2021-05-2787.34999852.11000137.919998105.120003137.91000470.66000464.809998123.120003173.08000286.307999
2021-05-2887.26000252.20999937.990002105.120003138.39999470.80999865.160004123.599998172.83999686.307999

4741 rows × 10 columns

9개 섹터 중 12개월 누적 수익률 상위 5개 섹터를 동일 비중으로 가져가고, 누적 수익률이 -인 경우는 SHY etf로 대체합니다. 그러기 위해서는 12개월 모멘텀이 필요합니다. 현재 주가 x에 대해서 섹터별로 현재 시점보다 365~370일 이전 값 중 가장 최근 값을 12개월 이전 주가로 놓고 모멘텀을 구합니다.

def get_momentum(x):
    temp = [0 for _ in range(len(x.index))]
    momentum = pd.Series(temp, index=x.index)
    
    try:
        before_12m = df_sector[x.name-timedelta(days=370):x.name-timedelta(days=365)].iloc[-1]
        momentum = x / before_12m - 1
        
    except:
        pass
    
    return momentum
momentum_col = [col + '_m' for col in df_sector.columns]
df_sector[momentum_col] = df_sector.apply(lambda x: get_momentum(x), axis=1)
df_sector
XLBXLEXLFXLIXLKXLPXLUXLVXLYSHYXLB_mXLE_mXLF_mXLI_mXLK_mXLP_mXLU_mXLV_mXLY_mSHY_m
Date
2002-07-3113.53838614.19631112.65717915.61010411.06343513.37126310.52612819.72809819.04136859.0079460.0000000.0000000.0000000.0000000.0000000.0000000.0000000.0000000.0000000.000000
2002-08-0113.45279213.35975812.34417815.26320710.57670713.24571210.37342519.17766018.45886459.1241530.0000000.0000000.0000000.0000000.0000000.0000000.0000000.0000000.0000000.000000
2002-08-0212.88649613.20879612.03118214.70818510.35265213.22059710.16982719.02353317.64022359.2403410.0000000.0000000.0000000.0000000.0000000.0000000.0000000.0000000.0000000.000000
2002-08-0512.44531312.71818411.55893814.2225399.90455313.0385509.99167318.20886217.23090459.3346900.0000000.0000000.0000000.0000000.0000000.0000000.0000000.0000000.0000000.000000
2002-08-0612.97209813.17735611.93783014.70818510.32175313.05110910.40396618.65656117.74255859.1895070.0000000.0000000.0000000.0000000.0000000.0000000.0000000.0000000.0000000.000000
...............................................................
2021-05-2487.37000352.68000037.759998103.680000138.61000171.23000366.040001124.480003170.22000186.2880100.6829650.4433940.7602690.6494460.4510310.2738960.2081690.2694160.4152370.001353
2021-05-2586.62999751.61000137.380001103.370003138.64999471.25000065.239998124.110001170.75999586.2880100.6687100.4140770.7425540.6445140.4514500.2742540.1935340.2656430.4197260.001353
2021-05-2686.66000452.07000037.490002103.699997138.63999971.09999865.250000123.389999172.38999986.3079990.6247780.3880830.6612830.5832970.4528510.2647490.1859480.2605790.4109320.001585
2021-05-2787.34999852.11000137.919998105.120003137.91000470.66000464.809998123.120003173.08000286.3079990.6213160.3714640.6104980.5531440.4370010.2382780.1640270.2449250.3927170.001816
2021-05-2887.26000252.20999937.990002105.120003138.39999470.80999865.160004123.599998172.83999686.3079990.6047470.4136180.6393630.5679710.4449390.2300010.1367240.2336930.4078700.001354

4741 rows × 20 columns

모멘텀 데이터가 구해진 구간에서만 해야 합니다. 넉넉하게 2003년 9월부터로 합니다. 월말 리밸런싱을 가정하여 월말 데이터만 뽑을 것입니다.

df_sector = df_sector.loc[df_sector.index >= '2003-09-01']
df_sector
XLBXLEXLFXLIXLKXLPXLUXLVXLYSHYXLB_mXLE_mXLF_mXLI_mXLK_mXLP_mXLU_mXLV_mXLY_mSHY_m
Date
2003-09-0215.67587415.94960914.44346917.17488514.59481013.28082611.39337220.98487523.29025160.5459480.1641110.1068300.1202310.1176270.330342-0.0146280.0411080.0535150.1873090.021664
2003-09-0315.68261915.95603414.47150217.31578114.74974313.31290911.43057020.90313523.28233160.5902820.1998140.1530770.1802090.1673770.4141770.0098590.0739790.0950020.2308650.019995
2003-09-0415.54103115.93034514.40984517.30169314.88919313.37706111.43588520.95516223.35362860.6716800.1842100.1538430.1534850.1550820.3844750.0065770.0812000.0649720.2119390.021741
2003-09-0515.48034715.85971614.34258417.11148514.81172113.23591411.41463120.90313523.12389060.8788260.2124360.1466380.1655200.1628500.425399-0.0035730.0823160.0747550.2224880.024101
2003-09-0815.61519416.07803314.48271017.19602015.08284613.27440711.49965821.27469123.13182360.8048670.1988840.1436980.1593180.1539060.404500-0.0006750.0903780.0938590.1858950.024863
...............................................................
2021-05-2487.37000352.68000037.759998103.680000138.61000171.23000366.040001124.480003170.22000186.2880100.6829650.4433940.7602690.6494460.4510310.2738960.2081690.2694160.4152370.001353
2021-05-2586.62999751.61000137.380001103.370003138.64999471.25000065.239998124.110001170.75999586.2880100.6687100.4140770.7425540.6445140.4514500.2742540.1935340.2656430.4197260.001353
2021-05-2686.66000452.07000037.490002103.699997138.63999971.09999865.250000123.389999172.38999986.3079990.6247780.3880830.6612830.5832970.4528510.2647490.1859480.2605790.4109320.001585
2021-05-2787.34999852.11000137.919998105.120003137.91000470.66000464.809998123.120003173.08000286.3079990.6213160.3714640.6104980.5531440.4370010.2382780.1640270.2449250.3927170.001816
2021-05-2887.26000252.20999937.990002105.120003138.39999470.80999865.160004123.599998172.83999686.3079990.6047470.4136180.6393630.5679710.4449390.2300010.1367240.2336930.4078700.001354

4467 rows × 20 columns

df_sector = df_sector.resample(rule='M').last()
df_sector
XLBXLEXLFXLIXLKXLPXLUXLVXLYSHYXLB_mXLE_mXLF_mXLI_mXLK_mXLP_mXLU_mXLV_mXLY_mSHY_m
Date
2003-09-3014.79593215.52012114.31708716.46527514.16100213.08161211.74232820.72658921.87705061.2043760.2626470.1597830.2546300.2112240.5493960.0462160.2207630.1092580.2244240.025187
2003-10-3116.20572315.66854915.30874217.48330915.19906013.67389111.82275220.86818923.86802960.9547960.3082610.1496840.2291420.2258260.3328560.0526280.2581010.0552980.2513980.018522
2003-11-3016.57171815.79116215.25240317.82971615.41597013.83484011.84956221.31536524.08219360.9183850.2209370.1262780.1706520.1852930.1601000.0888490.2305940.0414640.1954460.022362
2003-12-3118.19624917.85860115.94418319.01140015.89933914.08093112.62595222.54884525.04674061.5803990.3741280.2581170.3049180.3185760.3867550.1117330.2646590.1493010.3720010.022204
2004-01-3117.39176618.26049816.46564119.20321116.57806014.08093112.88572023.16960124.90356461.6550030.3971790.3461100.3918830.4312840.4789300.1810310.3367490.2087110.4232820.023939
...............................................................
2021-01-3170.34294938.90719228.82225484.509895128.57484463.74531261.689369114.632141161.75303686.3049700.225154-0.239690-0.0293530.0417590.3318880.031193-0.0708590.1567820.3106090.025973
2021-02-2873.24071547.64448932.16742790.331978130.33000262.95956457.927460112.220512160.89439486.2549970.4103670.1334540.2129180.2420240.5089370.100085-0.0602490.2210420.3978080.018839
2021-03-3178.80000349.06000134.04999998.449997132.80999868.30999864.040001116.739998168.07000786.2070310.7837840.7799430.6711830.6944400.6691730.2889590.1942040.3388600.7271830.002123
2021-04-3083.04000149.38999936.259998101.930000139.69999769.58000266.720001121.330002178.91999886.2560120.6319310.3703440.6258150.6122610.5437150.2275140.2039830.2359290.546639-0.000034
2021-05-3187.26000252.20999937.990002105.120003138.39999470.80999865.160004123.599998172.83999686.3079990.6047470.4136180.6393630.5679710.4449390.2300010.1367240.2336930.4078700.001354

213 rows × 20 columns

9개 섹터 중 모멘텀이 좋은 5개 섹터를 선택합니다. 선택된 5개 섹터는 동일 비중으로 투자합니다. 모멘텀이 음수이거나 SHY보다 못하면 SHY로 대체합니다. 선택한 섹터와 그 섹터 ETF의 가격을 데이터프레임에 담습니다. 모멘텀 값들을 추출해서 sorting하고 크기 순으로 5개를 잡아줍니다.

def select_sector(x):
    selected_sector = pd.Series([0,0,0,0,0,0,0,0,0,0], index=['SECTOR1','PRICE1','SECTOR2','PRICE2','SECTOR3','PRICE3','SECTOR4','PRICE4','SECTOR5','PRICE5'])
    
    momentum1 = None
    momentum2 = None
    momentum3 = None
    momentum4 = None
    momentum5 = None
    
    sorted_momentum = x[['XLB_m','XLE_m','XLF_m','XLI_m','XLK_m','XLP_m','XLU_m','XLV_m','XLY_m','SHY_m']].sort_values(ascending=False)
    
    # SHY가 가장 좋을 경우
    if x['SHY_m'] == sorted_momentum[0]:
        momentum1 = sorted_momentum[0]
        momentum2 = sorted_momentum[0]
        momentum3 = sorted_momentum[0]
        momentum4 = sorted_momentum[0]
        momentum5 = sorted_momentum[0]

        selected_sector['SECTOR1'] = x[x==momentum1].index[0][:3]
        selected_sector['PRICE1'] = x[selected_sector['SECTOR1']]
        selected_sector['SECTOR2'] = x[x==momentum2].index[0][:3]
        selected_sector['PRICE2'] = x[selected_sector['SECTOR2']]
        selected_sector['SECTOR3'] = x[x==momentum3].index[0][:3]
        selected_sector['PRICE3'] = x[selected_sector['SECTOR3']]
        selected_sector['SECTOR4'] = x[x==momentum4].index[0][:3]
        selected_sector['PRICE4'] = x[selected_sector['SECTOR4']]
        selected_sector['SECTOR5'] = x[x==momentum5].index[0][:3]
        selected_sector['PRICE5'] = x[selected_sector['SECTOR5']]
        
    # SHY가 두번째 좋을 경우
    if x['SHY_m'] == sorted_momentum[1]:
        momentum1 = sorted_momentum[0]
        momentum2 = sorted_momentum[1]
        momentum3 = sorted_momentum[1]
        momentum4 = sorted_momentum[1]
        momentum5 = sorted_momentum[1]

        selected_sector['SECTOR1'] = x[x==momentum1].index[0][:3]
        selected_sector['PRICE1'] = x[selected_sector['SECTOR1']]
        selected_sector['SECTOR2'] = x[x==momentum2].index[0][:3]
        selected_sector['PRICE2'] = x[selected_sector['SECTOR2']]
        selected_sector['SECTOR3'] = x[x==momentum3].index[0][:3]
        selected_sector['PRICE3'] = x[selected_sector['SECTOR3']]
        selected_sector['SECTOR4'] = x[x==momentum4].index[0][:3]
        selected_sector['PRICE4'] = x[selected_sector['SECTOR4']]
        selected_sector['SECTOR5'] = x[x==momentum5].index[0][:3]
        selected_sector['PRICE5'] = x[selected_sector['SECTOR5']]
        
    # SHY가 세번째 좋을 경우
    if x['SHY_m'] == sorted_momentum[2]:
        momentum1 = sorted_momentum[0]
        momentum2 = sorted_momentum[1]
        momentum3 = sorted_momentum[2]
        momentum4 = sorted_momentum[2]
        momentum5 = sorted_momentum[2]

        selected_sector['SECTOR1'] = x[x==momentum1].index[0][:3]
        selected_sector['PRICE1'] = x[selected_sector['SECTOR1']]
        selected_sector['SECTOR2'] = x[x==momentum2].index[0][:3]
        selected_sector['PRICE2'] = x[selected_sector['SECTOR2']]
        selected_sector['SECTOR3'] = x[x==momentum3].index[0][:3]
        selected_sector['PRICE3'] = x[selected_sector['SECTOR3']]
        selected_sector['SECTOR4'] = x[x==momentum4].index[0][:3]
        selected_sector['PRICE4'] = x[selected_sector['SECTOR4']]
        selected_sector['SECTOR5'] = x[x==momentum5].index[0][:3]
        selected_sector['PRICE5'] = x[selected_sector['SECTOR5']]
        
    # SHY가 네번째 좋을 경우
    if x['SHY_m'] == sorted_momentum[3]:
        momentum1 = sorted_momentum[0]
        momentum2 = sorted_momentum[1]
        momentum3 = sorted_momentum[2]
        momentum4 = sorted_momentum[3]
        momentum5 = sorted_momentum[3]

        selected_sector['SECTOR1'] = x[x==momentum1].index[0][:3]
        selected_sector['PRICE1'] = x[selected_sector['SECTOR1']]
        selected_sector['SECTOR2'] = x[x==momentum2].index[0][:3]
        selected_sector['PRICE2'] = x[selected_sector['SECTOR2']]
        selected_sector['SECTOR3'] = x[x==momentum3].index[0][:3]
        selected_sector['PRICE3'] = x[selected_sector['SECTOR3']]
        selected_sector['SECTOR4'] = x[x==momentum4].index[0][:3]
        selected_sector['PRICE4'] = x[selected_sector['SECTOR4']]
        selected_sector['SECTOR5'] = x[x==momentum5].index[0][:3]
        selected_sector['PRICE5'] = x[selected_sector['SECTOR5']]
        
    # SHY가 다섯번째 이하일 경우
    else:
        momentum1 = sorted_momentum[0]
        momentum2 = sorted_momentum[1]
        momentum3 = sorted_momentum[2]
        momentum4 = sorted_momentum[3]
        momentum5 = sorted_momentum[4]

        selected_sector['SECTOR1'] = x[x==momentum1].index[0][:3]
        selected_sector['PRICE1'] = x[selected_sector['SECTOR1']]
        selected_sector['SECTOR2'] = x[x==momentum2].index[0][:3]
        selected_sector['PRICE2'] = x[selected_sector['SECTOR2']]
        selected_sector['SECTOR3'] = x[x==momentum3].index[0][:3]
        selected_sector['PRICE3'] = x[selected_sector['SECTOR3']]
        selected_sector['SECTOR4'] = x[x==momentum4].index[0][:3]
        selected_sector['PRICE4'] = x[selected_sector['SECTOR4']]
        selected_sector['SECTOR5'] = x[x==momentum5].index[0][:3]
        selected_sector['PRICE5'] = x[selected_sector['SECTOR5']]

    return selected_sector
df_sector[['SECTOR1','PRICE1','SECTOR2','PRICE2','SECTOR3','PRICE3','SECTOR4','PRICE4','SECTOR5','PRICE5']] = df_sector.apply(lambda x: select_sector(x), axis=1) 
df_sector
XLBXLEXLFXLIXLKXLPXLUXLVXLYSHY...SECTOR1PRICE1SECTOR2PRICE2SECTOR3PRICE3SECTOR4PRICE4SECTOR5PRICE5
Date
2003-09-3014.79593215.52012114.31708716.46527514.16100213.08161211.74232820.72658921.87705061.204376...XLK14.161002XLB14.795932XLF14.317087XLY21.877050XLU11.742328
2003-10-3116.20572315.66854915.30874217.48330915.19906013.67389111.82275220.86818923.86802960.954796...XLK15.199060XLB16.205723XLU11.822752XLY23.868029XLF15.308742
2003-11-3016.57171815.79116215.25240317.82971615.41597013.83484011.84956221.31536524.08219360.918385...XLU11.849562XLB16.571718XLY24.082193XLI17.829716XLF15.252403
2003-12-3118.19624917.85860115.94418319.01140015.89933914.08093112.62595222.54884525.04674061.580399...XLK15.899339XLB18.196249XLY25.046740XLI19.011400XLF15.944183
2004-01-3117.39176618.26049816.46564119.20321116.57806014.08093112.88572023.16960124.90356461.655003...XLK16.578060XLI19.203211XLY24.903564XLB17.391766XLF16.465641
..................................................................
2021-01-3170.34294938.90719228.82225484.509895128.57484463.74531261.689369114.632141161.75303686.304970...XLK128.574844XLY161.753036XLB70.342949XLV114.632141XLI84.509895
2021-02-2873.24071547.64448932.16742790.331978130.33000262.95956457.927460112.220512160.89439486.254997...XLK130.330002XLB73.240715XLY160.894394XLI90.331978XLV112.220512
2021-03-3178.80000349.06000134.04999998.449997132.80999868.30999864.040001116.739998168.07000786.207031...XLB78.800003XLE49.060001XLY168.070007XLI98.449997XLF34.049999
2021-04-3083.04000149.38999936.259998101.930000139.69999769.58000266.720001121.330002178.91999886.256012...XLB83.040001XLF36.259998XLI101.930000XLY178.919998XLK139.699997
2021-05-3187.26000252.20999937.990002105.120003138.39999470.80999865.160004123.599998172.83999686.307999...XLF37.990002XLB87.260002XLI105.120003XLK138.399994XLE52.209999

213 rows × 30 columns

각각의 수익률을 계산합니다.

return_col = [ticker + '_r' for ticker in tickers]
df_sector[return_col] = df_sector[tickers].pct_change()
df_sector
XLBXLEXLFXLIXLKXLPXLUXLVXLYSHY...XLB_rXLE_rXLF_rXLI_rXLK_rXLP_rXLU_rXLV_rXLY_rSHY_r
Date
2003-09-3014.79593215.52012114.31708716.46527514.16100213.08161211.74232820.72658921.87705061.204376...NaNNaNNaNNaNNaNNaNNaNNaNNaNNaN
2003-10-3116.20572315.66854915.30874217.48330915.19906013.67389111.82275220.86818923.86802960.954796...0.0952820.0095640.0692640.0618290.0733040.0452760.0068490.0068320.091008-0.004078
2003-11-3016.57171815.79116215.25240317.82971615.41597013.83484011.84956221.31536524.08219360.918385...0.0225840.007825-0.0036800.0198140.0142710.0117710.0022680.0214290.008973-0.000597
2003-12-3118.19624917.85860115.94418319.01140015.89933914.08093112.62595222.54884525.04674061.580399...0.0980300.1309240.0453550.0662760.0313550.0177880.0655210.0578680.0400520.010867
2004-01-3117.39176618.26049816.46564119.20321116.57806014.08093112.88572023.16960124.90356461.655003...-0.0442110.0225040.0327050.0100890.0426890.0000000.0205740.027529-0.0057160.001211
..................................................................
2021-01-3170.34294938.90719228.82225484.509895128.57484463.74531261.689369114.632141161.75303686.304970...-0.0241750.037467-0.017978-0.042688-0.008383-0.049815-0.0087720.0140160.0076500.000232
2021-02-2873.24071547.64448932.16742790.331978130.33000262.95956457.927460112.220512160.89439486.254997...0.0411950.2245680.1160620.0688920.013651-0.012326-0.060981-0.021038-0.005308-0.000579
2021-03-3178.80000349.06000134.04999998.449997132.80999868.30999864.040001116.739998168.07000786.207031...0.0759040.0297100.0585240.0898690.0190290.0849820.1055210.0402730.044598-0.000556
2021-04-3083.04000149.38999936.259998101.930000139.69999769.58000266.720001121.330002178.91999886.256012...0.0538070.0067260.0649050.0353480.0518790.0185920.0418490.0393180.0645560.000568
2021-05-3187.26000252.20999937.990002105.120003138.39999470.80999865.160004123.599998172.83999686.307999...0.0508190.0570970.0477110.031296-0.0093060.017677-0.0233810.018709-0.0339820.000603

213 rows × 40 columns

섹터 로테이션 전략의 월별 수익률을 구합니다. 직전 달 모멘텀 값 기준으로 이번 달 투자 대상을 결정하는 것이니 주의합니다.

df_sector['RETURN'] = 0
df_sector['RETURN_ACC'] = 0
df_sector['LOG_RETURN'] = 0
df_sector['LOG_RETURN_ACC'] = 0

for i in range(len(df_sector)):
    strat_return = 0
    log_strat_return = 0
    
    # 직전 달 모멘텀이 좋은 것으로 리밸런싱해서 앞으로 한 달 가져가는 것
    if i > 0:
        strat_return = (df_sector[df_sector.iloc[i-1]['SECTOR1']+'_r'].iloc[i]+df_sector[df_sector.iloc[i-1]['SECTOR2']+'_r'].iloc[i]+df_sector[df_sector.iloc[i-1]['SECTOR3']+'_r'].iloc[i]+df_sector[df_sector.iloc[i-1]['SECTOR4']+'_r'].iloc[i]+df_sector[df_sector.iloc[i-1]['SECTOR5']+'_r'].iloc[i])/5
        log_strat_return = math.log(strat_return + 1)
        
    df_sector.loc[df_sector.index[i], 'RETURN'] = strat_return
    # 누적 = 직전 누적 * 현재
    df_sector.loc[df_sector.index[i], 'RETURN_ACC'] = (1+df_sector.loc[df_sector.index[i-1], 'RETURN_ACC'])*(1+strat_return)-1
    df_sector.loc[df_sector.index[i], 'LOG_RETURN'] = log_strat_return
    # 로그누적 = 직전 로그누적 + 현재 로그
    df_sector.loc[df_sector.index[i], 'LOG_RETURN_ACC'] = df_sector.loc[df_sector.index[i-1], 'LOG_RETURN_ACC'] + log_strat_return
    
# 수익률 * 100
df_sector[['RETURN','RETURN_ACC','LOG_RETURN','LOG_RETURN_ACC']] = df_sector[['RETURN','RETURN_ACC','LOG_RETURN','LOG_RETURN_ACC']]*100
df_sector[return_col] = df_sector[return_col] * 100
df_sector
XLBXLEXLFXLIXLKXLPXLUXLVXLYSHY...XLK_rXLP_rXLU_rXLV_rXLY_rSHY_rRETURNRETURN_ACCLOG_RETURNLOG_RETURN_ACC
Date
2003-09-3014.79593215.52012114.31708716.46527514.16100213.08161211.74232820.72658921.87705061.204376...NaNNaNNaNNaNNaNNaN0.0000000.0000000.0000000.000000
2003-10-3116.20572315.66854915.30874217.48330915.19906013.67389111.82275220.86818923.86802960.954796...7.3304014.5275720.6849090.6831799.100762-0.4077826.7141356.7141356.4983446.498344
2003-11-3016.57171815.79116215.25240317.82971615.41597013.83484011.84956221.31536524.08219360.918385...1.4271241.1770520.2267642.1428600.897287-0.0597350.8883197.6620970.8843967.382740
2003-12-3118.19624917.85860115.94418319.01140015.89933914.08093112.62595222.54884525.04674061.580399...3.1355071.7787776.5520575.7868144.0052261.0867236.30469514.4498646.11392713.496667
2004-01-3117.39176618.26049816.46564119.20321116.57806014.08093112.88572023.16960124.90356461.655003...4.2688660.0000002.0574172.752940-0.5716320.1211490.71110615.2637230.70858914.205256
..................................................................
2021-01-3170.34294938.90719228.82225484.509895128.57484463.74531261.689369114.632141161.75303686.304970...-0.838339-4.981469-0.8771951.4016170.7650160.023166-1.071591413.258151-1.077374163.560875
2021-02-2873.24071547.64448932.16742790.331978130.33000262.95956457.927460112.220512160.89439486.254997...1.365086-1.232636-6.098149-2.103798-0.530836-0.0579021.947834423.2555651.929106165.489981
2021-03-3178.80000349.06000134.04999998.449997132.80999868.30999864.040001116.739998168.07000786.207031...1.9028598.49820610.5520624.0273264.459828-0.0556105.393464451.4771645.253043170.743024
2021-04-3083.04000149.38999936.259998101.930000139.69999769.58000266.720001121.330002178.91999886.256012...5.1878621.8591784.1848853.9318186.4556380.0568184.506847476.3313934.408240175.151264
2021-05-3187.26000252.20999937.990002105.120003138.39999470.80999865.160004123.599998172.83999686.307999...-0.9305681.767743-2.3381261.870928-3.3981680.0602701.730772486.3063781.715965176.867230

213 rows × 44 columns

# MDD

df_sector['BALANCE'] = (1+df_sector['RETURN']/100).cumprod()
df_sector['DRAWDOWN'] = -(df_sector['BALANCE'].cummax() - df_sector['BALANCE']) / df_sector['BALANCE'].cummax()

df_sector[['BALANCE','DRAWDOWN']] = df_sector[['BALANCE','DRAWDOWN']] * 100
df_sector
XLBXLEXLFXLIXLKXLPXLUXLVXLYSHY...XLU_rXLV_rXLY_rSHY_rRETURNRETURN_ACCLOG_RETURNLOG_RETURN_ACCBALANCEDRAWDOWN
Date
2003-09-3014.79593215.52012114.31708716.46527514.16100213.08161211.74232820.72658921.87705061.204376...NaNNaNNaNNaN0.0000000.0000000.0000000.000000100.000000-0.000000
2003-10-3116.20572315.66854915.30874217.48330915.19906013.67389111.82275220.86818923.86802960.954796...0.6849090.6831799.100762-0.4077826.7141356.7141356.4983446.498344106.714135-0.000000
2003-11-3016.57171815.79116215.25240317.82971615.41597013.83484011.84956221.31536524.08219360.918385...0.2267642.1428600.897287-0.0597350.8883197.6620970.8843967.382740107.662097-0.000000
2003-12-3118.19624917.85860115.94418319.01140015.89933914.08093112.62595222.54884525.04674061.580399...6.5520575.7868144.0052261.0867236.30469514.4498646.11392713.496667114.449864-0.000000
2004-01-3117.39176618.26049816.46564119.20321116.57806014.08093112.88572023.16960124.90356461.655003...2.0574172.752940-0.5716320.1211490.71110615.2637230.70858914.205256115.263723-0.000000
..................................................................
2021-01-3170.34294938.90719228.82225484.509895128.57484463.74531261.689369114.632141161.75303686.304970...-0.8771951.4016170.7650160.023166-1.071591413.258151-1.077374163.560875513.258151-1.071591
2021-02-2873.24071547.64448932.16742790.331978130.33000262.95956457.927460112.220512160.89439486.254997...-6.098149-2.103798-0.530836-0.0579021.947834423.2555651.929106165.489981523.255565-0.000000
2021-03-3178.80000349.06000134.04999998.449997132.80999868.30999864.040001116.739998168.07000786.207031...10.5520624.0273264.459828-0.0556105.393464451.4771645.253043170.743024551.477164-0.000000
2021-04-3083.04000149.38999936.259998101.930000139.69999769.58000266.720001121.330002178.91999886.256012...4.1848853.9318186.4556380.0568184.506847476.3313934.408240175.151264576.331393-0.000000
2021-05-3187.26000252.20999937.990002105.120003138.39999470.80999865.160004123.599998172.83999686.307999...-2.3381261.870928-3.3981680.0602701.730772486.3063781.715965176.867230586.306378-0.000000

213 rows × 46 columns

기본적인 성과 분석 지표를 직접 구해봅니다.

total_month = len(df_sector)
profit_month = len(df_sector[df_sector['RETURN'] >= 0])
loss_month = len(df_sector[df_sector['RETURN'] < 0])
win_rate = profit_month / total_month * 100
CAGR = ((1+df_sector['RETURN_ACC'][-1]/100)**(1/(total_month/12)))-1
STDEV = np.std(df_sector['RETURN'][1:])*math.sqrt(12)
RRR = CAGR * 100 / STDEV

print(total_month, "개월 중 수익 월 :", profit_month, "개월")
print(total_month, "개월 중 손실 월 :", loss_month, "개월")
print("승률 :", round(win_rate, 2))

print('CAGR : ', round(CAGR*100, 2))
print('MDD : ', round(df_sector['DRAWDOWN'].min(), 2))
print('STDEV :', round(STDEV, 2))
print('Return-Risk Ratio: ', round(RRR, 2))
213 개월 중 수익 월 : 147 개월
213 개월 중 손실 월 : 66 개월
승률 : 69.01
CAGR :  10.48
MDD :  -34.98
STDEV : 12.51
Return-Risk Ratio:  0.84

CAGR 10.48%, 변동성 12.51%, MDD -35% 정도입니다. 동일 기간 S&P 500 ETF인 SPY 100%를 투자한다면 거의 동일한 CAGR에 14.2% 변동성, -50% MDD로 섹터 로테이션 듀얼 모멘텀 전략이 벤치마크인 SPY ETF보다 좋습니다.

plt.figure(figsize=(15,5))
seaborn.lineplot(data=df_sector, x=df_sector.index, y=df_sector['LOG_RETURN_ACC'])
<matplotlib.axes._subplots.AxesSubplot at 0x25066016c08>

output_21_1

plt.figure(figsize=(15,5))
seaborn.lineplot(data=df_sector, x=df_sector.index, y=df_sector['DRAWDOWN'])
<matplotlib.axes._subplots.AxesSubplot at 0x250660ad608>

output_22_1

아래 그림의 샤프 비율은 월간 데이터로 구해진 것이라 왜곡되어 있습니다. 환산시킨 0.86이나 Return-Risk Ratio 0.84가 더 믿을 만 합니다. 월간 수익률 차트는 참고할 만 합니다. 2008-2009 구간에 크게 당한 것 말고는 2020 코로나 구간도 괜찮은 방어력을 보여주었습니다.

quantstats.stats.sharpe(df_sector['RETURN'])/math.sqrt(252/12)
0.8624672303343784
quantstats.reports.plots(df_sector['RETURN']/100, mode='basic')

output_25_0 output_25_1




© 2021.03. by JacobJinwonLee

Powered by theorydb