Skip to content

Vasicek Curve

quantflow.rates.vasicek.VasicekCurve pydantic-model

Bases: YieldCurve

Yield curve derived from the Vasicek short-rate model.

The Vasicek model describes the short rate as a mean-reverting Ornstein-Uhlenbeck process:

\[\begin{equation} dr_t = \kappa(\theta - r_t)\, dt + \sigma\, dW_t \end{equation}\]

The model admits a closed-form discount factor; see discount_factor.

Throughout, the auxiliary quantity is:

\[\begin{equation} B(\tau) = \frac{1 - e^{-\kappa\tau}}{\kappa} \end{equation}\]

Fields:

curve_type pydantic-field

curve_type = 'vasicek_curve'

rate pydantic-field

rate = Decimal('0.05')

Initial value \(x_0\)

kappa pydantic-field

kappa = Decimal('1.0')

Mean reversion speed \(\kappa\)

theta pydantic-field

theta = Decimal('0.05')

Mean level \(\theta\)

sigma pydantic-field

sigma = Decimal('0.01')

Volatility \(\sigma\)

ref_date pydantic-field

ref_date

Reference date for the yield curve

calibrator

calibrator()

Return a VasicekCurveCalibration wrapping this curve.

Source code in quantflow/rates/vasicek.py
def calibrator(self) -> VasicekCurveCalibration:
    """Return a [VasicekCurveCalibration][...VasicekCurveCalibration] wrapping
    this curve."""
    return VasicekCurveCalibration(yield_curve=self)

process

process()

Return the underlying Vasicek process corresponding to this curve.

Source code in quantflow/rates/vasicek.py
def process(self) -> Vasicek:
    """Return the underlying [Vasicek][quantflow.sp.ou.Vasicek] process
    corresponding to this curve."""
    return Vasicek(
        rate=float(self.rate),
        kappa=float(self.kappa),
        theta=float(self.theta),
        bdlp=WienerProcess(sigma=float(self.sigma)),
    )

instantaneous_forward_rate

instantaneous_forward_rate(ttm)

Calculate the instantaneous forward rate for the Vasicek model.

\[\begin{equation} f(\tau) = r_0\, e^{-\kappa\tau} + \theta(1 - e^{-\kappa\tau}) - \frac{\sigma^2}{2\kappa}\, B(\tau)\, e^{-\kappa\tau} \end{equation}\]
Source code in quantflow/rates/vasicek.py
def instantaneous_forward_rate(self, ttm: FloatArrayLike) -> FloatArrayLike:
    r"""Calculate the instantaneous forward rate for the Vasicek model.

    \begin{equation}
        f(\tau) = r_0\, e^{-\kappa\tau}
            + \theta(1 - e^{-\kappa\tau})
            - \frac{\sigma^2}{2\kappa}\, B(\tau)\, e^{-\kappa\tau}
    \end{equation}
    """
    arr = np.asarray(ttm, dtype=float)
    ttma = np.maximum(arr, 0.0)
    kappa = float(self.kappa)
    theta = float(self.theta)
    sigma = float(self.sigma)
    rate = float(self.rate)
    s2 = sigma * sigma
    et = np.exp(-kappa * ttma)
    b = (1.0 - et) / kappa
    fwd = rate * et + theta * (1.0 - et) - s2 / (2.0 * kappa) * b * et
    return fwd if fwd.ndim > 0 else float(fwd)

discount_factor

discount_factor(ttm)

Calculate the discount factor using the Vasicek closed-form solution.

The discount factor is:

\[\begin{equation} D(\tau) = e^{A(\tau) - B(\tau)\, r_0} \end{equation}\]

where:

\[\begin{equation} A(\tau) = \left(\theta - \frac{\sigma^2}{2\kappa^2}\right) \bigl(B(\tau) - \tau\bigr) + \frac{\sigma^2 B(\tau)^2}{4\kappa} \end{equation}\]
Source code in quantflow/rates/vasicek.py
def discount_factor(self, ttm: FloatArrayLike) -> FloatArrayLike:
    r"""Calculate the discount factor using the Vasicek closed-form solution.

    The discount factor is:

    \begin{equation}
        D(\tau) = e^{A(\tau) - B(\tau)\, r_0}
    \end{equation}

    where:

    \begin{equation}
        A(\tau) = \left(\theta - \frac{\sigma^2}{2\kappa^2}\right)
            \bigl(B(\tau) - \tau\bigr)
            + \frac{\sigma^2 B(\tau)^2}{4\kappa}
    \end{equation}
    """
    arr = np.asarray(ttm, dtype=float)
    ttma = np.maximum(arr, 0.0)
    kappa = float(self.kappa)
    theta = float(self.theta)
    sigma = float(self.sigma)
    rate = float(self.rate)
    s2 = sigma * sigma
    b = (1.0 - np.exp(-kappa * ttma)) / kappa
    a = (theta - s2 / (2.0 * kappa * kappa)) * (b - ttma) + s2 * b * b / (
        4.0 * kappa
    )
    df = np.exp(a - rate * b)
    return df if df.ndim > 0 else float(df)

