TimeVaryingNelsonSiegelResult#

class cultivars.multivariate.reduced_form.term_structure.TimeVaryingNelsonSiegelResult(*, panel, maturities, mu, ar, state_innovation_cov, measurement_var, log_decay_mean, decay_ar, decay_sd, factors, factor_cov, log_decay, log_decay_std, llf, filter, nobs, n_params)[source]

Bases: _SummaryMixin, _ComparisonMixin

A fitted Nelson-Siegel model with a time-varying loading decay.

The Koopman-Mallee-van der Wel extension: the decay becomes a fourth latent factor with autoregressive dynamics in its log,

\[y_t(\tau) = L_t + S_t\, s(\tau, \lambda_t) + C_t\, c(\tau, \lambda_t) + \varepsilon_t(\tau), \qquad \log\lambda_t - \bar\ell = \phi_\lambda\,(\log\lambda_{t-1} - \bar\ell) + \zeta_t,\]

with the three factors keeping their diagonal autoregressions. The transition stays linear-Gaussian; the measurement is nonlinear only through the loadings’ dependence on \(\lambda_t\), so the system is additive-Gaussian and the extended or unscented filter reads it. The factor and decay paths here are the matching smoother’s output.

Note

The likelihood is an approximation and the record says so wherever it appears. llf is the chosen filter’s Gaussian approximation to the exact likelihood, so information criteria against the constant-decay model – whose likelihood is exact – rank by a heuristic, and the two filters’ values are not comparable with each other either. The decay path is weakly identified by construction: it moves the curve only through the shape of the slope and curvature loadings, so on a sparse maturity grid or a short panel the fit can collapse to a nearly constant decay with the curvature factor absorbing the slack, and a tiny decay_sd with a negative decay_ar is that collapse, not a finding. Many maturities across the hump region are what identify it.

Variables:
  • panel – The observed (nobs, p) yield panel.

  • maturities – The (p,) maturities.

  • mu – (3,) factor means (level, slope, curvature).

  • ar – (3,) diagonal factor persistences.

  • state_innovation_cov – (3, 3) factor innovation covariance.

  • measurement_var – (p,) per-maturity measurement variances.

  • log_decay_mean – Unconditional mean of the log decay.

  • decay_ar – Persistence of the log decay.

  • decay_sd – Innovation standard deviation of the log decay.

  • factors – (nobs, 3) smoothed factor paths.

  • factor_cov – (nobs, 3, 3) smoothed factor covariances.

  • log_decay – (nobs,) smoothed log-decay path.

  • log_decay_std – (nobs,) smoothed log-decay standard deviations.

  • llf – The approximate Gaussian log-likelihood of the chosen filter. Not the exact likelihood: information criteria are comparable against the constant-decay model only as a heuristic, and the summary says so.

  • filter – "extended" or "unscented".

  • nobs – Curve dates.

  • n_params – Free parameters the likelihood was maximized over.

See also

References

Koopman, S. J., Mallee, M. I. P., & van der Wel, M. (2010). Analyzing the term structure of interest rates using the dynamic Nelson-Siegel model with time-varying parameters. Journal of Business & Economic Statistics, 28(3), 329-343.

Diebold, F. X., & Li, C. (2006). Forecasting the term structure of government bond yields. Journal of Econometrics, 130(2), 337-364.

Example

Eight maturities across the hump region, sixty dates, and a decay that wanders between 0.44 and 1.5 around 0.6. The unscented fit tracks the level almost exactly and the log-decay path well enough to move the fitted hump with it:

>>> import numpy as np
>>> from cultivars.engine._core._polynomials import _nelson_siegel_loadings
>>> rng = np.random.default_rng(0)
>>> taus = np.array([0.25, 0.5, 1.0, 2.0, 3.0, 5.0, 7.0, 10.0])
>>> mu, bar = np.array([5.0, -1.5, 1.0]), np.log(0.6)
>>> f, log_lam = np.tile(mu, (60, 1)), np.full(60, bar)
>>> for t in range(1, 60):
...     f[t] = mu + 0.9 * (f[t - 1] - mu) + 0.2 * rng.standard_normal(3)
...     log_lam[t] = bar + 0.95 * (log_lam[t - 1] - bar) + 0.15 * rng.standard_normal()
>>> curves = np.stack(
...     [_nelson_siegel_loadings(taus, np.exp(log_lam[t])) @ f[t] for t in range(60)]
... )
>>> curves = curves + 0.02 * rng.standard_normal((60, 8))
>>> res = TimeVaryingNelsonSiegel(curves, taus).fit()
>>> res.filter, res.nobs, res.n_maturities, res.n_params
('unscented', 60, 8, 23.0)
>>> bool(np.corrcoef(res.level, f[:, 0])[0, 1] > 0.98)
True
>>> bool(np.corrcoef(res.log_decay, log_lam)[0, 1] > 0.7)
True
>>> bool(res.hump_maturity.max() - res.hump_maturity.min() > 1.0)
True

Private attributes#

_params

The parameter record, rebuilt for the emitter.

Private methods#

_comparison_label()

Specification label used when this result appears in a ranking.

_from_fit(fit, model)

Assemble the public result from a raw fit and its specification.

_summary_table()

Structured summary rendered by every display path.