Skip to content

mfe.univariate

mfe.univariate

mfe.univariate — Univariate time series models.

Note: ARCH/GARCH/EGARCH/FIGARCH/APARCH are in the arch package (Kevin Sheppard). This module provides: har_rv HAR-RV with vector and matrix interval notation, NW SEs har_rv_j HAR-RV-J with jump component har_forecast Multi-step point forecast from fitted HAR HEAVY Joint model of returns and realized variance (Shephard & Sheppard 2010)

HARResult dataclass

HARResult(params: FloatArray, std_errors: FloatArray, t_stats: FloatArray, p_values: FloatArray, r_squared: float, r_squared_adj: float, residuals: FloatArray, fitted: FloatArray, n_obs: int, bandwidth: int, param_names: list[str] = list(), spec: str = 'standard', intervals: list[tuple[int, int]] = list())

HAR-RV estimation result.

HEAVY

HEAVY(realized_measure: str = 'rv')

HEAVY model — joint model of returns and realized variance.

Shephard & Sheppard (2010). Not available in the arch package (stubbed but never completed as of 2026).

Parameters:

Name Type Description Default
realized_measure 'rv' | 'bpv' | 'kernel'

Which realized measure to use (informational, does not change estimation).

'rv'
Source code in src/mfe/univariate/heavy.py
def __init__(self, realized_measure: str = "rv") -> None:
    self.realized_measure = realized_measure

fit

fit(returns: FloatArray, realized: FloatArray, starting_values: FloatArray | None = None, method: str = 'L-BFGS-B', options: dict | None = None) -> HEAVYResult

Estimate HEAVY by joint QML.

Parameters:

Name Type Description Default
returns FloatArray
required
realized (T,) daily realized variance series (same frequency)
required
Source code in src/mfe/univariate/heavy.py
def fit(
    self,
    returns: FloatArray,
    realized: FloatArray,
    starting_values: FloatArray | None = None,
    method: str = "L-BFGS-B",
    options: dict | None = None,
) -> HEAVYResult:
    """
    Estimate HEAVY by joint QML.

    Parameters
    ----------
    returns  : (T,) daily return series (demeaned)
    realized : (T,) daily realized variance series (same frequency)
    """
    r = np.asarray(returns, dtype=np.float64)
    rm = np.asarray(realized, dtype=np.float64)
    T = len(r)

    if len(rm) != T:
        raise ValueError(f"returns and realized must have same length, got {T} vs {len(rm)}")
    if np.any(rm <= 0):
        raise ValueError("realized must be strictly positive (use realized variance, not returns)")

    x0 = starting_values if starting_values is not None else _heavy_starting_values(r, rm)
    x0 = np.asarray(x0, dtype=np.float64)

    # Bounds: all positive, alpha+beta < 1
    bounds = [
        (1e-8, None),   # omega_r
        (1e-6, 0.999),  # alpha_r
        (1e-6, 0.999),  # beta_r
        (1e-8, None),   # omega_rm
        (1e-6, 0.999),  # alpha_rm
        (1e-6, 0.999),  # beta_rm
    ]

    result = minimize(
        _heavy_loglik,
        x0,
        args=(r, rm),
        method=method,
        bounds=bounds,
        options=options or {"maxiter": 1000, "ftol": 1e-10, "gtol": 1e-7},
    )

    if not result.success:
        warnings.warn(
            f"HEAVY did not converge: {result.message}",
            ConvergenceWarning,
            stacklevel=2,
        )

    params = result.x
    h_r, h_rm = _heavy_recursion(params, r, rm)

    resid_r  = r / np.sqrt(h_r)
    resid_rm = rm / h_rm

    return HEAVYResult(
        params=params,
        log_likelihood=-result.fun,
        h_returns=h_r,
        h_realized=h_rm,
        residuals_r=resid_r,
        residuals_rm=resid_rm,
        converged=result.success,
        n_obs=T,
        diagnostics={
            "optimizer_result": result,
            "realized_measure": self.realized_measure,
        },
    )

forecast

