monteCarlo

首发版本:3.00.6.4

语法

monteCarlo(evolve, payoff, initialState, timeGrid, numPaths, [numFactors=1], [params=NULL], [options=NULL])

详情

执行通用蒙特卡洛数值求解,按用户定义的随机过程生成模拟路径,在路径上计算用户定义的目标函数,并给出该目标函数期望值的估计 θ = E[f(X)]。其中:

  • X 为用户定义的随机变量或随机过程,本函数中由 evolve 定义;

  • f(X) 为用户定义的目标函数,本函数中由 payoff 定义。

本函数负责生成随机驱动、处理随机因子相关性、按批次模拟路径并聚合统计结果,支持单状态或多状态过程、单因子或多因子驱动,以及单输出或多输出目标函数。金融衍生品定价是其主要应用场景之一,产品规则和折现逻辑由用户在 payoff 中定义。

注:
  • 当前仅支持标准正态伪随机驱动和单节点 CPU 执行,不支持 GPU 或跨节点分布式调度。

  • 参数、回调或任一批次执行失败时,整次调用失败,不返回部分统计结果、路径或随机数。

  • 对路径依赖产品,模拟路径只包含模拟起点及之后的状态。已实现历史状态由用户通过 params 传入,并在目标函数中与未来模拟路径合并;本函数不自动处理观察时间映射或历史数据。

参数

evolve 函数定义,指定自定义随机过程状态演化,函数签名固定为:evolve(t, state, dt, noise, params)。

参数 类型与形式 说明
t DOUBLE 标量 指定当前时间节点 timeGrid[i]。
state DOUBLE 矩阵 指定当前批次所有路径的当前状态,形状为 batchPathCount × stateDim。
dt DOUBLE 标量 指定当前时间步长度,等于相邻两个时间点之差 timeGrid[i+1] - timeGrid[i]。
noise DOUBLE 矩阵

指定当前批次的随机驱动,形状为 batchPathCount × numFactors。

  • 未指定 "corr" 时,符合标准正态分布: noise i , j ∼ N ( 0 , 1 ) \textit{noise}_{i,j} \sim N(0,1) ;
  • 指定 "corr" 时,每条路径在同一时间步的随机因子满足用户指定的相关矩阵。
params ANY 调用 monteCarlo 传入的自定义参数 params。

其中:

  • batchPathCount 为当前批次实际包含的路径数;

  • stateDim 为状态维度,等于 initialState 的元素个数,标量对应 1;

  • numFactors 为调用 monteCarlo 传入的 numFactors 参数。

evolve 每批、每个时间步调用一次,返回 batchPathCount × stateDim 的 DOUBLE 矩阵。

payoff 函数定义,指定自定义目标函数。

  • 当指定 options 参数的 "pathMode" 为 "full" 时,函数签名固定为:payoff(paths, timeGrid, params)。

参数 类型与形式 说明
paths ANY 向量

指定完整模拟路径,可由 mcSimulate 返回。长度为 size(timeGrid),其中第 i 个元素为 DOUBLE 矩阵,形状为 batchPathCount × stateDim,表示 timeGrid[i] 时刻的状态。因此:

  • paths[0] 为估值时点初始状态矩阵;
  • paths[-1] 为终止时间状态矩阵。
注:
  • paths 仅包含估值时点及之后的状态,不包含估值时点之前的历史状态。
  • paths 的第一个元素是估值时点的初始状态,默认只作为模拟起点,不参与产品收益计算。若产品在估值时点也要取一次观察,需在观察时点列表中把该时点一并列出,由目标函数读取。
timeGrid 数值向量 指定模拟时间网格。至少包含两个非 NULL 的有限数值,且严格递增。支持非等间隔时间网格。
params ANY 调用 monteCarlo 传入的自定义参数 params。
  • 当指定 options 参数的 "pathMode" 为 "terminal" 时,函数签名固定为:payoff(terminalState, timeGrid, params)。其中:
    • terminalState 是 batchPathCount × stateDim 的 DOUBLE 矩阵,仅包含当前批次的终值状态;
    • timeGrid 与 params 参数与 payoff(paths, timeGrid, params) 一致。

目标函数按批次调用:

  • 单输出时,返回长度为 batchPathCount 的数值向量,每个元素对应一条模拟路径的目标函数样本;
  • 多输出时,返回形为 batchPathCount × numOutputs 的数值矩阵,每列对应一个独立待估计目标。
注:
payoff 必须将已实现历史状态与未来模拟路径合并计算最终样本,不得重新模拟估值时点之前的历史状态。

initialState 数值标量或向量,指定随机过程的初始状态,即 timeGrid 第一个时间点的初始状态。指定为标量时表示单状态过程,指定为向量则表示多状态过程。

