☰
MATLAB实现Nelson-Siegel模型拟合债券收益率曲线
2026/10/14 16:50:34 网站建设 项目流程

简介:这份资源是围绕Nelson-Siegel-Svensson模型展开的国债收益率曲线研究代码合集,源自作者本科论文的完整实现,面向金融工程、宏观经济学方向的学生与研究者,适合需要复现收益率曲线建模、开展主成分分析与宏观变量关联研究的读者。压缩包共41个文件,约36.33MB,包含18个pdf说明文档、6个ipynb交互式笔记本、3个rmd与3个eps图表脚本,以及mp4动画、gif动图、py辅助脚本等,覆盖模型估计、三维曲面绘制、PCA分解与t-SNE可视化等环节。资源中既有中美收益率曲线结构对比的实证流程,也包含水平、斜率、曲率因子与通货膨胀、货币政策关系的分析思路,可帮助读者理解短期利率波动特征、利差差异及利率发现功能。目前已有942人学习下载,适合作为金融时间序列建模与论文复现的参考素材。

1. 纳尔逊-西格尔模型在 MATLAB 里到底能干什么

如果你手头有一串债券收益率数据,想拟合出一条平滑的期限结构曲线,还要让参数有明确的经济含义,纳尔逊-西格尔(Nelson-Siegel)模型大概率是你绕不开的起点。它用三个因子——水平、斜率、曲率——把整条收益率曲线压缩成四个参数,其中还有一个决定曲率峰值的衰减系数。听起来简单,但真正在 MATLAB 里跑起来,你会发现参数初值怎么选、目标函数怎么写、边界怎么设,每一步都能让结果从“完美拟合”变成“完全离谱”。

这个标题对应的是一套 MATLAB 代码实现,核心任务就是:给定一组到期日和对应的收益率,用非线性最小二乘或其他优化方法,估计出 Nelson-Siegel 模型的四个参数,并画出拟合曲线。它适合做固定收益研究、风险建模、债券定价辅助的从业者,也适合想从零手写一遍期限结构拟合的学生。我见过太多人直接调fit或者lsqcurvefit就完事,结果参数没有经济意义,曲线尾部翘到天上。这篇笔记就把我踩过的坑和能复现的步骤拆开讲。

2. 先把 Nelson-Siegel 的公式和参数含义钉死

2.1 三个因子与一个衰减系数:公式到底怎么读

Nelson-Siegel 模型的标准形式是:

$$ y(\tau) = \beta_0 + \beta_1 \left( \frac{1 - e^{-\lambda \tau}}{\lambda \tau} \right) + \beta_2 \left( \frac{1 - e^{-\lambda \tau}}{\lambda \tau} - e^{-\lambda \tau} \right) $$

其中 $\tau$ 是到期时间(年),$y(\tau)$ 是即期收益率,$\beta_0$、$\beta_1$、$\beta_2$ 和 $\lambda$ 是待估参数。$\beta_0$ 是水平因子,代表长期收益率;$\beta_1$ 是斜率因子,负值通常意味着收益率曲线向上倾斜;$\beta_2$ 是曲率因子,控制中期凸起;$\lambda$ 决定曲率因子的衰减速度,$\lambda$ 越大,曲率峰值越靠近短端。

很多人第一次看这个公式会懵:为什么 $\lambda$ 在分母上?其实它是通过极限来保证数值稳定的。当 $\lambda \tau \to 0$ 时,$(1 - e^{-\lambda \tau}) / (\lambda \tau) \to 1$,所以短端收益率趋近 $\beta_0 + \beta_1$。当 $\tau \to \infty$ 时,两项载荷都趋于 0,收益率趋近 $\beta_0$。这就是为什么 $\beta_0$ 被解释为长期水平。

在 MATLAB 里实现时,我一般会把公式写成匿名函数或者单独的函数文件,避免在优化循环里重复写长表达式。下面是一个最简实现:

function y = nelson_siegel(tau, beta0, beta1, beta2, lambda) % tau: 到期时间,列向量,单位年 % beta0, beta1, beta2, lambda: 标量参数 % 返回:与 tau 同维度的收益率拟合值 x = lambda * tau; % 处理 x 接近 0 的情况,避免 0/0 factor1 = ones(size(x)); idx = x > 1e-8; factor1(idx) = (1 - exp(-x(idx))) ./ x(idx); factor2 = factor1 - exp(-x); y = beta0 + beta1 * factor1 + beta2 * factor2; end

这段代码的关键在于idx那行。如果直接写(1 - exp(-x)) ./ x,当tau里有 0 或者非常小的值时,会得到NaN。我一般把阈值设在1e-8,小于这个值就直接用极限 1 代替。这个细节在优化过程中特别重要,因为优化器可能会试探性地把lambda压到很小,导致x接近 0,然后整个目标函数返回NaN,优化直接中断。