forecast(result: HEAVYResult, horizon: int = 1, last_realized: float | None = None) -> tuple[FloatArray, FloatArray]

Multi-step ahead forecasts of h_r and h_rm.

Parameters:

Name Type Description Default
result HEAVYResult
required
horizon int
1
last_realized float | None
         if None, uses the last value from the estimation sample
None

Returns:

Type Description
(h_r_forecast, h_rm_forecast) — both (horizon,) arrays
Source code in src/mfe/univariate/heavy.py
def forecast(
    self,
    result: HEAVYResult,
    horizon: int = 1,
    last_realized: float | None = None,
) -> tuple[FloatArray, FloatArray]:
    """
    Multi-step ahead forecasts of h_r and h_rm.

    Parameters
    ----------
    result         : fitted HEAVYResult
    horizon        : number of steps ahead
    last_realized  : RM value at time T (for recursion start);
                     if None, uses the last value from the estimation sample

    Returns
    -------
    (h_r_forecast, h_rm_forecast) — both (horizon,) arrays
    """
    params = result.params
    omega_r, alpha_r, beta_r, omega_rm, alpha_rm, beta_rm = params

    h_r_last  = float(result.h_returns[-1])
    h_rm_last = float(result.h_realized[-1])

    if last_realized is None:
        # Use the last fitted h_rm as a proxy for E[RM_T | F_{T-1}]
        rm_last = h_rm_last
    else:
        rm_last = float(last_realized)

    h_r_fc  = np.empty(horizon, dtype=np.float64)
    h_rm_fc = np.empty(horizon, dtype=np.float64)

    # One step ahead: use last_realized directly
    h_r_fc[0]  = omega_r  + alpha_r  * rm_last + beta_r  * h_r_last
    h_rm_fc[0] = omega_rm + alpha_rm * rm_last + beta_rm * h_rm_last

    # Further steps: replace RM_{t-1} with its conditional expectation h_{RM,t-1}
    for h in range(1, horizon):
        h_r_fc[h]  = omega_r  + alpha_r  * h_rm_fc[h - 1] + beta_r  * h_r_fc[h - 1]
        h_rm_fc[h] = omega_rm + alpha_rm * h_rm_fc[h - 1] + beta_rm * h_rm_fc[h - 1]

    return h_r_fc, h_rm_fc

HEAVYResult dataclass

HEAVYResult(params: FloatArray, log_likelihood: float, h_returns: FloatArray, h_realized: FloatArray, residuals_r: FloatArray, residuals_rm: FloatArray, converged: bool, n_obs: int, diagnostics: dict = dict())

HEAVY model estimation result.

har_rv

har_rv(rv: FloatArray, p=(1, 5, 22), horizon: int = 1, nw_lags: int | None = None, spec: str = 'standard') -> HARResult

HAR-RV estimation by OLS with Newey-West standard errors.

Parameters:

Name Type Description Default
rv FloatArray
required
p
(1, 5, 22)
horizon forecast horizon h (LHS is h-day forward average)
1
nw_lags Newey-West bandwidth; None => 2*horizon
None
spec str
'standard'
Source code in src/mfe/univariate/har.py
def har_rv(
    rv: FloatArray,
    p=(1, 5, 22),
    horizon: int = 1,
    nw_lags: int | None = None,
    spec: str = "standard",
) -> HARResult:
    """
    HAR-RV estimation by OLS with Newey-West standard errors.

    Parameters
    ----------
    rv      : (T,) daily realized variance
    p       : vector [1,5,22] or matrix [[1,1],[1,5],[1,22]] or [[1,1],[2,5],[6,22]]
    horizon : forecast horizon h (LHS is h-day forward average)
    nw_lags : Newey-West bandwidth; None => 2*horizon
    spec    : "standard" (overlapping) | "modified" (non-overlapping intervals)
    """
    rv = np.asarray(rv, dtype=np.float64)
    T = len(rv)
    intervals = _parse_intervals(list(p), spec)
    max_end = max(e for _, e in intervals)

    X_regs, _ = _build_har_regressors(rv, intervals)
    n = len(X_regs)

    if horizon == 1:
        y = rv[max_end: max_end + n]
    else:
        kernel = np.ones(horizon) / horizon
        rv_fwd = np.convolve(rv, kernel[::-1], mode="full")[:T]
        rv_fwd_shifted = np.roll(rv_fwd, -horizon)
        n = min(n, T - max_end - horizon)
        y = rv_fwd_shifted[max_end: max_end + n]
        X_regs = X_regs[:n]

    X = np.column_stack([np.ones(n), X_regs])
    k = X.shape[1]
    if nw_lags is None:
        nw_lags = max(1, 2 * horizon)

    beta, se, t_stat, p_val, r2, r2_adj, resid, fitted = _fit(y, X, nw_lags, n, k, intervals, spec)

    names = ["const"] + [
        f"RV_lag{s}" if s == e else f"RV_avg{s}to{e}"
        for s, e in intervals
    ]
    return HARResult(
        params=beta, std_errors=se, t_stats=t_stat, p_values=p_val,
        r_squared=float(r2), r_squared_adj=float(r2_adj),
        residuals=resid, fitted=fitted, n_obs=n, bandwidth=nw_lags,
        param_names=names, spec=spec, intervals=intervals,
    )