timeGrid 数值向量,指定模拟时间网格。至少包含两个非 NULL 的有限数值,且严格递增。支持非等间隔时间网格。

numPaths INT 或 LONG 标量,指定模拟路径总数。必须大于等于 2;启用对偶变量时,必须为大于 2 的偶数。

numFactors 可选参数, INT 标量,指定每个时间步输入状态演化函数的随机因子数,默认值为 1,必须大于 0。随机因子数与状态维度相互独立。

params 可选参数, ANY 类型对象,默认值为 NULL。指定用户自定义参数对象,传递给 evolve 和 payoff 函数。同一次模拟中的回调应将其作为只读对象使用。

options 可选参数,字典(STRING, ANY),默认值为 NULL。支持以下键:

键 类型与形式 默认值 说明
"randomMethod" STRING "pseudo" 指定随机采样方法。当前仅支持 "pseudo",即 Pseudo Random Monte Carlo,生成标准正态伪随机驱动。
"seed" INT / LONG 自动生成 指定随机种子。未指定时由系统自动生成,并在结果字段 seed 中返回实际值。
"corr" 数值矩阵 NULL 指定随机因子之间的相关矩阵,维度为 numFactors × numFactors。必须对称、正定、对角线元素为 1,且不包含 NULL、NaN 或 Inf。系统在模拟开始前完成相关性分解,并将相关化后的标准正态随机驱动传入 evolve。相关性作用于随机因子,而非直接作用于状态变量。
"antithetic" BOOL false 指定是否启用对偶变量法(Antithetic Variates)。指定为 true 时启用对偶变量法,numPaths 必须为偶数且仍表示最终有效路径数,系统将基础随机驱动 Z 与 -Z 成对用于模拟。
"batchSize" INT / LONG 自动确定 指定每批有效路径数,必须大于 0,用于控制批量计算粒度和内存占用。最后一个 batch 的路径数可以小于 "batchSize"。若 batchSize > numPaths,则按单批次执行。启用对偶模式时,必须为偶数。
"confidenceLevel" DOUBLE 0.95 指定置信水平,取值范围为 (0,1)。当前使用正态近似构造置信区间,结果字段中返回实际使用的置信水平。
"returnPaths" BOOL false 指定是否返回全部模拟路径。指定为 false 时仅在当前 batch 内保留路径;指定为 true 时返回结果额外包含 "paths" 键。仅在 pathMode="full" 且 resultMode="detail" 下可指定为 true。
"returnRandomNumbers" BOOL false 指定是否返回实际传入 evolve 的随机驱动。指定为 true 时结果额外包含 "randomNumbers" 键,其中已包含 Antithetic 配对和 "corr" 相关性变换后的结果。
"parallel" BOOL true 指定是否允许不同批次在本机 CPU 上并行执行。指定为 false 时串行执行。当前仅支持单节点 CPU 并行;不支持 GPU backend 和跨节点分布式调度。
"resultMode" STRING "detail" 指定返回形式。指定为 "detail" 时返回统计结果字典;指定为 "value" 时仅返回估计值。
"pathMode" STRING "full" 指定目标函数的输入形式。指定为 "full" 传入当前批次的完整路径;指定为 "terminal" 时仅传入终值状态矩阵。

返回值

返回值形式由 options 中的 "resultMode" 决定:

  • 指定为 "value" 时,仅返回 Monte Carlo 估计值。单输出目标函数对应 DOUBLE 标量,多输出目标函数对应 DOUBLE 向量。
  • 指定为 "detail" 时,返回统计结果字典,包含以下键:
键 类型与形式 说明
"estimate" DOUBLE 标量或向量 Monte Carlo 估计值
"variance" DOUBLE 标量或向量 Payoff 样本方差
"stdError" DOUBLE 标量或向量 Monte Carlo 标准误
"confidenceLower" DOUBLE 标量或向量 置信区间下限
"confidenceUpper" DOUBLE 标量或向量 置信区间上限
"confidenceLevel" DOUBLE 标量 实际使用的置信水平
"numPaths" LONG 标量 实际生成并传入目标函数的有效路径数
"randomMethod" STRING 标量 当前固定为 "pseudo"
"seed" LONG 标量 本次计算实际使用的随机种子
"paths" ANY 向量 完整模拟路径。指定 returnPaths=true 时输出,长度等于 size(timeGrid),每个元素是 numPaths × stateDim 的 DOUBLE 矩阵。
"randomNumbers" ANY 向量 实际传入状态演化函数的随机驱动。指定 returnRandomNumbers=true 时输出,长度等于 size(timeGrid) - 1,每个元素是 numPaths × numFactors 的 DOUBLE 矩阵。

单输出时,"estimate"、"variance"、"stdError"、"confidenceLower"、"confidenceUpper" 为标量;多输出时,为与输出列数相同的向量。

