modelx 简介#

modelx 是一个Python包,用于开发、运行、调试和保存复杂的数值模型,就像使用电子表格一样。modelx最适合精算科学、量化金融和风险管理等领域的模型,这些领域的计算逻辑通常以递归公式表示。

modelx提供了诸如ModelUserSpaceCells等类, 供您创建其实例。 ModelUserSpaceCells在modelx中的作用, 就如同Excel中的WorkbookWorksheetRange

Cells对象可以从Python函数创建,并像缓存函数一样工作。 UserSpace对象作为Cells对象的容器, 并为包含的Cells对象提供命名空间。

以下是modelx的主要功能列表:

  • 一个用户空间可以通过其中定义的名称快速参数化,这样你就可以动态地创建多个用户空间的副本

  • 快速参数化一个UserSpace,使用其中定义的名称,并创建其动态副本

  • 快速构建面向对象模型,利用继承和组合

  • 追踪公式依赖关系以便调试

  • 导入并使用任何Python模块,例如NumpypandasSciPyscikit-learn等。

  • 在出错时查看公式回溯并检查局部变量

  • 将模型保存为文本文件并使用Git进行版本控制

  • 在modelx中保存数据,如将pandas DataFrames保存为Excel或CSV文件

  • 通过Python文档生成器(如Sphinx)自动保存模型的文档

  • 使用带有modelx插件(spyder-modelx)的Spyder,通过图形界面与modelx交互

为了感受modelx如何让我们的生活更轻松,让我们使用modelx构建一个简单模型。

modelx 快速导览#

构建并运行模型#

假设我们想构建一个模型,执行蒙特卡洛模拟来生成10,000条遵循几何布朗运动的股票价格随机路径,并为该股票的欧式看涨期权定价。目前,我们先忽略布莱克-舒尔斯公式能给出解析解这一事实。稍后,我们将验证解析方法是否给出相同结果。

以下是使用modelx构建模型的完整脚本。

import modelx as mx
import numpy as np

model = mx.new_model()                  # Create a new Model named "Model1"
space = model.new_space("MonteCarlo")   # Create a UserSpace named "MonteCarlo"

# Define names in MonteCarlo
space.np = np
space.M = 10000     # Number of scenarios
space.T = 3         # Time to maturity in years
space.N = 36        # Number of time steps
space.S0 = 100      # S(0): Stock price at t=0
space.r = 0.05      # Risk Free Rate
space.sigma = 0.2   # Volatility
space.K = 110       # Option Strike


# Define Cells objects in MonteCarlo from function definitions
@mx.defcells
def std_norm_rand():
    gen = np.random.default_rng(1234)
    return gen.standard_normal(size=(N, M))


@mx.defcells
def stock(i):
    """Stock price at time t_i"""
    dt = T/N
    if i == 0:
        return np.full(shape=M, fill_value=S0)
    else:
        epsilon = std_norm_rand()[i-1]
        return stock(i-1) * np.exp((r - 0.5 * sigma**2) * dt + sigma * epsilon * dt**0.5)


@mx.defcells
def call_opt():
    """Call option price by Monte Carlo"""
    return np.average(np.maximum(stock(N) - K, 0)) * np.exp(-r*T)

从IPython控制台执行上述代码后,调用call_opt可得到欧式期权的价格。

>>> call_opt()
16.26919556999345

call_opt 是一个单元格对象,它会保留返回值直到需要重新计算为止。 不仅call_opt会保留返回值,所有用于计算目标call_opt的中间值也会被保留,并且可以免费使用。

>>> stock(space.N)      # Stock price at i=N i.e. t=T
array([ 78.58406132,  59.01504804, 115.148291  , ..., 155.39335662,
        74.7907511 , 137.82730703])

如果想查看其他行权价对应的期权价格,只需将新的行权价赋值给K

>>> space.K = 100   # Cache is cleared by this assignment

>>> call_opt()    # New option price for the updated strike
20.96156962064

你可以通过用rsigma参数化MonteCarlo,动态创建具有不同rsigma组合的多个MonteCarlo副本:

>>> space.parameters = ("r", "sigma")   # Parameterize MonteCarlo with r and sigma

>>> space[0.03, 0.15].call_opt()  # Dynamically create a copy of MonteCarlo with r=3% and sigma=15%
14.812014828333284