har_rv_j

har_rv_j(rv: FloatArray, jump: FloatArray, p=(1, 5, 22), horizon: int = 1, nw_lags: int | None = None) -> HARResult

HAR-RV-J: HAR augmented with a jump component.

Andersen, Bollerslev & Diebold (2007). The jump regressor is the daily jump contribution J_t = max(RV_t - BPV_t, 0).

Source code in src/mfe/univariate/har.py
def har_rv_j(
    rv: FloatArray,
    jump: FloatArray,
    p=(1, 5, 22),
    horizon: int = 1,
    nw_lags: int | None = None,
) -> HARResult:
    """
    HAR-RV-J: HAR augmented with a jump component.

    Andersen, Bollerslev & Diebold (2007). The jump regressor is the daily
    jump contribution J_t = max(RV_t - BPV_t, 0).
    """
    rv = np.asarray(rv, dtype=np.float64)
    jump = np.asarray(jump, dtype=np.float64)
    intervals = _parse_intervals(list(p), "standard")
    max_end = max(e for _, e in intervals)

    X_regs, _ = _build_har_regressors(rv, intervals)
    n = len(X_regs)
    jump_lag = jump[max_end - 1: max_end - 1 + n]

    if horizon == 1:
        y = rv[max_end: max_end + n]
    else:
        T = len(rv)
        kernel = np.ones(horizon) / horizon
        rv_fwd = np.convolve(rv, kernel[::-1], mode="full")[:T]
        rv_fwd_shifted = np.roll(rv_fwd, -horizon)
        n = min(n, T - max_end - horizon)
        y = rv_fwd_shifted[max_end: max_end + n]
        X_regs = X_regs[:n]; jump_lag = jump_lag[:n]

    X = np.column_stack([np.ones(n), X_regs, jump_lag])
    k = X.shape[1]
    if nw_lags is None:
        nw_lags = max(1, 2 * horizon)

    beta, se, t_stat, p_val, r2, r2_adj, resid, fitted = _fit(y, X, nw_lags, n, k, intervals, "standard")

    names = ["const"] + [
        f"RV_lag{s}" if s == e else f"RV_avg{s}to{e}"
        for s, e in intervals
    ] + ["Jump_lag1"]
    return HARResult(
        params=beta, std_errors=se, t_stats=t_stat, p_values=p_val,
        r_squared=float(r2), r_squared_adj=float(r2_adj),
        residuals=resid, fitted=fitted, n_obs=n, bandwidth=nw_lags,
        param_names=names, spec="standard", intervals=intervals,
    )

har_forecast

har_forecast(result: HARResult, last_rv: FloatArray, horizon: int = 1) -> FloatArray

Multi-step HAR-RV point forecast.

Parameters:

Name Type Description Default
result HARResult
required
last_rv FloatArray
required
horizon int
1

Returns:

