Post List

2017년 10월 31일 화요일

Most Diversified Portfolio (MDP) - R Code (DIY & Packages)




MDP(Most Diversified Portfolio) 의 경우
설명이나 코드가 구글링을 해도 잘 안나옵니다.

왜그럴까요?

아마 수식도 복잡하고, 코드 짜기도 귀찮고 하니..... 

해당 글에서는 R에서 MDP 구현하는 방법들을 설명하도록 하겠습니다.


먼저 MDP 란 DR(분산비율)을 최대화 하는 포트폴리오 입니다.
분자는 개별 표준편차의 가중합이며,
분모는 포트폴리오의 분산입니다.


분모의 Ω에는 개별 표준편차 외 종목간 상관관계가 존재하니
포트폴리오의 분산은 개별 표준편차의 가중합 보다 항상 낮습니다.
(대부분 상관관계는 1보다는 언제나 낮으니...)

따라서, 분산효과가 커질수록, 즉 상관관계를 낮게 포트폴리오를 구성할수록
분모는 점점 작아져서 결과적으로 DR 값은 커지게 됩니다.




이를 코드로 녹이는 방법은?
뭐 별로 안어렵습니다. 논문에 다 나와있거든요.



1. Choueifaty Synthetic Asset Back-Transformation 이용


이름은 어렵게 보여도 별거 아닙니다.
Choueifaty는 MDP 전략 개발하신 분이고
Synthetic Asset Back-Transformation는
A → B 했다가 B→ A 하는 수학적 테크닉입니다.

자세한건 이분들 논문에 나옵니다.




....





S는 종목당 weight이고 V는 variance covariance matrix 입니다.
즉, DR을 최대화 하는 값은
w'Cw를 최소화 하는 것과 같습니다.

C는 correlation matrix 이고요,




그리고 이렇게 계산된 weight에서 한번 더 작업을 해주어야 합니다.
논문을 읽어보면 알겠지만, 위에서는 가상의 작업이므로,
real portfolio로 transformation 해주기 위해
weight를 각각의 volatility로 나누어 주어야 합니다.




요약 하자면

(1) w'Cw가 최소화가 되는 weight를 구함
(2) 구해진 weight를 각 종목들의 sigma로 나눠줌
(3) 합이 1이 되도록 스케일 해줌


사실 과정은 관심 없으실 테니 
코드로 넘어가보죠


#--- Most Diversified Portfolio : Transformation ---#

MDP1 = function(covmat) {
  
  co = ncol(covmat)
  
  #--- MDP objective ---#
  objective = function(x) {
    corr = cov2cor(covmat)
    obj = t(x) %*% corr %*% x
    return(obj)
  }
  
  #--- Inequality Objective ---#
  hin.objective = function(x) {
    return(x)
  }
  
  #--- Equality Objective ---#
  heq.objective = function(x) {
    return( sum(x) - 1 )
  }
  
  #--- Set Initial Seed (Equal Weight) ---#
  x0.equal = rep(1/co, co)
  
  #--- Run Optimization ---#
  result = slsqp( x0 = x0.equal,
                  fn = objective,
                  hin = hin.objective,
                  heq = heq.objective,
                  control = list(xtol_rel = 1e-10, maxeval = 1000))
  
  w = result$par
  w = w / sqrt(diag(covmat))
  MDP_wt = round(w / sum(w), 4)
  names(MDP_wt) = colnames(covmat)
  return(MDP_wt)                       
}


먼저 입력값은 covariance matrix를 받아옵니다.
cov2cor는 이를 correlation matrix로 변환하는 함수이며,
obj에 w'Cw, 즉 우리가 최소화 하고자 하는 값을 입력해 줍니다.

hin.objective, 즉 비등식 제약조건에는 weight(x)가 0보다 큰 제약식을,
eq.objective, 즉 등식 제약조건에는 sum of weight가 1인 제약식을
입력해 줍니다.