2.2 为什么选 Nelson-Siegel 而不是样条插值

样条插值能把曲线拟合得很光滑,但它的参数没有经济含义,而且远端外推经常失控。Nelson-Siegel 的优势在于参数可解释:你能直接告诉别人“水平因子上升了 10 个基点”,而不是“第三个节点的二阶差分变了”。另外,它只有四个参数,对数据量的要求低,即使只有 10 个到期点也能估。常见做法是把它作为基准模型,再考虑 Svensson 扩展(多一个曲率项)或者动态 Nelson-Siegel。

在 MATLAB 里,我一般会先用fit或者lsqcurvefit做非线性最小二乘。lsqcurvefit属于 Optimization Toolbox,如果你没有这个工具箱,可以用fminsearch或者自己写高斯-牛顿迭代。下面是一个用lsqcurvefit的示例:

% 假设 tau 和 y_obs 已经准备好,都是列向量 % 初值:beta0 用远端收益率,beta1 用短端减长端,beta2 用 0,lambda 用 0.5 beta0_0 = y_obs(end); beta1_0 = y_obs(1) - y_obs(end); beta2_0 = 0; lambda_0 = 0.5; x0 = [beta0_0, beta1_0, beta2_0, lambda_0]; % 目标函数:残差向量 objfun = @(x) nelson_siegel(tau, x(1), x(2), x(3), x(4)) - y_obs; % 设置边界:lambda 必须为正,其他参数不设硬边界但可以给宽松范围 lb = [-0.5, -1, -1, 0.01]; ub = [1.5, 1, 1, 5]; % 优化选项:显示迭代过程,设置容差 options = optimoptions('lsqcurvefit', 'Display', 'iter', ... 'FunctionTolerance', 1e-10, 'StepTolerance', 1e-10, ... 'MaxFunctionEvaluations', 5000); [x_est, resnorm] = lsqcurvefit(objfun, x0, [], [], lb, ub, options);

这里objfun返回的是残差向量,而不是残差平方和,因为lsqcurvefit内部会处理平方。lb和ub的设置很关键:lambda下界设 0.01 是为了防止它跑到 0 导致数值问题;上界 5 是经验值,一般收益率曲线的曲率峰值不会超过这个范围。如果你不设边界,优化器可能会给出负的lambda,虽然数学上也能算,但经济含义就没了。

2.3 参数初值怎么给才不容易翻车

初值选不好,非线性优化很容易掉进局部极小值。我一般用下面的规则:

  • beta0_0:取最长期限的收益率,或者所有收益率的均值。
  • beta1_0:取最短期限收益率减去最长期限收益率。
  • beta2_0:先设 0,如果拟合残差在中期有明显凸起,再手动调成 0.1 或 -0.1。
  • lambda_0:根据曲率峰值位置估计。如果中期(2-5 年)凸起明显,lambda大约在 0.3 到 0.8 之间;如果短端凸起,lambda可以设大一点,比如 1.5。

我见过有人直接把lambda_0设成 1,结果拟合出来的曲线在短端完全贴不住。后来他把lambda_0改成 0.4,同样的数据,残差平方和降了一个数量级。这就是初值的玄学——没有理论最优,只能靠经验加网格搜索。一个实用的技巧是:先固定lambda,对beta0、beta1、beta2做线性回归,因为给定lambda后模型对三个 beta 是线性的。这样你可以快速扫一遍lambda的候选值,选残差最小的那个作为初值。

lambda_grid = 0.05:0.05:2; best_rss = inf; best_lambda = 0.5; for lam = lambda_grid X = [ones(size(tau)), ... (1 - exp(-lam*tau))./(lam*tau), ... (1 - exp(-lam*tau))./(lam*tau) - exp(-lam*tau)]; beta_hat = X \ y_obs; rss = sum((y_obs - X*beta_hat).^2); if rss < best_rss best_rss = rss; best_lambda = lam; best_beta = beta_hat; end end

这段代码就是所谓的“网格搜索 + 线性回归”组合。它把四维非线性优化降维成一维搜索,速度快,而且不容易陷入局部极小。我一般会先用这个方法找到lambda的大致范围,再把它作为lsqcurvefit的初值,最后做一次全参数优化。

3. 在 MATLAB 里跑通拟合的完整流程

3.1 数据准备:到期日和收益率的对齐与清洗