>>> space[0.06, 0.4].call_opt()   # Dynamically create another copy with r=6% and sigma=40%
33.90481014639403

拥有两份MonteCarlo副本可以轻松执行诸如比较相同项目值的任务,例如期权价格,或到期前或到期时的股票价格,使用不同的参数。 动态UserSpaces是不可变的,当基础MonteCarlo更新时会被销毁。

深入了解modelx#

现在,让我们回顾并仔细查看初始脚本,以更深入地理解我们在构建模型时发生的情况。

import modelx as mx
import numpy as np

第一个import语句在后台启动了modelx,并定义了mx作为modelx模块的别名以便使用。

第二个import语句对大多数Python用户来说应该很熟悉。 它将numpy模块以np的形式导入到__main__模块的全局命名空间中,也就是我们当前正在工作的模块。 稍后我们会看到,在__main__的全局命名空间中定义np并不会使其在Formulas中可用。

通过下一条语句,我们正在创建一个新的Model对象并将其赋值给名称model。由于我们没有为new_model函数指定显式名称,modelx将该模型命名为Model1Model对象对于modelx而言,就像Workbook对于Excel一样。 它是其中包含的所有对象的最外层容器。

接下来的语句在模型中创建了一个名为MonteCarloUserSpace对象。 UserSpace对于modelx而言,就像Worksheet对于Excel一样。 它是一个容器,我们将在其中创建Cells对象。

model = mx.new_model()                  # Create a new Model named "Model1"
space = model.new_space("MonteCarlo")   # Create a UserSpace named "MonteCralo"

一个Cells对象的行为类似于缓存函数。 它可以像函数一样被调用,返回值会被保留直到需要更新。 Cells对象类似于Excel中的单元格,但与Excel单元格不同的是, 它的公式可以包含参数,因此可以保留多个值, 每个参数值集合对应一个值。

除了作为包含单元格的父级外,用户空间还有另一个重要作用,即为包含单元格公式提供命名空间。从这个意义上说,用户空间类似于Python模块。

一个Cells对象关联着一个Formula对象。 Formula对象本质上是一个Python函数, 不同之处在于它不是在Python的全局命名空间(本例中是__main__的命名空间)中求值, 而是在父级UserSpace提供的命名空间中进行求值。 您可以通过属性赋值操作在UserSpace的命名空间中定义名称。

接下来的代码块将我们在模型中使用的值和对象分配给MonteCarlo命名空间中的名称。

# Define names in MonteCarlo
space.np = np
space.M = 10000     # Number of scenarios
space.T = 3         # Time to maturity in years
space.N = 36        # Number of time steps
space.S0 = 100      # stock(0): Stock price at t=0
space.r = 0.05      # Risk Free Rate
space.sigma = 0.2   # Volatility
space.K = 110       # Option Strike

在内部,modelx将这些名称及其值保存为引用对象。

下一部分构建我们模型计算逻辑的主体。 它在MonteCarlo中创建了3个Cells对象:std_norm_randstockcall_optCells对象的行为类似于缓存函数。 它可以像函数一样被调用,返回值会保留直到需要更新。

defcells 是一个便捷的装饰器,用于快速从函数定义创建Cells对象。 第一个带有defcells装饰器的def语句会创建一个名为std_norm_randCells对象,并将该对象赋值给__main__全局命名空间中的std_norm_rand名称。 此外, 该语句从std_norm_rand函数定义中为Cells对象定义了formula属性。formula属性持有Formula对象, 它本质上是装饰后的Python函数的副本, 但Formula中的全局名称引用的是我们上面刚刚赋值的值。

同样适用于stockcall_opt。 需要注意的是,在公式定义中, 我们既可以引用MonteCarlo中定义的其他单元格,也可以引用上面定义的名称。 另外请注意,我们可以直接引用这些名称, 而无需在前面加上对象名称和点号。

# Create Cells objects in MonteCarlo and define their formulas from function definitions

@mx.defcells
def std_norm_rand():
    gen = np.random.default_rng(1234)
    return gen.standard_normal(size=(N, M))


