harlstmgarch
A Python implementation of the three-stage hybrid HAR-LSTM-GARCH framework for realized-volatility forecasting, with Bayesian-optimized LSTM tuning, a full suite of econometric/ML benchmarks, and the Model Confidence Set test for statistically rigorous model comparison.
Author: Belhimer Hocine — Maître de conférences A (Lecturer A), Higher School of Accounting and Finance of Constantine (ESCF Constantine) — hbelhimer@escf.constantine.dz
Method reference: Ben Romdhane, W., & Boubaker, H. (2026). A Hybrid HAR-LSTM-GARCH Model for Forecasting Volatility in Energy Markets. Journal of Risk and Financial Management, 19(2), 77. https://doi.org/10.3390/jrfm19010077
Table of contents
- The model
- Installation
- Quick start
- API reference (detailed syntax)
- End-to-end example
- Reproducing the paper
- Methodology ↔ paper equations
- Limitations
- Citation
- Changelog
- License
The model
The framework follows a sequential decomposition principle — each stage models what the previous stage leaves behind:
| Stage | Component | Role | Eqs. |
|---|---|---|---|
| 1 | HAR (Corsi, 2009) | Linear filter for persistent, multi-scale dynamics (daily/weekly/monthly RV, OLS) | 6, 24–25 |
| 2 | LSTM | Recurrent net learning the non-linear patterns left in the HAR residuals; tuned by Bayesian Optimization | 17–23, 27–28 |
| 3 | GARCH(1,1) | Conditional variance of the hybrid forecast errors → time-varying prediction intervals | 30–33 |
RV_t, RV_t^w, RV_t^m
│
┌────────────▼────────────┐ e_t = RV_t − L_t
│ Stage 1: HAR (OLS) ├──────────────┐
└────────────┬────────────┘ │
L_{t+1}│ ▼
│ ┌──────────────────────────┐
│ │ Stage 2: LSTM (Bayes-opt) │
│ │ ê_{t+1}=f(e_t,…) │
│ └────────────┬─────────────┘
▼ │ ê_{t+1}
H_{t+1}=L_{t+1}+ê_{t+1} ◄────────────────────┘
│ z_t = RV_t − H_t
┌────────────▼────────────┐
│ Stage 3: GARCH(1,1) │ σ̂²_{z,t+1}
└────────────┬────────────┘
▼
Point: H_{t+1} Interval: H_{t+1} ± 1.96·σ̂_{z,t+1}
Installation
pip install harlstmgarch
With development tools (build / twine / pytest):
pip install "harlstmgarch[dev]"
Requirements (installed automatically): Python ≥ 3.10, numpy, pandas,
scipy, statsmodels, arch, tensorflow ≥ 2.13, scikit-learn,
matplotlib.
Quick start
import pandas as pd
from harlstmgarch import (
realized_vol_from_returns, har_components, chronological_split,
HARLSTMGARCH, metrics,
)
# 1. Realized volatility from a daily price series (paper Eq. 2)
prices = pd.read_csv("BrentCrude.csv", parse_dates=["DATE"]).set_index("DATE")["Close"]
rv = realized_vol_from_returns(prices, window=22)["RV"]
# 2. HAR features (RV in levels) + chronological 70/15/15 split
df = har_components(rv)
train, val, test = chronological_split(df, train=0.70, val=0.15)
# 3. Tune the residual LSTM by Bayesian Optimization, then fit all 3 stages
model = HARLSTMGARCH(lstm_kwargs={"lookback": 20})
model.tune_lstm(train, val, n_calls=20) # GP + Matérn 5/2
model.fit(train, val)
# 4. Strictly one-step-ahead out-of-sample forecast (point + 95% interval)
fc = model.forecast(test)
print(metrics.scoreboard(test["RV"],
{"HAR": fc.har, "HAR-LSTM-GARCH": fc.point}))
print("95% coverage:",
metrics.interval_coverage(test["RV"], fc.lower, fc.upper))
API reference (detailed syntax)
All public names are importable directly from the top level, e.g.
from harlstmgarch import HARLSTMGARCH, metrics.
harlstmgarch.data
load_realized_measures(path, date_col="DATE", rv_col="RV") -> pd.DataFrame
Load a realized-measures CSV (dates as YYYYMMDD). The input rv_col is treated
as a realized variance; its square root is returned as a RV column
(volatility scale), indexed by date.
realized_vol_from_returns(prices, window=22, annualize=True) -> pd.DataFrame
Build daily RV from a daily closing-price pd.Series (paper Eq. 2):
RV_t = sqrt( (252/window) · Σ_{i=0}^{window-1} r_{t-i}² ) with
r_t = ln(P_t/P_{t-1}). Returns columns RV and returns.
har_components(rv, weekly=5, monthly=22) -> pd.DataFrame
HAR design matrix. For each day t: target RV (=RV_t) and the lagged
regressors RV_d = RV_{t-1}, RV_w = mean(last 5), RV_m = mean(last 22) —
all computed from information available at t-1 (no look-ahead).
chronological_split(df, train=0.70, val=0.15) -> (train, val, test)
Time-ordered split (never shuffled). Test fraction is 1 − train − val.
harlstmgarch.har
class HARModel
.fit(df) -> self # OLS on RV ~ RV_d + RV_w + RV_m
.predict(df) -> pd.Series # linear forecast
.residuals(df) -> pd.Series # RV − prediction
.coefficients() -> pd.DataFrame # Estimate / Std.Error / t / p (cf. Table 6)
.rsquared -> float
harlstmgarch.lstm
class ResidualLSTM(lookback=20, units=32, learning_rate=5.5e-4,
batch_size=32, epochs=300, patience=25, seed=42)
.fit(train_resid, val_resid=None) -> self
.predict(resid_history, resid_future_index) -> pd.Series
A single-layer LSTM (+ linear dense head) that forecasts the next residual
from its own history; inputs are min/max-scaled to [-1, 1], loss is MSE
(Adam), early stopping on validation loss. predict is rolling and strictly
one-step-ahead over resid_future_index.
harlstmgarch.tune
class BayesianLSTMTuner(lookback=20, n_calls=20, n_init=6, patience=20,
max_eval_epochs=120, seed=42)
.optimize(e_train, e_val) -> self
.best_params_ # dict: {units, learning_rate, batch_size, epochs}
.best_score_ # best validation MSE
.history_ # pd.DataFrame of every trial
SEARCH_SPACE # dict of the paper's ranges (units, learning_rate, batch_size, epochs)
Gaussian-Process Bayesian Optimization (Matérn 5/2 kernel + Expected
Improvement) of the residual-LSTM hyperparameters, minimizing one-step
validation MSE — reproduces paper §3.4.2 using only scikit-learn.
max_eval_epochs caps epochs during search for speed (set None to honour
the sampled value exactly).
harlstmgarch.garch
class ResidualGARCH(rescale_factor=100.0)
.fit(z) -> self # zero-mean GARCH(1,1), Normal innovations
.conditional_sigma(z) -> pd.Series # recursive one-step σ_t (OOS)
.parameters() -> pd.DataFrame # ω, α, β with SE / t / p
.persistence -> float # α + β
Stage 3: models the conditional variance of the hybrid forecast errors z_t
(σ_t² = ω + α·z_{t-1}² + β·σ_{t-1}²). The conditional σ gives the prediction
intervals.
harlstmgarch.hybrid
class HARLSTMGARCH(lstm_kwargs={}, z_level=1.96)
.tune_lstm(train, val, n_calls=20, n_init=6,
max_eval_epochs=120, seed=42) -> dict # runs Bayesian Opt., rebuilds the LSTM
.fit(train, val=None) -> self # fits all three stages
.forecast(test) -> HybridForecast
.har, .lstm, .garch, .tuner_ # fitted components
@dataclass HybridForecast
.point # H_t = HAR + LSTM-residual forecast
.har # stage-1 linear forecast
.sigma # stage-3 conditional std of the forecast error
.lower # point − z_level·sigma
.upper # point + z_level·sigma
train/val/test are frames produced by har_components. Set z_level
for other interval widths (1.96 → 95%, 2.576 → 99%).
harlstmgarch.benchmarks
class GARCHBenchmark(returns, rescale_factor=1.0) # GARCH(1,1), Skewed-Student
class GJRGARCHBenchmark(returns, rescale_factor=1.0) # GJR-GARCH(1,1), leverage
.fit() -> self
.forecast_sigma(index) -> pd.Series # one-step conditional volatility
class StandaloneLSTM(lookback=20, units=97, learning_rate=5.81e-3,
batch_size=12, epochs=198, patience=25, seed=42)
.fit(train_rv, val_rv=None) -> self
.forecast(rv_history, index) -> pd.Series # LSTM on raw RV (no HAR)
The competitors used in the paper's Table 4 comparison. StandaloneLSTM
defaults to the paper's standalone hyperparameters (97 units, lr 5.81e-3, …).
harlstmgarch.metrics
# point-forecast losses
rmse(actual, forecast) -> float
mae(actual, forecast) -> float
mape(actual, forecast) -> float
r2(actual, forecast) -> float
qlike(actual, forecast) -> float # mean( RV/RV̂ − log(RV/RV̂) − 1 )
qlike_loss_series(actual, forecast) -> np.ndarray # per-obs QLIKE (for the MCS)
scoreboard(actual, forecasts: dict[str, Series]) -> pd.DataFrame
# one row per model: RMSE, MAE, MAPE, R2, QLIKE
# statistical tests
mcleod_li(residuals, lags=20) -> (Q, p_value) # ARCH-type non-linearity (Eq. 26)
diebold_mariano(actual, f1, f2, power=2) -> (DM, p) # equal predictive accuracy
interval_coverage(actual, lower, upper) -> float # empirical PI coverage
model_confidence_set(losses: dict[str, ndarray], alpha=0.05,
n_boot=5000, block=None, seed=42) -> pd.DataFrame
# Hansen-Lunde-Nason (2011) MCS; columns: MCS_p, eliminated_order
# models with MCS_p >= alpha are in the confidence set
harlstmgarch.plots
Publication-quality matplotlib figures (each returns a Figure; pass path=...
to save a PNG):
plots.timeseries_with_split(rv, splits, title, path=None)
plots.forecast_comparison(actual, forecasts, title, path=None)
plots.prediction_band(actual, point, lower, upper, title, path=None)
plots.scatter_fit(actual, forecasts, title, path=None)
plots.residual_diagnostics(har_resid, hybrid_resid, title, lags=30, path=None)
plots.metric_bars(scoreboard, metric, title, path=None, lower_is_better=True)
plots.training_history(history, title, path=None)
End-to-end example
import pandas as pd
from harlstmgarch import (
realized_vol_from_returns, har_components, chronological_split,
HARModel, HARLSTMGARCH, GARCHBenchmark, GJRGARCHBenchmark,
StandaloneLSTM, metrics,
)
# --- data ---------------------------------------------------------------
raw = pd.read_csv("BrentCrude.csv", parse_dates=["DATE"]).set_index("DATE")
out = realized_vol_from_returns(raw["Close"], window=22)
rv, ret = out["RV"], out["returns"]
df = har_components(rv)
train, val, test = chronological_split(df, 0.70, 0.15)
# --- stage 1: HAR + justification for the LSTM --------------------------
har = HARModel().fit(train)
print(har.coefficients().round(5)) # cf. paper Table 6
Q, p = metrics.mcleod_li(har.residuals(train), lags=20)
print(f"McLeod-Li Q(20) = {Q:.1f}, p = {p:.3g}") # non-linearity remains
# --- stages 1-3: Bayesian-optimized hybrid ------------------------------
model = HARLSTMGARCH(lstm_kwargs={"lookback": 20})
print("best LSTM:", model.tune_lstm(train, val, n_calls=20))
model.fit(train, val)
fc = model.forecast(test)
print(model.garch.parameters().round(4), "persistence:", model.garch.persistence)
# --- benchmarks + scoreboard --------------------------------------------
rv_lstm = StandaloneLSTM().fit(train["RV"], val["RV"])
hist = pd.concat([train["RV"], val["RV"], test["RV"]])
forecasts = {
"HAR": fc.har,
"LSTM": rv_lstm.forecast(hist, test.index),
"GARCH": GARCHBenchmark(ret).fit().forecast_sigma(test.index),
"GJR-GARCH": GJRGARCHBenchmark(ret).fit().forecast_sigma(test.index),
"HAR-LSTM-GARCH": fc.point,
}
board = metrics.scoreboard(test["RV"], forecasts)
print(board.round(5)) # cf. paper Table 4
# --- Model Confidence Set on QLIKE --------------------------------------
losses = {k: metrics.qlike_loss_series(test["RV"], f.reindex(test.index))
for k, f in forecasts.items()}
print(metrics.model_confidence_set(losses).round(4)) # cf. paper Table 5
Reproducing the paper
examples/run_case_study.py runs the entire pipeline (data → HAR → McLeod-Li →
Bayesian-optimized LSTM → GARCH → benchmarks → scoreboard → MCS → figures) and
writes all tables/figures to assets/:
python examples/run_case_study.py --prices BrentCrude.csv --n-calls 20
If no price file is supplied it falls back to a bundled realized-measures CSV so the pipeline is runnable out of the box; supply the Brent series to reproduce the published results.
Methodology ↔ paper equations
| Concept | Paper | Code |
|---|---|---|
| Realized volatility (22-day) | Eq. 2 | realized_vol_from_returns |
| HAR-RV model | Eq. 6, Table 6 | HARModel |
| LSTM gates / cell | Eqs. 17–23 | ResidualLSTM |
| Bayesian Optimization (GP, Matérn 5/2) | §3.4.2, Table 3 | BayesianLSTMTuner, HARLSTMGARCH.tune_lstm |
| HAR residuals → LSTM | Eqs. 24–28 | HARLSTMGARCH.fit |
| McLeod-Li test | Eq. 26 (Q(20)=4869.18) | metrics.mcleod_li |
| GARCH(1,1) on hybrid errors | Eqs. 30–33 | ResidualGARCH |
| Benchmarks (GARCH/GJR/LSTM) | Table 4 | benchmarks |
| QLIKE | §3.5 | metrics.qlike |
| Model Confidence Set | §3.5, Table 5 | metrics.model_confidence_set |
Limitations
Mirroring the reference study: results are demonstrated on a single asset class (crude oil); RV is a proxy from daily squared returns (not intraday/jump-robust); the pipeline (HAR + Bayesian optimization + GARCH) is computationally non-trivial; and the LSTM stage remains a "grey box" (attention/SHAP are natural extensions). The framework is univariate — exogenous drivers (OPEC, inventories, geopolitical indices) are not yet included.
Citation
If you use this package, please cite the method paper:
@article{BenRomdhane2026HARLSTMGARCH,
author = {Ben Romdhane, Wiem and Boubaker, Heni},
title = {A Hybrid HAR-LSTM-GARCH Model for Forecasting Volatility in Energy Markets},
journal = {Journal of Risk and Financial Management},
year = {2026},
volume = {19},
number = {2},
pages = {77},
doi = {10.3390/jrfm19010077}
}
Changelog
0.2.1
- Fix
ResidualGARCH.conditional_sigma: fall back to the sample variance when the GARCH fit is (near-)integrated (alpha + beta≈ 1), instead of dividing by1 - (alpha + beta)≈ 0 — prevents an exploding prediction interval at the first observation. - Sync
__version__with the package metadata.
0.2.0
- Bayesian Optimization of the LSTM (
tune.BayesianLSTMTuner,HARLSTMGARCH.tune_lstm) — GP with Matérn 5/2 kernel, no extra dependency. - Benchmark models (
benchmarks): GARCH(1,1)-skewt, GJR-GARCH, standalone LSTM. - Model Confidence Set test (
metrics.model_confidence_set). - QLIKE corrected to the paper's level-ratio definition; added
qlike_loss_series. - Case study rewritten for full paper reproduction (Brent RV, MCS, benchmarks).
- Added
LICENSE, classifiers and project metadata.
0.1.0
- Initial three-stage HAR-LSTM-GARCH implementation.
License
MIT — see LICENSE. © 2026 Belhimer Hocine.
Metadata
Release files for harlstmgarch 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| harlstmgarch-0.2.1.tar.gz | 28.3 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| harlstmgarch-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 53.5 kB
Release files / harlstmgarch-0.2.1.tar.gz
| Download URL | harlstmgarch-0.2.1.tar.gz |
|---|---|
| Size | 28.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
071d9f9faa05e887f7cb7b59a32694e82ab399157c3064ef22d76bd459549aa4
|
|
BLAKE2b-256 checksum How to use checksums |
24f5323d63f379512d0d302133f9b568f414e9f9f8f5c4eca6b1f2194371b6d7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.0
|
Release files / harlstmgarch-0.2.1-py3-none-any.whl
| Download URL | harlstmgarch-0.2.1-py3-none-any.whl |
|---|---|
| Size | 25.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ecc3ab8661407dc9cc5d5982b36d5067b843b1533a203828762252bf28380caf
|
|
BLAKE2b-256 checksum How to use checksums |
1050a19624d050b3161e82ac4f8a2cf7d3cac844e556fa69933a450927cacbcd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.0
|
Security for harlstmgarch 0.2.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.