02 现代投资组合理论

It is the part of a wise man...not to venture all his eggs in one basket.
--Miguel de Cervantes "Don Quixote"

"Don't put all your eggs in one basket" is all wrong. I tell you "put all your eggs in one basket, and then watch that basket."
-- Andrew Carnegie


"Harry M. Markowitz - Facts". Nobelprize.org. Nobel Prize Outreach 2025. Mon. 8 Sep 2025.

组合管理的主要思路

  • 投资一个仔细选择的组合(portfolio)而不是某一个资产

  • 如何选择组合?

    • 在保证组合风险(不高于给定水平)的前提下最大化组合收益
    • 在保证组合收益(不低于给定水平)的前提下最小化组合风险
  • 历史背景

    • 最优化理论的发展: K-K-T条件(Kuhn & Tucker,1951; Karash, 1939)
    • 计算机(1946)的广泛应用

在保证组合风险(不高于给定水平)的前提下最大化组合收益

在保证组合收益(不低于给定水平)的前提下最小化组合风险


  • 如何刻画(组合)收益风险
  • 怎样的组合是可行的?他们有何性质?
  • 是否存在最优组合?如何得到最优组合?

马科维茨与组合管理的小故事

Finally, I would like to add a comment concerning portfolio theory as a part of the microeconomics of action under uncertainty. It has not always been considered so. For example, when I defended my dissertation as a student in the Economics Department of the University of Chicago, Professor Milton Friedman argued that portfolio theory was not Economics, and that they could not award me a Ph.D. degree in Economics for a dissertation which was not in Economics. I assume that he was only half serious, since they did award me the degree without long debate. As to the merits of his arguments, at this point I am quite willing to concede: at the time I defended my dissertation, portfolio theory was not part of Economics. But now it is.

source: Harry M. Markowitz – Prize Lecture. NobelPrize.org. Nobel Prize Outreach 2025. Tue. 9 Sep 2025.

内容概要

注:“Mathematics of MPT”和“延申学习:其它组合优化模型”两部分不作考核要求。

组合收益

收益

  • 收益的来源

    • 现金股利、利息等周期性收益
    • 资本增值或损失
  • Example
    As reported by Yahoo! Finance, the S&P 500 Index of U.S.
    stocks
    was at 903.25 on 31 December 2008. Similarly, Yahoo! Finance reported that
    the index closed on 30 July 2002 at 902.78, implying a return of close to 0 percent
    over the approximately six-and-a-half-year period. The results are very different,
    however, if the total return S&P 500 Index is considered. The index was at 1283.62 on 30 July 2002 and had risen 13.2 percent to 1452.98 on 31 December 2008, giving an annual return of 1.9 percent.

  • 用什么表示(量化)收益?

持有期收益

  • 持有期收益是在一段特定的时间内持有资产获得的收益
  • 公式

  • 期望收益

组合收益(Portfolio Return)

将单位货币分配到每一个(风险/无风险)资产中:

  • 组合收益是各资产收益的加权平均
  • 所有之和必须为
  • 对任意的, 不必一定为正

组合风险

资产收益的风险

  • 方差, 或者风险, 是对收益的波动性或者其分散程度的度量
    • 总体方差

    • 样本方差

  • 资产收益的标准差(standard deviation, s.d.)是其方差的平方根

  • 标准差度量了随机变量偏离其期望的平均距离

投资组合收益的风险

假设为各资产占整个投资组合的权重,组合的收益为

因此,其方差可以表示为

由两个风险资产组成的组合

组合收益:

组合风险:

因此,其标准差为

取不同数值时组合收益与风险的关系(假设不变)

    • :
    • :

例子:组合收益 vs. 组合风险

Weight in Asset 1 (%) Portfolio Return Portfolio Risk with Correlation of
1.00.50.2-1.0
015.025.025.025.025.0
1014.223.723.122.821.3
2013.422.421.320.617.6
3012.621.119.618.613.9
4011.819.817.916.610.2
5011.018.516.314.96.5
6010.217.215.013.42.8
709.415.913.812.30.9
808.614.612.911.74.6
907.813.312.211.68.3
1007.012.012.012.012.0

来源: 2022 CFA Program curriculum Reading 49

数值实验:两风险资产投资组合

  • imports
import numpy as np
import pandas as pd
import tushare as ts
import matplotlib 
import joblib
%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
  • load data
pro = ts.pro_api(token='')
df = pro.daily(ts_code='600519.SH,000333.SZ,000001.SZ', 
               start_date='20180101', 
               end_date='20181231')

ts_code trade_date open high low close pre_close change pct_chg vol amount
0 000001.SZ 20181228 9.31 9.46 9.31 9.38 9.28 0.10 1.0776 576604.00 541571.004
1 000333.SZ 20181228 36.75 37.40 36.31 36.86 36.80 0.06 0.1630 211676.03 782455.535
2 600519.SH 20181228 563.30 596.40 560.00 590.01 563.00 27.01 4.7975 63678.37 3705150.490
... ... ... ... ... ... ... ... ... ... ... ...
699 600519.SH 20180102 700.00 710.16 689.89 703.85 697.49 6.36 0.9100 49612.48 3482407.646

  • 提取三只股票回报率
stk1 = df.loc[df.ts_code == '600519.SH', ['ts_code','trade_date','pct_chg']]
stk2 = df.loc[df.ts_code == '000333.SZ', ['ts_code','trade_date','pct_chg']]
stk3 = df.loc[df.ts_code == '000001.SZ', ['ts_code','trade_date','pct_chg']]
stk = pd.merge(stk1,stk2,on = 'trade_date')
stk = pd.merge(stk,stk3,on = 'trade_date')
  • 计算期望回报率
stk_return = []
stk_return.append(stk.pct_chg_x.mean())
stk_return.append(stk.pct_chg_y.mean())
stk_return.append(stk.pct_chg.mean())
  • 计算波动率
stk_risk = []
stk_risk.append(stk.pct_chg_x.var())
stk_risk.append(stk.pct_chg_y.var())
stk_risk.append(stk.pct_chg.var())
stk_risk=list(np.sqrt(stk_risk))

  • 生成数据
rho=.4
w1=[0.01*n for n in range(101)]
portfolio_return=[w*stk_return[0]+(1-w)*stk_return[1] for w in w1]
portfolio_risk=[np.sqrt(w**2*stk_risk[0]**2
                        +2*rho*w*(1-w)*stk_risk[0]*stk_risk[1]
                        +(1-w)**2*stk_risk[1]**2)
                for w in w1]
  • 两风险资产投资组合
fig = plt.figure()
ax = plt.axes()
ax.scatter(portfolio_risk, portfolio_return)
  • 计算波动率
stk_risk = []
stk_risk.append(stk.pct_chg_x.var())
stk_risk.append(stk.pct_chg_y.var())
stk_risk.append(stk.pct_chg.var())
stk_risk=list(np.sqrt(stk_risk))