jacobian

jacobian(ttm)

Analytical Jacobian of discount factors w.r.t. \([r_0, \kappa, \theta, \sigma]\). Returns shape (len(ttm), 4).

Source code in quantflow/rates/vasicek.py
def jacobian(self, ttm: FloatArrayLike) -> FloatArray | None:
    r"""Analytical Jacobian of discount factors w.r.t.
    $[r_0, \kappa, \theta, \sigma]$. Returns shape (len(ttm), 4).
    """
    arr = np.asarray(ttm, dtype=float)
    ttma = np.maximum(arr, 0.0)
    kappa = float(self.kappa)
    theta = float(self.theta)
    sigma = float(self.sigma)
    rate = float(self.rate)
    s2 = sigma * sigma
    et = np.exp(-kappa * ttma)
    b = (1.0 - et) / kappa
    a = (theta - s2 / (2.0 * kappa * kappa)) * (b - ttma) + s2 * b * b / (
        4.0 * kappa
    )
    d = np.exp(a - rate * b)

    # ∂D/∂r0
    d_rate = -b * d

    # ∂D/∂κ
    db_k = (ttma * et * kappa - (1.0 - et)) / (kappa * kappa)
    da_k = (
        s2 / (kappa**3) * (b - ttma)
        + (theta - s2 / (2.0 * kappa * kappa)) * db_k
        + s2 * b * db_k / (2.0 * kappa)
        - s2 * b * b / (4.0 * kappa * kappa)
    )
    d_kappa = d * (da_k - rate * db_k)

    # ∂D/∂θ
    d_theta = d * (b - ttma)

    # ∂D/∂σ
    da_s = (-sigma / (kappa * kappa)) * (b - ttma) + sigma * b * b / (2.0 * kappa)
    d_sigma = d * da_s

    return np.column_stack([d_rate, d_kappa, d_theta, d_sigma])

continuously_compounded_rate

continuously_compounded_rate(ttm)

Calculate the continuously compounded rate for a given time to maturity.

The continuously compounded rate is related to the discount factor by the following formula:

\[\begin{equation} r(\tau) = -\frac{\ln D(\tau)}{\tau} \end{equation}\]

where \(D(\tau)\) is the discount factor for a given time to maturity \(\tau\).

Accepts a scalar float or a float array. Returns a scalar float for scalar input and a numpy float array for array input.

PARAMETER DESCRIPTION
ttm

Time to maturity in years

TYPE: ArrayLike

Source code in quantflow/rates/yield_curve.py
def continuously_compounded_rate(
    self, ttm: Annotated[ArrayLike, Doc("Time to maturity in years")]
) -> FloatArrayLike:
    r"""Calculate the continuously compounded rate for a given time to maturity.

    The continuously compounded rate is related to the discount factor
    by the following formula:

    \begin{equation}
        r(\tau) = -\frac{\ln D(\tau)}{\tau}
    \end{equation}

    where $D(\tau)$ is the discount factor for a given time to maturity $\tau$.

    Accepts a scalar float or a float array. Returns a scalar float for scalar
    input and a numpy float array for array input.
    """
    ttm_ = np.asarray(ttm, dtype=float)
    df = np.asarray(self.discount_factor(ttm_), dtype=float)
    result = np.where(
        ttm_ <= 0, self.instantaneous_forward_rate(0.0), -np.log(df) / ttm_
    )
    return maybe_float(result)

plot

plot(ttm_max=10.0, n=200, **kwargs)

Plot the continuously compounded rate vs time to maturity.

Requires plotly to be installed.

PARAMETER DESCRIPTION
ttm_max

Maximum time to maturity in years

TYPE: float DEFAULT: 10.0

n

Number of points to evaluate

TYPE: int DEFAULT: 200

Source code in quantflow/rates/yield_curve.py
def plot(
    self,
    ttm_max: Annotated[float, Doc("Maximum time to maturity in years")] = 10.0,
    n: Annotated[int, Doc("Number of points to evaluate")] = 200,
    **kwargs: Any,
) -> Any:
    """Plot the continuously compounded rate vs time to maturity.

    Requires plotly to be installed.
    """
    return plot.plot_yield_curve(self, ttm_max=ttm_max, n=n, **kwargs)

register_curve_types classmethod

register_curve_types(*curve_classes)

Register a yield curve subclass for deserialization.

Source code in quantflow/rates/yield_curve.py
@classmethod
def register_curve_types(cls, *curve_classes: type[YieldCurve]) -> None:
    """Register a yield curve subclass for deserialization."""
    for curve_cls in curve_classes:
        name = snake_case(curve_cls.__name__)
        if current_type := _CURVE_TYPES.pop(name, None):
            _TYPES_TO_NAMES.pop(current_type, None)
        _CURVE_TYPES[name] = curve_cls
        _TYPES_TO_NAMES[curve_cls] = name