真实数据往往不是整齐的。你可能从某处拿到一组债券数据,到期日有 0.5 年、1 年、2 年、3 年、5 年、7 年、10 年,但收益率有缺失,或者某个期限的收益率明显异常。我一般会做三件事:

  1. 把到期日转成年为单位,比如 180 天就是 0.5 年。
  2. 检查是否有负收益率或者超过 20% 的异常值,有的话先标记,不要直接删,因为可能是数据源问题。
  3. 按到期日排序,确保tau是单调递增的。
% 假设原始数据在 table 里,列名为 Maturity 和 Yield % Maturity 单位是天 tau = data.Maturity / 365; y_obs = data.Yield / 100; % 如果原始数据是百分比,转成小数 % 排序 [tau, sort_idx] = sort(tau); y_obs = y_obs(sort_idx); % 检查异常 if any(y_obs < 0) || any(y_obs > 0.2) warning('存在异常收益率,请检查数据源'); end

注意y_obs的单位。很多数据源给的是百分比,比如 2.5 表示 2.5%,但模型里用的是小数 0.025。如果你忘了除 100,拟合出来的beta0会是 2.5 左右,看起来也能拟合,但参数解释就全错了。这个坑我踩过,当时对着结果愣了半小时,后来才发现是单位问题。

3.2 用 lsqcurvefit 做全参数优化:代码与参数说明

数据准备好之后,就可以调用lsqcurvefit了。下面是一个完整的脚本,包括初值网格搜索和最终优化:

% 假设 tau 和 y_obs 已经准备好 % 第一步:网格搜索 lambda,线性回归 beta lambda_grid = 0.05:0.05:2; best_rss = inf; best_beta = [0, 0, 0]; best_lambda = 0.5; for lam = lambda_grid X = [ones(size(tau)), ... (1 - exp(-lam*tau))./(lam*tau), ... (1 - exp(-lam*tau))./(lam*tau) - exp(-lam*tau)]; beta_hat = X \ y_obs; rss = sum((y_obs - X*beta_hat).^2); if rss < best_rss best_rss = rss; best_lambda = lam; best_beta = beta_hat; end end % 第二步:全参数优化 x0 = [best_beta(1), best_beta(2), best_beta(3), best_lambda]; objfun = @(x) nelson_siegel(tau, x(1), x(2), x(3), x(4)) - y_obs; lb = [-0.5, -1, -1, 0.01]; ub = [1.5, 1, 1, 5]; options = optimoptions('lsqcurvefit', 'Display', 'final', ... 'FunctionTolerance', 1e-12, 'StepTolerance', 1e-12, ... 'OptimalityTolerance', 1e-12, 'MaxFunctionEvaluations', 10000); [x_est, resnorm, residual] = lsqcurvefit(objfun, x0, [], [], lb, ub, options); % 输出结果 fprintf('beta0 = %.6f\n', x_est(1)); fprintf('beta1 = %.6f\n', x_est(2)); fprintf('beta2 = %.6f\n', x_est(3)); fprintf('lambda = %.6f\n', x_est(4)); fprintf('残差平方和 = %.6e\n', resnorm); % 画图 tau_fine = linspace(min(tau), max(tau), 200)'; y_fit = nelson_siegel(tau_fine, x_est(1), x_est(2), x_est(3), x_est(4)); figure; plot(tau, y_obs, 'ko', 'MarkerFaceColor', 'k'); hold on; plot(tau_fine, y_fit, 'b-', 'LineWidth', 1.5); xlabel('到期时间(年)'); ylabel('收益率'); legend('观测值', 'Nelson-Siegel 拟合'); grid on;

这段代码里,options的容差设得很小,是为了让优化器尽量收敛到高精度。但实际中如果你发现迭代次数太多,可以适当放宽到1e-8。MaxFunctionEvaluations设 10000 是防止死循环。resnorm是残差平方和,可以用来比较不同模型或者不同数据集的拟合优度。

3.3 拟合结果怎么验证:残差图和参数经济含义检查

拟合完不能只看resnorm小就完事。我一般会做两个检查:

第一,画残差图。如果残差在某个期限段有系统性偏离,比如中期残差全是正的,说明模型形式可能不够,需要考虑 Svensson 或者加入更多因子。

figure; stem(tau, residual, 'filled'); xlabel('到期时间(年)'); ylabel('残差'); title('残差分布'); grid on;

第二,检查参数的经济含义。beta0应该在合理范围内,比如 0.01 到 0.1 之间(对应 1% 到 10% 的长期收益率)。beta1通常是负的,因为收益率曲线向上倾斜。lambda应该在 0.1 到 2 之间。如果lambda跑到 0.01 或者 4.9,说明边界起作用了,可能数据本身不适合这个模型,或者初值有问题。