例子

例 1. 通用定价的基本模式

本例演示通用定价的基本模式:evolve 定义模型,payoff 定义产品,options 控制数值计算。结果依次为估值、标准误、95% 置信区间下限和上限。

def gbmEvolve(t, state, dt, noise, p) {
    mu = (p["r"] - p["q"] - 0.5 * p["sigma"] * p["sigma"]) * dt
    return state * exp(mu + p["sigma"] * sqrt(dt) * noise)
}
// 按自己的产品定义合适的收益函数
def callPayoff(terminalState, timeGrid, p) {
    terminal = terminalState[, 0]
    T = timeGrid[size(timeGrid) - 1] - timeGrid[0]
    return exp(-p["r"] * T) * max(terminal - p["K"], 0.0)
}
p = dict(STRING, DOUBLE)
p["r"] = 0.03
p["q"] = 0.0
p["sigma"] = 0.20
p["K"] = 100.0
mcOpt = dict(STRING, ANY)
mcOpt["seed"] = 42
mcOpt["antithetic"] = true
mcOpt["batchSize"] = 2000
mcOpt["pathMode"] = "terminal"
mcResult = monteCarlo(gbmEvolve, callPayoff, 100.0, 0..12 / 12.0, 20000, 1, p, mcOpt)
[mcResult["estimate"], mcResult["stdError"], mcResult["confidenceLower"], mcResult["confidenceUpper"]]
// output: [9.184365113062904, 0.073151245422059, 9.040991306611417, 9.32773891951439]

例 2. 使用完整路径计算欧式看涨期权的估值

本例由用户定义几何布朗运动及折现后的期权回报。固定随机种子和批次大小,开启对偶变量。结果依次为估值、标准误、95% 置信区间下限和上限。

def gbmEvolve(t, state, dt, noise, params) {
    drift = (params["rate"] - 0.5 * params["volatility"] * params["volatility"]) * dt
    return state * exp(drift + params["volatility"] * sqrt(dt) * noise)
}
def callPayoff(paths, timeGrid, params) {
    terminal = paths[size(paths) - 1][, 0]
    maturity = timeGrid[size(timeGrid) - 1] - timeGrid[0]
    return exp(-params["rate"] * maturity) * max(terminal - params["strike"], 0.0)
}
params = dict(keyType=STRING, valueType=DOUBLE)
params["rate"] = 0.03
params["volatility"] = 0.2
params["strike"] = 100.0
options = dict(keyType=STRING, valueType=ANY)
options["seed"] = 42
options["batchSize"] = 1000
options["antithetic"] = true
options["pathMode"] = "full"
result = monteCarlo(evolve=gbmEvolve, payoff=callPayoff, initialState=100.0,
    timeGrid=0..12 / 12.0, numPaths=10000, params=params, options=options)
[result["estimate"], result["stdError"], result["confidenceLower"], result["confidenceUpper"]]
// output: [9.32114319573, 0.106148772868, 9.11309542391, 9.52919096756]

例 3. 使用终值模式同时计算看涨、看跌与数字期权的估值

目标函数返回三列样本,三列来自同一批路径,估计值之间可直接比较;指定 pathMode="terminal" 使引擎不必保存中间时点。结果依次为三个估计值、三个标准误、三个解析值,以及两者的逐列差值。

def gbmEvolve(t, state, dt, noise, p) {
    drift = (p["r"] - p["q"] - 0.5 * p["sigma"] * p["sigma"]) * dt
    return state * exp(drift + p["sigma"] * sqrt(dt) * noise)
}
def vanillaPayoff(terminalState, timeGrid, p) {
    ST = terminalState[, 0]
    T = timeGrid[size(timeGrid) - 1] - timeGrid[0]
    df = exp(-p["r"] * T)
    callPV = df * max(ST - p["K"], 0.0)
    putPV = df * max(p["K"] - ST, 0.0)
    digitalPV = df * iif(ST > p["K"], 1.0, 0.0)
    return matrix(callPV, putPV, digitalPV)
}
p = dict(STRING, DOUBLE)
p["S0"] = 100.0
p["K"] = 100.0
p["r"] = 0.03
p["q"] = 0.0
p["sigma"] = 0.20
opt = dict(STRING, ANY)
opt["seed"] = 42
opt["antithetic"] = true
opt["batchSize"] = 10000
opt["pathMode"] = "terminal"
result = monteCarlo(gbmEvolve, vanillaPayoff, p["S0"], 0..12 / 12.0, 100000, 1, p, opt)
T = 1.0
d1 = (log(p["S0"] / p["K"]) + (p["r"] - p["q"] + 0.5 * p["sigma"] * p["sigma"]) * T) / (p["sigma"] * sqrt(T))
d2 = d1 - p["sigma"] * sqrt(T)
bsCall = p["S0"] * exp(-p["q"] * T) * cdfNormal(0.0, 1.0, d1) - p["K"] * exp(-p["r"] * T) * cdfNormal(0.0, 1.0, d2)
bsPut = p["K"] * exp(-p["r"] * T) * cdfNormal(0.0, 1.0, -d2) - p["S0"] * exp(-p["q"] * T) * cdfNormal(0.0, 1.0, -d1)
bsDigital = exp(-p["r"] * T) * cdfNormal(0.0, 1.0, d2)
analytic = [bsCall, bsPut, bsDigital]
assert 1, all(abs(result["estimate"] - analytic) <= 4.0 * result["stdError"])
[result["estimate"], result["stdError"], analytic, result["estimate"] - analytic]
// output: [[9.414867268425256, 6.464866000587869, 0.503573891813656], [0.033089109740657, 0.021276101501186, 0.000413951476158], [9.413403383853016, 6.457956738703836, 0.504572291844394], [0.001463884572241, 0.006909261884034, -0.000998400030737]]

