최근 트럼프 정부의 관세 정책으로 금융 시장의 변동성이 크게 증가함에
따라 시장 및 금리 위험 관리의 중요성이 커지고 있다. 특히, 채권
포트폴리오는 이러한 변동성 장세에서 안정적인 수익으로 제공하는
투자수단으로 여겨지지만, 금리 변동과 신용 리스크 확대로 인해 기대만큼의
방어력을 발휘하지 못하기도 한다.
본 리포트는 채권 ETF를 활용한 포트폴리오 구성을 통해, 현재와 같은 변동성
장세에서 채권 포트폴리오가 얼마나 효과적으로 투자자를 보호할 수 있는지를
검증하고자 한다. 이를 통해 채권 포트폴리오의 수익 안정성과 시장 변동성
대응 능력을 객관적으로 평가하여, 장기 투자 전략 수립에 대한 인사이트를
얻고자 한다.
library(purrr)
package <- c("tidyverse", "quantmod", "broom", "timetk", "pander", "broom", "scales", "PerformanceAnalytics", "PortfolioAnalytics", "cccp")
pack_map <- map(package, ~ library(.x, character.only = TRUE))
options(scipen = 999)
ETF_tickers <- tibble(ticker = c("TLT", "IEF", "SHY", "LQD", "HYG", "TIP"))
ETF_weights <- c(0.3, 0.2, 0.1, 0.2, 0.1, 0.1)
ETF_df <- ETF_tickers %>%
mutate(data = map(ticker, ~getSymbols(Symbols = .x,
from = "2007-02-01",
to = "2025-04-07",
auto.assign = FALSE,
warnings = FALSE) %>%
Ad() %>%
na.omit()),
monthly_ret = map(data, ~monthlyReturn(.x, type = "log")))
ETF_monthly_ret_matrix <- purrr::reduce(ETF_df$monthly_ret, cbind) %>%
na.omit() %>%
`colnames<-`(ETF_tickers$ticker)
Fixed_Income_ETF_portfolio <- Return.portfolio(R = ETF_monthly_ret_matrix, weights = ETF_weights, rebalance_on = "quarters")
charts.PerformanceSummary(Fixed_Income_ETF_portfolio, main = "Fixed Income ETF Portfolio")
Benchmark_tickers <- tibble(ticker = c("SPY", "GLD", "VNQ"))
Benchmark_df <- Benchmark_tickers %>%
mutate(data = map(ticker, ~getSymbols(Symbols = .x,
from = "2007-02-01",
to = "2025-04-07",
auto.assign = FALSE,
warnings = FALSE) %>%
Ad() %>%
na.omit()),
monthly_ret = map(data, ~monthlyReturn(.x, type = "log")))
Benchmark_monthly_ret_matrix <- purrr::reduce(Benchmark_df$monthly_ret, cbind)
ALL_ret <- cbind(Fixed_Income_ETF_portfolio, Benchmark_monthly_ret_matrix) %>%
`colnames<-`(c("Fixed Income ETF", "SPY", "GLD", "VNQ"))
charts.PerformanceSummary(ALL_ret, main = "Fixed Income ETF vs. Benchmark Assets")
| Fixed Income ETF | SPY | GLD | VNQ | |
|---|---|---|---|---|
| Annualized Return | 0.0322 | 0.0771 | 0.0677 | 0.0127 |
| Annualized Std Dev | 0.0756 | 0.1593 | 0.168 | 0.2335 |
| Annualized Sharpe (Rf=0%) | 0.4265 | 0.4841 | 0.4031 | 0.0544 |
| Semi Deviation | 0.0152 | 0.0356 | 0.0344 | 0.0527 |
| Gain Deviation | 0.0151 | 0.0244 | 0.0301 | 0.0368 |
| Loss Deviation | 0.0136 | 0.035 | 0.03 | 0.0592 |
| Downside Deviation (MAR=10%) | 0.0183 | 0.0361 | 0.0353 | 0.055 |
| Downside Deviation (Rf=0%) | 0.0137 | 0.0322 | 0.0308 | 0.0512 |
| Downside Deviation (0%) | 0.0137 | 0.0322 | 0.0308 | 0.0512 |
| Maximum Drawdown | 0.2709 | 0.53 | 0.4674 | 0.7501 |
| Historical VaR (95%) | -0.0329 | -0.0831 | -0.0662 | -0.0875 |
| Historical ES (95%) | -0.0435 | -0.1062 | -0.0976 | -0.1749 |
| Modified VaR (95%) | -0.031 | -0.0763 | -0.0746 | -0.1198 |
| Modified ES (95%) | -0.0419 | -0.1097 | -0.1008 | -0.2557 |
# 포트폴리오 현재 가치
pf_value <- 1
# 신뢰수준 99% Z-score
z_score <- qnorm(0.99)
# 투자기간
horizon <- 10
# 자산 비중 벡터
pf_w <- c(0.3, 0.2, 0.1, 0.2, 0.1, 0.1)
# 공분산 행렬
cov_matrix <- cov(ETF_monthly_ret_matrix)
# 포트폴리오 수익률 월간 변동성(표준편차) 계산
# 표준편차 함수 정의
pf_sd_function <- function(w, covmat) {
as.vector(sqrt(t(w) %*% covmat %*% w))
}
pf_sd_monthly <- pf_sd_function(w = pf_w, covmat = cov_matrix)
# 99% 신뢰수준에서의 10 Day VaR
pf_VaR <- pf_value * pf_sd_monthly * z_score * sqrt(10/20)
\(MVaR_i = \alpha~\times~\frac{Cov_{i,p}}{\sigma_p}\)
# MVaR 함수 정의
MVaR_function <- function(w, covmat, z_score) {
sd <- pf_sd_function(w, covmat)
round(as.vector(covmat %*% w / sd) * z_score, 4)
}
pf_MVaR <- MVaR_function(w = pf_w, covmat = cov_matrix, z_score = z_score)
tibble(MVaR = ETF_tickers$ticker,
VALUE = pf_MVaR) %>%
pander()
| MVaR | VALUE |
|---|---|
| TLT | 0.0887 |
| IEF | 0.0419 |
| SHY | 0.0064 |
| LQD | 0.0473 |
| HYG | 0.0253 |
| TIP | 0.0308 |
\(\frac{(R_i - R_f)}{MVaR_i} = \frac{(R_j - R_f)}{MVaR_j}\)
위 공식처럼 위험 1단위 당 얻는 excess return이 같아질 때가 Optimal Portfolio이다.
pf_ret <- round(Return.annualized(Fixed_Income_ETF_portfolio, scale = 12), 4)
pf_sh <- round(pf_ret / (pf_sd_monthly * sqrt(12)), 4)
가장 불균형했던 TLT(30% -> 20%)와 SHY(10% -> 20%)의 비중을 다시 설정한다.
# 리밸런싱 포트폴리오 비중
pf_w_rebal <- c(0.2, 0.2, 0.2, 0.2, 0.1, 0.1)
Fixed_Income_ETF_portfolio_rebal <- Return.portfolio(R = ETF_monthly_ret_matrix, weights = pf_w_rebal, rebalance_on = "quarters")
pf_ret_rebal <- round(Return.annualized(Fixed_Income_ETF_portfolio_rebal, scale = 12), 4)
pf_sd_monthly_rebal <- sqrt(t(pf_w_rebal) %*% cov_matrix %*% pf_w_rebal)
pf_sh_rebal <- round(pf_ret_rebal / (pf_sd_monthly_rebal * sqrt(12)), 4)
그럼 이제 포트폴리오 개별 자산들의 MVaR가 어떻게 변했는지 확인해보자.
pf_MVaR_rebal <- MVaR_function(w = pf_w_rebal, covmat = cov_matrix, z_score = z_score)
tibble(MVaR = ETF_tickers$ticker,
VALUE = pf_MVaR_rebal) %>%
pander()
| MVaR | VALUE |
|---|---|
| TLT | 0.0855 |
| IEF | 0.0413 |
| SHY | 0.0065 |
| LQD | 0.0492 |
| HYG | 0.0296 |
| TIP | 0.0318 |
\(Component VaR = V_i~\times~MVaR_i\)
cccp::rp함수를 통해 쉽게 위험 균형 포트폴리오 비중을
계산할 수 있다.# 초기 조건 설정
n_assets <- length(ETF_tickers$ticker)
w_0 <- rep(1/n_assets, n_assets)
# Risk Contribution 함수 정의
get_RC = function(w, covmat) {
MVaR <- MVaR_function(w, covmat, z_score)
RC = MVaR * w
RC = c(RC / sum(RC))
return(RC)
}
# 최적화 함수
opt <- cccp::rp(x0 = w_0, P = cov_matrix, mrc = w_0)
pf_w_risk_parity = getx(opt) %>% drop()
pf_w_risk_parity = (pf_w_risk_parity / sum(pf_w_risk_parity)) %>%
round(., 4)
RC_tibble <- tibble(Ticker = ETF_tickers$ticker,
Weights = pf_w_risk_parity,
RC = get_RC(pf_w_risk_parity, cov_matrix))
| Ticker | Weights | RC |
|---|---|---|
| TLT | 0.0527 | 0.1666 |
| IEF | 0.1006 | 0.1667 |
| SHY | 0.5526 | 0.1671 |
| LQD | 0.0777 | 0.1665 |
| HYG | 0.1018 | 0.1665 |
| TIP | 0.1147 | 0.1666 |
RC_port <- Return.portfolio(R = ETF_monthly_ret_matrix, weights = RC_tibble$Weights, rebalance_on = "quarters")
ALL_ret2 <- cbind(RC_port, Fixed_Income_ETF_portfolio) %>%
`colnames<-`(c("Risk Parity ETF", "Fixed Income ETF"))
rbind(table.AnnualizedReturns(ALL_ret2, scale = 12), table.DownsideRisk(ALL_ret2, scale = 12)) %>%
pander()
| Risk Parity ETF | Fixed Income ETF | |
|---|---|---|
| Annualized Return | 0.0261 | 0.0322 |
| Annualized Std Dev | 0.0348 | 0.0756 |
| Annualized Sharpe (Rf=0%) | 0.7494 | 0.4265 |
| Semi Deviation | 0.0072 | 0.0152 |
| Gain Deviation | 0.0069 | 0.0151 |
| Loss Deviation | 0.0068 | 0.0136 |
| Downside Deviation (MAR=10%) | 0.0109 | 0.0183 |
| Downside Deviation (Rf=0%) | 0.0061 | 0.0137 |
| Downside Deviation (0%) | 0.0061 | 0.0137 |
| Maximum Drawdown | 0.1145 | 0.2709 |
| Historical VaR (95%) | -0.0162 | -0.0329 |
| Historical ES (95%) | -0.0208 | -0.0435 |
| Modified VaR (95%) | -0.0139 | -0.031 |
| Modified ES (95%) | -0.0205 | -0.0419 |
ETF_duration <- c(15.88, 7.06, 1.86, 7.96, 3.35, 6.48)
ETF_convexity <- c(3.46, 0.59, 0.05, 1.10, -0.04, 0.81)
ETF_weights <- c(0.3, 0.2, 0.1, 0.2, 0.1, 0.1)
pf_duration <- as.vector(ETF_duration %*% ETF_weights)
# 금리 변화 시나리오
delta_y <- seq(-0.02, 0.02, by = 0.005)
# 민감도 계산 함수 정의
price_change <- function(D, C, dy){
- D * dy + 0.5 * C * dy^2
}
# 포트폴리오 민감도 계산
portfolio_change <- sapply(delta_y, function(dy) {
changes <- mapply(price_change, D = ETF_duration, C = ETF_convexity, MoreArgs = list(dy = dy))
sum(ETF_weights * changes)
})
sensitivity_df <- data.frame(
Rate_Changes = delta_y,
Value_Changes = portfolio_change
)
pander(sensitivity_df)
| Rate_Changes | Value_Changes |
|---|---|
| -0.02 | 0.179 |
| -0.015 | 0.1342 |
| -0.01 | 0.08944 |
| -0.005 | 0.0447 |
| 0 | 0 |
| 0.005 | -0.04467 |
| 0.01 | -0.0893 |
| 0.015 | -0.1339 |
| 0.02 | -0.1784 |
본 리포트에서는 채권 ETF로 구성된 포트폴리오가 급변하는 금융환경에서
투자자들에게 방어막 역할을 해줄 수 있는지에 대해 분석하였고 채권이 주식,
원자재, 부동산 자산군 보다 확실하게 방어적인 자산임을 알 수 있었다.
하지만, 금리 상승 시, 장기채 중심의 포트폴리오는 큰 손실에 노출되며,
듀레이션과 컨벡서티를 반영한 민감도 분석에서도 방어 성능에 한계가
존재함을 확인하였다. 이는 금리 변동성이 높은 현재 시장에서 채권에 대한
과도한 편중이 오히려 리스크 요인이 될 수 있음을 시사한다.
이에 따라, 금리 상승 국면에서는 포트폴리오의 듀레이션을 축소하고, TIPS
및 단기 국채의 비중을 확대하여 민감도를 조정할 필요가 있다. 또한, 금리
리스크 완충을 위해 주식이나 원자재 등 다른 자산군의 전략적 활용도
병행되어야 하며, 이는 궁극적으로 포트폴리오의 회복탄력성과 안정성을
높이는 데 기여할 것이다.