Post List

2017년 10월 29일 일요일

R에서 nloptr 패키지를 이용한 Risk Parity Portfolio (R Code)



예전 파이썬의 scipy.optimize.minimize 을 이용하여  Risk Parity 구하는 법에 이어,
이번에는 모국어인 R 로 돌아와 Risk Parity를 구하는 방법을 올립니다.

물론 R에서는 FRAPO 패키지 내 PERC 함수를 사용하여
매우 손쉽게 Risk Parity (Portfolio Equal Risk Contribution)을 구할 수 있지만
패키지는 응용이 불가능 하기에,
손으로 구하는법 정도는 알아두면 유용하겠죠?


여러번 설명했듯이,
RP는 각 종목이 가져가는 Risk Contribution을 동일하게 만드는 방법입니다.





install.packages("nloptr")
library(nloptr)

R내에서 최적화에 많이 쓰이는 패키지인 nloptr 패키지를 먼저 설치합니다.



RiskParity_objective = function(x) {
    
    variance = t(x) %*% covmat %*% x
    sigma = sqrt(variance)
    MRC = (covmat %*% x) / as.numeric(sigma)
    
    rc = x * MRC
    rc_target = matrix(1/ncol(covmat) * sigma, nrow(covmat), 1)
    sum_risk_diff = sum( (rc - rc_target)^2 )
    
    return(sum_risk_diff)
    
 }


다음부터 본격적인 코드입니다.
먼저 x는 입력되는 weight 입니다.

variance는 알다시피 w'Ωw 로 구해지며
sigma는 이 값의 제곱근이죠

MRC는 Ωw / sigma 로 구해지며,
RC는 MRC값에 weight를 곱한 값으로 계산됩니다.

우리가 원하는 Target은 모든 종목의 RC 값이 동일하게 되는 것이며,
그 값은 sigma / # of Asset 으로 되게하는 거죠.

예를 들어 포트폴리오 변동성이 3%인 경우
종목이 2개라면 각 종목당 RC값이 1.5%씩,
종목이 3개라면 각 종목당 RC값이 1%씩 가져가면 됩니다.


다음으로 sum_risk_diff   는 최소화 하고자 하는 목표값입니다.

아래 표에서 처럼 최적화 전 RC값이 각각 0.50, 0.30, 0.20 이라면
RC값과 Target값 차이의 제고의 합은 0.05가 됩니다.

만일 RC 값이 모두 0.33인 weight를 찾는다면
이 합계는 0이 되겠죠?

최적화 방법론에서는 이 합계값을
될 수 있는한 작게 만드는 작업을 합니다.

물론 가져가는 RC값이 동일한게 아니라
A,B,C 종목이 가져가는 RC값의 비중을 50% : 30% : 20%
즉 Risk Budget 포트폴리오로도 하고 싶으면

Target 값에 저 값을 집어 넣으면 됩니다.


RC
Target
Diff
Diff^2
A
0.50
0.33
0.167
0.028
B
0.30
0.33
-0.033
0.001
C
0.20
0.33
-0.133
0.018
Sum
1.00
1.00
0.05




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


다음은 비균등제약조건과 균등제약조건의 설정입니다.

먼저 비균등제약조건은 종목이 0보다 큰 제약식을 넣어줍니다.
패키지내에서 기본적으로 A >= 0 으로 인식하기 때문에
이 A값에 weight인 x를 입력해주면 끝입니다.

다음으로 균등제약조건은 비중의 합이 1이 되는 제약식을 놓어줍니다.
패키지내에서는 A = 0 으로 인식하므로
sum(weight) = 1 → sum(weight) - 1  형식으로 바꿔주면 되겠죠?




#--- Set Initial Seed (Equal Weight ---#
  x0.equal = rep(1/ncol(covmat), ncol(covmat))


다음은 초기값 설정입니다.
그냥 심플하게 동일비중을 입력합니다.




result = slsqp( x0 = x0.equal, fn = RiskParity_objective,
                  hin = hin.objective,
                  heq = heq.objective,
                  control = list(xtol_rel = 1e-20, maxeval = 5000))