Type Description
(horizon,) forecast array
Source code in src/mfe/univariate/har.py
def har_forecast(result: HARResult, last_rv: FloatArray, horizon: int = 1) -> FloatArray:
    """
    Multi-step HAR-RV point forecast.

    Parameters
    ----------
    result   : fitted HARResult
    last_rv  : recent RV history (at least max interval end observations)
    horizon  : steps ahead

    Returns
    -------
    (horizon,) forecast array
    """
    rv_hist = list(np.asarray(last_rv, dtype=np.float64))
    forecasts = []
    for _ in range(horizon):
        rv_arr = np.array(rv_hist)
        T = len(rv_arr)
        regs = []
        for start, end in result.intervals:
            end_c = min(end, T)
            start_c = min(start, T)
            regs.append(float(np.mean(rv_arr[T - end_c: T - start_c + 1])) if T >= end_c else float(rv_arr[-1]))
        x = np.array([1.0] + regs)
        fc = float(result.params[:len(x)] @ x)
        forecasts.append(fc)
        rv_hist.append(fc)
    return np.array(forecasts)

har

HAR-RV model and extensions.

Corsi, F. (2009): "A Simple Approximate Long-Memory Model of Realized Volatility", JFEC.

Extensions vs. original har.py
  • Matrix interval notation: P=[[1,1],[2,5],[6,22]] (non-overlapping intervals)
  • MODIFIED spec: non-overlapping reparameterisation (same fit, different interp)
  • HAR-RV-J: jump-augmented HAR
  • har_forecast: multi-step point forecast from fitted result

HARResult dataclass

HARResult(params: FloatArray, std_errors: FloatArray, t_stats: FloatArray, p_values: FloatArray, r_squared: float, r_squared_adj: float, residuals: FloatArray, fitted: FloatArray, n_obs: int, bandwidth: int, param_names: list[str] = list(), spec: str = 'standard', intervals: list[tuple[int, int]] = list())

HAR-RV estimation result.

har_rv

har_rv(rv: FloatArray, p=(1, 5, 22), horizon: int = 1, nw_lags: int | None = None, spec: str = 'standard') -> HARResult

HAR-RV estimation by OLS with Newey-West standard errors.

Parameters:

Name Type Description Default
rv FloatArray
required
p
(1, 5, 22)
horizon forecast horizon h (LHS is h-day forward average)
1
nw_lags Newey-West bandwidth; None => 2*horizon
None
spec str
'standard'
Source code in src/mfe/univariate/har.py
def har_rv(
    rv: FloatArray,
    p=(1, 5, 22),
    horizon: int = 1,
    nw_lags: int | None = None,
    spec: str = "standard",
) -> HARResult:
    """
    HAR-RV estimation by OLS with Newey-West standard errors.

    Parameters
    ----------
    rv      : (T,) daily realized variance
    p       : vector [1,5,22] or matrix [[1,1],[1,5],[1,22]] or [[1,1],[2,5],[6,22]]
    horizon : forecast horizon h (LHS is h-day forward average)
    nw_lags : Newey-West bandwidth; None => 2*horizon
    spec    : "standard" (overlapping) | "modified" (non-overlapping intervals)
    """
    rv = np.asarray(rv, dtype=np.float64)
    T = len(rv)
    intervals = _parse_intervals(list(p), spec)
    max_end = max(e for _, e in intervals)

    X_regs, _ = _build_har_regressors(rv, intervals)
    n = len(X_regs)

    if horizon == 1:
        y = rv[max_end: max_end + n]
    else:
        kernel = np.ones(horizon) / horizon
        rv_fwd = np.convolve(rv, kernel[::-1], mode="full")[:T]
        rv_fwd_shifted = np.roll(rv_fwd, -horizon)
        n = min(n, T - max_end - horizon)
        y = rv_fwd_shifted[max_end: max_end + n]
        X_regs = X_regs[:n]

    X = np.column_stack([np.ones(n), X_regs])
    k = X.shape[1]
    if nw_lags is None:
        nw_lags = max(1, 2 * horizon)

    beta, se, t_stat, p_val, r2, r2_adj, resid, fitted = _fit(y, X, nw_lags, n, k, intervals, spec)

    names = ["const"] + [
        f"RV_lag{s}" if s == e else f"RV_avg{s}to{e}"
        for s, e in intervals
    ]
    return HARResult(
        params=beta, std_errors=se, t_stats=t_stat, p_values=p_val,
        r_squared=float(r2), r_squared_adj=float(r2_adj),
        residuals=resid, fitted=fitted, n_obs=n, bandwidth=nw_lags,
        param_names=names, spec=spec, intervals=intervals,
    )

