资产组合理论与实战(3) - 性能优化

本文介绍了如何通过向量化技术,如NumPy和Python库scipy.optimize,提升金融模型计算效率,从5.2秒缩短至0.65秒,讨论了蒙特卡洛方法在大量资产组合优化中的局限性,并展示了使用优化算法解决这类问题的实际应用。
摘要由CSDN通过智能技术生成

使用向量化以优化性能

上一篇文章中,我们使用蒙特卡洛方法,运行5000次,大约花了5.2秒。我们可以使用向量化运算方法来提升这个速度:

我们可以使用下面的算法,把部分计算移出了循环:

from empyrical import sharpe_ratio
import time
start = time.time()
num_ports = 5000
all_weights = np.random.rand(num_ports, len(stocks))

shape = (len(all_weights), -1)
all_weights = all_weights/all_weights.sum(axis=1).reshape(shape)

# got 241 daily summed returns over 5000 iters
all_port_returns = np.dot(all_weights,returns.T)

all_cov = np.cov(all_port_returns).diagonal()

# 计算波动率
all_vol = []
all_sharpe = []

annual_return = np.prod((1 + all_port_returns), axis=1) - 1

for i in range(num_ports):
    all_vol.append(np.sqrt(np.dot(all_weights[i].T, np.dot(all_cov[i], all_weights[i]))))
    all_sharpe.append(sharpe_ratio(all_port_returns[i]))

end = time.time()
print("计算用时", end-start)

# 绘图
all_vol = np.array(all_vol)
pos = np.argsort(all_vol)

plt.scatter(all_vol, annual_return, c=all_sharpe, cmap='RdYlBu')
plt.colorbar(label='Sharpe Ratio')

max_sharpe_pos = np.argmax(all_sharpe)
plt.scatter(all_vol[max_sharpe_pos], annual_return[max_sharpe_pos], c='red',s=80)

不过,在按行计算波动率和夏普率的时候,我不得不做出妥协,改用循环。在这种情况下(投资组合中只有4只标的),我们现在的执行时间需要0.65秒。提升了大概8倍的速度(初始为5.2s左右)。

PyPortfolioOpt的作者曾在一个示例中用到了这样的方法:

n_samples = 10000
w = np.random.dirichlet(np.ones(ef.n_assets), n_samples)
rets = w.dot(ef.expected_returns)
stds = np.sqrt(np.diag(w @ ef.cov_matrix @ w.T))
sharpes = rets / stds

计算速度非常之快(小于0.1秒),但需要对其中的ef.cov_matrix进行替换,感兴趣的同学可以研究下它的源码。

对于一个仅有4支标的的组合来说,这个速度(不到1秒)显然可以令人满意了。但我们也在上一篇文章中,提出一个问题,随着标的数的增加,使用蒙特卡洛模拟方法,我们需要暴力搜索的空间也随之变大。那么,究竟会增大多少呢,此时还可不可行?

我们先来看只有两支标的情况。假设资产权重的间隔是1%,即某个资产要么分配1%,要么分配2%,而不会分配1.05%这样的非整数权重。这样我们就可以算得,如果要把这些权重分布全部模拟到,搜索空间将是:

A: 1-100 ~ 101个选择项
B: 1个选择项

当A的权重选定以后,在权重和为1的约束下,B就只有一个选择。因此,总共是101次搜索。

当如果标的数量上升,我们为了模拟尽可能多的情况,一定会扩大模拟次数,因此总的运行时长迅速上涨。同时随着标的数的增加,我们要搜索的次数需要呈指数级增加,而不是按标的数线性增加。

因此,搜索次数是一个组合问题。假设存在四个标的,并不是简单的4×100就可以模拟出所有组合,而是存在:
C 99 3 C_{99}^3 C993种分配方案。

对任意n个标的,每个标的的权重间隔按1%计算的话,这个搜索空间将是:

C 101 − ( n − 1 ) n − 1 C_{101-(n-1)}^{n-1} C101(n1)n1次。如果我们有50支标的,则需要搜索最少1.998e+21亿次!

!!! tip
可以使用 math.comb来计算组合数。

所以,使用蒙特卡洛方法,会存在无法穷尽所有样本空间的可能性。我们必须采用数学的方法,来优化这个求解过程。

优化算法–scipy.optimize

寻找给定收益率下的最小波动率,或者给定波动率下的最大夏普率,这实际上是一类常见的优化问题,即:

我们可以使用 Scipy.optimize工具库来求解类似问题。

