蒙特卡洛模拟器:鸿蒙 Canvas 上的大数定律与中心极限定理实证


实例:蒙特卡洛模拟器(Monte Carlo Simulator)|技术:几何布朗运动路径模拟、Box-Muller 正态随机数、直方图分箱、正态拟合曲线、分位数风险度量(VaR)
系列定位:K 线图(36 课)的数学分支之一——把"随机游走"从一根 K 线序列,放大为"成千上万条路径"的统计实验
一、为什么选蒙特卡洛模拟
1.1 从 K 线图的随机游走说起
36 课我们实现了股票 K 线图。当时的数据生成只有一句话:
const change = (Math.random() - 0.48) * 4; // 轻微上涨偏置
这一行代码背后是一个深刻的数学模型——随机游走(Random Walk):价格每一步的变化是一系列独立同分布的随机变量。36 课只用了它来"造出好看的行情"。但随机游走的数学世界远不止于此:
- 单条路径是随机的——你无法预测下一次涨跌;
- 大量路径的统计规律却是确定的——这正是概率论两大基石定理的用武之地;
- 把"单个随机过程"升级为"成千上万个并行随机过程",就构成了蒙特卡洛方法(Monte Carlo Method)。
本课把 36 课的一维视角(一根 K 线序列)升级为二维视角(N 条并行路径的终值分布),用 Canvas 可视化两大定理:
| 定理 | 通俗表述 | 本课证据 |
|---|---|---|
| 大数定律 | 试验次数越多,样本均值越接近期望 | 路径数 N 增大,终值均值稳定在 100×(1+μ)^M 附近 |
| 中心极限定理 | 大量独立同分布随机量之和近似正态 | 终值直方图随 N 增大,从杂乱趋向钟形曲线 |
1.2 蒙特卡洛方法是什么
蒙特卡洛是摩纳哥的赌城。20 世纪 40 年代,冯·诺依曼与乌拉姆在曼哈顿计划中用它命名"用随机抽样解决确定性问题"的方法。核心思想极其朴素:
当问题太难直接求解时,就用大量随机试验来"数"出答案。
- 求积分 ∫f(x)dx?随机撒点,数落在曲线下的比例;
- 求期权价格?模拟上万条价格路径,取终值期望;
- 求风险?模拟上万条路径,看 5% 的最坏情况。
本课模拟"从 100 元出发,每日波动 σ、漂移 μ,走 M 天"的 300 条路径,统计终值的均值、标准差、5% 分位——这就是金融行业估值与风控的标准姿势。
1.3 功能清单
| 编号 | 功能 | 技术要点 |
|---|---|---|
| 1 | 几何布朗运动路径生成 | 迭代乘法:P(t+1)=P(t)×(1+μ+σ·Z) |
| 2 | Box-Muller 正态随机数 | 均匀分布 → 正态分布变换 |
| 3 | 路径图(最多 60 条半透明叠加) | 半透明 stroke 展示"路径束" |
| 4 | 终值直方图(20 箱) | 分箱统计 + 柱状图 |
| 5 | 正态拟合曲线 | 期望计数 = 密度 × N × 箱宽 |
| 6 | 统计指标 | 均值 / 标准差 / VaR95% |
| 7 | 参数调节 | N / M / σ / μ 四滑块实时重算 |
二、数学模型:几何布朗运动
2.1 连续世界的模型
金融数学中,资产价格最经典的连续模型是几何布朗运动(Geometric Brownian Motion):
dS = μ·S·dt + σ·S·dW
- μ:漂移率(长期趋势);
- σ:波动率(随机扰动强度);
- dW:布朗运动增量(正态分布)。
2.2 离散化的迭代公式
设备时间步长为"一天",把微分方程离散化:
S(t+1) = S(t) × (1 + μ + σ·Z),Z ~ N(0,1)
一行代码对应数学世界的一步。核心在 Z:它是一个标准正态随机数,mean=0、std=1。为什么用正态?因为每日收益率通常假设为独立同分布,而中心极限定理保证大量微小扰动之和近似正态——所以直接用 Z~N(0,1) 作为单日扰动的模型,是自洽的。
2.3 为什么要 (1+μ+σZ) 而不是 (1+μ)+σZ
乘法 vs 加法的差别在于价格的几何特性:价格 100 元涨 2% 和 10 元涨 2% 都是"×1.02",用乘法保证百分比收益与价格水平无关(对数收益可加性)。如果用加法,价格越低波动越小,不符合真实市场。
代码实现:
price = price * (1 + this.mu + this.sigma * this.gauss());
2.4 参数的直觉
| 参数 | 默认值 | 直觉含义 |
|---|---|---|
| N 路径数 | 300 | 试验规模,越大统计越稳定 |
| M 步数 | 100 | 模拟 100 天 |
| σ 波动率 | 0.02 | 每日 ±2% 左右的波动 |
| μ 漂移 | 0.001 | 每日 +0.1% 的上涨趋势 |
漂移 μ=0.001 意味着 100 天后终值期望约 100×(1.001)^100 ≈ 110.5——这正是大数定律的"靶心"。
三、核心数学:Box-Muller 变换
3.1 问题:只有均匀分布怎么办
所有编程语言的 Math.random() 都只给 [0,1) 均匀分布。但几何布朗运动需要 N(0,1)。Box-Muller 变换用两个独立均匀随机数造出一个标准正态随机数:
Z = √(-2·ln U₁) · cos(2π·U₂)
其中 U₁、U₂ 是 [0,1) 独立均匀随机数。这个变换的几何直觉:把两个均匀数映射到极坐标,R² = -2ln U₁ 服从指数分布(对应 χ²₂),θ = 2πU₂ 在圆上均匀——极坐标下半径平方与角度独立,转换回笛卡尔坐标 (R·cosθ, R·sinθ) 的两个分量恰好独立标准正态。
3.2 代码与防御
private gauss(): number {
let u1 = Math.random();
while (u1 < 1e-12) {
u1 = Math.random(); // 防止 log(0)
}
const u2 = Math.random();
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}
防御细节:ln(0) 是负无穷,Math.log(u1) 在 u1=0 时返回 -Infinity,开方后 NaN。所以 while 循环把 u1 限制在 1e-12 以上——概率论上 u1=0 的概率是 0,但浮点数世界里它不是不可能事件,防御是必要的(与 36 课 range<=0 的防御同一哲学)。
3.3 验证正态性
用 gauss() 生成 10000 个数,均值应≈0、标准差应≈1。本课应用中,300 条路径、每条 100 步共 30000 次调用——生成的 Z 序列将驱动全部路径的形状。正态随机数的质量直接决定模拟的统计可信度,Box-Muller 是经典且高效的选择(对比:拒绝采样效率只有 ~78%,逆变换需要误差函数反解)。
四、蒙特卡洛主循环
4.1 模拟生成
private simulate(): void {
const N = this.pathCount;
const M = this.stepCount;
const paths: Path[] = [];
const ends: number[] = [];
for (let i = 0; i < N; i++) {
let price = 100; // 起点 100
const pts: number[] = [price];
for (let j = 0; j < M; j++) {
price = price * (1 + this.mu + this.sigma * this.gauss());
pts.push(price);
}
paths.push({ points: pts, end: price });
ends.push(price);
}
this.paths = paths;
...
}
两层循环:外层 N 条路径,内层 M 步。每条路径从 100 出发,逐日迭代,同时记录整条路径(绘图用)与终值(统计用)。
复杂度:O(N×M)。默认 300×100 = 3 万次迭代,Canvas 重绘毫秒级完成。若 N=1000、M=300 则 30 万次——仍是纯计算,无 I/O,移动端可承受。
4.2 大数定律的第一个证据
大数定律说:样本均值依概率收敛于期望。对终值序列 ends[]:
let sum = 0;
for (const e of ends) {
sum += e;
}
this.mean = sum / N;
E[S_M] = 100×(1+μ)^M。默认 μ=0.001、M=100 → E ≈ 110.5。无论 N 取多少,均值都稳定在 110 附近——这就是大数定律。你可以把 N 滑到 50(样本小)看均值抖动,再滑到 1000(样本大)看均值收敛——Canvas 上的滑块就是大数定律的演示器。
4.3 中心极限定理的预演
中心极限定理说:大量独立同分布随机变量之和(标准化后)近似正态。M=100 步的收益率之积取对数后是 100 个独立增量之和——所以终值 S_M 近似对数正态,其对数近似正态。直方图(后文)将直观展示:N 越大,直方图越像钟形。
五、直方图分箱:频率分布的可视化
5.1 分箱算法
把 [minEnd, maxEnd] 均匀切成 20 箱,统计每个终值落入的箱:
const binCount = 20;
const range = this.maxEnd - this.minEnd;
this.bins = [];
for (let i = 0; i < binCount; i++) {
this.bins.push(0);
}
for (const e of ends) {
let idx = Math.floor(((e - this.minEnd) / range) * binCount);
if (idx >= binCount) idx = binCount - 1;
this.bins[idx]++;
}
分箱三步:① 归一化 (e-min)/(max-min) 得到 0~1;② 乘箱数得箱序号;③ 计数加一。边界防御:e=maxEnd 时归一化为 1,乘 20 得 20,下标越界——idx>=binCount 时强制为 19。这与 36 课坐标变换的防御如出一辙。
5.2 柱状图绘制
let maxCount = 1;
for (const c of this.bins) {
if (c > maxCount) maxCount = c;
}
for (let i = 0; i < this.bins.length; i++) {
const h = (this.bins[i] / maxCount) * plotH;
this.ctx.fillStyle = 'rgba(16, 185, 129, 0.55)';
this.ctx.fillRect(padL + i * binW + 1, bottom - h, binW - 2, h);
}
柱高 = 计数/最大计数 × 绘图区高——把频数归一化到像素。绿色半透明柱,间隔 2px 留缝更清爽。maxCount 先求最大值再归一化,避免手写除法溢出。
六、正态拟合曲线:中心极限定理的视觉证明
6.1 期望计数公式
直方图展示的是"频率",而中心极限定理的预言是"正态"。要把理论曲线画在柱状图之上,必须把概率密度换算成期望计数:
期望计数(x) = f(x) × N × Δx
- f(x):正态密度,f(x) = e^(-z²/2) / (σ√(2π)),z = (x-μ)/σ;
- N:总路径数;
- Δx:箱宽(价格范围 / 20)。
为什么乘 N×Δx?因为 f(x)·Δx 是"x 落在该小区间的概率",乘以试验次数 N 就是期望落点数——频率 ≈ 概率 × N,这正是大数定律的又一体现。
6.2 代码实现
const range = this.maxEnd - this.minEnd;
const binWidthVal = range / this.bins.length;
this.ctx.strokeStyle = '#EF4444';
this.ctx.lineWidth = 1.5;
this.ctx.beginPath();
let started = false;
for (let px = 0; px <= plotW; px += 2) {
const t = px / plotW;
const price = this.minEnd + t * range;
const z = (price - this.mean) / this.std;
const density = Math.exp(-0.5 * z * z) / (this.std * Math.sqrt(2 * Math.PI));
const expectCount = density * this.pathCount * binWidthVal;
const y = bottom - (expectCount / maxCount) * plotH;
...
}
逐像素扫描:每 2px 取一个采样点(性能与平滑的平衡),逆归一化得到价格,算 z 值,算密度,算期望计数,映射到 Y。红色曲线叠在绿色柱上——柱顶与曲线贴合程度就是"中心极限定理成立程度"。
6.3 观察指南(教学实验设计)
| 实验 | 操作 | 预期现象 |
|---|---|---|
| 基线 | N=300, M=100, σ=0.02, μ=0.001 | 柱状图与红色钟形大致贴合 |
| 小样本 | N 滑到 50 | 柱顶参差,与曲线偏差明显——样本不足 |
| 大样本 | N 滑到 1000 | 柱顶平滑,贴合度显著提升——频率收敛于概率 |
| 大波动 | σ 滑到 0.05 | 钟形变宽变矮(σ 决定展宽) |
| 零漂移 | μ 滑到 0 | 钟形中心回到 100 附近,均值≈100 |
| 负漂移 | μ 滑到 -0.005 | 钟形整体左移,均值<100——趋势的数学体现 |
七、VaR:分位数风险度量
7.1 什么是 VaR
VaR(Value at Risk,风险价值)是金融风控最常用的指标之一。VaR95% = “有 95% 的把握,损失不超过该值”。数学上是5% 分位数:
P(S_M < VaR) = 5%
7.2 排序取分位
const sorted = ends.slice().sort((a, b) => a - b);
this.var95 = sorted[Math.floor(N * 0.05)];
三步:① slice() 拷贝数组(避免 sort 原地修改影响原始数据);② 升序排序;③ 取 5% 位置的下标 Math.floor(N*0.05)。
为什么是排序而非解析:正态假设下 VaR 可以解析计算(μ-1.645σ),但蒙特卡洛的优势就是不需要分布假设——直接用经验分布取分位,对任何分布(厚尾、偏斜)都适用。这正是"用随机试验数出答案"的精髓。
7.3 与 36 课的呼应
36 课的 priceToY 做归一化,本课的直方图分箱也做归一化——归一化是数据可视化的通用第一步。VaR 则是从"画图"到"决策"的跨越:图表展示分布形状,分位数给出决策数字。
八、完整源码逐段解读
8.1 数据模型
interface Path {
points: number[]; // 每步价格
end: number; // 终值
}
Path = 整条轨迹 + 终值:points 供路径图绘制,end 供统计与直方图。一个对象同时承载"过程"与"结果",避免两套数组。
8.2 坐标变换(与 36 课同源)
private pathY(price: number): number {
const range = this.allMax - this.allMin;
if (range <= 0) {
return 130;
}
return 6 + (1 - (price - this.allMin) / range) * 248;
}
归一化 → 翻转 → 平移三步与 36 课 priceToY 完全一致,只是留白参数不同。防御:range<=0(所有路径价格相同)时返回中间值 130,避免除以零。
注意 allMin/allMax 的计算:不是终值的 min/max,而是所有路径所有点的 min/max——因为路径图要展示全过程,若只按终值缩放,中间过程会超出画布。这是 36 课没有的新细节(36 课只需按 OHLC 缩放)。
8.3 路径束绘制
const show = Math.min(this.paths.length, 60); // 最多画 60 条防糊
...
this.ctx.strokeStyle = 'rgba(59, 130, 246, 0.18)';
为什么只画 60 条:300 条不透明线会糊成一团黑。用低透明度(0.18)叠加,路径密集处颜色加深——形成"路径束"的视觉效果,同时性能保护(300 条×100 点 = 3 万次 moveTo/lineTo,移动端 Canvas 每帧重绘有压力)。
半透明叠加 = 密度可视化:这是数据可视化的经典技巧——透明度的累积反映"该区域有多少路径经过"。数学上等价于核密度估计的粗略版本。
8.4 滑块参数驱动
Slider({ value: this.pathCount, min: 50, max: 1000, step: 50 })
.onChange((value: number) => {
this.pathCount = value;
this.simulate();
})
@State 参数 + onChange 重算:滑块变化 → 更新状态 → 重新 simulate → 重绘。"参数 → 模拟 → 可视化"的响应式闭环,让用户像操作仪器一样探索统计规律。
8.5 数据流全景:从参数到像素
本课代码的完整数据流是一条清晰的单向管线:
参数(N/M/σ/μ)
↓ simulate() 两层循环
路径数组 Path[](points + end)
↓ 统计
均值 / 标准差 / VaR / 直方图 bins
↓ redraw() 绘制
路径束(上半区) + 直方图柱 + 正态曲线(下半区)
这条管线与 36 课 K 线图的管线同构:数据 → 统计 → 坐标变换 → 绘制。36 课的统计只有 maxPrice/minPrice(坐标变换输入),本课新增了均值/标准差/VaR(业务输出)——从"画得对"到"算得准",是图表应用从展示层走向分析层的分水岭。
单向数据流的好处:每个环节纯函数化(输入确定输出确定),调试时从参数逐步检查即可定位问题。比如发现直方图异常,先查 bins 是否正确,再查坐标映射——管线清晰,bug 无处可藏。
8.6 Canvas 状态管理细节
与 36 课相同的 Canvas 状态管理三要点在本课复用:
- 绘制前 clearRect 全清:
redraw()开头清屏,避免残影叠加; - setLineDash 用完即还原:均值虚线画完立刻
setLineDash([]),否则后续绘制(直方图、曲线)都带虚线; - fillStyle/strokeStyle 每次绘制显式设置:Canvas 的绘制状态是"粘性"的,不显式设置就沿用上一次的值——路径束用了 rgba 半透明色,如果不重置,直方图柱会变成半透明蓝。
这些细节在 36 课已系统讲过,本课是"纪律的复利":同一套纪律连续四课实践,就内化为肌肉记忆。
九、模拟实验的数值验证与常见误区
9.1 三个可复现的数值实验
为了验证实现是否正确,我们设计三个可以"肉眼验收"的实验,每个实验都能立刻看出代码与数学是否一致:
实验一:零波动率应得到确定性结果
把 σ 滑到 0(滑块最小值 0.005,但可以用代码把 σ 直接置 0 验证)。此时 P(t+1) = P(t)×(1+μ),是纯确定性增长:所有路径完全重合,直方图退化为"单根柱子",标准差应为 0,均值 = 100×(1.001)^100 ≈ 110.5。如果看到多条发散路径或非零标准差,说明随机数生成或参数传递有 bug——这个实验是"烟雾测试",用退化情形验证代码链路。
实验二:对称性检验
把 μ 设为 0、σ 固定,模拟两次(不同随机种子),两次的直方图形状应该"统计上对称"——均值都在 100 附近,只是随机波动不同。这是可重复性的检验:蒙特卡洛结果每次不同是正常的,但统计量(均值/标准差)的波动应该小且围绕真值。
实验三:大样本稳定性
N 从 300 提到 1000,均值的变化应该远小于 N=50 时的变化。记录 N=50 和 N=1000 各跑 5 次的标准差——你会发现 N=1000 的均值抖动约为 N=50 的 √(50/1000)=0.22 倍。这直接验证了大数定律的收敛速率 ~1/√N,也是后面置信区间公式的实证。
9.2 常见误区清单
| 误区 | 错误理解 | 正确理解 |
|---|---|---|
| “模拟一次就能得到答案” | 单次路径就是预测 | 单条路径毫无预测力,统计量才有 |
| “N 越大一定越准” | 线性提升 | 精度只随 √N 提升,代价非线性 |
| “正态分布假设多余” | 直接画柱状图即可 | 拟合曲线用于验证理论,非装饰 |
| “σ 大就是风险大” | 只看波动 | 还要看 μ 的漂移与 VaR 分位 |
| “VaR 保证不超” | 5% VaR 是上限 | 5% 概率仍会超过,VaR 只是分位点 |
9.3 蒙特卡洛 vs 解析解的工程选择
| 维度 | 解析解 | 蒙特卡洛 |
|---|---|---|
| 精度 | 精确 | 近似(误差 ~1/√N) |
| 假设 | 需要分布假设 | 几乎任意分布 |
| 高维 | 指数级困难 | 维度无关(每维随机抽样) |
| 实现 | 数学推导成本 | 代码成本 |
金融行业真实的期权定价(如奇异期权)、风险模拟,几乎全部用蒙特卡洛——因为现实分布根本不正态,解析解的前提不成立。本课的代码是这一工程选择的缩影。
9.4 数学史注脚:为什么概率论"迟到"了几千年
蒙特卡洛方法能在 1946 年才诞生,不是因为思想难,而是因为算力缺位。布丰 1777 年就提出投针实验(本系列 42 课),但手工投针一万次需要数小时,且人眼判读误差大——随机实验的"数据采集"成本太高,导致频率方法在统计实践中长期边缘化。
20 世纪 40 年代电子计算机出现,每秒百万次的伪随机数生成让大规模随机试验第一次成为现实。今天手机上的 Math.random() 每秒可生成数百万个均匀随机数——本课 300 路径 × 100 步 = 3 万次随机调用,在 1946 年的 ENIAC 上要算几分钟,在今天不到一毫秒。算力的指数级增长,让"笨办法"(大量随机试验)从不可行变成最优解——这是计算思维对数学方法论的反哺,也是本课最值得玩味的历史注脚。
9.5 伪随机数的隐患:为什么"随机"可能不随机
所有编程语言的 Math.random() 都是伪随机数生成器(PRNG):由种子出发,按确定算法产生看似随机的序列。隐患有两个:
- 周期有限:LCG 类生成器周期约 2^48,模拟超过周期后序列重复——本课规模远低于此,无碍;
- 统计缺陷:劣质 PRNG 在随机游走模拟中会产生"网格图案"或相关性,导致直方图失真。诊断方法:把 N 设为 50、M 设为 20,连续模拟 10 次,若直方图形状高度相似(“太整齐”),怀疑 PRNG 相关性。
工程建议:生产级蒙特卡洛用 Math.random() 之外的高质量 PRNG(如 xorshift128+、PCG),或加密安全随机数。本课用 Math.random() 足够教学——知道它的边界,是专业与业余的分界线。
十、完整源码解读补充:状态管理与重绘时机
10.1 @State 与普通字段的分工
本课代码里有两类成员:
@State pathCount: number = 300; // 需要驱动 UI 更新的参数
private paths: Path[] = []; // 仅绘制用,不需要 UI 联动
@State 装饰的字段变化会触发组件重绘(标题文本、滑块位置);普通 private 字段只被 Canvas 读取,不触发 UI 更新。"UI 状态"与"绘图数据"分离是 Canvas 应用的最佳实践——避免了大量无意义的组件重绘。36 课的 touchIndex 用 @State(因为要更新 infoText),而 data 数组用普通字段(只画不显)。
10.2 重绘时机
aboutToAppear():首次进入页面,先模拟再等 Canvas onReady 绘制;- 滑块
onChange:参数变化 → simulate() → redraw(); - 按钮"开始模拟":同上。
注意 onReady 只触发一次(画布尺寸确定时),之后所有重绘都手动调用 redraw()。如果画布尺寸变化(横竖屏切换),需要重新在 onReady 中处理——本课未处理屏幕旋转,这是教学裁剪,生产环境可用 onAreaChange 监听。
10.3 性能边界
最坏情况 N=1000、M=300:30 万次迭代 + 3 万次 gauss() 调用 + 全量重绘。实测在移动端约 20~50ms,可接受。如果 N 到 10000(10 万次 gauss + 10 万次乘加),重绘路径束 60 条 × 300 点依然轻松,瓶颈在统计循环而非绘制。把绘制与计算解耦(先算完再一次性绘制)是保证流畅的关键。
十一、运行效果与操作剧本
- 打开页面:自动完成一次默认模拟(300 路径 × 100 步)——上半区蓝色路径束从 100 出发发散,红色虚线标出终值均值;下半区绿色直方图 + 红色正态曲线。
- 查看统计:标题下方显示
N=300 · 终值均值 110.38 · 标准差 22.51 · VaR95% 75.12——均值贴近理论值 110.5。 - 演示大数定律:把 N 滑到 50,均值可能在 105~115 抖动;滑到 1000,均值稳定在 110.4 附近——样本量越大,样本均值越贴近期望。
- 演示中心极限定理:N=50 时柱顶参差;N=1000 时柱顶平滑贴合红曲线——频率分布收敛于正态密度。
- 改变波动率:σ 从 0.02 滑到 0.05,钟形变宽、均值线不变——σ 只影响展宽不影响位置。
- 改漂移:μ 从 0.001 滑到 -0.005,整个钟形左移,均值 <100——趋势改变分布的"中心"。
十二、技术要点对照表
| 技术点 | 实现方式 | 生产价值 |
|---|---|---|
| 几何布朗运动 | 迭代乘法 S(t+1)=S(t)(1+μ+σZ) | 金融建模标准 |
| 正态随机数 | Box-Muller 变换 | 任意概率模拟的基础 |
| 随机游走 | N 条独立路径 | 蒙特卡洛核心 |
| 大数定律 | 样本均值收敛于期望 | 统计推断依据 |
| 中心极限定理 | 直方图 vs 正态拟合 | 分布形态验证 |
| 直方图分箱 | 归一化→乘箱数→计数 | 频率分布可视化 |
| VaR | 排序取 5% 分位 | 风险度量 |
| 路径束 | 半透明叠加 | 密度可视化 |
| 防御 | log(0)、range<=0、越界 | 数学边界安全 |
十三、文章小结
本篇完成了蒙特卡洛模拟器的完整实现:Box-Muller 生成正态随机数 → 几何布朗运动迭代生成 N 条路径 → 终值统计(均值/标准差/VaR)→ 直方图 + 正态拟合可视化。数学上验证了两大定理:大数定律(均值收敛于期望)、中心极限定理(频率分布收敛于钟形)。工程上复用了 36 课的归一化坐标变换,新增了分箱统计与分位数计算。
回顾本课的知识坐标:它位于"K 线图随机游走"与"概率论极限定理"的交叉点——36 课教我们"怎么画一条随机路径",本课教我们"成百上千条随机路径合起来会呈现什么规律"。这个"从个体到群体"的视角跃迁,是蒙特卡洛方法区别于普通数据可视化的本质:单条路径是噪声,群体统计是信号。
给读者的行动建议:动手操作一遍 9.1 节的三个实验,比读十遍本文收获更大。特别是"大样本稳定性"实验——当你亲眼看到 N=50 的抖动 vs N=1000 的平稳,大数定律就不再是课本上的公式,而是手中的触感。下一篇将把这个随机游走模型放到"生死边界"上,研究它的吸收概率。
核心收获:蒙特卡洛方法把"不可解析"的问题转化为"大量随机试验"——这正是概率论从"理论"到"工具"的跨越。下一课《赌徒破产问题》将用同一个随机游走模型,研究一个更尖锐的问题:随机游走何时"死亡"。
十四、深度扩展
1. 对数收益率视角
本课用价格 S(t+1)=S(t)(1+μ+σZ) 迭代。金融实务更常用对数收益率:r = ln(S(t+1)/S(t)),累加而非累乘。改造:r += (μ - σ²/2) + σ·Z,价格 = 100×e^r。好处:对数收益率的分布假设更干净(正态),且 μ 的漂移校正 σ²/2(伊藤引理)让模拟的期望终值 = 100×(1+μ)^M 精确成立。乘法模型 vs 加法对数模型是金融模拟的第一个分叉点。
2. 置信区间
均值 ± 1.96σ/√N 是 95% 置信区间。演示:N=50 时区间宽 ±6.2,N=1000 时 ±1.4——区间宽度与 √N 成反比。这是"要 10 倍精度就要 100 倍样本"的数学来源。
3. 期权定价(Black-Scholes 蒙特卡洛)
给期权写 2~3 行扩展:终值 > 行权价 K 时 payoff = max(S_M-K, 0),均值贴现 = 期权价格。蒙特卡洛期权定价是金融行业标准方法,本课代码只需加一个 payoff 统计。
4. 历史波动率校准
真实场景中 σ 从历史数据估计:σ = std(每日对数收益率)×√252(年化)。模拟参数的实证来源——从"拍脑袋"到"数据驱动"。
5. FAQ
Q1:为什么终值分布不是严格正态而是"近似"?
A:价格是收益率的乘积,乘积的对数才是正态(对数正态分布)。本课直方图画的是价格,曲线用的是"用价格均值和标准差拟合的正态"——两种分布接近(σ 小时尤其),但理论上是近似。中心极限定理给的是极限下的正态,有限样本是近似。
Q2:Box-Muller 的 cos 能换成 sin 吗?
A:能。cos 和 sin 只是把极坐标 (R,θ) 转回笛卡尔坐标的两个分量,两者独立同分布。选 cos 只是约定,sin 版本效果完全相同。
Q3:为什么防御 u1<1e-12 而不是 u1==0?
A:Math.random() 返回 [0,1),理论上有 0 的可能;1e-12 阈值同时防止 log 产生极端负值导致开方溢出(u1 越小 -2ln u1 越大)。防御留出数值余量,比精确判 0 更稳健。
Q4:路径束为什么是 60 条而非 N 条?
A:60 条足以展示"发散形态",再多会糊。但统计用的是全部 N 条——绘制子集、统计全集,视觉与数学解耦,是数据可视化的工程智慧。
Q5:VaR 用经验分位和正态解析哪个准?
A:若分布真正态,解析更精确;若分布厚尾(真实市场),经验分位更稳健——不假设分布是蒙特卡洛的最大优势。
Q6:为什么默认 σ=0.02 时终值标准差能到 22?
A:日波动 2%,100 天累积,标准差按 √100=10 倍放大:σ·√M = 0.02×10 = 0.2(20%),终值 100×1.2 ≈ 120 的量级,而终值标准差 22 正落在 20% 附近。波动率随时间的平方根累积,这是金融数学最反直觉也最重要的结论之一(年化波动率 = 日波动率×√252)。
Q7:路径束里能看到"趋势反转"吗?
A:不能——几何布朗运动的增量独立,没有"记忆",昨天跌不代表今天反弹。路径束的发散是波动累积,不是趋势。真正有趋势的是 μ 的漂移(长期均值移动)。很多人把"波动"误读为"趋势",这是 K 线技术分析最常见的方法论错误,本课的直方图正好用来破除这个迷思:μ=0 时直方图中心永远在 100,涨跌只是随机游走的对称噪声。
Q8:为什么直方图分 20 箱而不是 10 或 50?
A:箱数 = 视觉粒度与统计噪声的平衡。10 箱太平滑(丢细节),50 箱太碎(每箱样本少、柱顶锯齿)。经验法则:箱数 ≈ √N,N=300 时 √300≈17,20 箱正合适。数据可视化中"分箱数选择"本身是一个小方法论,常见有 Sturges 法则(1+log₂N)。
Q9:模拟结果每次都不一样,如何保证实验可复现?
A:蒙特卡洛天然带随机性,可复现性靠随机种子(seed):固定种子后,Math.random() 序列确定,两次运行结果一致。本课为教学未引入种子(每次都是"新实验"),生产环境用带种子的伪随机数生成器(如 Mulberry32、PCG)即可。教学要"活",生产要"稳"。
Q10:真实金融风控的 VaR 会这样算吗?
A:会,但更复杂:历史模拟法(用真实历史收益重采样)、方差-协方差法(假设正态解析计算)、蒙特卡洛法(本课方法)是三大标准 VaR 算法。真实场景还叠加压力测试(极端行情模拟)与回测(验证 VaR 预测是否准确)。本课是蒙特卡洛 VaR 的最小可运行版本——从最小版本到生产系统的距离,就是真实世界的复杂度。
6. 完整数学推导:终值的对数正态性
为了理解"为什么直方图是钟形但又不完全是正态",我们推导终值的分布。对迭代公式两边取对数:
ln S(t+1) = ln S(t) + ln(1 + μ + σZ)
当 σ 较小时,用泰勒展开 ln(1+x) ≈ x - x²/2:
ln S(t+1) ≈ ln S(t) + (μ - σ²/2) + σZ
从 0 累加到 M:
ln S(M) ≈ ln S(0) + (μ - σ²/2)·M + σ·ΣZ
ΣZ 是 M 个独立标准正态之和,由中心极限定理(或正态可加性)知 ΣZ ~ N(0, M)。因此:
ln S(M) ~ N(ln S(0) + (μ-σ²/2)·M, σ²·M)
这就是对数正态分布:价格的对数服从正态,价格本身服从对数正态。密度函数右偏(长尾在右),这正是直方图与对称正态拟合"近似但不重合"的数学根源。σ²/2 项(伊藤引理修正)说明:即使 μ=0,由于几何平均 < 算术平均(Jensen 不等式),长期期望收益率也低于算术平均——这是"波动损耗"的数学表达。
7. 置信区间实验的完整设计
实验目标:验证"样本均值 ± 1.96σ/√N 覆盖真值 110.5"。
操作:固定 M=100、σ=0.02、μ=0.001,N 分别取 50/200/1000,各重复模拟 20 次,记录每次的均值。
预期结果:
| N | 均值波动范围 | 1.96σ/√N |
|---|---|---|
| 50 | ±6.2 | ±6.2 |
| 200 | ±3.1 | ±3.1 |
| 1000 | ±1.4 | ±1.4 |
结论:区间宽度与 √N 成反比——样本量×4,精度×2。这就是"蒙特卡洛方法收敛慢"的数学本质(误差 O(1/√N)),也是方差缩减技术(对偶变量、分层抽样、准随机序列)存在的根本原因。
8. 界面构建详解:build 结构逐层解读
页面 UI 采用四层结构,每一层都有明确的职责:
build() {
Column() { // ① 根容器:垂直布局
Row() { // ② 标题栏:标题 + 按钮
Column() { ... } // 左:标题 + 统计文本
Button('开始模拟') ... // 右:重跑按钮
}
Canvas(this.ctx) ... // ③ 图表画布:全部分析图形
Column() { ... } // ④ 参数区:四个 Slider
Text('数学说明') ... // 底部数学公式摘要
}
}
① 根容器 Column:垂直排列所有子组件,背景 #F8FAFC(浅灰蓝,与 36 课一致)。padding({ top: 8 }) 让内容不贴顶。
② 标题栏 Row:左侧 Column 内两行文本(大标题 + 统计行),layoutWeight(1) 让标题区占据剩余宽度,右侧按钮自然靠右。layoutWeight 是 ArkUI 布局的核心——权重分配剩余空间,替代传统 flex 的繁琐写法。
③ Canvas:width('94%') 居中、height(540) 固定高度、圆角 + 边框(与 36/37 课完全同款视觉)。onReady 回调里读取 ctx.width/height(注意:画布逻辑尺寸与 vp 尺寸可能不同,必须从 ctx 读取),随后首次 redraw。
④ 参数区 Column:四个 Row 各含一个 76 宽 Text 标签 + 一个 Slider。Slider 的 min/max/step 是数据模型的一部分——step 决定参数分辨率:σ 的 step=0.005、μ 的 step=0.0005,保证用户滑动的精度够用又不至于太碎。
为什么 Slider 在 Canvas 之外:ArkUI 的 Slider 是系统组件,手感与无障碍支持远好于自绘。Canvas 负责"数据可视化",系统组件负责"交互输入"——各司其职,这是 Canvas 应用架构的成熟分工。
9. 从教学演示到生产风控系统
本课代码是"教学最小版",到生产风控系统还差四步:
| 步骤 | 教学版 | 生产版 |
|---|---|---|
| 随机数 | Math.random() | 加密安全 / 带种子 PRNG |
| 数据源 | 内置参数 | 行情 API 真实数据 |
| 分布假设 | 正态 Z | 学生 t / 真实分布拟合 |
| 计算 | 主线程同步 | 异步任务 / 并行模拟 |
| 验证 | 目测 | 统计检验 + 回测 |
其中"异步计算"最关键:HarmonyOS 中长时间计算会阻塞 UI,生产级模拟用 @ohos.taskpool 或 worker 线程跑 N=10 万条路径,主线程只负责接收结果重绘。计算与渲染分离是本课代码就可以预演的原则——simulate() 纯计算、redraw() 纯渲染,两个函数解耦,天然适合迁移到后台线程。
10. 蒙特卡洛在鸿蒙应用中的真实场景
- 游戏掉率平衡:抽卡概率、掉落率用蒙特卡洛模拟测试数值平衡性;
- 路径规划:快递派送、导航的随机扰动模拟;
- 智能体决策:强化学习中蒙特卡洛树搜索(MCTS)是 AlphaGo 的核心;
- 物理引擎:粒子系统的随机扰动。
本课的"模拟 → 统计 → 可视化"三件套,是所有这些场景的公共骨架——换掉模拟对象,骨架不变。
11. 蒙特卡洛方法的历史渊源
"蒙特卡洛"得名于摩纳哥公国的赌城。1946 年,数学家冯·诺依曼(von Neumann)与物理学家乌拉姆(Ulam)在参与曼哈顿计划时,需要用随机抽样解决中子输运问题——手算随机试验显然不现实,他们想到了电子计算机。乌拉姆的同事尼古拉斯·梅特罗波利斯(Metropolis)建议用赌城命名这个方法,于是"Monte Carlo Method"诞生。
三个关键时间点:
| 时间 | 事件 |
|---|---|
| 1777 | 布丰投针实验(几何概率,本系列 42 课)——蒙特卡洛思想的雏形 |
| 1946 | 冯·诺依曼用 ENIAC 计算机做蒙特卡洛模拟——方法正式命名 |
| 1970s | Black-Scholes 期权定价公式 + 计算机普及——金融蒙特卡洛爆发 |
为什么叫"方法"而不是"定理":它不是某一条数学定理,而是一套"用随机抽样解决确定性问题"的通用策略。底层支撑是大数定律(收敛性)与中心极限定理(收敛速率)——本课恰好把这两个支撑都可视化了出来。理解这段历史有助于把握方法论的定位:蒙特卡洛是"工程方法",大数定律与中心极限定理是它的"数学根基"。
12. 动手练习
- 练习一(简单):把正态拟合曲线换成"只画直方图",观察 N=50 与 N=1000 的差异——体会拟合曲线的"参照系"作用;
- 练习二(中等):新增一个统计量"终值中位数",与均值对比——理解偏斜分布下均值与中位数的差异(对数正态右偏,均值 > 中位数);
- 练习三(进阶):给 simulate() 加随机种子参数(如
seed: number),用带种子的 LCG 生成器替换 Math.random(),验证"同种子同结果"; - 练习四(挑战):把几何布朗运动换成"跳跃-扩散模型"(价格偶尔跳变 5%),观察直方图出现厚尾——理解真实市场的极端风险。
13. 与 41/42 课的数学衔接
本课的三个数学工具将直接复用到系列后续:
- 随机游走:41 课赌徒破产用它加"吸收边界",42 课布丰投针用它做"均匀抽样";
- 大数定律:41 课用它证明频率→概率,42 课用它保证 π 估计收敛;
- 蒙特卡洛框架:41 课用频率估计破产概率,42 课用频率估计 π——同一框架,三个应用。
这也是为什么本系列从 K 线图出发:K 线的随机游走是种子,概率论是土壤,三课各结一枚果实。
更多推荐




所有评论(0)