例 4. 亚式看涨期权(算术平均与几何平均)

亚式期权的赔付依赖整条路径上的观察价格。本例只在 12 个月末观察点取样、不把 t=0 计入平均值,并用同一批路径同时给出算术平均与几何平均两个估计值。结果依次为两个估计值和两个标准误。

def gbmEvolve(t, state, dt, noise, p) {
    drift = (p["r"] - p["q"] - 0.5 * p["sigma"] * p["sigma"]) * dt
    return state * exp(drift + p["sigma"] * sqrt(dt) * noise)
}
def asianPayoff(paths, timeGrid, p) {
    n = rows(paths[0])
    m = size(paths) - 1
    // 只用未来 12 个观察时点,不含 t=0
    prices = matrix(DOUBLE, n, m)
    for (i in 1..m) {
        prices[, i - 1] = paths[i][, 0]
    }
    arithmeticAverage = rowAvg(prices)
    geometricAverage = exp(rowAvg(log(prices)))
    T = timeGrid[size(timeGrid) - 1] - timeGrid[0]
    df = exp(-p["r"] * T)
    return df * matrix(max(arithmeticAverage - p["K"], 0.0), max(geometricAverage - p["K"], 0.0))
}
p = dict(STRING, DOUBLE)
p["r"] = 0.03
p["q"] = 0.0
p["sigma"] = 0.20
p["K"] = 100.0
opt = dict(STRING, ANY)
opt["seed"] = 42
opt["antithetic"] = true
opt["batchSize"] = 5000
opt["pathMode"] = "full"
res = monteCarlo(gbmEvolve, asianPayoff, 100.0, 0..12 / 12.0, 100000, 1, p, opt)
[res["estimate"], res["stdError"]]
// output: [[5.609002269061869, 5.413219715290193], [0.019006421439442, 0.018509710160461]]

例 5. 存续期亚式看涨期权

合约已进入平均期,估值时点前已有 6 个 Fixing 已确定,剩余 6 个尚未发生。历史累计值不能写入 initialState,而应随 params 传入;剩余观察时点在 timeGrid 中定位后取用。结果依次为估值、标准误、95% 置信区间下限和上限。

def gbmEvolve(t, state, dt, noise, p) {
    drift = (p["r"] - p["q"] - 0.5 * p["sigma"] * p["sigma"]) * dt
    return state * exp(drift + p["sigma"] * sqrt(dt) * noise)
}
def seasonedAsianPayoff(paths, timeGrid, p) {
    n = rows(paths[0])
    futureSum = take(0.0, n)
    futureCount = size(paths) - 1
    // 从估值日后的第一个待观察日开始,不重复计入当日价格
    for (i in 1..futureCount) {
        futureSum += paths[i][, 0]
    }
    averagePrice = (p["realizedSum"] + futureSum) / (p["realizedCount"] + futureCount)
    remainingT = timeGrid[size(timeGrid) - 1] - timeGrid[0]
    return exp(-p["r"] * remainingT) * max(averagePrice - p["K"], 0.0)
}
p = dict(STRING, DOUBLE)
p["r"] = 0.03
p["q"] = 0.0
p["sigma"] = 0.20
p["K"] = 100.0
p["realizedCount"] = 6.0
p["realizedSum"] = 98.0 + 99.0 + 101.0 + 102.0 + 96.0 + 104.0
opt = dict(STRING, ANY)
opt["seed"] = 2026
opt["antithetic"] = true
opt["batchSize"] = 5000
opt["pathMode"] = "full"
seasoned = monteCarlo(gbmEvolve, seasonedAsianPayoff, 102.0, 0..6 / 12.0, 100000, 1, p, opt)
[seasoned["estimate"], seasoned["stdError"], seasoned["confidenceLower"], seasoned["confidenceUpper"]]
// output: [2.633867593334059, 0.006737018412437, 2.620663279882499, 2.647071906785618]