har_rv_j

har_rv_j(rv: FloatArray, jump: FloatArray, p=(1, 5, 22), horizon: int = 1, nw_lags: int | None = None) -> HARResult

HAR-RV-J: HAR augmented with a jump component.

Andersen, Bollerslev & Diebold (2007). The jump regressor is the daily jump contribution J_t = max(RV_t - BPV_t, 0).

Source code in src/mfe/univariate/har.py
def har_rv_j(
    rv: FloatArray,
    jump: FloatArray,
    p=(1, 5, 22),
    horizon: int = 1,
    nw_lags: int | None = None,
) -> HARResult:
    """
    HAR-RV-J: HAR augmented with a jump component.

    Andersen, Bollerslev & Diebold (2007). The jump regressor is the daily
    jump contribution J_t = max(RV_t - BPV_t, 0).
    """
    rv = np.asarray(rv, dtype=np.float64)
    jump = np.asarray(jump, dtype=np.float64)
    intervals = _parse_intervals(list(p), "standard")
    max_end = max(e for _, e in intervals)

    X_regs, _ = _build_har_regressors(rv, intervals)
    n = len(X_regs)
    jump_lag = jump[max_end - 1: max_end - 1 + n]

    if horizon == 1:
        y = rv[max_end: max_end + n]
    else:
        T = len(rv)
        kernel = np.ones(horizon) / horizon
        rv_fwd = np.convolve(rv, kernel[::-1], mode="full")[:T]
        rv_fwd_shifted = np.roll(rv_fwd, -horizon)
        n = min(n, T - max_end - horizon)
        y = rv_fwd_shifted[max_end: max_end + n]
        X_regs = X_regs[:n]; jump_lag = jump_lag[:n]

    X = np.column_stack([np.ones(n), X_regs, jump_lag])
    k = X.shape[1]
    if nw_lags is None:
        nw_lags = max(1, 2 * horizon)

    beta, se, t_stat, p_val, r2, r2_adj, resid, fitted = _fit(y, X, nw_lags, n, k, intervals, "standard")

    names = ["const"] + [
        f"RV_lag{s}" if s == e else f"RV_avg{s}to{e}"
        for s, e in intervals
    ] + ["Jump_lag1"]
    return HARResult(
        params=beta, std_errors=se, t_stats=t_stat, p_values=p_val,
        r_squared=float(r2), r_squared_adj=float(r2_adj),
        residuals=resid, fitted=fitted, n_obs=n, bandwidth=nw_lags,
        param_names=names, spec="standard", intervals=intervals,
    )

har_forecast

har_forecast(result: HARResult, last_rv: FloatArray, horizon: int = 1) -> FloatArray

Multi-step HAR-RV point forecast.

Parameters:

Name Type Description Default
result HARResult
required
last_rv FloatArray
required
horizon int
1

Returns:

Type Description
(horizon,) forecast array
Source code in src/mfe/univariate/har.py
def har_forecast(result: HARResult, last_rv: FloatArray, horizon: int = 1) -> FloatArray:
    """
    Multi-step HAR-RV point forecast.

    Parameters
    ----------
    result   : fitted HARResult
    last_rv  : recent RV history (at least max interval end observations)
    horizon  : steps ahead

    Returns
    -------
    (horizon,) forecast array
    """
    rv_hist = list(np.asarray(last_rv, dtype=np.float64))
    forecasts = []
    for _ in range(horizon):
        rv_arr = np.array(rv_hist)
        T = len(rv_arr)
        regs = []
        for start, end in result.intervals:
            end_c = min(end, T)
            start_c = min(start, T)
            regs.append(float(np.mean(rv_arr[T - end_c: T - start_c + 1])) if T >= end_c else float(rv_arr[-1]))
        x = np.array([1.0] + regs)
        fc = float(result.params[:len(x)] @ x)
        forecasts.append(fc)
        rv_hist.append(fc)
    return np.array(forecasts)