由多个风险资产组成的投资组合

假设代表平均方差和平均协方差,我们有下式

为平均相关系数, 我们有

有多于2个风险资产时,所有可行组合的风险和收益的关系不再能够由一条曲线表示。

数值实验:三风险资产投资组合

  • 生成权向量
w1=[0.001*n for n in range(1001)]
w=[[w, .001*n, 1-w-.001*n] 
   for n in range(1001) 
   for w in w1 if w+.001*n<=1]
  • 生成期望回报率向量与方差协方差矩阵
ret=stk.loc[:,['pct_chg_x','pct_chg_y',
               'pct_chg']].mean().to_numpy()
cov=stk.loc[:,['pct_chg_x','pct_chg_y',
               'pct_chg']].cov().to_numpy()
  • 计算组合回报率与波动率
portfolio_return=[]
portfolio_risk=[]
for weight in w:
    #w=np.array([w1[n],w2[n],w3[n]])
    ww=np.array(weight)
    portfolio_return.append(ww.dot(ret.T))
    portfolio_risk.append(np.sqrt(ww.dot(cov).dot(ww.T)))
  • 绘图
fig = plt.figure()
ax = plt.axes()
ax.scatter(portfolio_risk, portfolio_return)

均值-方差最优化问题

均值-方差最优化(MVO)

或者

或者

集合定义了资产权重的所有可能取值, 比如:

  • 如果不允许卖空:
  • 如果允许卖空:

均值-方差最优化(MVO)的解

  • MVO的解

    • 二次效用函数与线性约束保证了解得存在性和唯一性
    • 问题的凸性保证了有效(率)的算法
    • 最优解中有可能包括风险(方差)较大或者/而且收益较小的资产吗?
  • The model is too naive for practical purpose

    • Single-period, does not consider non-market factors
    • The inclusion of transaction costs (such as market impact costs) and tax effects
    • The addition of various types of constraints that take specific investment
      guidelines and institutional features into account
    • Modeling and quantification of the impact of estimation errors in risk and return
      forecasts on the portfolios via Bayesian techniques, stochastic optimization, or
      robust optimization approaches;

投资机会集合(Investment Opportunity Set)

来源: 2022 CFA Program curriculum Reading 49

最小方差投资组合与有效前沿

来源: 2022 CFA Program curriculum Reading 49

一个无风险资产与多个风险资产


来源: 2022 CFA Program curriculum Reading 49

  • 资本分配线(Capital Allocation Line)

  • 资本市场线

  • 最优风险组合(Optimal Risky Portfolio)

基金分离定理


两基金分离定理(two-fund separation theorem): 所有的投资者,不管其喜好、风险偏好、初始财富,都将只持有两个组合(或基金):一个无风险资产和一个最优的风险资产。


  • 最优的风险资产(组合)是什么呢?

最优风险组合:市场组合(market portfolio)

  • 市场组合是由所有价值非负的风险资产(多头)构成的投资组合

    • 所有风险资产按其市场价值加权构成
    • 有一些风险资产不满足“可投资、可观测、可复制”
  • 投资者只会投资最优风险组合,不在最优风险组合中的资产价值为0,因此最优风险组合包括所有证券

  • 处于均衡状态时最优风险组合中个证券所占比例必须等于其市值占市场总市值的比例

  • 供给-需求分析

    • 供给:市场供给的所有证券即为市场组合
    • 需求:投资者只会投资最优风险组合,因此对风险证券的需求为若干份最优风险组合
    • 均衡状态下:供给=需求

数值实验:风险分散


  • 导入必要的库
import random
import pandas as pd
  • 准备数据
df=pd.read_csv('data/TRD_Dalyr.csv',sep='\t')
tic_vec=df.Stkcd.unique()
tic_vec=pd.DataFrame(tic_vec,columns=['tic'])
  • 设定实验参数
n=50
sample_size=1000
x=np.linspace(1,n,n)
Stkcd Trddt Opnprc Hiprc Loprc Clsprc Dretwd
900957 2018-12-24 0.623 0.633 0.622 0.628 0.008026
900957 2018-12-25 0.610 0.626 0.607 0.621 -0.011146
900957 2018-12-26 0.630 0.630 0.617 0.620 -0.001610
900957 2018-12-27 0.631 0.635 0.613 0.617 -0.004839
900957 2018-12-28 0.612 0.623 0.612 0.616 -0.001621

数值实验:风险分散

  • 生成数据
y=[]
for i in x:
    z=[]
    #smpl=pd.DataFrame({'Trddt':[],'Dretwd':[]})
    j=0
    while j < sample_size:
        # j-th sampling
        smpl_tic=tic_vec.sample(int(i),replace=True)
        smpl=pd.DataFrame({'Trddt':[],'Dretwd':[]})
        k=0
        while k<i:
            smpl=smpl.merge(df[['Trddt','Dretwd']]
                            [df.Stkcd==smpl_tic.tic.iloc[k]],
                            how='outer',on='Trddt')
            k+=1
        smpl.fillna(0)
        z.append(np.std(smpl.mean(axis=1,numeric_only=True)))
        j+=1
        #smpl.pivot_table()
    y.append(np.mean(z))
  • 绘图
%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('seaborn-whitegrid')
fig = plt.figure()
ax = plt.axes()
ax.plot(x, y);

风险偏好


假设投资者面对以下两种选择:

  • [A] 得到$50 (没有不确定性)
  • [B] 参加一项赌博:有50%的机会得到$100,另外50%的机会得到$0

投资者对风险的态度可分为以下几类:

  • 风险偏好(Risk Seeking): 选择B
  • 风险中性(Risk Neutral): 不区分A和B
  • 风险回避(Risk Averse): 选择A
  • 什么时候投资者愿意承担额外风险?

效用理论与无差异曲线

  • 效用函数被用来刻画投资者对风险的态度

    • 风险回避的投资者的效用函数是凹函数
    • 例子:指数函数、对数函数等
  • 无差异曲线刻画了在维持投资者效用不变的前提下所有风险-收益的集合。投资者不会区分某一条无差异曲线上不同的点所代表的投资机会,因为它们带来的效用是相等的。

来源: 2022 CFA Program curriculum Reading 49

无差异曲线与风险偏好

来源: 2022 CFA Program curriculum Reading 49

资本配置线(Capital Allocation Line,CAL)

  • 假设投资组合由一个无风险资产和一个风险资产组成

  • 组合的收益和方差为:

  • 资本配置线(CAL)

  • CAL类似消费者均衡分析中的预算线

来源: 2022 CFA Program curriculum Reading 49

组合选择

不同风险回避程度投资者的组合选择

假设投资者的效用函数为: .

来源: 2022 CFA Program curriculum Reading 49

最优投资者组合(Optimal Investor Portfolio)

来源: 2022 CFA Program curriculum Reading 49

Quadratic Programming (QP)

  • An example of QP

  • The Standard Form