scipy.optimize包的主要功能

  • SciPy Optimize提供最小化(或最大化)目标函数的功能,可能存在约束。
  • 它包括解决非线性问题(支持局部和全局优化算法) ,线性规划,约束和非线性最小二乘,根寻找和曲线拟合。
  • 当我们需要优化输入函数的参数时,scipy.optimize包含很多有用的方法,并可以处理不同种类的函数

它提供以下方法:

  • minimize_scalar( ):使一个变量最小化
  • minimize( ):使多个变量最小化
  • curve_fit( ):寻找最优曲线,适合a set of data
  • root_scalar( ):求一个变量的零点
  • root( ):求多个变量的零点
  • linprog( ):在线性等式和不等式的约束下,最小化线性目标函数

在这些方法中,我们主要介绍minimize_scalarminimize

minimize_scalar

有一类问题,比如解方程,它的解是一个数字,此时就可以用 minimize_scalar 方法来求解。

例如,我们的函数为:

y = 3 x 4 − 2 x + 1 y=3x^4-2x+1 y=3x42x+1

minimize_scalar( )就是帮助我们找到函数最小值(y最小)时精确的坐标。通俗地说,就是解方程。

from scipy.optimize import minimize_scalar

def objective_function(x):
    return 3 * x ** 4 - 2 * x + 1
# 定义需要求解的函数

res = minimize_scalar(objective_function)
# 求出函数的最小值

print(res)

我们重点解释一下输出结果,这将在后面继续使用:

 message: 
          Optimization terminated successfully;
          The returned value satisfies the termination criteria
          (using xtol = 1.48e-08 )
 success: True
     fun: 0.17451818777634331
       x: 0.5503212087491959
     nit: 12
    nfev: 15

这里的fun是函数值,x是变量值,如果我们把x的值代入到公式 3 ∗ x 4 − 2 ∗ x + 1 3*x^4-2*x+1 3x42x+1,我们会得得到0.17,即fun的值。

要注意,不是所有情况都能进行优化求解的。比如, y = x 3 y=x^3 y=x3没有最小值,如果我们对它求解,会导致overflowerror.

另外的情况是,存在多个最小值的情况,此时minimize_scalar不能保证找到函数的全局最小值。

我们以 y = x 4 − x 2 y=x^4-x^2 y=x4x2为例,它存在多个最小值,minimize_scalar ()不能保证找到函数的全局最小值。我们尝试一下:

from scipy.optimize import minimize_scalar
# help(minimize)

def objective_function(x):
    return x**4 - x**2

res = minimize_scalar(objective_function)
res

我们将得到如下结果:

 message: 
          Optimization terminated successfully;
          The returned value satisfies the termination criteria
          (using xtol = 1.48e-08 )
 success: True
     fun: -0.24999999999999994
       x: 0.7071067853059209
     nit: 11
    nfev: 14

这样求解出来的方程的根是0.707。但我们知道,该方程至少还有一个-0.707的根,因此, minimize_scalar默认情况下,是无法找出全部的根的。但我们可能通过设定参数,来控制用于优化的求解器。一共有三种设定方式:

  • brent:布伦特算法,也是该函数的默认算法
  • golden:黄金分割算法,根据已有研究,该方式效果略差于brent
  • bounded:布伦特算法的“有界实现”

brent和golden属于bracket:这是一个由两个或三个元素组成的序列,它提供了对最小区域边界的初始猜测。但是,这些求解器并不保证所找到的最小值将在此范围内

bounded属于bounds:这是一个两个元素的序列,严格限制搜索区域的最小值。因此当最小值在已知范围内时,限制搜索区域才是有用的。这个概念我们在后面还将进一步接触。我们来看使用bounds方式的例子:

# 当我们采用bounds方式限定时
res = minimize_scalar(objective_function, method='bounded', bounds=(-1, 0))
res

现在,我们求解的x将落在区间(-1,0)之间,我们因此得到:

 message: Solution found.
 success: True
  status: 0
     fun: -0.24999999999998732
       x: -0.707106701474177
     nit: 10
    nfev: 10

这次我们得到了期望中的负数解。

不过,最佳资产组合的求解要比这个复杂,我们必须使用另一个方法,即minimize方法,具体优化算法则是最小顺序二乘法。

【系列文章目录】
资产组合理论及实战(1)-基本概念
资产组合理论及实战(2) - 蒙特卡洛方法

  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

量化风云

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值