Bước 2: Xử lý
và Tính toán các tham số
2.1. Tính Tỷ
suất sinh lợi (Returns)
returns <- CalculateReturns(prices, method = "log")
returns <- returns[-1, ]
stock_returns <- returns[, tickers]
market_returns <- returns[, market_index]
2.2. Ước tính
Beta cho từng cổ phiếu
stock_excess_returns <- stock_returns - rf_daily
market_excess_returns <- market_returns - rf_daily
calculate_beta <- function(stock_excess_ret, market_excess_ret) {
model <- lm(stock_excess_ret ~ market_excess_ret)
return(coef(model)[2])
}
betas <- apply(stock_excess_returns, 2, calculate_beta, market_excess_ret = market_excess_returns)
beta_df <- data.frame(Ticker = names(betas), Beta = betas)
kable(beta_df, caption = "Hệ số Beta ước tính cho từng cổ phiếu")
Hệ số Beta ước tính cho từng cổ phiếu
|
Ticker |
Beta |
| VCB |
VCB |
0.7868643 |
| CTG |
CTG |
1.1349729 |
| ACB |
ACB |
0.5232669 |
| VIC |
VIC |
0.7191851 |
| KDH |
KDH |
0.8230070 |
| NLG |
NLG |
0.9281138 |
| VNM |
VNM |
0.5392409 |
| MSN |
MSN |
0.8666979 |
| MWG |
MWG |
0.9852989 |
| HPG |
HPG |
1.0303310 |
| REE |
REE |
0.7860779 |
| GMD |
GMD |
0.8185845 |
| FPT |
FPT |
0.7918349 |
| CMG |
CMG |
0.8077552 |
| ELC |
ELC |
0.6776843 |
| TCM |
TCM |
0.7030618 |
| GIL |
GIL |
0.8427838 |
| KMR |
KMR |
0.5583024 |
2.3. Ước tính
TSSL kỳ vọng theo CAPM
mean_market_return_daily <- mean(market_returns)
mean_market_return_annual <- mean_market_return_daily * 252
expected_returns_annual <- rf_annual + betas * (mean_market_return_annual - rf_annual)
er_df <- data.frame(Ticker = names(expected_returns_annual), ExpectedReturn = expected_returns_annual)
kable(er_df, caption = "Tỷ suất sinh lợi kỳ vọng (hàng năm) theo CAPM")
Tỷ suất sinh lợi kỳ vọng (hàng năm) theo CAPM
|
Ticker |
ExpectedReturn |
| VCB |
VCB |
0.0759208 |
| CTG |
CTG |
0.0896001 |
| ACB |
ACB |
0.0655624 |
| VIC |
VIC |
0.0732612 |
| KDH |
KDH |
0.0773410 |
| NLG |
NLG |
0.0814713 |
| VNM |
VNM |
0.0661901 |
| MSN |
MSN |
0.0790579 |
| MWG |
MWG |
0.0837185 |
| HPG |
HPG |
0.0854881 |
| REE |
REE |
0.0758899 |
| GMD |
GMD |
0.0771672 |
| FPT |
FPT |
0.0761161 |
| CMG |
CMG |
0.0767417 |
| ELC |
ELC |
0.0716304 |
| TCM |
TCM |
0.0726276 |
| GIL |
GIL |
0.0781182 |
| KMR |
KMR |
0.0669392 |
2.4. Tính Ma
trận Hiệp phương sai
cov_matrix <- cov(stock_returns) * 252
kable(round(cov_matrix[1:min(5, ncol(cov_matrix)), 1:min(5, ncol(cov_matrix))], 6), caption = "Một phần Ma trận Hiệp phương sai (thường niên hóa)")
Một phần Ma trận Hiệp phương sai (thường niên hóa)
|
VCB |
CTG |
ACB |
VIC |
KDH |
| VCB |
0.082697 |
0.055485 |
0.013487 |
0.025069 |
0.023838 |
| CTG |
0.055485 |
0.118272 |
0.025992 |
0.028670 |
0.034918 |
| ACB |
0.013487 |
0.025992 |
0.091586 |
0.012123 |
0.018645 |
| VIC |
0.025069 |
0.028670 |
0.012123 |
0.084762 |
0.024213 |
| KDH |
0.023838 |
0.034918 |
0.018645 |
0.024213 |
0.085604 |
2.5. Kiểm
định giả thiết thống kê về quy luật phân phối xác suất
# Áp dụng kiểm định Jarque-Bera cho từng chuỗi tỷ suất sinh lợi của cổ phiếu
jb_results <- lapply(tickers, function(ticker) {
# Lấy chuỗi TSSL của một cổ phiếu
returns_vector <- as.vector(stock_returns[, ticker])
# Thực hiện kiểm định
test <- jarque.bera.test(returns_vector)
# Trả về kết quả dưới dạng data frame
data.frame(
"Mã CK" = ticker,
"Hệ số Jarque-Bera" = round(test$statistic, 2),
"p-value" = test$p.value,
"Kết luận (mức ý nghĩa 5%)" = ifelse(test$p.value < 0.05, "Bác bỏ H0", "Không đủ bằng chứng bác bỏ H0"),
stringsAsFactors = FALSE,
check.names = FALSE
)
})
# Gộp tất cả kết quả lại thành một bảng duy nhất
jb_table <- do.call(rbind, jb_results)
# In bảng kết quả ra văn bản
kable(jb_table, row.names = FALSE, caption = "Kết quả kiểm định phân phối chuẩn Jarque-Bera")
Kết quả kiểm định phân phối chuẩn Jarque-Bera
| Mã CK |
Hệ số Jarque-Bera |
p-value |
Kết luận (mức ý nghĩa 5%) |
| VCB |
2385.84 |
0 |
Bác bỏ H0 |
| CTG |
559.89 |
0 |
Bác bỏ H0 |
| ACB |
16527.58 |
0 |
Bác bỏ H0 |
| VIC |
5254.68 |
0 |
Bác bỏ H0 |
| KDH |
1328.43 |
0 |
Bác bỏ H0 |
| NLG |
596.01 |
0 |
Bác bỏ H0 |
| VNM |
277379.43 |
0 |
Bác bỏ H0 |
| MSN |
3029.60 |
0 |
Bác bỏ H0 |
| MWG |
488.74 |
0 |
Bác bỏ H0 |
| HPG |
283.54 |
0 |
Bác bỏ H0 |
| REE |
818.92 |
0 |
Bác bỏ H0 |
| GMD |
14581.80 |
0 |
Bác bỏ H0 |
| FPT |
1348.15 |
0 |
Bác bỏ H0 |
| CMG |
2407.11 |
0 |
Bác bỏ H0 |
| ELC |
8305.43 |
0 |
Bác bỏ H0 |
| TCM |
292.06 |
0 |
Bác bỏ H0 |
| GIL |
6944.70 |
0 |
Bác bỏ H0 |
| KMR |
205.71 |
0 |
Bác bỏ H0 |
2.6. Phân
tích đặc điểm Rủi ro, Lợi nhuận của các cổ phiếu riêng lẻ
### 1.3. Thiết lập các tham số dựa trên dữ liệu đã tải (Đã sửa lỗi)
# BƯỚC 1: In ra tên cột thực tế để kiểm tra
print("Tên các cột trong file dữ liệu của bạn là:")
## [1] "Tên các cột trong file dữ liệu của bạn là:"
print(colnames(prices))
## [1] "VCB" "CTG" "ACB" "VIC" "KDH" "NLG" "VNM" "MSN" "MWG" "HPG" "REE" "GMD"
## [13] "FPT" "CMG" "ELC" "TCM" "GIL" "KMR" "VNI"
# BƯỚC 2: Khai báo chính xác tên cột chỉ số thị trường
# (Hãy nhìn kết quả in ra ở trên và điền đúng tên cột VN-Index vào đây)
market_index <- "VNINDEX"
# BƯỚC 3: Code sẽ tự động lấy các tên cột còn lại làm mã cổ phiếu
all_symbols <- colnames(prices)
tickers <- setdiff(all_symbols, market_index)
# In ra danh sách tickers đã được nhận diện để kiểm tra lại lần nữa
print("Các mã cổ phiếu được nhận diện để phân tích:")
## [1] "Các mã cổ phiếu được nhận diện để phân tích:"
print(tickers)
## [1] "VCB" "CTG" "ACB" "VIC" "KDH" "NLG" "VNM" "MSN" "MWG" "HPG" "REE" "GMD"
## [13] "FPT" "CMG" "ELC" "TCM" "GIL" "KMR" "VNI"
# Lãi suất phi rủi ro (Risk-Free Rate - Rf)
rf_annual <- 0.045
rf_daily <- (1 + rf_annual)^(1/252) - 1
### 2.6. Phân tích đặc điểm Rủi ro, Lợi nhuận của các cổ phiếu riêng lẻ (Đã sửa lỗi)
# BƯỚC 1: Tạo một data frame để định nghĩa ngành cho mỗi cổ phiếu
industry_map <- data.frame(
Ticker = c("VCB", "CTG", "ACB", "VIC", "KDH", "NLG",
"VNM", "MSN", "MWG", "HPG", "REE", "GMD",
"FPT", "CMG", "ELC", "TCM", "GIL", "KMR"),
Ngành = c("Ngân hàng", "Ngân hàng", "Ngân hàng",
"Bất động sản", "Bất động sản", "Bất động sản",
"Tiêu dùng & Bán lẻ", "Tiêu dùng & Bán lẻ", "Tiêu dùng & Bán lẻ",
"Công nghiệp & Vật liệu", "Công nghiệp & Vật liệu", "Công nghiệp & Vật liệu",
"Công nghệ & Viễn thông", "Công nghệ & Viễn thông", "Công nghệ & Viễn thông","Dệt may","Dệt may","Dệt may")
)
# BƯỚC 2: Gộp tất cả các kết quả đã tính vào một data frame duy nhất
# Sử dụng names(betas) làm nguồn ticker chính để đảm bảo chỉ lấy các mã đã được tính beta thành công
summary_df <- data.frame(
Ticker = names(betas),
"TSSL Kỳ vọng (Năm)" = expected_returns_annual[names(betas)],
"Rủi ro (Độ lệch chuẩn - Năm)" = sqrt(diag(cov_matrix))[names(betas)],
"Hệ số Beta" = betas,
row.names = NULL # Đảm bảo không có lỗi về tên hàng
)
# Thêm thông tin ngành vào bảng
summary_df <- merge(summary_df, industry_map, by = "Ticker")
# Sắp xếp lại thứ tự các cột cho đẹp
summary_df <- summary_df[, c("Ticker", "Ngành", "TSSL.Kỳ.vọng..Năm.", "Rủi.ro..Độ.lệch.chuẩn...Năm.", "Hệ.số.Beta")]
# Đổi lại tên cột cho đẹp
colnames(summary_df) <- c("Mã CK", "Ngành", "TSSL Kỳ vọng (Năm)", "Rủi ro (ĐLC - Năm)", "Hệ số Beta")
# BƯỚC 3: Trình bày bảng kết quả
kable(summary_df,
digits = 4,
row.names = FALSE,
caption = "Bảng 2.3: Đặc điểm Rủi ro và Lợi nhuận kỳ vọng của các Cổ phiếu Riêng lẻ")
Bảng 2.3: Đặc điểm Rủi ro và Lợi nhuận kỳ vọng của các Cổ phiếu
Riêng lẻ
| Mã CK |
Ngành |
TSSL Kỳ vọng (Năm) |
Rủi ro (ĐLC - Năm) |
Hệ số Beta |
| ACB |
Ngân hàng |
0.0656 |
0.3026 |
0.5233 |
| CMG |
Công nghệ & Viễn thông |
0.0767 |
0.4050 |
0.8078 |
| CTG |
Ngân hàng |
0.0896 |
0.3439 |
1.1350 |
| ELC |
Công nghệ & Viễn thông |
0.0716 |
0.4197 |
0.6777 |
| FPT |
Công nghệ & Viễn thông |
0.0761 |
0.2495 |
0.7918 |
| GIL |
Dệt may |
0.0781 |
0.4199 |
0.8428 |
| GMD |
Công nghiệp & Vật liệu |
0.0772 |
0.3357 |
0.8186 |
| HPG |
Công nghiệp & Vật liệu |
0.0855 |
0.3299 |
1.0303 |
| KDH |
Bất động sản |
0.0773 |
0.2926 |
0.8230 |
| KMR |
Dệt may |
0.0669 |
0.4145 |
0.5583 |
| MSN |
Tiêu dùng & Bán lẻ |
0.0791 |
0.3372 |
0.8667 |
| MWG |
Tiêu dùng & Bán lẻ |
0.0837 |
0.3370 |
0.9853 |
| NLG |
Bất động sản |
0.0815 |
0.3425 |
0.9281 |
| REE |
Công nghiệp & Vật liệu |
0.0759 |
0.2922 |
0.7861 |
| TCM |
Dệt may |
0.0726 |
0.3743 |
0.7031 |
| VCB |
Ngân hàng |
0.0759 |
0.2876 |
0.7869 |
| VIC |
Bất động sản |
0.0733 |
0.2911 |
0.7192 |
| VNM |
Tiêu dùng & Bán lẻ |
0.0662 |
0.2625 |
0.5392 |
Bước 3 & 4: Tối ưu
hóa và Trình bày Kết quả (Phương pháp Toàn diện)
## Bước 3 & 4: Tối ưu hóa và Trình bày Kết quả (Phiên bản Hoàn chỉnh)
# --- PHẦN 1: Chuẩn bị các tham số đầu vào ---
expected_returns_daily <- expected_returns_annual / 252
cov_matrix_daily <- cov(stock_returns)
library(Matrix)
##
## Attaching package: 'Matrix'
## The following objects are masked from 'package:tidyr':
##
## expand, pack, unpack
cov_matrix_stable <- as.matrix(nearPD(cov_matrix_daily, corr = FALSE)$mat)
min_ret <- min(expected_returns_daily) + 1e-6
max_ret <- max(expected_returns_daily) - 1e-6
target_returns <- seq(min_ret, max_ret, length.out = 100)
# --- PHẦN 2: HÀM TỐI ƯU HÓA (CẢI TIẾN ĐỂ TRẢ VỀ TỶ TRỌNG) ---
find_optimal_portfolio <- function(target_return, expected_returns, cov_matrix) {
n <- length(expected_returns)
objective <- Q_objective(Q = 2 * cov_matrix, L = rep(0, n))
constraints <- L_constraint(L = rbind(rep(1, n), expected_returns),
dir = c("==", "=="),
rhs = c(1, target_return))
opt_problem <- OP(objective = objective, constraints = constraints,
bounds = V_bound(li = 1:n, lb = rep(0, n)))
sol <- ROI_solve(opt_problem, solver = "quadprog")
weights <- sol$solution
names(weights) <- names(expected_returns)
risk <- sqrt(sum(weights * (cov_matrix %*% weights)))
# Trả về cả TSSL, Rủi ro và Tỷ trọng
return(c(Return = target_return, Risk = risk, weights))
}
# --- PHẦN 3: CHẠY VÒNG LẶP ---
efficient_portfolios <- lapply(target_returns, function(tr) {
tryCatch({
find_optimal_portfolio(tr, expected_returns_daily, cov_matrix_stable)
}, error = function(e) NULL)
})
frontier_df <- do.call(rbind, efficient_portfolios)
frontier_df <- as.data.frame(frontier_df) %>% filter(is.finite(Return) & is.finite(Risk))
# --- PHẦN 4: XÁC ĐỊNH CÁC DANH MỤC TIÊU BIỂU ---
# Thường niên hóa TSSL và Rủi ro
frontier_df$Return_annual <- frontier_df$Return * 252
frontier_df$Risk_annual <- frontier_df$Risk * sqrt(252)
# BƯỚC 1: Tính Tỷ lệ Sharpe cho TẤT CẢ các danh mục trên đường biên
frontier_df$Sharpe_Ratio <- (frontier_df$Return_annual - rf_annual) / frontier_df$Risk_annual
# BƯỚC 2: Bây giờ mới trích xuất các danh mục cụ thể
# Tìm Danh mục MVP (có Risk_annual nhỏ nhất)
mvp_portfolio <- frontier_df[which.min(frontier_df$Risk_annual), ]
# Tìm Danh mục có tỷ lệ Sharpe tối đa
max_sharpe_portfolio <- frontier_df[which.max(frontier_df$Sharpe_Ratio), ]
# --- PHẦN 5: TRÌNH BÀY BẢNG TỶ TRỌNG (PHIÊN BẢN SỬA LỖI CUỐI CÙNG) ---
# Hàm để định dạng bảng (Đã sửa lỗi)
# Hàm này sẽ tự động tìm các cột tỷ trọng thay vì dựa vào biến 'tickers' bên ngoài
format_portfolio_table <- function(portfolio, caption_text) {
# Tự động xác định các cột không phải là tỷ trọng
meta_cols <- c("Return", "Risk", "Return_annual", "Risk_annual", "Sharpe_Ratio")
# Lấy các cột còn lại làm cột tỷ trọng
ticker_cols <- setdiff(colnames(portfolio), meta_cols)
# Chuyển đổi dòng dữ liệu tỷ trọng thành một vector có tên
portfolio_weights_vector <- as.numeric(portfolio[, ticker_cols])
names(portfolio_weights_vector) <- ticker_cols
# Lọc và hiển thị các cổ phiếu có tỷ trọng > 0.01%
display_weights <- portfolio_weights_vector[portfolio_weights_vector > 0.0001]
df <- data.frame(
"Cổ phiếu" = names(display_weights),
"Tỷ trọng" = paste0(round(as.numeric(display_weights) * 100, 2), "%")
)
# Tạo bảng thông số
stats <- data.frame(
"Thuộc tính" = c("TSSL kỳ vọng (năm)", "Rủi ro - ĐLC (năm)", "Tỷ lệ Sharpe"),
"Giá trị" = c(paste0(round(portfolio$Return_annual * 100, 2), "%"),
paste0(round(portfolio$Risk_annual * 100, 2), "%"),
round(portfolio$Sharpe_Ratio, 2))
)
print(kable(df, row.names = FALSE, caption = caption_text, align = 'lr'))
print(kable(stats, row.names = FALSE, caption = "Thông số của Danh mục", align = 'lr'))
}
# Trình bày 2 bảng kết quả (cách gọi hàm đã thay đổi, không cần 'tickers')
format_portfolio_table(mvp_portfolio, "Bảng: Tỷ trọng Danh mục Rủi ro Tối thiểu (MVP)")
##
##
## Table: Bảng: Tỷ trọng Danh mục Rủi ro Tối thiểu (MVP)
##
## |Cổ.phiếu | Tỷ.trọng|
## |:--------|--------:|
## |VCB | 9.11%|
## |ACB | 15.87%|
## |VIC | 13.87%|
## |KDH | 5.69%|
## |VNM | 19.1%|
## |MSN | 3.3%|
## |REE | 5.61%|
## |GMD | 1.03%|
## |FPT | 7.31%|
## |CMG | 3.39%|
## |ELC | 5.34%|
## |TCM | 3.61%|
## |GIL | 1.68%|
## |KMR | 5.08%|
##
##
## Table: Thông số của Danh mục
##
## |Thuộc.tính | Giá.trị|
## |:------------------|-------:|
## |TSSL kỳ vọng (năm) | 7.15%|
## |Rủi ro - ĐLC (năm) | 16.35%|
## |Tỷ lệ Sharpe | 0.16|
format_portfolio_table(max_sharpe_portfolio, "Bảng: Tỷ trọng Danh mục có Tỷ lệ Sharpe Tối đa")
##
##
## Table: Bảng: Tỷ trọng Danh mục có Tỷ lệ Sharpe Tối đa
##
## |Cổ.phiếu | Tỷ.trọng|
## |:--------|--------:|
## |VCB | 6.93%|
## |CTG | 10.63%|
## |ACB | 5.23%|
## |VIC | 12.35%|
## |KDH | 7.29%|
## |NLG | 5.41%|
## |VNM | 5.22%|
## |MSN | 7.43%|
## |MWG | 6.28%|
## |HPG | 8.2%|
## |REE | 3.6%|
## |GMD | 2.81%|
## |FPT | 7.1%|
## |CMG | 4.75%|
## |ELC | 2.79%|
## |TCM | 0.35%|
## |GIL | 3.57%|
## |KMR | 0.05%|
##
##
## Table: Thông số của Danh mục
##
## |Thuộc.tính | Giá.trị|
## |:------------------|-------:|
## |TSSL kỳ vọng (năm) | 7.79%|
## |Rủi ro - ĐLC (năm) | 18.61%|
## |Tỷ lệ Sharpe | 0.18|
# --- PHẦN 6: VẼ ĐỒ THỊ HOÀN CHỈNH ---
# (Phần này giữ nguyên như cũ, không cần thay đổi)
plot(frontier_df$Risk_annual, frontier_df$Return_annual, type = "l", col = "blue", lwd = 2,
xlim = c(0, max(sqrt(diag(cov_matrix))) * 1.1), ylim = c(0, max(expected_returns_annual) * 1.1),
xlab = "Rủi ro (Độ lệch chuẩn hàng năm)", ylab = "TSSL kỳ vọng (hàng năm)",
main = "Đường biên hiệu quả và các Danh mục Tối ưu")
points(sqrt(diag(cov_matrix)), expected_returns_annual, pch = 19, col = "grey")
text(sqrt(diag(cov_matrix)), expected_returns_annual, labels = tickers, cex = 0.7, pos = 4, col = "grey")
points(mvp_portfolio$Risk_annual, mvp_portfolio$Return_annual, pch = 19, col = "green", cex = 1.5)
text(mvp_portfolio$Risk_annual, mvp_portfolio$Return_annual, "MVP", pos = 2, col = "green")
points(max_sharpe_portfolio$Risk_annual, max_sharpe_portfolio$Return_annual, pch = 19, col = "red", cex = 1.5)
text(max_sharpe_portfolio$Risk_annual, max_sharpe_portfolio$Return_annual, "Max Sharpe", pos = 2, col = "red")
legend("bottomright", legend = c("Đường biên hiệu quả", "Cổ phiếu riêng lẻ", "Danh mục MVP", "Danh mục Sharpe tối đa"),
col = c("blue", "grey", "green", "red"), lty = c(1, NA, NA, NA), pch = c(NA, 19, 19, 19), cex = 0.8)