Primal, Lagrangian Function, & Dual

  • Primal

  • The Lagrangian
    • The Lagrangian Function

  • An observatoin

  • The primal can be thought as

  • what about

  • The Dual

  • The Dual for Standard Form Primal

  • Strong Duality: Assume one of the primal or dual problem is feasible. Then this problem is bounded if and only if the other one is feasible. In that case both problems have optimal solutions and their optimal values are the same.

Optimality Conditions for QP

The vectors and are optimal solutions to primal and dual problem respectively if and only if and

For a QP in standard form, the optimality conditions can be written as follows:

The Model



Analytical Solutions

  • Minimum Risk and Characteristic Portfolios
    • the minimum risk problem

  • the characteristic portfolios

  • the minimum-risk portfolio with unit exposure to a vector of attributes a associated with the assets.

  • solution

  • Separation Theorems
    • if no risk-free asset available

There exist two efficient portfolios (funds), namely

  • if one risk-free asset available

there exists a fully invested efficient portfolio (fund) namely

such that every efficient portfolio - that is, every solution for some - is a combination of this portfolio and the risk-free asset.

CVXPY简介

  • 什么是 CVXPY
    • 基于“纪律化凸规划”(DCP)的 Python 建模库:用接近数学的方式描述优化,底层自动转换为标准锥规划并调用求解器
    • 典型问题:LP/QP/SOCP/SDP、熵/指数锥;也支持小到中型的混合整数
  • 基本对象
    • Variable(变量)、Parameter(参数,可重复求解)、Problem(问题)、Minimize/Maximize(目标)、Constraints(约束)
    • 原子函数(atoms):quad_form, norm, sum, sum_squares, exp, log, max
    • DCP 规则:最小化凸函数/最大化凹函数;约束形如 (凸)、(仿射)
  • 求解器与实践
    • 常用开源:OSQP(QP), ECOS(SOCP), SCS(通用锥), GLPK(LP)
    • 商业:GUROBI/CPLEX/MOSEK(更稳健更快)
    • 数值小贴士:保证协方差矩阵正半定(可对角漂移 );检查 problem.status;用 warm_start=True 加快参数扫描

CVXPY示例:经典均值-方差优化问题

import cvxpy as cp
import numpy as np
np.random.seed(42)

# 变量与数据
n = 5
w = cp.Variable(n)    # 定义变量
mu = np.random.randn(n) * 0.01    # 生成收益率向量(标准正态*0.01)
Sigma = np.cov(np.random.randn(200, n), rowvar=False)   # 计算方差-协方差矩阵
Sigma += 1e-8 * np.eye(n)   # 数值稳定性调整

# 目标与约束(示例)
obj = cp.Minimize(0.5 * cp.quad_form(w, Sigma) - 3.0 * mu @ w) 
cons = [cp.sum(w) == 1, w >= 0]

# 求解
prob = cp.Problem(obj, cons)
prob.solve(solver=cp.OSQP, warm_start=True)
print(prob.status, prob.value, w.value[:3])
optimal 0.08332029182527874 [0.24197489 0.17954082 0.16895055]

CVXPY示例:均值-方差有效前沿

主要思路

  • 任务:绘制三条均值-方差有效前沿曲线,分别基于前 10、50、100 支股票
  • 一次性生成数据:
    • 生成全维
    • 对每个 取切片:
  • 前沿定义(目标收益最小方差):

  • 求解步骤:
    1. 在约束下先求可行收益区间
    2. 在该区间内网格化多个,逐点解二次规划
    3. 记录每个点的 并绘图比较三条前沿

主程序

import cvxpy as cp
import numpy as np
import matplotlib.pyplot as plt

# --------- 一次性生成全维 mu 与 Sigma ---------
Nmax = 100
T = 600  # T > Nmax,保证样本协方差通常是正定的
mu_full = np.random.randn(Nmax) * 0.01
X_full = np.random.randn(T, Nmax)
Sigma_full = np.cov(X_full, rowvar=False)  

# --------- 基于切片的前沿绘制(n=10,50,100) ---------
plt.figure(figsize=(8, 5), dpi=140)
colors = {10: "tab:blue", 50: "tab:orange", 100: "tab:green"}

for n in [10, 50, 100]:
    mu = mu_full[:n]
    Sigma = Sigma_full[:n, :n]
    rets, vols, _ = efficient_frontier(mu, Sigma, n_points=60, 
        allow_short=False, solver="OSQP")
    plt.plot(vols, rets, lw=2, color=colors[n], label=f"n={n}")

plt.xlabel("Volatility (Std)")
plt.ylabel("Expected Return")
plt.title("Mean-Variance Efficient Frontier (Long-only)")
plt.grid(True, ls="--", alpha=0.4)
plt.legend()
plt.tight_layout()
plt.show()

结果

主要函数

# --------- 求可行收益区间:max/min mu'w ---------
def max_min_return(mu, allow_short=False, solver="OSQP"):
    n = mu.shape[0]
    w = cp.Variable(n)
    cons = [cp.sum(w) == 1]
    if not allow_short:
        cons.append(w >= 0)

    # 最大收益
    prob_max = cp.Problem(cp.Maximize(mu @ w), cons)
    try:
        prob_max.solve(solver=solver, warm_start=True, 
              verbose=False)
        if prob_max.status not in ("optimal", 
              "optimal_inaccurate"):
            raise RuntimeError(prob_max.status)
    except Exception:
        prob_max.solve(solver="SCS", verbose=False)
    r_max = float(mu @ w.value)

    # 最小收益
    prob_min = cp.Problem(cp.Minimize(mu @ w), cons)
    try:
        prob_min.solve(solver=solver, warm_start=True, 
            verbose=False)
        if prob_min.status not in ("optimal", 
              "optimal_inaccurate"):
            raise RuntimeError(prob_min.status)
    except Exception:
        prob_min.solve(solver="SCS", verbose=False)
    r_min = float(mu @ w.value)
    return r_min, r_max
# --------- 计算有效前沿(目标收益最小方差) ---------
def efficient_frontier(mu, Sigma, n_points=60, allow_short=False, solver="OSQP"):
    n = mu.shape[0]
    w = cp.Variable(n)
    cons = [cp.sum(w) == 1]
    if not allow_short:
        cons.append(w >= 0)

    r_min, r_max = max_min_return(mu, allow_short=allow_short, solver=solver)

    # 向内收缩端点,避免数值不稳
    eps = 1e-8
    if r_max - r_min < 1e-12:
        r_targets = np.array([r_min])
    else:
        r_targets = np.linspace(r_min + eps, r_max - eps, n_points)

    rets, vols, ws = [], [], []
    for rt in r_targets:
        obj = cp.Minimize(0.5 * cp.quad_form(w, Sigma))
        prob = cp.Problem(obj, cons + [mu @ w >= rt])

        solved = False
        for s in (solver, "SCS"):
            try:
                prob.solve(solver=s, warm_start=True, verbose=False)
                if prob.status in ("optimal", "optimal_inaccurate") and w.value is not None:
                    solved = True
                    break
            except Exception:
                continue
        if not solved:
            continue

        w_opt = np.asarray(w.value).ravel()
        port_ret = float(mu @ w_opt)
        port_vol = float(np.sqrt(w_opt @ Sigma @ w_opt))
        rets.append(port_ret)
        vols.append(port_vol)
        ws.append(w_opt)

    return np.array(rets), np.array(vols), ws