例 6. 双资产篮子看涨期权

篮子期权按等权持有两只股票,两者收益相关,"corr" 作用于随机因子而非状态本身。示例同时演示了 numFactors=2 与向量型 initialState。

def twoAssetEvolve(t, state, dt, noise, p) {
    s1 = state[, 0] * exp((p["rate"] - 0.5 * p["vol1"] * p["vol1"]) * dt + p["vol1"] * sqrt(dt) * noise[, 0])
    s2 = state[, 1] * exp((p["rate"] - 0.5 * p["vol2"] * p["vol2"]) * dt + p["vol2"] * sqrt(dt) * noise[, 1])
    return matrix(s1, s2)
}
def basketPayoff(terminalState, timeGrid, p) {
    basketPrice = 0.5 * terminalState[, 0] + 0.5 * terminalState[, 1]
    maturity = timeGrid[size(timeGrid) - 1] - timeGrid[0]
    return exp(-p["rate"] * maturity) * max(basketPrice - p["strike"], 0.0)
}
p = dict(STRING, ANY)
p["rate"] = 0.03
p["vol1"] = 0.20
p["vol2"] = 0.25
p["strike"] = 100.0
options = dict(STRING, ANY)
options["seed"] = 42
options["antithetic"] = true
options["batchSize"] = 5000
options["pathMode"] = "terminal"
options["corr"] = matrix(1.0 0.4, 0.4 1.0)
result = monteCarlo(twoAssetEvolve, basketPayoff, 100.0 95.0, 0..12 / 12.0, 100000, 2, p, options)
[result["estimate"], result["stdError"]]
// output: [7.52196546515141, 0.030194782803288]

例 7. 障碍期权与回望期权

向上敲出看涨与回望看涨的赔付都依赖整条路径上的最高价,必须用默认的 pathMode="full"。两者共用同一批路径,一次模拟给出两个估计值。这里只按 12 个离散观察点判断障碍是否被触及,与连续监测的理论价格不可直接比较。结果依次为两个估计值和两个标准误。

def gbmEvolve(t, state, dt, noise, p) {
    drift = (p["r"] - p["q"] - 0.5 * p["sigma"] * p["sigma"]) * dt
    return state * exp(drift + p["sigma"] * sqrt(dt) * noise)
}
def barrierLookbackPayoff(paths, timeGrid, p) {
    maxSpot = paths[0][, 0]
    for (i in 1..(size(paths) - 1)) {
        maxSpot = max(maxSpot, paths[i][, 0])
    }
    ST = paths[size(paths) - 1][, 0]
    T = timeGrid[size(timeGrid) - 1] - timeGrid[0]
    df = exp(-p["r"] * T)
    hit = maxSpot >= p["barrier"]
    upOut = df * iif(hit, 0.0, max(ST - p["K"], 0.0))
    lookback = df * max(maxSpot - p["K"], 0.0)
    return matrix(upOut, lookback)
}
p = dict(STRING, DOUBLE)
p["r"] = 0.03
p["q"] = 0.0
p["sigma"] = 0.20
p["K"] = 100.0
p["barrier"] = 120.0
opt = dict(STRING, ANY)
opt["seed"] = 42
opt["antithetic"] = true
opt["batchSize"] = 5000
opt["pathMode"] = "full"
res = monteCarlo(gbmEvolve, barrierLookbackPayoff, 100.0, 0..12 / 12.0, 100000, 1, p, opt)
[res["estimate"], res["stdError"]]
// output: [[1.80604717983654, 14.783698314987175], [0.011698546708195, 0.028289631948896]]

例 8. 双资产篮子期权与交换期权

篮子期权按等权持有两只股票,交换期权到期以资产 2 换资产 1,两个标的价格相关。本例指定 numFactors=2、向量型 initialState 与 "corr":相关矩阵只作用于随机因子,不直接改变状态变量。结果依次为两个估计值、两个标准误、Margrabe 解析值和差值。

