文章

QuantLib 金融计算——数学工具之优化器

介绍 QuantLib 中的数学工具。

由于版本问题,代码可能与最新版不兼容。

QuantLib 金融计算——数学工具之优化器

载入模块

1
2
3
4
import QuantLib as ql
import scipy

print(ql.__version__)
1
1.12

概述

在量化金融的模型校准过程中,最重要的工具是对函数 $f : R^n \to R$ 的优化器。通常遇到的最优化问题是一个最小二乘问题。例如,寻找一个模型的参数使得某些损失函数最小化。

quantlib-python 中的最优化计算委托给 Optimizer 类,用户需要配置合适的参数以描述最优化问题,需要注意的是 Optimizer 对象默认求解的是某个函数“最小化”问题。

Optimizer

Optimizer 类的构造函数不接受参数,求解最优化问题的方式也非常简单,仅需调用 solve 函数即可:

1
2
3
4
5
solve(function,
      c,
      m,
      e,
      iv)
  • function:函数或函数对象,返回一个浮点数,所接受的参数是若干独立的浮点数;
  • cConstraint 对象,描述优化问题的约束条件;
  • mOptimizationMethod 对象,优化算法引擎;
  • eEndCriteria 对象,描述优化问题的终止条件;
  • ivArray 对象,优化计算的初始值。

solve 函数返回一个 Array 对象,存储找到的最小值点。

Constraint

quantlib-python 提供的具体约束条件均继承自 Constraint 类,有如下几种:

  • NoConstraint:无约束
  • PositiveConstraint:要求所有参数为正数
  • BoundaryConstraint:要求所有参数在某个区间内
  • CompositeConstraint:要求所有参数同时满足两个约束条件
  • NonhomogeneousBoundaryConstraint:对每个参数分别约束,要求其在某个区间内

OptimizationMethod

quantlib-python 提供的具体优化算法均继承自 OptimizationMethod 类,有如下几种:

  • LevenbergMarquardt:Levenberg-Marquardt 算法,实现基于 MINPACK;
  • Simplex:单纯形法;
  • ConjugateGradient:共轭梯度法;
  • SteepestDescent:最速下降法;
  • BFGS:Broyden-Fletcher-Goldfarb-Shanno 算法;
  • DifferentialEvolution:微分进化算法;
  • GaussianSimulatedAnnealing:高斯模拟退火算法;
  • MirrorGaussianSimulatedAnnealing:镜像高斯模拟退火算法;
  • LogNormalSimulatedAnnealing:对数高斯模拟退火算法。

EndCriteria

最优化计算通常是一个迭代过程,我们需要定义一个终止条件以引导最优化计算结束,否则可能一直计算下去。终止条件由 EndCriteria 类参数化,其构造函数如下

1
2
3
4
5
EndCriteria(maxIteration,
            maxStationaryStateIterations,
            rootEpsilon,
            functionEpsilon,
            gradientNormEpsilon)
  • maxIteration:整数,最大迭代次数;
  • maxStationaryStateIterations:整数,稳定点(函数值和根同时稳定)的最大迭代次数;
  • rootEpsilon:浮点数,当前根与最新根的绝对差小于 rootEpsilon 时停止计算;
  • functionEpsilon:浮点数,当前函数值与最新函数值的绝对差小于 functionEpsilon 时停止计算;
  • gradientNormEpsilon:浮点数,当前梯度与最新梯度差的范数小于 gradientNormEpsilon 时停止计算;

注意,对于每种优化器来讲,并不是所有参数多是必须的。

示例

Rosenbrock 问题

我们以 Rosenbrock 函数(也简称为香蕉函数)为例测试优化器,这是一个经典的优化问题。函数定义如下:

\[f(x,y) = (1-x)^2 + 100(y-x^2)^2\]

最小值点落在 $(x,y)=(1, 1)$,此时的函数值 $f(x,y)=0$。

首先定义 Rosenbrock 函数,注意,每个参数是独立的浮点数。

1
2
3
4
def RosenBrockFunction(x0, x1):
    res = (1 - x0) * (1 - x0) + 100.0 * (x1 - x0 * x0) * (x1 - x0 * x0)

    return res

接着,配置优化器,并测试 SimplexConjugateGradient 算法。初始值设定为 $(x, y) = (0.1, 0.1)$,最优化类型为“无约束”的。

例子 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def testOptimizer1():
    maxIterations = 1000
    minStatIterations = 100
    rootEpsilon = 1e-8
    functionEpsilon = 1e-9
    gradientNormEpsilon = 1e-5

    myEndCrit = ql.EndCriteria(
        maxIterations,
        minStatIterations,
        rootEpsilon,
        functionEpsilon,
        gradientNormEpsilon)

    constraint = ql.NoConstraint()

    solver1 = ql.Simplex(0.1)
    solver2 = ql.ConjugateGradient()

    minimize = ql.Optimizer()

    min1 = minimize.solve(
        function=RosenBrockFunction,
        c=constraint,
        m=solver1,
        e=myEndCrit,
        iv=ql.Array(2, 0.1))

    min2 = minimize.solve(
        function=RosenBrockFunction,
        c=constraint,
        m=solver2,
        e=myEndCrit,
        iv=ql.Array(2, 0.1))

    print('{0:<30}{1}'.format('Root Simplex', min1))
    print('{0:<30}{1}'.format('Root ConjugateGradient', min2))
    print('{0:<40}{1}'.format(
        'Min F Value Simplex',
        RosenBrockFunction(min1[0], min1[1])))
    print('{0:<40}{1}'.format(
        'Min F Value ConjugateGradient',
        RosenBrockFunction(min2[0], min2[1])))