Common Constraints

  • The mean-variance model

  • Other simple constraints

  • Budget constraints, such as fully invested portfolios.
  • Upper and/or lower bounds on the size of individual positions.
  • Upper and/or lower bounds on exposure to industries or sectors

Leverage constraints such as long-only, or 130/30(Active Extension) constraints

  • long-only constraint

  • constraints on the amount of short position (the value of the total short positions to be at most )

Turnover constraints: a constraint on the total change in the portfolio positions.

  • initial portfolio:
  • new portfolio:
  • the total turnover (the two-sided turnover):
  • the turnover constraint

Maximizing the Sharpe Ratio

  • The Sharp Ratio

  • Maximizing the Sharpe Ratio (Non-Convex)

  • Maximizing the Sharpe Ratio (Convex)

MIP and Complex Portfolio Constraints

Variables

  • Weights: (net); or split into long/short with .

  • Selection binaries: for asset activation.

  • Group/sector activation: to switch sector/theme on/off.

  • Trading indicators: for fixed-cost trades.

Linkage and budget (generic)

  • Buy-in and per-name cap:

  • Long/short split linkage (optional):

  • Budget and exposure (examples):

    • Fully invested long-only:
    • Generic long–short:

Common constraint templates: Cardinality, buy-in, sector/theme “enter-and-size” and logical relations

  • Limit number of names:

  • Per-name min/max (buy-in + cap):

  • Split counts for long/short (if used):

  • Activate sector before assigning weight:

  • Minimum breadth in active sectors:

  • Mutual exclusion:
  • Choose at least/most/exactly:

  • If A then B:
  • No long-and-short on same name:

Common constraint templates: Turnover, trade count, active share and L1 deviations

  • Decompose changes vs. current weights :

  • Trading indicators with big‑M:

  • Fixed + proportional trading costs in objective:

  • Active Share vs. benchmar :

    Linearize with :

Common constraint templates: Correlation, factors, discrete weights, borrow

  • Correlation/diversification:
    • If , enforce
    • Cluster caps:
  • Factor/style activation:

    Optional:
  • Discrete weight buckets:

  • Short borrow capacity & costs:

Integrated MILP of Portfolio: Example

  • Variables:

    • Weights:
    • Selection:
    • Sector activation:
    • Turnover auxiliaries:
    • Active Share auxiliaries:
  • Objective (linear example):

    A pure MILP (solver-friendly in CVXPY with GLPK_MI/ECOS_BB/GUROBI/CPLEX).

  • Constraints
    • Budget:
    • Buy-in:
    • Cardinality:
    • Sector activation: ,
    • Sector breadth:
    • Turnover: ,
    • Active Share: , ,

CVXPY example (1/2): setup

# pip install cvxpy numpy
import cvxpy as cp
import numpy as np

# Problem size and data
n = 50
mu = np.full(n, 0.08)         # expected returns 
                              # (example)
b  = np.full(n, 1/n)          # benchmark weights
w0 = np.zeros(n)              # current weights

l_buyin, u_cap = 0.005, 0.06  # per-name min/max
K = 25                        # max names
AS_max = 0.8                  # Active Share cap
turnover_cap = 1.02           # turnover cap
kappa = 5e-4                  # proportional 
                              # trading cost
# Example sectors (index lists)
sectors = {
    "Tech": [0, 1, 2, 3, 4],
    "Fin" : [5, 6, 7, 8, 9],
}
g_names = list(sectors.keys())
G = len(g_names)
L_g = {"Tech": 0.00, "Fin": 0.00}  # min sector weight 
                                   # if active
U_g = {"Tech": 0.40, "Fin": 0.35}  # max sector weight
m_g = {"Tech": 2, "Fin": 2}        # min names 
                                   # if sector active
S_max = G                          # max active sectors 
                                   # (no restriction here)

# Variables
x = cp.Variable(n, nonneg=True)       # weights
y = cp.Variable(n, boolean=True)      # selection
z = cp.Variable(G, boolean=True)      # sector activation
u = cp.Variable(n, nonneg=True)       # turnover + part
v = cp.Variable(n, nonneg=True)       # turnover - part
a = cp.Variable(n, nonneg=True)       # Active Share auxiliaries

CVXPY example (2/2): constraints, objective, solve

cons = []

# Budget
cons += [cp.sum(x) == 1]

# Buy-in and caps
cons += [x >= l_buyin * y, x <= u_cap * y]

# Cardinality
cons += [cp.sum(y) <= K]

# Sector activation and breadth
for k, g in enumerate(g_names):
    mask = np.zeros(n)
    mask[sectors[g]] = 1.0
    cons += [L_g[g] * z[k] <= cp.sum(cp.multiply(mask, x))]
    cons += [cp.sum(cp.multiply(mask, x)) <= U_g[g] * z[k]]
    cons += [cp.sum(y[sectors[g]]) >= m_g[g] * z[k]]
cons += [cp.sum(z) <= S_max]
# Turnover
cons += [x - w0 == u - v, cp.sum(u + v) <= turnover_cap]

# Active Share linearization
cons += [a >= x - b, a >= -(x - b), cp.sum(a) <= 2 * AS_max]

# Objective: maximize return minus proportional turnover cost
obj = cp.Maximize(mu @ x - kappa * cp.sum(u + v))

prob = cp.Problem(obj, cons)
prob.solve(solver=cp.ECOS_BB)  
# or solver='GUROBI'/'CPLEX'/'GLPK_MI'

print("Status:", prob.status)
print("Objective:", prob.value)
print("Nonzero names:", int(np.round(y.value).sum()))
Status: optimal
Objective: 0.07949999999970724
Nonzero names: 17

Practical tips

  • Tight big‑M: Use realistic bounds (weight caps, sector caps, turnover caps) to reduce relaxation gaps and speed up MILP.
  • Scalability: Cardinality/logic constraints are expensive; pre-filter universe, warm-start, or two‑stage solve (select then size).
  • Risk terms: Prefer linear objectives for open-source solvers; use MIQP only with capable solvers (e.g., GUROBI/CPLEX).
  • Cutting planes: Conflict graphs, cover inequalities, cluster caps help MIP performance.
  • Robustness: Buy-in thresholds, sector breadth, and trade-count limits reduce noisy turnover.

Putting Portfolio Theory to Work

  • Estimation of Inputs to Mean–Variance Models

  • Model the sensitiv

  • Solver

  • Performance Analysis

We will cover most of the above topics in later lectures.