@mx.defcells
def stock(i):
    """Stock price at time t_i"""
    dt = T/N
    if i == 0:
        return np.full(shape=M, fill_value=S0)
    else:
        epsilon = std_norm_rand()[i-1]
        return stock(i-1) * np.exp((r - 0.5 * sigma**2) * dt + sigma * epsilon * dt**0.5)


@mx.defcells
def call_opt():
    """Call option price by Monte Carlo"""
    return np.average(np.maximum(stock(N) - K, 0)) * np.exp(-r*T)

调试模型#

我们经常需要调试构建的模型,以确保其结果的正确性。modelx提供了相关功能来辅助此类调试工作。

其中一个特性是modelx能够追踪计算依赖关系。precedents方法在Cells上会返回给定参数的前驱列表。该列表包含ReferencesNodes,它们代表与参数关联的Cells,这些参数和Cells会使用它们。

继续上面的例子,下面展示了call_opt()stock(36)的前置节点。

>>> call_opt()    # Make suer this is run.
20.96156962064

>>> call_opt.precedents()   # Returns precedents of call_opt()
[Model1.MonteCarlo.stock(i=36)=
 array([ 78.58406132,  59.01504804, 115.148291  , ..., 155.39335662,
         74.7907511 , 137.82730703]),
 Model1.MonteCarlo.np=<module 'numpy' from 'C:\\Users\\...\\__init__.py'>,
 Model1.MonteCarlo.N=36,
 Model1.MonteCarlo.K=100,
 Model1.MonteCarlo.r=0.05,
 Model1.MonteCarlo.T=3]

>>> stock.precedents(36)     # Reteruns precedents of stock(36)
[Model1.MonteCarlo.std_norm_rand()=
 array([[-1.60383681,  0.06409991,  0.7408913 , ...,  0.82163882,
         -0.49991377,  1.17804635],
        [-0.67804259,  1.35072849,  2.07565699, ...,  0.32146055,
         -0.7599273 ,  1.73113515],
        [-1.42381038, -0.36400253, -0.55303109, ...,  0.04814081,
         -1.19998129, -0.08490359],
        ...,
        [-2.12588633, -0.19431652, -1.68358751, ..., -0.3466555 ,
         -0.10290633, -0.68737272],
        [-1.32955138,  0.28343894, -2.01866314, ...,  1.58520134,
          0.30001717, -0.63270348],
        [ 2.02929671, -1.42904385,  0.26366402, ..., -0.05042656,
          0.14542656, -0.21076562]]),
 Model1.MonteCarlo.stock(i=35)=
 array([ 69.72140666,  63.93061459, 113.12553545, ..., 155.45729533,
         73.98023943, 139.16636139]),
 Model1.MonteCarlo.T=3,
 Model1.MonteCarlo.N=36,
 Model1.MonteCarlo.np=<module 'numpy' from 'C:\\Users\\...\\__init__.py'>,
 Model1.MonteCarlo.M=10000,
 Model1.MonteCarlo.S0=100,
 Model1.MonteCarlo.r=0.05,
 Model1.MonteCarlo.sigma=0.2]

相反,succs方法返回一个使用节点的列表,例如sock(36)

>>> stock.succs(36)
[Model1.MonteCarlo.call_opt()=20.96156962064]

modelx的另一个特性是便于追踪错误。 当Cells调用中发生错误时, modelx会打印出调用的回溯信息。 让我们故意在stock(10)返回前插入一个raise语句使其抛出错误。

@mx.defcells
def stock(i):
    """Stock price at time t_i"""
    dt = T/N
    if i == 0:
        return np.full(shape=M, fill_value=S0)
    else:
        epsilon = std_norm_rand()[i-1]
        if i == 10:
          raise ValueError('Error raised')
        return stock(i-1) * np.exp((r - 0.5 * sigma**2) * dt + sigma * epsilon * dt**0.5)

执行call_opt最终到达stock(10)时抛出错误。 错误信息会打印出执行的追溯记录。

>>> call_opt()
...
FormulaError: Error raised during formula execution
ValueError: Error raised

Formula traceback:
0: Model1.MonteCarlo.call_opt(), line 3
...
25: Model1.MonteCarlo.stock(i=12), line 10
26: Model1.MonteCarlo.stock(i=11), line 10
27: Model1.MonteCarlo.stock(i=10), line 9