heavy

HEAVY (High frEquency bAsed VolatilitY) model.

Shephard, N. & Sheppard, K. (2010): "Realising the Future: Forecasting with High-Frequency-Based Volatility (HEAVY) Models", Journal of Applied Econometrics, 25(2), 197-231.

Model specification

The HEAVY model jointly models daily returns r_t and realized measures RM_t (e.g. realized variance) using two equations:

h_{r,t} = omega_r + alpha_r * RM_{t-1} + beta_r * h_{r,t-1} h_{RM,t} = omega_RM + alpha_RM * RM_{t-1} + beta_RM * h_{RM,t-1}

where h_{r,t} is the conditional variance of returns and h_{RM,t} is the conditional mean of the realized measure.

The second equation is a separate GARCH-like model for RM itself.

Estimation: joint QML assuming: r_t | F_{t-1} ~ N(0, h_{r,t}) RM_t | F_{t-1} ~ Gamma(nu, nu / h_{RM,t}) (variance = h_{RM,t}^2 / nu)

The joint log-likelihood is: L = L_r + L_RM L_r = -0.5 * sum_t [log(h_{r,t}) + r_t^2 / h_{r,t}] L_RM = -0.5 * nu * sum_t [log(h_{RM,t}) + RM_t/h_{RM,t} - log(RM_t/h_{RM,t}) - 1] (Gamma log-likelihood in terms of scale; simplified constant dropped)

This is the model that arch partially stubs (ResearchModel) but never completes.

Key innovation vs. standard GARCH

By using realized measures in the variance equation, HEAVY produces forecasts that update faster: when overnight volatility is high (RM_{t-1} large), h_{r,t} responds immediately rather than waiting for the squared return signal.

Reference implementation cross-check

MATLAB mfe-toolbox: heavy.m The MATLAB version computes analytic gradients. We compute numerical gradients via scipy; analytic scores are a future optimization.

HEAVYResult dataclass

HEAVYResult(params: FloatArray, log_likelihood: float, h_returns: FloatArray, h_realized: FloatArray, residuals_r: FloatArray, residuals_rm: FloatArray, converged: bool, n_obs: int, diagnostics: dict = dict())

HEAVY model estimation result.

HEAVY

HEAVY(realized_measure: str = 'rv')

HEAVY model — joint model of returns and realized variance.

Shephard & Sheppard (2010). Not available in the arch package (stubbed but never completed as of 2026).

Parameters:

Name Type Description Default
realized_measure 'rv' | 'bpv' | 'kernel'

Which realized measure to use (informational, does not change estimation).

'rv'
Source code in src/mfe/univariate/heavy.py
def __init__(self, realized_measure: str = "rv") -> None:
    self.realized_measure = realized_measure
fit
fit(returns: FloatArray, realized: FloatArray, starting_values: FloatArray | None = None, method: str = 'L-BFGS-B', options: dict | None = None) -> HEAVYResult

Estimate HEAVY by joint QML.

Parameters:

