class: center, middle, inverse, title-slide .title[ # 量化金融与金融编程 ] .subtitle[ ## L12 投资组合优化 ] .author[ ###
曾永艺 ] .institute[ ### 厦门大学管理学院 ] .date[ ###
2022-12-09 ] --- class: inverse, center, middle
# 1. 投资组合优化 .font150[_Portfolio Optimization_] --- class: middle <img src="imgs/naive.png" width="90%" style="display: block; margin: auto;" /> --- class: middle <img src="imgs/mark.png" width="100%" style="display: block; margin: auto;" /> --- > .font140[**Minimum-Variance Frontier**] <br> .font120[_Portfolios that provide the lowest possible risk for any given level of expected return._] -- <img src="imgs/ret_risk.png" width="68%" style="display: block; margin: auto;" /> --- class: inverse, center, middle # 2. `PortfolioAnalytics` <sup>.font60[v1.1.0]</sup> --- layout: true ### >> 2.1 Strengths --- > .font140[`PortfolioAnalytics` _is designed to provide numerical solutions and visualizations for portfolio optimization problems with complex constraints and objectives._] -- .font130[ - Multiple and modular constraint and objective types - An objective function can be any valid R function - User defined moment functions (e.g., covariance matrix, return projections) - Visualizations - Solver agnostic - Parallel computing ] --- layout: true ### >> 2.2 Framework --- <img src="imgs/PA.svg" width="90%" style="display: block; margin: auto;" /> --- layout: true ### >> 2.3 Workflow --- .full-width[.content-box-blue.bold.font120[Portfolio Specification]] .code100[ ```r # install.packages("PortfolioAnalytics") library(PortfolioAnalytics) ``` ] -- .code100[ ```r # portfolio.spec(assets = NULL, category_labels = NULL, # weight_seq = NULL, message = FALSE) # eg., character vector of assets portfolio.spec(assets = c("SP500", "FTSE100", "DAX", "CAC40")) # eg., named vector of assets with initial weights initial_weights <- c("SP500" = 0.5, "FTSE100" = 0.3, "NIKKEI" = 0.2) portfolio.spec(assets = initial_weights) # eg., scalar of number of assets portfolio.spec(assets = 4) ``` ] --- .full-width[.content-box-blue.bold.font120[Add Constraints]] .code100[ ```r add.constraint(portfolio, type, enabled = TRUE, message = FALSE, ..., indexnum = NULL) ``` ] .code85[ ```r # type: # 'weight_sum'|'leverage'|'weight' ('full_investment', 'dollar_neutral'|'active'); # 'box' ('long-only'); 'group'; 'turnover'; 'diversification'; 'position_limit'; # 'return'; 'factor_exposure'; 'leverage_exposure' ``` ] -- .code95[ ```r # initialize portfolio specification p <- portfolio.spec(assets = 4) # eg., add full investment constraint (i.e., `type = "full_investment"`) p <- add.constraint(portfolio = p, type = "weight_sum", min_sum = 1, max_sum = 1) # eg., add box constraint p <- add.constraint(portfolio = p, type = "box", min = 0.2, max = 0.6) ``` ] --- .full-width[.content-box-blue.bold.font120[Add Objectives]] .code100[ ```r add.objective(portfolio, constraints = NULL, type, name, arguments = NULL, enabled = TRUE, ..., indexnum = NULL) # type: 'return', 'risk', 'risk_budget', 'quadratic utility' or # 'weight_concentration' ``` ] -- .code100[ ```r # initialize portfolio specification p <- portfolio.spec(assets = 4) # eg., add mean return objective p <- add.objective(portfolio = p, type = "return", name = "mean") # eg., add expected shortfall risk objective p <- add.objective(portfolio = p, type = "risk", name = "ES", arguments = list(p = 0.9, method = "gaussian") ``` ] --- .full-width[.content-box-blue.bold.font120[Run Optimizations]] .code100[ ```r # Single period optimization optimize.portfolio( R, portfolio = NULL, constraints = NULL, objectives = NULL, optimize_method = c("DEoptim", "random", "ROI", "pso","GenSA"), search_size = 20000, trace = FALSE, ..., rp = NULL, momentFUN = "set.portfolio.moments", message = FALSE) ``` ] -- .code100[ ```r # Optimization with periodic rebalancing (backtesting) optimize.portfolio.rebalancing( R, portfolio = NULL, constraints = NULL, objectives = NULL, optimize_method = c("DEoptim", "random", "ROI"), search_size = 20000, trace = FALSE, ..., rp = NULL, rebalance_on = NULL, training_period = NULL, rolling_window = NULL) ``` ] --- .full-width[.content-box-blue.bold.font120[Run Optimizations: main options]] .code110[ ``` ## R an xts, vector, matrix, data.frame, timeSeries or zoo object of asset returns. ``` ] -- .code110[ ``` ## optimize_method - Global Solvers: - DEoptim: Differential Evolution Optimization - random: Random Portfolios Optimization, ?random_portfolios - GenSA: Generalized Simulated Annealing - pso: Particle Swarm Optimization - LP and QP Solvers: - ROI: R Optimization Infrastructure for linear and quadratic programming solvers ``` ] --- .full-width[.content-box-blue.bold.font120[Run Optimizations: main options]] .code100[ ``` ## momentFUN: the name of a function to call to set portfolio moments ``` ] -- .code90[ ``` + Asset Return Moment Estimates Methods: - Sample - Shrinkage Estimators - Factor Model - Expressing Views - Robust Statistics ``` ] -- .pull-left.code90[ ``` + default: set.portfolio.moments( R, portfolio, momentargs = NULL, method = c("sample", "boudt", "black_litterman", "meucci"), ... ) ``` ] -- .pull-right.code90[ ``` + user-defined moment function: - Arguments - `R` for asset returns - `portfolio` for the portfolio specification object - Return a named list of moments - `mu`: Expected returns vector - `sigma`: Variance-covariance matrix - `m3`: Coskewness matrix - `m4`: Cokurtosis matrix ``` ] --- .full-width[.content-box-blue.bold.font120[Analyze Results]] .pull-left.code100[ ```r # data extraction print() summary() extractObjectiveMeasures() extractStats() extractWeights() # visualization plot() chart.Weights() chart.EfficientFrontier() chart.Concentration() chart.RiskReward() chart.RiskBudget() ``` ] -- .pull-right.code100[ ```r # compute portfolio returns and # table and chart with # PerformanceAnalytics wts <- extractWeights(opt) Rp <- Return.portfolio( R, weights = wts ) table.*(Rp) charts.PerformanceSummary(Rp) ``` ] --- layout: false class: inverse, center, middle # 3. 案例 .font150[(_Case with `PortfolioAnalytics`_)] --- layout: true ### >> 3.1 导入并处理 [{{全球指数数据}}](data/global_indexes.csv) --- ```r # 加载必要的 R 包 library(tidyverse) library(tidyquant) library(PortfolioAnalytics) ``` .code100[ ```r # ... (导入与预处理代码从略) # 注意:变量名及第二行和第三行的处理 # 缺失值填补 # 日期型变量 indexes ``` ``` #> # A tibble: 3,168 × 9 #> date SPX FTSE FCHI GDAXI N225 HSI `000300` SENSEX #> <date> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> #> 1 2009-12-31 1115. 5413. 3936. 6048. 10655. 21872. 3576. 17465. #> 2 2010-01-04 1133. 5500. 4014. 6048. 10655. 21823. 3535. 17559. #> 3 2010-01-05 1137. 5522. 4013. 6032. 10682. 22280. 3564. 17686. #> 4 2010-01-06 1137. 5530. 4018. 6034. 10731. 22417. 3542. 17701. #> 5 2010-01-07 1142. 5527. 4025. 6019. 10682. 22269. 3471. 17616. #> # … with 3,163 more rows ``` ] --- ```r # 数据齐整:宽变长,方便下一步处理 indexes %>% pivot_longer(cols = -date, names_to = "symbol", values_to = "close") %>% arrange(symbol, date) %>% group_by(symbol) -> indexes_l # 基于日指数收盘点位,计算月度收益率数据 indexes_l %>% tq_transmute( select = close, mutate_fun = monthlyReturn ) %>% rename(return = monthly.returns) -> idx_rets_l idx_rets_l ``` ``` #> # A tibble: 1,248 × 3 #> # Groups: symbol [8] #> symbol date return #> <chr> <date> <dbl> #> 1 000300 2009-12-31 0 #> 2 000300 2010-01-29 -0.104 #> 3 000300 2010-02-26 0.0242 #> # … with 1,245 more rows ``` --- layout: true ### >> 3.2 探索性数据分析 --- ```r # 月度收益数据表长变宽,方便后续步骤 idx_rets_l %>% filter(row_number() != 1) %>% pivot_wider(names_from = symbol, values_from = return) -> idx_rets_w # 制表代码从略 ... ```
N
Min
P25
Mean
Median
P75
Max
SD
Histogram
000300
155
-0.210
-0.046
0.003
0.003
0.040
0.258
0.066
▂▃▇▄▁▁
FCHI
155
-0.172
-0.026
0.005
0.003
0.037
0.201
0.049
▂▅▇▆▄
FTSE
155
-0.138
-0.020
0.003
0.008
0.027
0.124
0.037
▁▂▄▇▄▂
GDAXI
155
-0.192
-0.021
0.007
0.007
0.040
0.150
0.052
▁▂▃▇▆▄▁
HSI
155
-0.147
-0.029
0.001
0.005
0.031
0.266
0.057
▁▂▄▇▅▂
N225
155
-0.117
-0.016
0.008
0.013
0.039
0.150
0.051
▁▂▁▃▇▄▄▁▁
SENSEX
155
-0.231
-0.019
0.010
0.008
0.041
0.144
0.049
▂▅▇▄▂▁
SPX
155
-0.125
-0.014
0.009
0.015
0.034
0.127
0.043
▁▁▂▄▇▅▂▁
--- .pull-left[ ```r # 用 ggplot2 绘制指数增长图,从略 ... ``` <!-- --> ] -- .pull-right.code85[ ```r # 绘制相关系数矩阵图 idx_rets_w %>% select(-date) %>% cor() %>% corrplot::corrplot.mixed( upper ='ellipse', order ='hclust') ``` <img src="L12_Portfolio_files/figure-html/unnamed-chunk-24-1.png" width="85%" style="display: block; margin: auto;" /> ] --- layout: true ### >> 3.3 基准投资组合 --- .code90[ ```r # PortfolioAnalytics 对 xts 数据的接受度更高 returns <- idx_rets_w %>% timetk::tk_xts(select = -date, date_var = date) head(returns, n = 5) ``` ``` #> 000300 FCHI FTSE GDAXI HSI N225 SENSEX SPX #> 2010-01-29 -0.1039 -0.0500 -0.0414 -0.07267 -0.08003 -0.04287 -0.06338 -0.0370 #> 2010-02-26 0.0242 -0.0082 0.0320 -0.00184 0.02419 -0.00706 0.00438 0.0285 #> 2010-03-31 0.0195 0.0715 0.0607 0.09915 0.03060 0.09519 0.06684 0.0588 #> 2010-04-30 -0.0832 -0.0395 -0.0222 -0.00290 -0.00616 -0.00293 0.00177 0.0148 #> 2010-05-31 -0.0959 -0.0811 -0.0657 -0.02793 -0.06364 -0.11655 -0.03497 -0.0820 ``` ```r # compute the benchmark returns with equal weights # Naive diversification equal_weights <- rep(1 / ncol(returns), ncol(returns)) bnchmk_rets <- returns %>% Return.portfolio( # PerformanceAnalytics weights = equal_weights, rebalance_on = "quarters" ) colnames(bnchmk_rets) <- "benchmark" # default: "portfolio.returns" ``` ] --- .code75[ ```r # Benchmark performance table.AnnualizedReturns(cbind(bnchmk_rets, returns)) # PerformanceAnalytics ``` ``` #> benchmark X000300 FCHI FTSE GDAXI HSI N225 SENSEX SPX #> Annualized Return 0.0577 0.0058 0.0425 0.0263 0.0694 -0.0125 0.0776 0.104 0.106 #> Annualized Std Dev 0.1336 0.2276 0.1686 0.1278 0.1791 0.1967 0.1751 0.170 0.150 #> Annualized Sharpe (Rf=0%) 0.4322 0.0255 0.2521 0.2061 0.3878 -0.0634 0.4432 0.612 0.703 ``` ] -- .code75[ ```r library(dygraphs) cumprod(1 + cbind(bnchmk_rets, returns)) %>% dygraph(width = 1000, height = 300) %>% dyRangeSelector() ```
] --- layout: true ### >> 3.4 投资组合优化(基础版) --- .code90[ ```r # Base portfolio specification (portfolio.spec(assets = colnames(returns)) %>% * add.constraint(type = "full_investment") %>% * add.constraint(type = "long_only") %>% * add.objective(type = "risk", name = "StdDev") -> base_ps) ``` ] -- .code70[ ``` #> ************************************************** #> PortfolioAnalytics Portfolio Specification #> ************************************************** #> #> Call: #> portfolio.spec(assets = colnames(returns)) #> #> Number of assets: 8 *#> Asset Names #> [1] "000300" "FCHI" "FTSE" "GDAXI" "HSI" "N225" "SENSEX" "SPX" #> *#> Constraints #> Enabled constraint types #> - full_investment #> - long_only #> *#> Objectives: #> Enabled objective names #> - StdDev ``` ] --- .code90[ ```r # Run the optimization with periodic rebalancing # install.packages(c("ROI", "ROI.plugin.glpk", "ROI.plugin.quadprog")) opt_base <- optimize.portfolio.rebalancing( R = returns, portfolio = base_ps, * optimize_method = "ROI", rebalance_on = "quarters", training_period = 60, # 60 months rolling_window = 60 # 60 months ) # Calculate portfolio returns base_rets <- Return.portfolio(returns, extractWeights(opt_base)) colnames(base_rets) <- "base" ``` ] -- .code90[ ```r # Compare annualized performance table.AnnualizedReturns(cbind(bnchmk_rets, base_rets) %>% na.omit()) ``` ``` #> benchmark base #> Annualized Return 0.0512 0.0331 #> Annualized Std Dev 0.1377 0.1263 #> Annualized Sharpe (Rf=0%) 0.3716 0.2619 ``` ] --- .code90[ ```r # chart.Weights(opt_base) library(plotly) extractWeights(opt_base) %>% timetk::tk_tbl(rename_index = "date") %>% tidyr::pivot_longer(cols = -date, names_to="index", values_to="weights") %>% plot_ly(x=~date, y=~weights, color=~index, type="bar", height=320) %>% layout(barmode = "stack", xaxis = list(title = ""), yaxis = list(title = "Weights(%)", tickformat = ".0%")) ```
] --- layout: true ### >> 3.5 投资组合优化(改进版) --- .code90[ ```r # Update the constraint box_ps <- base_ps %>% * add.constraint(type = "box", min = 0.05, max = 0.4, indexnum = 2) # Q:如何给单个资产设置权重上下限? ``` ```r # Backtest opt_box <- optimize.portfolio.rebalancing( R = returns, portfolio = box_ps, optimize_method = "ROI", rebalance_on = "quarters", training_period = 60, rolling_window = 60 ) # Calculate portfolio returns box_rets <- Return.portfolio(returns, extractWeights(opt_box)) colnames(box_rets) <- "box" ``` ] --- .code90[ ```r # Compare annualized performance cbind(bnchmk_rets, base_rets, box_rets) %>% na.omit() %>% table.AnnualizedReturns() ``` ``` #> benchmark base box #> Annualized Return 0.0512 0.0331 0.0368 #> Annualized Std Dev 0.1377 0.1263 0.1294 #> Annualized Sharpe (Rf=0%) 0.3716 0.2619 0.2848 ``` ] --
--- layout: false class: inverse, center, middle # 课后作业 --- ### >> 课后作业 .font140.bold[ 1. 理解投资组合优化的原理以及 `PortfolioAnalytics` 包的实现 > .code70[ ``` vignette("portfolio_vignette", package = "PortfolioAnalytics") # 31 demos in total demo(package = "PortfolioAnalytics") ``` ] 2. 个人编程作业:复现 / 改进本讲“3. 案例”的代码 ] .font120[ - 综合利用本课程所讲 / 所学知识完成数据导入、清洁 -> 探索性分析 -> 构建投资组合 -> 分析投资组合绩效; - 使用 📑 _Rmd_ 或 _qmd_ 文档记录你的研究过程(含R代码),并对结果进行简要分析; - 将数据、Rmd文档/qmd文档、`knitr`后生成的html文档等打包为“L12\_投资组合\_[学号].zip”,于.red.bold[12月25日24:00前]提交至.bold[[{{坚果云收件箱 📬}}](https://send2me.cn/t6dwGFZ-/Tqi83uQyAxrY4g==)]。 ] --- class: center middle background-image: url(imgs/xaringan.png) background-size: 12% background-position: 50% 40% <br><br><br><br><br><br><br> <hr color='#f00' size='2px' width='80%'> <br> .Large.red[_**本网页版讲义的制作由 R 包 [{{`xaringan`}}](https://github.com/yihui/xaringan) 赋能!**_]