// corr 仅相关化随机因子,不直接修改状态变量
def twoAssetEvolve(t, state, dt, noise, p) {
    s1 = state[, 0] * exp((p["r"] - p["q1"] - 0.5 * p["sig1"] * p["sig1"]) * dt + p["sig1"] * sqrt(dt) * noise[, 0])
    s2 = state[, 1] * exp((p["r"] - p["q2"] - 0.5 * p["sig2"] * p["sig2"]) * dt + p["sig2"] * sqrt(dt) * noise[, 1])
    return matrix(s1, s2)
}
def twoAssetPayoff(terminal, timeGrid, p) {
    s1 = terminal[, 0]
    s2 = terminal[, 1]
    T = timeGrid[size(timeGrid) - 1] - timeGrid[0]
    df = exp(-p["r"] * T)
    basket = df * max(0.5 * s1 + 0.5 * s2 - p["basketK"], 0.0)
    exchange = df * max(s1 - s2, 0.0)
    return matrix(basket, exchange)
}
p = dict(STRING, DOUBLE)
p["S1"] = 100.0
p["S2"] = 95.0
p["r"] = 0.03
p["q1"] = 0.01
p["q2"] = 0.02
p["sig1"] = 0.20
p["sig2"] = 0.25
p["rho"] = 0.40
p["basketK"] = 100.0
R = matrix(1.0 0.0, 0.0 1.0)
R[0, 1] = p["rho"]
R[1, 0] = p["rho"]
opt = dict(STRING, ANY)
opt["seed"] = 42
opt["antithetic"] = true
opt["batchSize"] = 10000
opt["pathMode"] = "terminal"
opt["corr"] = R
res = monteCarlo(twoAssetEvolve, twoAssetPayoff, [p["S1"], p["S2"]], 0..12 / 12.0, 200000, 2, p, opt)
T = 1.0
sigM = sqrt(p["sig1"] * p["sig1"] + p["sig2"] * p["sig2"] - 2.0 * p["rho"] * p["sig1"] * p["sig2"])
d1 = (log(p["S1"] / p["S2"]) + (p["q2"] - p["q1"] + 0.5 * sigM * sigM) * T) / (sigM * sqrt(T))
d2 = d1 - sigM * sqrt(T)
reference = p["S1"] * exp(-p["q1"] * T) * cdfNormal(0.0, 1.0, d1) - p["S2"] * exp(-p["q2"] * T) * cdfNormal(0.0, 1.0, d2)
assert 1, abs(res["estimate"][1] - reference) <= 4.0 * res["stdError"][1]
[res["estimate"], res["stdError"], reference, res["estimate"][1] - reference]
// output: [[6.775993366037865, 12.799129569741291], [0.020845620528879, 0.020807928733168], 12.783129860200672, 0.015999709540617]

例 9. Heston 随机波动率模型

方差本身是随机过程,状态为 (S, v) 两维,并需要两个相关系数为 -0.7 的随机因子:noise[, 0] 驱动价格,noise[, 1] 驱动方差。方差采用全截断 Euler 更新,更新前后都截断到零,避免出现负方差。第二列输出是到期方差的期望,不是期权现金流,因此不参与折现。结果依次为两个估计值和两个标准误。

// state 两列:[股价 S, 瞬时方差 v]
// noise 两列:已相关化的两个标准正态驱动
def hestonEvolve(t, state, dt, noise, p) {
    S = state[, 0]
    v = state[, 1]
    vPos = max(v, 0.0)
    nextS = S * exp((p["r"] - p["q"] - 0.5 * vPos) * dt + sqrt(vPos * dt) * noise[, 0])
    nextV = max(v + p["kappa"] * (p["theta"] - vPos) * dt + p["xi"] * sqrt(vPos * dt) * noise[, 1], 0.0)
    return matrix(nextS, nextV)
}
def hestonPayoff(terminalState, timeGrid, p) {
    T = timeGrid[size(timeGrid) - 1] - timeGrid[0]
    ST = terminalState[, 0]
    vT = terminalState[, 1]
    callPV = exp(-p["r"] * T) * max(ST - p["K"], 0.0)
    // 第二列是 E[vT],不是期权现金流,不能再折现
    return matrix(callPV, vT)
}
p = dict(STRING, DOUBLE)
p["r"] = 0.03
p["q"] = 0.0
p["K"] = 100.0
p["kappa"] = 2.0
p["theta"] = 0.04
p["xi"] = 0.50
p["rho"] = -0.70
R = matrix(1.0 0.0, 0.0 1.0)
R[0, 1] = p["rho"]
R[1, 0] = p["rho"]
opt = dict(STRING, ANY)
opt["seed"] = 42
opt["batchSize"] = 10000
opt["antithetic"] = true
opt["pathMode"] = "terminal"
opt["corr"] = R
res = monteCarlo(hestonEvolve, hestonPayoff, 100.0 0.04, 0..252 / 252.0, 300000, 2, p, opt)
[res["estimate"], res["stdError"]]
// output: [[8.945060830037322, 0.040530651223046], [0.012914704996569, 6.5769063872e-05]]

例 10. 雪球自动赎回结构

到期赔付取决于存续期内是否触及敲出价、是否触及敲入价,不能只看终值。首次敲出后该路径不再产生未来现金流,且兑付须按实际存续时间折现,不能一律按到期日折现。本例按每 63 个模拟日(约每季度)观察敲出、每个模拟日记录敲入,指定 pathMode="full" 逐点推进路径状态。兑付按名义本金 100 计,因此结果量级约为 100,不是期权价格。结果依次为兑付现值、标准误、95% 置信区间下限和上限。