CVXPY实验:组合优化

  • Generate data for long only portfolio optimization.
import numpy as np
import scipy.sparse as sp
np.random.seed(1)
n = 10
mu = np.abs(np.random.randn(n, 1))
Sigma = np.random.randn(n, n)
Sigma = Sigma.T.dot(Sigma)
  • Long only portfolio optimization.
import cvxpy as cp
w = cp.Variable(n)
gamma = cp.Parameter(nonneg=True)
ret = mu.T@w 
risk = cp.quad_form(w, Sigma)
prob = cp.Problem(cp.Maximize(ret - gamma*risk), 
               [cp.sum(w) == 1, 
                w >= 0])
  • Compute trade-off curve.
SAMPLES = 100
risk_data = np.zeros(SAMPLES)
ret_data = np.zeros(SAMPLES)
gamma_vals = np.logspace(-2, 3, num=SAMPLES)
for i in range(SAMPLES):
    gamma.value = gamma_vals[i]
    prob.solve()
    risk_data[i] = cp.sqrt(risk).value
    ret_data[i] = ret.value

CVXPY实验:组合优化

  • Plot long only trade-off curve.
import matplotlib.pyplot as plt
%matplotlib inline
%config InlineBackend.figure_format = 'svg'

markers_on = [29, 40]
fig = plt.figure()
ax = fig.add_subplot(111)
plt.plot(risk_data, ret_data, 'g-')
for marker in markers_on:
    plt.plot(risk_data[marker], ret_data[marker], 'bs')
    ax.annotate(r"$\gamma = %.2f$" % gamma_vals[marker], 
                xy=(risk_data[marker]+.08, ret_data[marker]-.03))
for i in range(n):
    plt.plot(cp.sqrt(Sigma[i,i]).value, mu[i], 'ro')
plt.xlabel('Standard deviation')
plt.ylabel('Return')
plt.show()

CVXPY实验:组合优化

  • plot return distributions for two points on the trade-off curve.
import scipy.stats as spstats

plt.figure()
for midx, idx in enumerate(markers_on):
    gamma.value = gamma_vals[idx]
    prob.solve()
    x = np.linspace(-2, 5, 1000)
    plt.plot(x, spstats.norm.pdf(x, ret.value, risk.value), 
             label=r"$\gamma = %.2f$" % gamma.value)

plt.xlabel('Return')
plt.ylabel('Density')
plt.legend(loc='upper right')
plt.show()

课后阅读与练习

课后阅读与练习

  • 课后阅读:教材第十三章相关内容(pp221-233)
  • 练习
    • 教材pp232:1-8
    • 在线练习

拓展学习

  • Roy安全第一准则及其发展
  • Black-Litterman模型及其发展
  • 风险平价(Risk Parity)策略

参考文献

[1] Black F, Litterman R. Global portfolio optimization[J]. Financial Analysts Journal, 1992, 48(5): 28-43.

[2] Boyd S, Boyd S P, Vandenberghe L. Convex optimization[M]. Cambridge university press; 2004 Mar 8.

[3] Cochrane J H. Asset pricing: Revised edition[M]. Princeton university press; 2009 Apr 11.

[4] Cornuejols G, Tütüncü R. Optimization methods in finance[M]. Cambridge University Press; 2006 Dec 21.

[5] Delage E, Ye Y. Distributionally robust optimization under moment uncertainty[J]. Operations Research, 2010, 58(3): 595-612.

[6] Goldfarb D, Iyengar G. Robust portfolio selection problems[J]. Mathematics of Operations Research, 2003, 28(1): 1-38.

[7] Gomes F. Portfolio choice over the life cycle: A survey[J]. Annual Review of Financial Economics, 2020, 12: 277-304.

[8] Ledoit O, Wolf M. Honey, I shrunk the sample covariance matrix[J]. The Journal of Portfolio Management, 2004, 30(4): 110-119.

[9] Li B, Hoi S C H. Online portfolio selection: A survey[J]. ACM Computing Surveys (CSUR), 2014, 46(3): 1-36.

[10] Litterman R. Common sense on risk[R]. Goldman Sachs Asset Management; 1999.

[11] Maillard S, Roncalli T, Teïletche J. The properties of equally weighted risk contribution portfolios[J]. The Journal of Portfolio Management, 2010, 36(4): 60-70.

[12] Markowitz H M. Portfolio selection[J]. The Journal of finance, 1952, 7(1): 77-91.

[13] Markowitz H M. Portfolio selection[M]//Portfolio selection. Yale university press, 1968.

[14] Markowitz H M. Mean—variance analysis[M]//Finance. Palgrave Macmillan, London, 1989: 194-198.

[15] Markowitz H M. The early history of portfolio theory: 1600–1960[J]. Financial analysts journal, 1999, 55(4): 5-16.

[16] Meucci A. The Black–Litterman approach: Original model and extensions[R]. SSRN Working Paper; 2005.

[17] Qian E. Risk parity and diversification[J]. The Journal of Investing, 2011, 20(1): 119-127.

[18] Roy A D. Safety first and the holding of assets[J]. Econometrica: Journal of the econometric society, 1952: 431-49.

[19] Rubinstein M. Markowitz's "portfolio selection": A fifty-year retrospective[J]. The Journal of finance, 2002, 57(3): 1041-1045.

Roy 安全第一准则(SFC)

直觉与定义

  • 目标:最小化组合收益低于“灾难线”的概率
  • 正态近似下:若,则

  • 与夏普比率类比:将无风险利率替换为

SFC 的机会约束与 SOCP 形式

  • 机会约束(置信水平 ):

    其中
  • 性质:二阶锥规划(SOCP)
  • 直观解释:在“均值减去分位数倍的标准差”意义下,保证不低于

SFC:非正态扩展与实践要点

  • 非正态情形:

    • 切比雪夫/贝内特不等式提供保守下界(不依赖正态)
    • 用历史分位或情景模拟替代正态近似中的
  • 与 VaR / ES(CVaR)的关系:

    • 取 ( ) 为某分位(或 0),SFC 接近“短缺控制”;CVaR 直接优化尾部期望损失
  • 常见陷阱:

    • 过高导致不可行或极端集中
    • 资产高度偏态/厚尾时,正态近似误导约束强度

SFC:CVXPY 示例(SOCP)

import cvxpy as cp
import numpy as np
from scipy.stats import norm

# ----- 1) 准备数据:风险资产 + 现金 -----
# 假设已有风险资产的 mu_risky, Sigma_risky
# (与频率一致:日/月/年)
rng = np.random.default_rng(42)
n_risky = 10
mu_risky = rng.normal(loc=0.006, scale=0.002, 
      size=n_risky)   # 例:月度均值 ~0.6%
X = rng.standard_normal((600, n_risky))
Sigma_risky = np.cov(X, rowvar=False)