위에 입력된 값을 바탕으로 slsqp 함수를 이용해 최적화 작업을 합니다.
x0는 초기값이므로 위에서 구한 동일비중을
fn는 최소화 하고 싶은 목적함수이므로
sum of RC difference를,
hin은 inequality 제약조건이므로 비중이 0보다 크다는 함수를,
heq은 equality 제약조건이므로 비중의 합이 0이라는 함수를
control에는 iteration max 값과 tolerance 값을 입력합니다.


자 이제 제대로 작동하는지 봐야겠쥬?




symbols = c("SPY","IEV","EWJ","EEM","TLT","IEF","IYR","RWX","GLD","DBC")
getSymbols(symbols, src="yahoo")

prices = list()
for(i in 1:length(symbols)) {
  prices[[i]] = Ad(get(symbols[[i]]))
}
prices = do.call(cbind, prices)
rets = na.omit(Return.calculate(prices))
covmat = cov(rets)


먼저 getSymbols 패키지를 이용해 10개 종목의 데이터를 받습니다.
그 후, 수익률을 계산하고 covariance matrix를 구합니다.



#--- Risk Parity ---#

RiskParity = function(covmat) {
  
  #--- Risk Parity objective ---#
  RiskParity_objective = function(x) {
    
    variance = t(x) %*% covmat %*% x
    sigma = sqrt(variance)
    MRC = (covmat %*% x) / as.numeric(sigma)
    
    rc = x * MRC
    rc_target = matrix(1/ncol(covmat) * sigma, nrow(covmat), 1)
    sum_risk_diff = sum( (rc - rc_target)^2 )
    
    return(sum_risk_diff)
    
  }
  
  #--- 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/ncol(covmat), ncol(covmat))
  
  #--- Run Optimization ---#
  result = slsqp( x0 = x0.equal, fn = RiskParity_objective,
                  hin = hin.objective,
                  heq = heq.objective,
                  control = list(xtol_rel = 1e-20, maxeval = 5000))
  
  return(result)
}


위에서 구한 것들을 function으로 감쌌습니다.
간단하죠??



RiskParity(covmat)
RP_wt = RiskParity(covmat)$par

먼저 위의 RiskParity 패키지를 돌리면 아래와 같이 나옵니다.


$par는 계산된 값, 즉 Risk Parity를 만드는 weight 값입니다.
$value는 목적함수의 값, 즉 sum_risk_diff  입니다. 거의 0에 가깝죠?
$iter는 몇번만에 이 값을 구했느냐 입니다. 즉 125번 계산만에 구한거죠

밑에 것들은 별로 안중요하고, 결국 중요한건 $par값, 즉 weight 겠죠
RP_wt에 함수에서 나온 $par 값을 입력해 줍니다.


barplot(mrc(RP_wt, covmat))

다음은 FRAPO내 mrc 패키지를 이용해 RC값을 구해줍니다.
(엄밀히 말하면 RC인데 패키지 만든 사람이 MRC로 잘못 알고 만든...)

weight과 covariance matrix를 입력하면 RC 값을 계산해 주고,
이를 barplot으로 도식화 합니다.



10개 종목의 RC가 10%로 동일함이 확인됩니다.
그뤠이트!!




RP_package = Weights(PERC(covmat, percentage = FALSE))


PERC 패키지를 사용하여 구해보기도 합니다.
covmat을 입력하고. 퍼센티지로 나타낼까는 FALSE로 입력합니다.
그 후 Weights 값만 뽑아냅니다.



barplot( rbind(RP_wt, RP_package), beside = TRUE )

직접 만든 패키지와 기존 패키지에서 나온 값의 비중을 비교해봅니다.
rbind를 통해 묶고, barplot을 통해 도식화 해봅니다.





정말 티도 안날만큼 차이가 있다는 걸 확인할 수 있습니다.



nlopt의 패키지 내 slsqp 함수를 이용한 최적화를 통해
Risk Parity 구하는 법을 해보았습니다.

목적함수와 제약조건만 바꾼다면
Risk Budget Portfolio,
Minimum Volatility Portfolio,
Target Volatility Portfolio

얼마든지 응용이 가능합니다 


댓글 2개:

  1. 안녕하세요? 저 질문이 있습니다..
    맨 위에 W`오메가W를 편미분 할 때, 변수가 w1이니 미분하면 0 아닌가요? 미분 절차좀 상세히 알려주실 수 있으신지.. ㅠㅠ

    답글삭제