def gbmEvolve(t, state, dt, noise, p) {
    drift = (p["r"] - p["q"] - 0.5 * p["sigma"] * p["sigma"]) * dt
    return state * exp(drift + p["sigma"] * sqrt(dt) * noise)
}
def snowballPayoff(paths, timeGrid, p) {
    n = rows(paths[0])
    alive = take(true, n)
    knockedIn = take(false, n)
    pv = take(0.0, n)
    last = size(paths) - 1
    for (i in 1..last) {
        spot = paths[i][, 0]
        t = timeGrid[i] - timeGrid[0]
        // 每个模拟日观察敲入,敲出后的状态不影响后续给付
        knockedIn = knockedIn || (alive && (spot <= p["kiLevel"]))
        // 本例第 63、126、189、252 个模拟日为敲出观察日
        if (i == 63 || i == 126 || i == 189 || i == 252) {
            newKO = alive && (spot >= p["koLevel"])
            redemption = p["notional"] * (1.0 + p["coupon"] * t)
            pv += iif(newKO, exp(-p["r"] * t) * redemption, 0.0)
            alive = alive && (!newKO)
        }
    }
    ST = paths[last][, 0]
    T = timeGrid[last] - timeGrid[0]
    noKnockInPay = p["notional"] * (1.0 + p["coupon"] * T)
    knockInPay = p["notional"] * (1.0 + min(ST / p["referenceSpot"] - 1.0, 0.0))
    maturityPay = iif(knockedIn, knockInPay, noKnockInPay)
    // 仅未提前敲出的路径领取到期现金流
    pv += iif(alive, exp(-p["r"] * T) * maturityPay, 0.0)
    return pv
}
p = dict(STRING, DOUBLE)
p["r"] = 0.03
p["q"] = 0.0
p["sigma"] = 0.20
p["notional"] = 100.0
p["referenceSpot"] = 100.0
p["koLevel"] = 103.0
p["kiLevel"] = 80.0
p["coupon"] = 0.12
opt = dict(STRING, ANY)
opt["seed"] = 42
opt["antithetic"] = true
opt["batchSize"] = 5000
opt["pathMode"] = "full"
res = monteCarlo(gbmEvolve, snowballPayoff, 100.0, 0..252 / 252.0, 100000, 1, p, opt)
[res["estimate"], res["stdError"], res["confidenceLower"], res["confidenceUpper"]]
// output: [99.09039224470335, 0.036261836916808, 99.01932035033315, 99.16146413907356]

例 11. 数字期权与区间二元期权

赔付是不连续的 0/1 跳变,靠 iif 表达。数字期权对路径数远比欧式期权敏感,同一精度下通常需要更多样本。

def gbmEvolve(t, state, dt, noise, p) {
    return state * exp((p["rate"] - 0.5 * p["volatility"] * p["volatility"]) * dt + p["volatility"] * sqrt(dt) * noise)
}
def digitalPayoff(terminalState, timeGrid, p) {
    maturity = timeGrid[size(timeGrid) - 1] - timeGrid[0]
    discount = exp(-p["rate"] * maturity)
    spot = terminalState[, 0]
    cashDigital = discount * iif(spot > p["strike"], 1.0, 0.0)
    rangeBinary = discount * iif((spot >= p["lower"]) && (spot <= p["upper"]), 1.0, 0.0)
    return matrix(cashDigital, rangeBinary)
}
p = dict(STRING, ANY)
p["rate"] = 0.03
p["volatility"] = 0.2
p["strike"] = 100.0
p["lower"] = 90.0
p["upper"] = 110.0
options = dict(STRING, ANY)
options["seed"] = 42
options["antithetic"] = true
options["batchSize"] = 5000
options["pathMode"] = "terminal"
result = monteCarlo(gbmEvolve, digitalPayoff, 100.0, 0..12 / 12.0, 200000, 1, p, options)
[result["estimate"], result["stdError"]]
// output: [[0.504947072243628, 0.372025147513488], [0.000303013624505, 0.001406220595986]]

例 12. 取回完整路径做分布分析

定价之外还想看期末价格分布(风险报告、压力测试、画图)。指定 returnPaths=true 使返回结果额外带完整路径,可以直接对路径做分位数等自定义统计。注意它仅在默认的 pathMode="full" 且 resultMode="detail" 下可用。