Diễn giải: Đường cong màu xanh lá cây là
Đường biên hiệu quả. Mọi danh mục nằm trên đường này là
tối ưu. Các điểm màu xanh dương là vị trí của từng cổ phiếu riêng lẻ. Có
thể thấy, bằng cách kết hợp chúng, chúng ta có thể tạo ra các danh mục
có rủi ro thấp hơn và/hoặc lợi nhuận cao hơn so với việc chỉ đầu tư vào
một cổ phiếu.
Kết
luận
Dựa trên mô hình CAPM và lý thuyết tối ưu hóa Markowitz, chúng tôi đã
xây dựng thành công đường biên hiệu quả cho danh mục 15 cổ phiếu ngành
Ngân hàng. Danh mục có rủi ro thấp nhất (MVP) đã được xác định với tỷ
trọng cụ thể cho từng tài sản
library(ggplot2)
library(ggrepel)
## Warning: package 'ggrepel' was built under R version 4.3.3
# Data điểm từng cổ phiếu (ĐÃ SỬA LỖI)
df_points <- data.frame(
Risk = sqrt(diag(cov_matrix)),
Return = as.numeric(expected_returns_annual[names(betas)]),
Ticker = names(betas)
)
# Đường biên hiệu quả
df_frontier <- data.frame(
Risk = frontier_df$Risk_annual,
Return = frontier_df$Return_annual
)
# MVP & Max Sharpe
df_special <- data.frame(
Risk = c(mvp_portfolio$Risk_annual, max_sharpe_portfolio$Risk_annual),
Return = c(mvp_portfolio$Return_annual, max_sharpe_portfolio$Return_annual),
Label = c("MVP", "Max Sharpe")
)
# VẼ ĐỒ THỊ
ggplot() +
geom_line(data = df_frontier,
aes(x = Risk, y = Return),
color = "#F39C12", linewidth = 1.5) +
geom_point(data = df_points,
aes(Risk, Return),
size = 3,
color = "#8E44AD") +
geom_text_repel(data = df_points,
aes(Risk, Return, label = Ticker),
size = 4,
color = "#5B2C6F",
force = 2,
max.overlaps = Inf,
min.segment.length = 0) +
geom_point(data = df_special,
aes(Risk, Return, color = Label),
size = 5) +
geom_text_repel(data = df_special,
aes(Risk, Return, label = Label, color = Label),
size = 5,
fontface = "bold",
box.padding = 0.4,
point.padding = 0.6,
max.overlaps = Inf) +
scale_color_manual(values = c(
"MVP" = "forestgreen",
"Max Sharpe" = "red3"
)) +
labs(
title = "ĐƯỜNG BIÊN HIỆU QUẢ & DANH MỤC TỐI ƯU",
x = "Rủi ro (Độ lệch chuẩn - Năm)",
y = "Tỷ suất sinh lợi kỳ vọng (Năm)"
) +
theme_minimal(base_size = 15) +
theme(
plot.title = element_text(hjust = 0.5,
color = "darkblue",
face = "bold",
size = 18),
legend.title = element_blank(),
legend.position = "right",
panel.grid.minor = element_blank()
)
