在这里插入图片描述
在这里插入图片描述

实例:蒙特卡洛模拟器(Monte Carlo Simulator)|技术:几何布朗运动路径模拟、Box-Muller 正态随机数、直方图分箱、正态拟合曲线、分位数风险度量(VaR)
系列定位:K 线图(36 课)的数学分支之一——把"随机游走"从一根 K 线序列,放大为"成千上万条路径"的统计实验

一、为什么选蒙特卡洛模拟

1.1 从 K 线图的随机游走说起

36 课我们实现了股票 K 线图。当时的数据生成只有一句话:

const change = (Math.random() - 0.48) * 4;   // 轻微上涨偏置

这一行代码背后是一个深刻的数学模型——随机游走(Random Walk):价格每一步的变化是一系列独立同分布的随机变量。36 课只用了它来"造出好看的行情"。但随机游走的数学世界远不止于此:

  1. 单条路径是随机的——你无法预测下一次涨跌;
  2. 大量路径的统计规律却是确定的——这正是概率论两大基石定理的用武之地;
  3. 把"单个随机过程"升级为"成千上万个并行随机过程",就构成了蒙特卡洛方法(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 状态管理三要点在本课复用:

  1. 绘制前 clearRect 全清redraw() 开头清屏,避免残影叠加;
  2. setLineDash 用完即还原:均值虚线画完立刻 setLineDash([]),否则后续绘制(直方图、曲线)都带虚线;
  3. 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):由种子出发,按确定算法产生看似随机的序列。隐患有两个:

  1. 周期有限:LCG 类生成器周期约 2^48,模拟超过周期后序列重复——本课规模远低于此,无碍;
  2. 统计缺陷:劣质 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 点依然轻松,瓶颈在统计循环而非绘制。把绘制与计算解耦(先算完再一次性绘制)是保证流畅的关键。

十一、运行效果与操作剧本

  1. 打开页面:自动完成一次默认模拟(300 路径 × 100 步)——上半区蓝色路径束从 100 出发发散,红色虚线标出终值均值;下半区绿色直方图 + 红色正态曲线。
  2. 查看统计:标题下方显示 N=300 · 终值均值 110.38 · 标准差 22.51 · VaR95% 75.12——均值贴近理论值 110.5。
  3. 演示大数定律:把 N 滑到 50,均值可能在 105~115 抖动;滑到 1000,均值稳定在 110.4 附近——样本量越大,样本均值越贴近期望
  4. 演示中心极限定理:N=50 时柱顶参差;N=1000 时柱顶平滑贴合红曲线——频率分布收敛于正态密度
  5. 改变波动率:σ 从 0.02 滑到 0.05,钟形变宽、均值线不变——σ 只影响展宽不影响位置。
  6. 改漂移:μ 从 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 的繁琐写法。

③ Canvaswidth('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. 动手练习

  1. 练习一(简单):把正态拟合曲线换成"只画直方图",观察 N=50 与 N=1000 的差异——体会拟合曲线的"参照系"作用;
  2. 练习二(中等):新增一个统计量"终值中位数",与均值对比——理解偏斜分布下均值与中位数的差异(对数正态右偏,均值 > 中位数);
  3. 练习三(进阶):给 simulate() 加随机种子参数(如 seed: number),用带种子的 LCG 生成器替换 Math.random(),验证"同种子同结果";
  4. 练习四(挑战):把几何布朗运动换成"跳跃-扩散模型"(价格偶尔跳变 5%),观察直方图出现厚尾——理解真实市场的极端风险。

13. 与 41/42 课的数学衔接

本课的三个数学工具将直接复用到系列后续:

  • 随机游走:41 课赌徒破产用它加"吸收边界",42 课布丰投针用它做"均匀抽样";
  • 大数定律:41 课用它证明频率→概率,42 课用它保证 π 估计收敛;
  • 蒙特卡洛框架:41 课用频率估计破产概率,42 课用频率估计 π——同一框架,三个应用

这也是为什么本系列从 K 线图出发:K 线的随机游走是种子,概率论是土壤,三课各结一枚果实。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