modelx 简介#
modelx 是一个Python包,用于开发、运行、调试和保存复杂的数值模型,就像使用电子表格一样。modelx最适合精算科学、量化金融和风险管理等领域的模型,这些领域的计算逻辑通常以递归公式表示。
modelx提供了诸如Model、UserSpace和Cells等类, 供您创建其实例。 Model、UserSpace和Cells在modelx中的作用, 就如同Excel中的Workbook、Worksheet和Range。
Cells对象可以从Python函数创建,并像缓存函数一样工作。 UserSpace对象作为Cells对象的容器, 并为包含的Cells对象提供命名空间。
以下是modelx的主要功能列表:
一个用户空间可以通过其中定义的名称快速参数化,这样你就可以动态地创建多个用户空间的副本
快速参数化一个UserSpace,使用其中定义的名称,并创建其动态副本
快速构建面向对象模型,利用继承和组合
追踪公式依赖关系以便调试
导入并使用任何Python模块,例如Numpy、pandas、SciPy、scikit-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
你可以通过用r
和sigma
参数化MonteCarlo,动态创建具有不同r
和sigma
组合的多个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将该模型命名为Model1。
Model对象对于modelx而言,就像Workbook对于Excel一样。
它是其中包含的所有对象的最外层容器。
接下来的语句在模型中创建了一个名为MonteCarlo的UserSpace对象。 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_rand
、stock
和call_opt
。
Cells对象的行为类似于缓存函数。
它可以像函数一样被调用,返回值会保留直到需要更新。
defcells
是一个便捷的装饰器,用于快速从函数定义创建Cells对象。
第一个带有defcells
装饰器的def
语句会创建一个名为std_norm_rand
的Cells对象,并将该对象赋值给__main__
全局命名空间中的std_norm_rand
名称。
此外,
该语句从std_norm_rand
函数定义中为Cells对象定义了formula属性。formula属性持有Formula对象,
它本质上是装饰后的Python函数的副本,
但Formula中的全局名称引用的是我们上面刚刚赋值的值。
同样适用于stock
和call_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
上会返回给定参数的前驱列表。该列表包含References和Nodes,它们代表与参数关联的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可以通过write
或zip
方法保存为目录树中的多个文件,或者保存为一个单独的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小部件中以树状结构显示:

Spyder中的示例模型#
该小部件使编辑模型变得简单。 插件安装的其他小部件有助于查看modelx对象的数据 并分析它们的依赖关系。 有关插件的更多信息,请参阅Spyder插件。
modelx对象概述#
正如我们在上面的快速入门中所看到的, modelx允许我们构建由几种对象类型组成的模型。 Model、UserSpace、Cells、Reference是我们最常用的类型。 在本节中,我们将简要回顾这些对象类型, 以便对它们有基本的了解。
下图展示了这些对象之间的包含关系。

模型、空间与单元格#
模型是最高层级的容器对象。 模型可以保存到文件并重新加载。 在同一个Python会话中可以同时打开多个模型。
在modelx中,我们可以创建UserSpace对象。
UserSpace对象是可编辑的。
此外还有只读类型的空间对象,例如ItemSpace和DynamicSpace。
比如在上面的快速入门教程中,我们创建了
两个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)可以从
模型中任何位置定义的公式引用,除非其他引用
覆盖了模型中该引用定义的名称绑定。