Name Type Description Default
returns FloatArray
required
realized (T,) daily realized variance series (same frequency)
required
Source code in src/mfe/univariate/heavy.py
def fit(
    self,
    returns: FloatArray,
    realized: FloatArray,
    starting_values: FloatArray | None = None,
    method: str = "L-BFGS-B",
    options: dict | None = None,
) -> HEAVYResult:
    """
    Estimate HEAVY by joint QML.

    Parameters
    ----------
    returns  : (T,) daily return series (demeaned)
    realized : (T,) daily realized variance series (same frequency)
    """
    r = np.asarray(returns, dtype=np.float64)
    rm = np.asarray(realized, dtype=np.float64)
    T = len(r)

    if len(rm) != T:
        raise ValueError(f"returns and realized must have same length, got {T} vs {len(rm)}")
    if np.any(rm <= 0):
        raise ValueError("realized must be strictly positive (use realized variance, not returns)")

    x0 = starting_values if starting_values is not None else _heavy_starting_values(r, rm)
    x0 = np.asarray(x0, dtype=np.float64)

    # Bounds: all positive, alpha+beta < 1
    bounds = [
        (1e-8, None),   # omega_r
        (1e-6, 0.999),  # alpha_r
        (1e-6, 0.999),  # beta_r
        (1e-8, None),   # omega_rm
        (1e-6, 0.999),  # alpha_rm
        (1e-6, 0.999),  # beta_rm
    ]

    result = minimize(
        _heavy_loglik,
        x0,
        args=(r, rm),
        method=method,
        bounds=bounds,
        options=options or {"maxiter": 1000, "ftol": 1e-10, "gtol": 1e-7},
    )

    if not result.success:
        warnings.warn(
            f"HEAVY did not converge: {result.message}",
            ConvergenceWarning,
            stacklevel=2,
        )

    params = result.x
    h_r, h_rm = _heavy_recursion(params, r, rm)

    resid_r  = r / np.sqrt(h_r)
    resid_rm = rm / h_rm

    return HEAVYResult(
        params=params,
        log_likelihood=-result.fun,
        h_returns=h_r,
        h_realized=h_rm,
        residuals_r=resid_r,
        residuals_rm=resid_rm,
        converged=result.success,
        n_obs=T,
        diagnostics={
            "optimizer_result": result,
            "realized_measure": self.realized_measure,
        },
    )
forecast
forecast(result: HEAVYResult, horizon: int = 1, last_realized: float | None = None) -> tuple[FloatArray, FloatArray]

Multi-step ahead forecasts of h_r and h_rm.

Parameters:

Name Type Description Default
result HEAVYResult
required
horizon int
1
last_realized float | None
         if None, uses the last value from the estimation sample
None

Returns:

Type Description
(h_r_forecast, h_rm_forecast) — both (horizon,) arrays
Source code in src/mfe/univariate/heavy.py
def forecast(
    self,
    result: HEAVYResult,
    horizon: int = 1,
    last_realized: float | None = None,
) -> tuple[FloatArray, FloatArray]:
    """
    Multi-step ahead forecasts of h_r and h_rm.

    Parameters
    ----------
    result         : fitted HEAVYResult
    horizon        : number of steps ahead
    last_realized  : RM value at time T (for recursion start);
                     if None, uses the last value from the estimation sample

    Returns
    -------
    (h_r_forecast, h_rm_forecast) — both (horizon,) arrays
    """
    params = result.params
    omega_r, alpha_r, beta_r, omega_rm, alpha_rm, beta_rm = params

    h_r_last  = float(result.h_returns[-1])
    h_rm_last = float(result.h_realized[-1])

    if last_realized is None:
        # Use the last fitted h_rm as a proxy for E[RM_T | F_{T-1}]
        rm_last = h_rm_last
    else:
        rm_last = float(last_realized)

    h_r_fc  = np.empty(horizon, dtype=np.float64)
    h_rm_fc = np.empty(horizon, dtype=np.float64)

    # One step ahead: use last_realized directly
    h_r_fc[0]  = omega_r  + alpha_r  * rm_last + beta_r  * h_r_last
    h_rm_fc[0] = omega_rm + alpha_rm * rm_last + beta_rm * h_rm_last

    # Further steps: replace RM_{t-1} with its conditional expectation h_{RM,t-1}
    for h in range(1, horizon):
        h_r_fc[h]  = omega_r  + alpha_r  * h_rm_fc[h - 1] + beta_r  * h_r_fc[h - 1]
        h_rm_fc[h] = omega_rm + alpha_rm * h_rm_fc[h - 1] + beta_rm * h_rm_fc[h - 1]

    return h_r_fc, h_rm_fc