# 现金资产参数(与 mu_risky 的频率一致)
rf = 0.002  # 例:月度无风险收益 ~0.20%
mu = np.r_[mu_risky, rf]
Sigma = np.zeros((n_risky + 1, n_risky + 1))
Sigma[:n_risky, :n_risky] = Sigma_risky

# 可选:轻微岭化,增强数值稳定
Sigma = 0.5 * (Sigma + Sigma.T) 
      + 1e-8 * np.eye(Sigma.shape[0])

# ----- 2) 模型参数 -----
L, alpha = 0.0, 0.05
z = norm.ppf(1 - alpha)  # 约 1.64485
# ----- 3) 变量 -----
n = len(mu)
w = cp.Variable(n)

# ----- 4) 构造 SOC 约束 -----
C = np.linalg.cholesky(Sigma)      # Sigma = C C^T
risk = cp.norm(C @ w, 2)

constraints = [
    cp.sum(w) == 1,
    w >= 0, 
    z * risk <= mu @ w - L         # SFC (SOCP) 约束
]

# ----- 5) 目标函数 -----
objective = cp.Maximize(mu @ w)

# ----- 6) 求解 -----
prob = cp.Problem(objective, constraints)
prob.solve(solver=cp.ECOS)         # 或者 solver=cp.SCS

print("Status:", prob.status, "Obj:", prob.value)
print("w:", w.value)
Status: optimal Obj: 0.0020145906020810947 w: [4.61758424e-04 1.18520546e-04 
5.91766230e-04 5.69853934e-04 3.59272402e-10 9.37486505e-05 3.96081415e-04
4.19935818e-04 4.59174433e-04 2.51150303e-04 9.96638010e-01]

风险平价与风险预算

核心概念

  • 总风险:

  • 边际风险贡献(MRC)与风险贡献(RC):

  • 风险平价(等风险贡献):

  • 风险预算(给定占比):

风险预算的凸优化实现(Roncalli)

  • 目标函数(长多约束下凸):

  • KKT 条件推出:最优处 ( )
  • 优点:数值稳定,易于加入实际约束(行业、单票、换手)
  • 实现细节与常见问题
    • 相关性突变时风格漂移:定期再估协方差+平滑
    • 允许杠杆:配合目标波动控制(目标年化波动如 10%)
    • 结合约束:行业/风格/因子暴露上线,避免隐性集中

风险平价:CVXPY 示例与风险贡献检验

import cvxpy as cp, numpy as np

# ---------- 1) 造一个对称正定协方差 ----------
rng = np.random.default_rng(42)
n = 8
A = rng.normal(size=(n, n))
Sigma = A.T @ A     # SPD

def solve_risk_budget(Sigma, b, ub=None, solver="ECOS", 
                      eps=1e-10, verbose=False):
    n, b = Sigma.shape[0], np.asarray(b)
    assert b.shape == (n,) and np.all(b >= 0) and 
          np.isclose(b.sum(), 1.0), "b 必须非负且和为 1"
    # 变量:长多权重
    w = cp.Variable(n)
    # 目标:0.5 w'Σw - sum b_i log(w_i)
    obj = 0.5 * cp.quad_form(w, Sigma) 
          - cp.sum(cp.multiply(b, cp.log(w + eps)))
    cons = [cp.sum(w) == 1, w >= 0]
    if ub is not None:
        cons += [w <= ub]
    prob = cp.Problem(cp.Minimize(obj), cons)
    prob.solve(solver=getattr(cp, solver), verbose=verbose)
    if prob.status not in ("optimal", "optimal_inaccurate"):
        raise RuntimeError(f"Solve failed: {prob.status}")
    w_opt = np.array(w.value).ravel()
    # 风险与风险贡献
    sigma_p = float(np.sqrt(w_opt @ Sigma @ w_opt))
    mrc = (Sigma @ w_opt) / sigma_p
    RC = w_opt * mrc                   # 绝对风险贡献,和为 sigma_p
    RC_frac = RC / sigma_p             # 风险贡献占比,和为 1
    return dict(status=prob.status, obj=float(prob.value), w=w_opt, 
                sigma_p=sigma_p, mrc=mrc, RC=RC, RC_frac=RC_frac)
# ---------- 2) 等风险贡献:b = 1/n ----------
b_equal = np.ones(n) / n
res_equal = solve_risk_budget(Sigma, b_equal, solver="ECOS")
print("== 等风险贡献(Risk Parity)==")
print("Weights:", np.round(res_equal["w"], 4))
print("RC fraction:", np.round(res_equal["RC_frac"], 4))
print("Max |RC_frac - b|:", np.max(np.abs(res_equal["RC_frac"]
       - b_equal)))

# ---------- 3) 自定义风险预算:b 非均匀 ----------
b_custom = np.array([0.18, 0.10, 0.12, 0.08, 0.14, 0.10, 0.12, 0.16])
b_custom = b_custom / b_custom.sum()  # 归一化,确保和为 1
# 可选:加入单资产权重上限(如 30%)
ub = 0.30
res_custom = solve_risk_budget(Sigma, b_custom, ub=ub, solver="ECOS")
print("\n== 风险预算(自定义 b)==")
print("Weights:", np.round(res_custom["w"], 4))
print("RC fraction:", np.round(res_custom["RC_frac"], 4))
print("Max |RC_frac - b|:", np.max(np.abs(res_custom["RC_frac"]
       - b_custom)))
== 等风险贡献(Risk Parity)==
Weights: [0.1524 0.1295 0.1624 0.1343 0.0955 0.0936 0.077  0.1553]
RC fraction: [-0.0113  0.1027 -0.0612  0.0789  0.2717  0.2811  0.3635 -0.0254]
Max |RC_frac - b|: 0.23848734022116108

== 风险预算(自定义 b)==
Weights: [0.199  0.106  0.1559 0.099  0.0991 0.0784 0.0758 0.187 ]
RC fraction: [ 0.0724  0.0661 -0.0833 -0.0275  0.372   0.2226  0.3707  0.007 ]
Max |RC_frac - b|: 0.25067047147858185

Black–Litterman(BL)

  • MVO 对 极度敏感,历史均值不稳 → 权重极端
  • BL 思路:用“市场均衡先验收益”作为基线,再与研究观点“贝叶斯融合”,得到稳定可解释的

BL 步骤 1:先验(均衡)收益与参数

  • 先验:
    • :市值加权市场组合(与投资宇宙一致)
    • :风险厌恶系数(可由市场夏普率、历史校准)
  • 先验不确定性缩放:,常取 0.025–0.10

BL 步骤 2:观点矩阵与不确定性

  • 绝对观点:某资产预期超额收益为
  • 相对观点:资产 A 相对 B 高出,令
  • 置信度: 越小表示越确信(量纲与一致)

BL 步骤 3:后验融合

  • 实务中常用 替换,协方差仍用(保守)
  • 直觉:当小(更信先验)或大(观点不确定),后验更接近先验