이는 slsqp 함수에 넣고 최적화를 하면 먼저 비중 w가 구해줍니다.

diag(covmat)은 행렬줄 대각행렬을 뽑는 값이며,
해당값에 루트(sqrt)를 해주면 해당 종목의 volatility가 구해지죠



미리 구해준 w에 volatility 를 나눠준 후, 스케일링을 해주면
최종적으로 MDP를 만드는 weight가 계산됩니다.



2. Duality 이용


이것 또한 저작들 논문에 답을 주셨습니다.





요런저런 테크닉은 쓰면 MDP의 수식은
min w'Σw, 즉 포트폴리오의 변동성을 최소화하는 최소분산 포트폴리오

개별종목 변동성의 가중합이 1이라는 제약조건을 추가하면 됩니다.

see Berkovitz를 보라니 한번 보죠.





문충이라 죄송합니다.... 그냥 코드로 넘어갑시다.......


#--- Most Diversified Portfolio 2 : Duality ---#

MDP2 = function(covmat) {
  
  co = ncol(covmat)
  
  #--- MDP objective ---#
  objective = function(x) {
    obj = t(x) %*% covmat %*% x
    return(obj)
  }
  
  #--- Inequality Objective ---#
  hin.objective = function(x) {
    return(x)
  }
  
  #--- Equality Objective ---#
  heq.objective = function(x) {
    sum_x =  sqrt(diag(covmat)) %*% x
    return( sum_x - 1 )
  }
  
  #--- Set Initial Seed (Equal Weight) ---#
  x0.equal = rep(1/co, co)
  

  #--- Run Optimization ---#
  result = slsqp( x0 = x0.equal,
                  fn = objective,
                  hin = hin.objective,
                  heq = heq.objective,
                  control = list(xtol_rel = 1e-10, maxeval = 1000))
  
  w = result$par
  MDP_wt = round(w / sum(w), 4)
  names(MDP_wt) = colnames(covmat)
  return(MDP_wt)                       
}