Formula source:
def stock(i):
    """Stock price at time t_i"""
    dt = T/N
    if i == 0:
        return np.full(shape=M, fill_value=S0)
    else:
        epsilon = std_norm_rand()[i-1]
        if i == 10:
          raise ValueError('Error raised')
        return stock(i-1) * np.exp((r - 0.5 * sigma**2) * dt + sigma * epsilon * dt**0.5)

此外,trace_locals函数 可以帮助检查错误发生时本地变量所持有的值。

>>> mx.trace_locals()
{'i': 10,
 'dt': 0.08333333333333333,
 'epsilon': array([-0.52430375, -1.29168268,  0.04276587, ..., -0.45993114,
         1.33283969,  0.26335339])}

保存模型#

一个Model可以通过writezip方法保存为目录树中的多个文件,或者保存为一个单独的zip文件。

>>> model.write(r'C:\Users\mxuser\Model1')

>>> model.zip(r'C:\Users\mxuser\Model1.zip')

目录树的内容被编写为一个伪Python包,UserSpaces会输出到子目录的__init__.py文件中,就像它们是Python模块一样,而Cells则被输出为Python函数的形式。这意味着我们可以像对待Python代码一样使用Git对模型的输出进行版本控制,并通过文档生成器(如Shpinx)从文档字符串自动生成文档。

在Spyder中使用modelx#

Spyder 是一款流行的开源 Python 集成开发环境,它允许安装插件来扩展自身功能。modelx 的 Spyder 插件作为一个独立软件包提供,可在 Spyder 中增强 modelx 的用户界面。该插件添加了自定义 IPython 控制台和 GUI 组件,方便在 Spyder 中使用 modelx。

使用Spyder及其插件,示例模型在GUI小部件中以树状结构显示:

../_images/MonteCarloInSpyder.png

Spyder中的示例模型#

该小部件使编辑模型变得简单。 插件安装的其他小部件有助于查看modelx对象的数据 并分析它们的依赖关系。 有关插件的更多信息,请参阅Spyder插件

modelx对象概述#

正如我们在上面的快速入门中所看到的, modelx允许我们构建由几种对象类型组成的模型。 ModelUserSpaceCellsReference是我们最常用的类型。 在本节中,我们将简要回顾这些对象类型, 以便对它们有基本的了解。

下图展示了这些对象之间的包含关系。

../_images/ModelStructure.png

模型、空间与单元格#

模型是最高层级的容器对象。 模型可以保存到文件并重新加载。 在同一个Python会话中可以同时打开多个模型。

在modelx中,我们可以创建UserSpace对象。 UserSpace对象是可编辑的。 此外还有只读类型的空间对象,例如ItemSpaceDynamicSpace。 比如在上面的快速入门教程中,我们创建了 两个ItemSpace对象:MonteCarlo[0.03, 0.15]MonteCarlo[0.06, 0.4]

>>> space[0.03, 0.15].call_opt()  # space refers to the MonteCarlo space
14.812014828333284

>>> space[0.06, 0.4].call_opt() 
33.90481014639403

我们统称它们为Space对象,或简称为spaces,无论它们是可编辑还是只读类型。

空间(Spaces)作为容器使用, 将模型中的内容分隔成不同组件。 空间可以包含单元格(Cells)引用(Reference)对象以及其他空间对象, 从而在模型内形成树状结构。 这些空间还充当着命名空间的作用, 用于管理与空间本身或其中包含的单元格(Cells)对象相关联的公式。

我们将Cells对象简称为单元格。 单元格是具有一个公式并能保存其值的对象,就像 电子表格中的单元格可以包含公式和值一样。 但与电子表格单元格不同的是, 在modelx中,单元格的值要么通过 其公式计算得出,要么由用户为每个参数指定输入值。 当用户指定输入值时, 该参数的公式将不会被计算。

引用对象是绑定到任意对象的名称。 我们将引用对象称为references,或简称refs。 引用可以在空间或模型中定义。 在空间中定义的引用可以从该空间内定义的单元格公式, 或与该空间关联的公式中引用。 例如,Cells1.formula(以及Space1.formula如果有的话)可以 引用Ref2。 在模型中定义的引用(例如上图中的Ref1)可以从 模型中任何位置定义的公式引用,除非其他引用 覆盖了模型中该引用定义的名称绑定。