BL 步骤 4:落地与陷阱

  • 进入 MVO/约束 MVO 框架
  • 陷阱与规避:
    • 宇宙一致性:应与可投资集合一致
    • 尺度匹配:需同量纲
    • 观点强度/数量:过强或过多易导致不稳权重(可做灵敏度分析)

BL:NumPy 示例(后验计算)

import numpy as np
import cvxpy as cp

rng = np.random.default_rng(7)

# 0) 构造一个 SPD 的协方差矩阵 Σ 与市值权重 w_mkt
n = 6
G = rng.normal(size=(n, n)) # 随机生成 SPD
S = G.T @ G
# 设定目标年化波动(12%~25%),并用对角缩放匹配到 S 上
vols = rng.uniform(0.12, 0.25, size=n)
D = np.diag(vols / np.sqrt(np.diag(S)))
Sigma = D @ S @ D   # n x n, SPD, 对角即 vols^2

# 随机市值权重(长多、和为 1)
w_mkt = rng.dirichlet(alpha=np.ones(n))

# 1) 先验(市场均衡超额收益) π = δ Σ w_mkt
# 风险厌恶 δ、尺度 τ(经验常用)
delta, tau = 2.5, 0.05
pi = delta * (Sigma @ w_mkt)

# 2) 观点矩阵 P、观点 Q 与不确定性 Ω
P = np.zeros((2, n))
P[0, 0], P[0, 1] = 1.0, -1.0  # 观点1:资产0 优于 资产1
P[1, 2] = 1.0  # 观点2:对资产2 的绝对预期
# 观点值(200bp 相对差;8% 绝对期望)
Q = np.array([0.02, 0.08])        
# 观点方差(越小=越自信)     
Omega = np.diag([0.0004, 0.02**2])  

# 3) 后验融合:μ^BL 与(均值)后验协方差 S^BL
Ainv = np.linalg.inv(tau * Sigma)       
Omega_inv = np.linalg.inv(Omega)
M = Ainv + P.T @ Omega_inv @ P
rhs = Ainv @ pi + P.T @ Omega_inv @ Q
mu_bl = np.linalg.solve(M, rhs)         
S_mu_bl = np.linalg.inv(M)             

print("=== 后验均值 μ^BL(前5位小数)===")
print(np.round(mu_bl, 5))

# 观点检验(仅作直观说明;Ω>0 时一般不会完全等于 Q)
print("\nP @ μ^BL vs Q:")
print("P @ μ^BL =", np.round(P @ mu_bl, 5))
print("Q        =", np.round(Q, 5))
=== 后验均值 μ^BL(前5位小数)===
[0.04766 0.03676 0.06504 0.05528 0.00718 0.00948]
# 4) 落地:把 (μ^BL, Σ) 带入 CVXPY 的 MVO(长多、预算=1)
w = cp.Variable(n, nonneg=True)
objective = cp.Maximize(mu_bl @ w 
            - 0.5 * delta * cp.quad_form(w, Sigma))
constraints = [cp.sum(w) == 1]
prob = cp.Problem(objective, constraints)
prob.solve(solver=cp.ECOS)

w_star = np.array(w.value).ravel()
mu_p = float(mu_bl @ w_star)
sigma_p = float(np.sqrt(w_star @ Sigma @ w_star))
sr = mu_p / sigma_p