我遇到过一次,lambda被优化到 0.01,曲线几乎变成一条直线,但resnorm却很小。后来发现是数据里短端收益率异常低,模型为了拟合那个点,把lambda压到极小,牺牲了整条曲线的形状。这种时候我会把那个异常点剔除,重新拟合。

4. 避坑与排查:Nelson-Siegel 拟合中最容易翻车的五个地方

4.1 现象:优化返回 NaN 或 Inf

原因:lambda被试探到 0 或者负数,导致(1 - exp(-lambda*tau)) / (lambda*tau)出现 0/0 或者负数除法。另外,如果tau里有 0,也会直接除零。

解决:在nelson_siegel函数里加保护,如 2.1 节所示。同时给lambda设下界 0.01。如果tau里有 0,把 0 替换成 1/365(即一天)。

4.2 现象:拟合曲线在远端翘起或下沉

原因:beta0没有约束,优化器为了拟合短端,把beta0推到一个不合理的大值或小值。或者数据远端本身有异常。

解决:给beta0设一个宽松的边界,比如[-0.5, 1.5]。同时检查远端数据点,如果某个点明显偏离,考虑剔除或者用稳健回归。

4.3 现象:参数估计每次运行都不一样

原因:非线性优化对初值敏感,如果初值随机或者没有固定,结果会波动。另外,如果数据量太少(比如只有 5 个点),模型可能过拟合。

解决:固定初值生成规则,比如用 2.3 节的网格搜索。如果数据点少于 8 个,考虑固定lambda,只估三个 beta,或者改用线性插值。

4.4 现象:残差图显示中期系统性偏离

原因:Nelson-Siegel 只有一个曲率因子,无法同时拟合中期和短端的凸起。这是模型本身的局限。

解决:改用 Svensson 模型,它多一个曲率项,公式里增加一个beta3和lambda2。在 MATLAB 里只需要在nelson_siegel函数里多加两项,优化参数从 4 个变成 6 个。但要注意,参数越多,初值越难选,过拟合风险也越大。

4.5 现象:单位搞错导致参数解释完全错误

原因:收益率数据是百分比(如 2.5),但模型里用的是小数(0.025)。或者到期日单位是天,但模型里用的是年。

解决:在数据准备阶段强制统一单位。我一般会在脚本开头写注释,标明tau单位是年,y_obs单位是小数。如果数据源是百分比,先除 100。如果到期日是天,先除 365。

5. 进阶技巧:用动态 Nelson-Siegel 做滚动窗口拟合

静态 Nelson-Siegel 只能拟合一个时间点的曲线。如果你有多个时间点的数据,比如过去 60 个月的月末收益率曲线,你会想知道参数随时间怎么变。这就是动态 Nelson-Siegel 的用武之地。常见做法是滚动窗口:每次取过去 12 个月的数据,拟合一次,得到一组参数,然后向前滚动。

在 MATLAB 里,你可以把 3.2 节的拟合脚本封装成函数,然后在循环里调用。下面是一个框架:

function [beta_series, lambda_series] = rolling_ns(tau, y_matrix, window_size) % tau: 到期时间,列向量 % y_matrix: 每列是一个时间点的收益率 % window_size: 滚动窗口长度 [n_tau, n_time] = size(y_matrix); beta_series = zeros(n_time - window_size + 1, 3); lambda_series = zeros(n_time - window_size + 1, 1); for t = window_size:n_time y_window = y_matrix(:, t-window_size+1:t); y_mean = mean(y_window, 2); % 调用单期拟合函数,这里假设你已经把 3.2 节封装成 fit_ns [x_est, ~] = fit_ns(tau, y_mean); beta_series(t-window_size+1, :) = x_est(1:3); lambda_series(t-window_size+1) = x_est(4); end end

这个框架的关键是fit_ns函数,它内部做网格搜索和lsqcurvefit。滚动窗口的长度选择有讲究:太短,参数波动大;太长,反应迟钝。我一般用 12 到 24 个月,具体看数据频率。如果是月度数据,12 个月窗口能捕捉年度周期;如果是日度数据,60 个交易日窗口比较合适。

另外,动态 Nelson-Siegel 还有一个变体是状态空间模型,用卡尔曼滤波估计时变参数。那个实现起来更复杂,需要定义状态转移方程和观测方程,但好处是能给出参数的不确定性。如果你只是想做探索性分析,滚动窗口足够;如果要写论文或者做风险模型,状态空间更严谨。

最后说一个我自己的习惯:每次拟合完,我都会把参数序列画出来,看看beta0是不是平滑变化,lambda有没有跳变。如果lambda在某个时间点突然从 0.5 跳到 2.0,那大概率是数据问题或者优化失败,需要回去检查那个时间点的原始数据。这个习惯帮我省了很多后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询