def gbmEvolve(t, state, dt, noise, p) {
    return state * exp((p["rate"] - 0.5 * p["volatility"] * p["volatility"]) * dt + p["volatility"] * sqrt(dt) * noise)
}
def terminalPayoff(paths, timeGrid, p) {
    return paths[size(paths) - 1][, 0]
}
p = dict(STRING, ANY)
p["rate"] = 0.03
p["volatility"] = 0.2
options = dict(STRING, ANY)
options["seed"] = 42
options["batchSize"] = 5000
options["returnPaths"] = true
result = monteCarlo(gbmEvolve, terminalPayoff, 100.0, 0..12 / 12.0, 20000, 1, p, options)
terminalPrices = result["paths"][size(result["paths"]) - 1][, 0]
[size(result["paths"]), avg(terminalPrices), percentile(terminalPrices, 5.0), percentile(terminalPrices, 95.0)]
// output: [13, 102.8214156238338, 72.57171400837616, 139.8270247863367]

例 13. 利率上限单元

利率产品的赔付要同时用到期末利率和贴现因子。把贴现因子作为第二个状态随利率路径累积,回到估值时点直接相乘,避免在 payoff 里重复积分。

def vasicekEvolve(t, state, dt, noise, p) {
    rate = state[, 0]
    nextRate = rate + p["kappa"] * (p["theta"] - rate) * dt + p["sigma"] * sqrt(dt) * noise[, 0]
    nextDiscount = state[, 1] * exp(-0.5 * (rate + nextRate) * dt)
    return matrix(nextRate, nextDiscount)
}
def capletPayoff(terminalState, timeGrid, p) {
    terminalRate = terminalState[, 0]
    terminalDiscount = terminalState[, 1]
    return terminalDiscount * max(terminalRate - p["strike"], 0.0) * p["accrual"]
}
p = dict(STRING, ANY)
p["kappa"] = 0.8
p["theta"] = 0.03
p["sigma"] = 0.01
p["strike"] = 0.025
p["accrual"] = 0.25
options = dict(STRING, ANY)
options["seed"] = 42
options["antithetic"] = true
options["batchSize"] = 5000
options["pathMode"] = "terminal"
result = monteCarlo(vasicekEvolve, capletPayoff, 0.03 1.0, 0..12 / 12.0, 200000, 1, p, options)
[result["estimate"], result["stdError"]]
// output: [0.001462868116922, 1.236656302e-06]

例 14. 对偶变量法

指定 antithetic=true 时, Z 与 -Z 必须成对,numPaths 与 batchSize 都须为偶数,否则会在执行前被拒绝:

def gbmEvolve(t, state, dt, noise, p) {
    return state * exp((p["rate"] - 0.5 * p["volatility"] * p["volatility"]) * dt + p["volatility"] * sqrt(dt) * noise)
}
def callPayoff(terminalState, timeGrid, p) {
    maturity = timeGrid[size(timeGrid) - 1] - timeGrid[0]
    return exp(-p["rate"] * maturity) * max(terminalState[, 0] - p["strike"], 0.0)
}
p = dict(STRING, ANY)
p["rate"] = 0.03
p["volatility"] = 0.2
p["strike"] = 100.0
options = dict(STRING, ANY)
options["seed"] = 42
options["antithetic"] = true
options["batchSize"] = 5000
options["pathMode"] = "terminal"
monteCarlo(gbmEvolve, callPayoff, 100.0, 0..12 / 12.0, 10001, 1, p, options)
// output: antithetic simulation requires even numPaths and batchSize.

正确示例如下:

def gbmEvolve(t, state, dt, noise, p) {
    return state * exp((p["rate"] - 0.5 * p["volatility"] * p["volatility"]) * dt + p["volatility"] * sqrt(dt) * noise)
}
def callPayoff(terminalState, timeGrid, p) {
    maturity = timeGrid[size(timeGrid) - 1] - timeGrid[0]
    return exp(-p["rate"] * maturity) * max(terminalState[, 0] - p["strike"], 0.0)
}
p = dict(STRING, ANY)
p["rate"] = 0.03
p["volatility"] = 0.2
p["strike"] = 100.0
options = dict(STRING, ANY)
options["seed"] = 42
options["antithetic"] = true
options["batchSize"] = 5000
options["pathMode"] = "terminal"
r = monteCarlo(gbmEvolve, callPayoff, 100.0, 0..12 / 12.0, 10000, 1, p, options)
plainOptions = dict(STRING, ANY)
plainOptions["seed"] = 42
plainOptions["antithetic"] = false
plainOptions["batchSize"] = 5000
plainOptions["pathMode"] = "terminal"
plain = monteCarlo(gbmEvolve, callPayoff, 100.0, 0..12 / 12.0, 10000, 1, p, plainOptions)
[r["estimate"], r["stdError"], plain["estimate"], plain["stdError"], plain["stdError"] / r["stdError"]]
// output: [9.350067041131123, 0.105390522690999, 9.279319321832183, 0.138518059304004, 1.314331267813654]

相关函数: mcSimulate, mcEstimate