목적함수에는 최소화 하고자 하는 포트폴리오 변동성 (w'Σw)을 입력해 줍니다.

hin.objective, 즉 비등식 제약조건에는 weight(x)가 0보다 큰 제약식을,
eq.objective, 즉 등식 제약조건에는 개별종목 변동성의 가중합이 1이라는
제약조건을 입력해 줍니다.

사실 이 제약조건을

#--- Equality Objective ---#
heq.objective = function(x) {
return( sum(x) - 1 )
}

로 바꿔주면 최소분산 포트폴리오를 구하는 코드가 됩니다. 띠용?!

slsqp를 통해 최적화 값을 찾아주고 이를 스케일링 해주면
역시나 MDP weight가 계산됩니다.

첫번째 방법보다 귀찮은 작업이 몇개 사라지긴 했습니다.



3. Min (-)DR 이용

사실 MDP는 DR을 최대화 하는 방법입니다.
그렇다면 이는 그냥 -DR를 최소화 하는 것과 같습니다.

??!!



위의 1,2 번째 방법이 매우 학문적인 방법이었다면
이번 방법은 매우 테크니컬한 방법입니다.


#--- Most Diversified Portfolio 3 : Min -DR ---#

MDP3 = function(covmat) {
  
  co = ncol(covmat)
  
  #--- MDP objective ---#
  objective = function(x) {
    nom = x %*% sqrt(diag(covmat))
    denom = sqrt(t(x) %*% covmat %*% x)
    dr = nom / denom
    return(-dr)
  }
  
  #--- Inequality Objective ---#
  hin.objective = function(x) {
    return(x)
  }
  
  #--- Equality Objective ---#
  heq.objective = function(x) {
    return( sum(x) - 1 )
  }
  
  #--- Set Initial Seed (Equal Weight) ---#
  x0.equal = rep(1/co, co)
  
  
  #--- Run Optimization ---#
  result = slsqp( x0 = x0.equal,
                  fn = objective,
                  hin = hin.objective,
                  heq = heq.objective,
                  control = list(xtol_rel = 1e-10, maxeval = 1000))
  
  w = result$par
  MDP_wt = round(w / sum(w), 4)
  names(MDP_wt) = colnames(covmat)
  return(MDP_wt)                       
}


목적함수의 nom에는 DR의 분자인 개별 변동성의 가중합을,
denom에는 DR의 분모인 포트폴리오의 변동성을 입력합니다.

분자 / 분모를 해준 값이 dr이며
return에 -dr을 입력합니다.
즉. -dr 을 최소화 하겠다는 말이겠죠?

나머지 제약식은 위와 동일하며,
결과적으로 MDP를 만드는 weight가 반환됩니다.



4. FRAPO 패키지 내 PMD 활용

글로벌 운용사 Invesco에 근무하는 bernhard pfaff 아저씨가 만든
FRAPO 패키지, 정말 좋습니다.

PERC: Portfolio of Equal Risk Contribuion (리스크 패러티)
PGMV: Portfolio of Global Minimum Volatility (최소 분산 포트폴리오)
PMD: Portfolio of Maximum Diversification (최대 분산효과 포트폴리오)

굉장히 손쉽게 최적화 포트폴리오를 만들 수 있습니다.
이외에도 좋은 기능이 매우 많습니다.


library(FRAPO)
round(Weights(PMD(rets, percentage = FALSE)), 4)

PMD 함수는 특이하게 covmat이 아닌 return을 받습니다.
코드 뜯어보면 아시겠지만 함수 내에서 알아서 return을 cov matrix로 바꿉니다.

그런다음 percentage 를 FALSE, 즉 소수점 형태로 셋팅하고
Weights로 묶어서 비중만을 뽑아냅니다.

허무할 만큼 쉽죠?



5. RiskPortfolios 내 optimalPortfolio 활용


감히 말씀드려 패키지 계의 미친놈이 나타났습니다....
2년 내내 짰던게 그냥 패키지로 다 나오는 이 허무함....

설명서[link]를 보시면 만든 사람 중에 Kris Boudt 이분이 보입니다.
네, DEoptim 패키지 만드시고 R 세미나 마다 보이시는 그 교수님이십니다.

optimalPortfolio 라는 함수 내 type 만 변경해 주면,

Mean-Variance 포트폴리오, 최소분산 포트폴리오,
역분산 포트폴리오(나이브 리스크 패러티), 리스크 패러티 포트폴리오, 
Risk-Efficient 포트폴리오

모두 구할 수 있습니다.

뒤에서 설명하겠지만 FRAPO에서는 셋팅할 수 없는
비중의 최소, 최대까지 셋팅할 수 있습니다.....

세부 코드 궁금하신 분은 github[Link]에 코드 있으니 참조하세요!

참고로 해당 함수에서는 3번 방법을 이용하여 MDP를 구합니다.



library(RiskPortfolios)
round(optimalPortfolio(covmat, control = list(type = 'maxdiv', constraint = 'lo')), 4)


covariance matrix를 입력하고, control 값에 세부사항을 적어줍니다.
type에 maxdiv, 즉 MDP를 입력해 주고,
constraint에 lo, 즉 long only를 입력해 주면 MDP weight가 구해집니다.



위의 5가지 방법으로 구한 weight가 동일한지 체크를 해봅니다.
거의 일치하는 군요!


R1
16.75%
1.80%
3.29%
0.00%
27.13%
30.13%
2.58%
0.00%
7.56%
10.76%
R2
16.75%
1.80%
3.29%
0.00%
27.13%
30.13%
2.58%
0.00%
7.56%
10.76%
R3
16.75%
1.80%
3.29%
0.00%
27.13%
30.13%
2.58%
0.00%
7.56%
10.76%
R4
16.75%
1.80%
3.29%
0.00%
27.13%
30.13%
2.58%
0.01%
7.56%
10.76%
R5
16.75%
1.80%
3.29%
0.00%
27.13%
30.13%
2.58%
0.00%
7.56%
10.76%


작업시간도 한번 비교해 봅니다.
1번과 2번은 최적화 작업이 복잡하다 보니 시간이 좀 소요되지만,
- DR을 계산하는 매우 단순한 3번은 1초도 걸리지 않습니다.

패키지인 4번 5번 역시 1초도 걸리지 않고요





그러나 한가지 문제가 있습니다.
특정 종목은 비중이 0으로 떨어지는 반면,
특정 종목에는 비중이 너무 쏠리는 코너해 문제가 발생합니다.

이를 해결하기 위해 종목의 최소, 최대 비중을 셋팅해 줍니다.
이는 3번 방법, 그리고 5번 방법에서만 가능합니다.


먼저 3번 함수에 최소. 최대 비중을 적용해 봅니다.


원래 함수에 단순히 최소, 최대 비중 제약조건만 넣어주면 됩니다.
slsqp 내에도 이 제약식을 넣어주면,
제약비중이 고려된 최적화 값이 반환됩니다.

lb와 ub는 원하는 값으로 얼마든지 바꿀 수 있습니다.



#--- Most Diversified Portfolio, UPPER ---#
MDP_up = function(covmat, lb, ub) {
  
  co = ncol(covmat)
  
  #--- MDP objective ---#
  objective = function(x) {
    nom = x %*% sqrt(diag(covmat))
    denom = sqrt(t(x) %*% covmat %*% x)
    dr = nom / denom
    return(-dr)
  }
  
  #--- Inequality Objective ---#
  hin.objective = function(x) {
    return(x)
  }
  
  #--- Equality Objective ---#
  heq.objective = function(x) {
    return( sum(x) - 1 )
  }
  
  #--- Set Initial Seed (Equal Weight) ---#
  x0.equal = rep(1/co, co)
  
  #--- Lower & upper Boundary ---$
  lower_weight = rep(lb, co)
  upper_weight = rep(ub, co)
  
  #--- Run Optimization ---#
  result = slsqp( x0 = x0.equal,
                  fn = objective,
                  lower = lower_weight,
                  upper = upper_weight,
                  hin = hin.objective,
                  heq = heq.objective,
                  control = list(xtol_rel = 1e-10, maxeval = 1000))
  w = result$par
  MDP_wt = round(w / sum(w), 4)
  names(MDP_wt) = colnames(covmat)
  return(MDP_wt)                       
}



optimalPortfolio 함수에도 최소, 최대 비중 제약조건을 추가할 수 있습니다.

optimalPortfolio(covmat, control = list(type = 'maxdiv', LB = rep(0.05, 10), UB = rep(0.20, 10)))

LB와 UB에 원하는 최대, 최소 비중을 넣어주면 끝납니다.


물론 두 방법 모두 제약조건을 특정한 값이 아니라
종목 마다 다르게 지정할 수도 있습니다.


---- 로 표시된 선이 최소, 최대 투자비중 제약조건이며
이 비제약조건의 비중,
가 제약조건의 비중입니다.

정확이 제약조건 내에 비중이 걸리는 것을 확인할 수 있습니다.




FRAPO내 dr 함수를 이용해 diversification ratio를 확인할 수도 있습니다.

제약조건이 없는 최적화값은 dr이 2.12,
제약조건이 있는 최적화값은 dr이 1.93 으로 계산됩니다.

아무래도 제약조건이 있다보니 최적화 값에서는 좀 멀어지는게 당연하겠죠?






요약

1. 계산하는 법 정도는 알아두면 코드 응용할 때 편해요
2. 패키지 짱짱맨!!!
3. RiskPortfolios 패키지 개짱짱맨!!! 매니저는 뭘 먹고 사나요!!!

댓글 1개:

  1. 금융계량분석때 저거 증명한다고 뺑이쳤는데 패키지까지 있군여 ㅋㅋㅋ

    답글삭제