testOptimizer1()
1
2
3
4
Root Simplex                  [ 1; 1 ]
Root ConjugateGradient        [ 0.998904; 0.995025 ]
Min F Value Simplex                     2.929205541302239e-17
Min F Value ConjugateGradient           0.0007764961476745887

校准问题

下面虚拟一个模型校准问题。假设已知 4 个看涨期权的价格 $C_1 , C_2 , C_3 , C_4$,以及对应的敲定价 $K_i$,未知量是股票价格 $S_0$ 和波动率 $\sigma$,通过解决下面的最小二乘问题来求解出 $(\sigma, S_0)$,

\[f(\sigma, S_0) = \sum_{i=1}^4 (C(K_i, \sigma, S_0) - C_i)^2\]

首先定义损失函数(函数对象),

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class CallProblemFunction(object):
    def __init__(self,
                 rd, rf, tau, phi,
                 K1, K2, K3, K4,
                 C1, C2, C3, C4):
        self.rd_ = rd
        self.rf_ = rf
        self.tau_ = tau
        self.phi_ = phi
        self.K1_ = K1
        self.K2_ = K2
        self.K3_ = K3
        self.K4_ = K4
        self.C1_ = C1
        self.C2_ = C2
        self.C3_ = C3
        self.C4_ = C4

    @staticmethod
    def blackScholesPrice(spot, strike,
                          rd, rf,
                          vol, tau,
                          phi):
        domDf = scipy.exp(-rd * tau)
        forDf = scipy.exp(-rf * tau)
        fwd = spot * forDf / domDf
        stdDev = vol * scipy.sqrt(tau)

        dp = (scipy.log(fwd / strike) + 0.5 * stdDev * stdDev) / stdDev
        dm = (scipy.log(fwd / strike) - 0.5 * stdDev * stdDev) / stdDev

        res = phi * domDf * (fwd * norm.cdf(phi * dp) - strike * norm.cdf(phi * dm))

        return res

    def values(self,
               x0,
               x1):
        res = ql.Array(4)
        res[0] = self.blackScholesPrice(
            x0, self.K1_, self.rd_, self.rf_, x1, self.tau_, self.phi_) - self.C1_
        res[1] = self.blackScholesPrice(
            x0, self.K2_, self.rd_, self.rf_, x1, self.tau_, self.phi_) - self.C2_
        res[2] = self.blackScholesPrice(
            x0, self.K3_, self.rd_, self.rf_, x1, self.tau_, self.phi_) - self.C3_
        res[3] = self.blackScholesPrice(
            x0, self.K4_, self.rd_, self.rf_, x1, self.tau_, self.phi_) - self.C4_

        return res

    def __call__(self,
                 x0,
                 x1):
        tmpRes = self.values(x0, x1)

        res = tmpRes[0] * tmpRes[0]
        res += tmpRes[1] * tmpRes[1]
        res += tmpRes[2] * tmpRes[2]
        res += tmpRes[3] * tmpRes[3]

        return res

例子 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def testOptimizer2():
    spot = 98.51
    vol = 0.134
    K1 = 87.0
    K2 = 96.0
    K3 = 103.0
    K4 = 110.0
    rd = 0.002
    rf = 0.01
    phi = 1
    tau = 0.6

    C1 = CallProblemFunction.blackScholesPrice(
        spot, K1, rd, rf, vol, tau, phi)
    C2 = CallProblemFunction.blackScholesPrice(
        spot, K2, rd, rf, vol, tau, phi)
    C3 = CallProblemFunction.blackScholesPrice(
        spot, K3, rd, rf, vol, tau, phi)
    C4 = CallProblemFunction.blackScholesPrice(
        spot, K4, rd, rf, vol, tau, phi)

    optFunc = CallProblemFunction(
        rd, rf, tau, phi, K1, K2, K3, K4, C1, C2, C3, C4)

    maxIterations = 1000
    minStatIterations = 100
    rootEpsilon = 1e-5
    functionEpsilon = 1e-5
    gradientNormEpsilon = 1e-5

    myEndCrit = ql.EndCriteria(
        maxIterations,
        minStatIterations,
        rootEpsilon,
        functionEpsilon,
        gradientNormEpsilon)

    startVal = ql.Array(2)
    startVal[0] = 80.0
    startVal[1] = 0.20

    constraint = ql.NoConstraint()
    solver = ql.BFGS()

    minimize = ql.Optimizer()

    min1 = minimize.solve(
        function=optFunc,
        c=constraint,
        m=solver,
        e=myEndCrit,
        iv=startVal)

    print('Root', min1)
    print('Min Function Value', optFunc(min1[0], min1[1]))
1
2
Root [ 98.51; 0.134 ]
Min Function Value 5.979965971506814e-22
本文由作者按照 CC BY 4.0 进行授权