curve_types classmethod

curve_types()

Return the registered curve types.

Source code in quantflow/rates/yield_curve.py
@classmethod
def curve_types(cls) -> tuple[str, ...]:
    """Return the registered curve types."""
    return tuple(sorted(_CURVE_TYPES))

get_curve_class classmethod

get_curve_class(curve_type)

Get the yield curve class for a given curve type.

Source code in quantflow/rates/yield_curve.py
@classmethod
def get_curve_class(cls, curve_type: str) -> type[YieldCurve] | None:
    """Get the yield curve class for a given curve type."""
    return _CURVE_TYPES.get(curve_type)

quantflow.rates.vasicek.VasicekCurveCalibration pydantic-model

Bases: YieldCurveCalibration[VasicekCurve]

Calibration wrapper for a Vasicek yield curve.

Fields:

yield_curve pydantic-field

yield_curve

Yield curve to be calibrated

get_params

get_params()
Source code in quantflow/rates/vasicek.py
def get_params(self) -> FloatArray:
    c = self.yield_curve
    return np.array([float(c.rate), float(c.kappa), float(c.theta), float(c.sigma)])

set_params

set_params(params)
Source code in quantflow/rates/vasicek.py
def set_params(self, params: FloatArray) -> None:
    rate, kappa, theta, sigma = params
    self.yield_curve.rate = Decimal(str(round(float(rate), 10)))
    self.yield_curve.kappa = Decimal(str(round(float(kappa), 10)))
    self.yield_curve.theta = Decimal(str(round(float(theta), 10)))
    self.yield_curve.sigma = Decimal(str(round(float(sigma), 10)))

get_bounds

get_bounds()
Source code in quantflow/rates/vasicek.py
def get_bounds(self) -> Bounds:
    return Bounds([-1.0, 1e-4, -1.0, 0.0], [1.0, 1000.0, 1.0, 1.0])

calibrate

calibrate(ttm, rates)

Fit the Vasicek curve to continuously compounded rates via least squares.

PARAMETER DESCRIPTION
ttm

Times to maturity in years.

TYPE: ArrayLike

rates

Continuously compounded rates, same length as ttm.

TYPE: ArrayLike

Source code in quantflow/rates/vasicek.py
def calibrate(
    self,
    ttm: Annotated[ArrayLike, Doc("Times to maturity in years.")],
    rates: Annotated[
        ArrayLike, Doc("Continuously compounded rates, same length as ttm.")
    ],
) -> VasicekCurve:
    """Fit the Vasicek curve to continuously compounded rates via least squares."""
    ttm_arr = np.asarray(ttm, dtype=float)
    rates_arr = np.asarray(rates, dtype=float)

    def residuals(params: np.ndarray) -> np.ndarray:
        self.set_params(params)
        df = np.asarray(self.yield_curve.discount_factor(ttm_arr), dtype=float)
        return -np.log(df) / ttm_arr - rates_arr

    def jac(params: np.ndarray) -> FloatArray:
        self.set_params(params)
        df = np.asarray(self.yield_curve.discount_factor(ttm_arr), dtype=float)
        jac_d = np.asarray(self.yield_curve.jacobian(ttm_arr), dtype=float)
        return -jac_d / (df * ttm_arr)[:, None]

    x0 = np.array([rates_arr[0], 1.0, rates_arr[-1], 0.01])
    result = least_squares(
        residuals,
        jac=jac,
        x0=x0,
        bounds=([-1.0, 1e-4, -1.0, 0.0], [1.0, 1000.0, 1.0, 1.0]),
    )
    self.set_params(result.x)
    return self.yield_curve

calibrate_df

calibrate_df(ttm, target)

Fit the yield curve to target discount factors.

Converts discount factors to continuously compounded rates then calls [calibrate][quantflow.rates.vasicek.VasicekCurveCalibration.calibrate_df.calibrate].

PARAMETER DESCRIPTION
ttm

Times to maturity in years.

TYPE: ArrayLike

target

Target discount factors, same length as ttm.

TYPE: ArrayLike

Source code in quantflow/rates/options.py
def calibrate_df(
    self,
    ttm: Annotated[ArrayLike, Doc("Times to maturity in years.")],
    target: Annotated[
        ArrayLike, Doc("Target discount factors, same length as ttm.")
    ],
) -> Y:
    """Fit the yield curve to target discount factors.

    Converts discount factors to continuously compounded rates then calls
    [calibrate][.calibrate].
    """
    ttm_ = np.asarray(ttm, dtype=float)
    rates = -np.log(np.asarray(target, dtype=float)) / ttm_
    return self.calibrate(ttm_, rates)