print("\n=== CVXPY MVO 结果(长多、预算=1)===")
print("Weights (w*):", np.round(w_star, 4))
print(f"Return = {mu_p:.4%}, Vol = {sigma_p:.4%}, 
      Sharpe-like = {sr:.3f}")

# 与市值组合的“主动权重”
print("\nActive weights (w* - w_mkt):", 
      np.round(w_star - w_mkt, 4))
=== CVXPY MVO 结果(长多、预算=1)===
Weights (w*): [0.1648 0.     0.7075 0.1277 0.     0.    ]
Return = 6.0930%, Vol = 12.4077%, Sharpe-like = 0.491
# 5) 可选:更保守的“波动放大”做法(Σ^BL = Σ + S^BL)
Sigma_conservative = Sigma + S_mu_bl
w2 = cp.Variable(n, nonneg=True)
prob2 = cp.Problem(cp.Maximize(mu_bl @ w2
                   - 0.5 * delta * cp.quad_form(w2, 
                                Sigma_conservative)),
                   [cp.sum(w2) == 1])
prob2.solve(solver=cp.ECOS)

w_star2 = np.array(w2.value).ravel()
mu_p2 = float(mu_bl @ w_star2)
sigma_p2 = float(np.sqrt(w_star2 @ 
                  Sigma_conservative @ w_star2))
sr2 = mu_p2 / sigma_p2

print("\n=== 保守版(Σ + S^BL)MVO 结果 ===")
print("Weights (w†):", np.round(w_star2, 4))
print(f"Return = {mu_p2:.4%}, Vol = {sigma_p2:.4%}, 
      Sharpe-like = {sr2:.3f}")

=== 保守版(Σ + S^BL)MVO 结果 ===
Weights (w†): [0.167  0.     0.7053 0.1278 0.     0.    ]
Return = 6.0892%, Vol = 12.4920%, Sharpe-like = 0.487

鲁棒组合优化(Robust Portfolio Optimization, RPO)

目标与框架

  • 目标:在输入估计误差下,优化最坏情形表现
  • 形式:
    • 参数鲁棒: 决策
    • 分布鲁棒:

鲁棒均值不确定集与正则化等价

  • 盒式不确定集:目标中出现 稀疏化惩罚(抑制换手与集中)

  • 椭球不确定集:岭式 正则(抑制过拟合、提升稳定性)

鲁棒协方差与分布鲁棒 CVaR

  • 协方差不确定:

    • 谱范数/Frobenius 球 → 半定规划(SDP);实务常用收缩估计(Ledoit–Wolf, OAS)
  • 分布鲁棒 CVaR(样本/场景 ):

  • 交易成本/换手:权重正则天然抑制过度调仓

鲁棒 MVO:岭式正则的实现

import numpy as np, cvxpy as cp
rng = np.random.default_rng(0)

# ---------- 1) 构造一个易病态的协方差(高相关) ----------
n, rho = 12, 0.85
vols = rng.uniform(0.12, 0.30, size=n)  # 年化波动(12%~30%)
Corr = (1 - rho) * np.eye(n) + rho * np.ones((n, n))
Sigma = np.diag(vols) @ Corr @ np.diag(vols)  # SPD,条件数较大
mu = 0.06 + 0.04 * rng.random(n)  # 年化期望收益6%~10%

# ---------- 2) 基准 MVO vs. 岭式鲁棒 MVO ----------
gamma = 5.0        # 风险厌恶(越大越保守)
lam   = 5e-2       # L2 正则强度(鲁棒半径/平滑强度)

def solve_mvo(mu, Sigma, gamma, lam=0.0, solver="OSQP"):
    n = len(mu)
    w = cp.Variable(n, nonneg=True)
    obj = cp.Maximize(mu @ w - gamma * cp.quad_form(w, Sigma) 
                      - lam * cp.sum_squares(w))
    cons = [cp.sum(w) == 1]
    prob = cp.Problem(obj, cons)
    prob.solve(solver=getattr(cp, solver))
    if prob.status not in ("optimal", "optimal_inaccurate"):
        raise RuntimeError(prob.status)
    wv = np.array(w.value).ravel()
    ret = float(mu @ wv)
    vol = float(np.sqrt(wv @ Sigma @ wv))
    return wv, ret, vol, prob.value

w0, r0, s0, obj0 = solve_mvo(mu, Sigma, gamma, lam=0.0, solver="OSQP")
wr, rr, sr, objr = solve_mvo(mu, Sigma, gamma, lam=lam,  solver="OSQP")
# ---------- 3) 等价性检验:对角收缩 Σ̃ = Σ + (λ/γ) I ----------
Sigma_tilde = Sigma + (lam / gamma) * np.eye(n)
w_eq, r_eq, s_eq, _ = solve_mvo(mu, Sigma_tilde, 
                                gamma, lam=0.0, solver="OSQP")

# ---------- 4) 结果打印 ----------
def summarize(tag, w, r, s):
    print(f"{tag:<12s}  Return={r:6.2%}  
            Vol={s:6.2%}  MaxWeight={w.max():.3f}")

print("=== Portfolio statistics ===")
summarize("Baseline", w0, r0, s0)
summarize("Robust(L2)", wr, rr, sr)
summarize("Equiv Σ~", w_eq, r_eq, s_eq)

print("\nTurnover Baseline -> Robust:", np.sum(np.abs(wr - w0)))
print("Weight concentration (L2):")
print("  ||w_bas||₂ =", np.linalg.norm(w0), 
      "  ||w_rob||₂ =", np.linalg.norm(wr))

# 协方差条件数改善(更稳定)
cond_S  = np.linalg.cond(Sigma)
cond_Ss = np.linalg.cond(Sigma_tilde)
print("\nCond(Sigma) =", f"{cond_S:.1f}", 
      " |  Cond(Sigma+λ/γ I) =", f"{cond_Ss:.1f}")

# 等价性误差(数值)
print("\n||w_rob - w_equiv||_∞ =", np.max(np.abs(wr - w_eq)))

方法对比与选型建议

  • 估计依赖:风险平价 < 鲁棒 MVO < BL < 传统 MVO

  • 尾部控制:SFC/分布鲁棒 CVaR > 鲁棒 MVO > 风险平价 ≈ 传统 MVO

  • 解释性:BL(观点可解释)、风险平价(风险贡献可解释)更优

  • 适用场景:

    • 低信号/稳健基线:风险平价 / 最小方差 + 约束
    • 有明确观点/行业覆盖:BL
    • 监管或产品“保底/短缺”要求:SFC/机会约束
    • 小样本/高维:鲁棒优化(收缩 + 正则)

<img align="center" style="padding-right:10px;" width=80% src="../myfig/portfolio_assetpricing_emh/story.jpg">

[^1]

[^1]:Kolm P N, Reha Tütüncü, Fabozzi F J. 60 Years of portfolio optimization: Practical challenges and current trends[J]. European Journal of Operational Research, 2014, 234(2):356--371.

**<font color='red'>Discussion: Can we improve the above constraint?</font>**

- **<font color='red'>How to improve the above constraint?</font>**

# CVXPY example (runnable MILP): long-only with sectors, turnover, Active Share

教学要点:用一个示意图展示当 ( $L$ ) 提高时,可行域缩小、组合更保守。

与现有讲义交叉引用: - 第55–56页:从夏普最大化到 SFC 的并行“锥规划化”

课堂提示:与第59页“分布对比图”结合,展示 SFC 组合 vs. 夏普最优组合的下尾差异。

动机:弱化对 ( $\mu$ ) 的依赖,聚焦“风险如何被分配”。

对比(建议课堂图示): - 等权权重 ≠ 等权风险:高波动资产往往主导风险 - 最小方差可能“集中化” - 风险平价的风险贡献条形图通常更均衡

- 与第52–54页“约束”对齐:可添加“风险预算约束/目标”示例

实践提示:将风险平价作为“核心仓位”,在其上叠加 alpha(卫星)策略。

课堂展示:用柱状图对比等权/最小方差/风险平价的风险贡献。

与现有讲义交叉引用: - 第27–28页:市场组合与均衡逻辑 → 先验的经济解释 - 第21页:估计误差与贝叶斯/鲁棒方法的动机

教学提示:联系第25–28页资本配置线与市场组合,解释为何“均衡预期收益”可由反向优化得到。

例子:看多 A 胜 B 200bp:( $P=[1,-1,0,\dots],\ Q=0.02$ )

课堂演示:改变$\tau$、$\Omega$对权重的影响曲线。

# 之后:用 (mu_bl, Sigma) 做约束 MVO(见第57–59页 CVXPY 模板)

与现有讲义交叉引用: - 第21页:鲁棒优化作为应对估计误差的方法 - 第57–59页:与 CVXPY 框架无缝衔接

教学提示:用一页图示 $( L_1 )$ vs $( L_2 )$ 正则对权重的影响(稀疏 vs 平滑)。

实践建议:小样本/高维优先“估计层鲁棒”(因子/收缩)+“优化层鲁棒”(正则/边界)。

与现有讲义交叉引用: - 第52–54页:可加入“风险预算/风格暴露”约束示例 - 第57–59页:统一求解与可视化工作流

--- ### 课堂练习与小型实验(建议) - 练习 1(SFC):给定 ($(\hat\mu,\hat\Sigma)$)、( $L=0$ )、( $\alpha=5\%$ ),比较 SFC vs. 夏普最优 在 5% 下分位的差异 - 练习 2(风险平价):3–5 资产,作等权/最小方差/风险平价的风险贡献柱状图 - 练习 3(BL):两条相对观点,比较不同 ( $\tau$ )、( $\Omega$ ) 对 ( $\mu^{BL}$ ) 与权重的影响 - 练习 4(鲁棒 MVO):扫描 ( $L_2$ ) 正则强度,评估样本外波动与换手率

提示:直接复用第57–59页 CVXPY 框架;本节代码可嵌入为可执行单元。

--- ### 数据与参数小抄(教学/实务) - 估计期:月频 3–5 年;日频 1–2 年(配合收缩/因子) - 收缩:Ledoit–Wolf/OAS;均值向因子/行业基线收缩 - SFC:( $L$ ) 取 0 或产品保底线;( $\alpha$ )=1–5% 常见 - 风险平价:预算 ( $b_i$ ) 可按行业/资产类设定 - BL:( $\tau$ )=0.025–0.10;( $\Omega$ ) 与观点来源匹配(高置信=小方差) - 鲁棒:( $L_1/L_2$ ) 正则强度交叉验证;结合换手/交易成本