\[ % MathJax has no bm package; redefine \bm in terms of \boldsymbol, which it supports natively \newcommand{\bm}[1]{\boldsymbol{#1}} % --- Operators ----------------------------------------------------------- % Expectation operator \DeclareMathOperator{\E}{E} % Variance operator \DeclareMathOperator{\Var}{Var} % Covariance operator \DeclareMathOperator{\Cov}{Cov} % Correlation operator \DeclareMathOperator{\Corr}{Corr} % Rank operator (Spearman rank correlation) \DeclareMathOperator{\Rank}{Rank} % Skewness operator \DeclareMathOperator{\Skewness}{Skewness} % Kurtosis operator \DeclareMathOperator{\Kurtosis}{Kurtosis} % Difference/differential operator (upright d per ISO 80000-2) \newcommand{\Diff}{\mathrm{d}} % --- Risk Measures ------------------------------------------------------- % Value at Risk \DeclareMathOperator{\VaR}{VaR} % Expected Shortfall \DeclareMathOperator{\ES}{ES} % Marginal Value at Risk \DeclareMathOperator{\MVaR}{MVaR} % Component Value at Risk \DeclareMathOperator{\CompVaR}{CVaR} % Incremental Value at Risk \DeclareMathOperator{\IVaR}{IVaR} % Component Expected Shortfall \DeclareMathOperator{\CompES}{CES} % Weighted sensitivity for risk factor in FRTB SBM bucket \DeclareMathOperator{\WS}{WS} % Hedge-benefit ratio for FRTB DRC bucket \DeclareMathOperator{\HBR}{HBR} % Stress scenario risk measure for FRTB NMRF capital add-on \DeclareMathOperator{\SES}{SES} % Jump-to-default exposure for obligor (FRTB DRC) \DeclareMathOperator{\JTD}{JTD} % Loss given default for obligor (FRTB DRC) \DeclareMathOperator{\LGD}{LGD} % Default risk weight for obligor (FRTB DRC) \DeclareMathOperator{\RW}{RW} % ES ratio function \newcommand{\ESratio}{\lambda} % Arbitrary risk measure function (coherence axioms) \newcommand{\RiskMeasure}{\varphi} % Asset position (coherence axioms) \newcommand{\Asset}{A} % First named asset in examples \newcommand{\AssetA}{A} % Second named asset in examples \newcommand{\AssetB}{B} % Third named asset in examples \newcommand{\AssetC}{C} % Constant (risk measure axioms) \newcommand{\Constant}{c} % Bucket-level aggregate sensitivity in FRTB SBM \newcommand{\BucketAgg}{S} % Cross-bucket correlation in FRTB SBM \newcommand{\CrossBucketCorr}{\gamma} % Notional amount for an instrument (FRTB DRC and RRAO) \newcommand{\Notional}{\text{Notional}} % --- Distributions ------------------------------------------------------- % Binomial distribution \DeclareMathOperator{\Binomial}{Binomial} % Uniform distribution \DeclareMathOperator{\Uniform}{Uniform} % Normal distribution \newcommand{\NormalDist}{\mathcal{N}} % Student-t CDF \newcommand{\StudentCDF}{t} % Student-t PDF \newcommand{\StudentPDF}{f} % --- Returns ------------------------------------------------------------- % Price \newcommand{\Price}{P} % Simple (arithmetic) return \newcommand{\SimpleReturns}{R} % Log (compound) return \newcommand{\CompoundReturns}{y} % Matrix of compound returns \newcommand{\ReturnMatrix}{\bm{y}} % Dividend payment \newcommand{\dividend}{d} % --- Volatility ---------------------------------------------------------- % Realised variance \DeclareMathOperator{\RealisedVar}{RV} % Realised volatility (square root of realised variance) \DeclareMathOperator{\RealisedVol}{RVol} % Volatility \newcommand{\Vol}{\sigma} % GARCH intercept parameter \newcommand{\GARCHconst}{\omega} % ARCH coefficient (weight on past squared returns) \newcommand{\ARCHcoeff}{\alpha} % GARCH coefficient (weight on past variance) \newcommand{\GARCHcoeff}{\beta} % EWMA decay factor (typically 0.94) \newcommand{\EWMAdecay}{\lambda} % Standardized residual / error term \newcommand{\StdNormal}{\epsilon} % Leverage parameter in apARCH \newcommand{\APARCHleverage}{\zeta} % Power parameter in apARCH \newcommand{\APARCHpower}{\delta} % Degrees of freedom (Student-t) \newcommand{\DOF}{\nu} % GJR-GARCH leverage parameter \newcommand{\GJRleverage}{\gamma} % GARCH-X external regressor coefficient \newcommand{\RegressorCoeff}{\xi} % Mean (first moment) \newcommand{\Mean}{\mu} % Lag order in volatility models \newcommand{\Lag}{L} % Dummy variable (indicator in regressions) \newcommand{\DummyVar}{D} % --- Portfolio ----------------------------------------------------------- % Portfolio weight (scalar) \newcommand{\weight}{w} % Portfolio weight vector \newcommand{\weights}{\bm{w}} % Covariance matrix \newcommand{\CovMatrix}{\bm{\Sigma}} % Correlation matrix \newcommand{\CorrMatrix}{\bm{C}} % Correlation coefficient \newcommand{\correlation}{\rho} % Number of assets in portfolio \newcommand{\NumberAssets}{K} % Portfolio value \newcommand{\PortfolioValue}{\vartheta} % Asset beta (CAPM) \newcommand{\AssetBeta}{\beta} % Ledoit-Wolf shrinkage intensity \newcommand{\ShrinkageIntensity}{\delta} % Sample covariance matrix \newcommand{\SampleCov}{\bm{S}} % --- Time Series --------------------------------------------------------- % Sample size \newcommand{\SampleSize}{T} % Generic count (number of lags, payments, etc.) \newcommand{\Count}{N} % Estimation window length \newcommand{\EstWindow}{W_E} % Testing window length \newcommand{\TestWindow}{W_T} % Stress window length \newcommand{\StressWindow}{W_S} % AR coefficient \newcommand{\ARcoeff}{\phi} % MA coefficient \newcommand{\MAcoeff}{\psi} % --- Probability --------------------------------------------------------- % Probability level (e.g. 0.01 for 1% VaR) \newcommand{\probability}{p} % Cumulative sorted scenario weight \newcommand{\CumScenarioWeight}{\Omega} % p-quantile of the P and L distribution \newcommand{\Quantile}{q} % Quantile function \newcommand{\QuantileFunction}{Q} % Probability density function \newcommand{\PDF}{f} % Cumulative distribution function \newcommand{\CDF}{F} % Standard normal CDF \newcommand{\NormalCDF}{\Phi} % Inverse standard normal (quantile function) \newcommand{\NormalQuantile}{\Phi^{-1}} % Standard normal density function \newcommand{\NormalPDF}{\phi} % Likelihood function \newcommand{\lik}{\mathcal{L}} % Log-likelihood \newcommand{\LogLikelihood}{\ell} % Parameter vector (MLE estimation) \newcommand{\ParamSet}{\theta} % Parameter space \newcommand{\ParamSpace}{\Theta} % Fisher information matrix \newcommand{\FisherInfo}{\mathcal{I}} % Statistical power (Type II error rate) \newcommand{\StatPower}{\beta} % Significance level (CI coverage complement) \newcommand{\SignifLevel}{\gamma} % --- Options ------------------------------------------------------------- % Call option label \newcommand{\CallOption}{\text{call}} % Put option label \newcommand{\PutOption}{\text{put}} % Strike price \newcommand{\Strike}{X} % Risk-free interest rate \newcommand{\RiskFree}{r_f} % Option maturity (time to expiration) \newcommand{\OptionMaturity}{\tau} % Option delta \newcommand{\OptionDelta}{\Delta} % Option gamma \newcommand{\OptionGamma}{\Gamma} % Option vega \newcommand{\Vega}{\mathcal{V}} % Holding period \newcommand{\HoldingPeriod}{H} % Simulation profit/loss (signed; negative is a loss) \newcommand{\ProfitLoss}{\Pi} % Futures price \newcommand{\Futures}{F} % Hedge ratio \newcommand{\HedgeRatio}{h} % Units of basic asset held \newcommand{\StockHolding}{x^b} % Units of options held \newcommand{\OptionHolding}{x^o} % --- Interest Rates ------------------------------------------------------ % Bond convexity \newcommand{\Convexity}{C} % Interest rate / yield \newcommand{\Yields}{r} % Key rate change, basis points (scalar tenor component); used as \KeyRateChange_k or \KeyRateChange_{t,k}. EXCEPTION to the no-macro-to-macro-indirection rule, decided 2026-08-09: composed from \Yields on purpose, so a future change to the rate glyph propagates here without a follow-up edit. \Yields is defined earlier in generated output (same interest_rates category, TOML order), so expansion order is safe; verify this still holds if either entry is reordered. \newcommand{\KeyRateChange}{\Delta \Yields^{\mathrm{bp}}} % Key rate changes, basis points (vector); used bare or as \KeyRateChanges_t. EXCEPTION to the no-macro-to-macro-indirection rule, decided 2026-08-09: composed from \Yields on purpose, so a future change to the rate glyph propagates here without a follow-up edit. \bm{\Yields} depends on \bm/\boldsymbol correctly expanding a macro argument rather than a bare letter --- confirm this renders correctly the first time either consumer compiles it. \newcommand{\KeyRateChanges}{\Delta \bm{\Yields}^{\mathrm{bp}}} % Dollar value of a basis point \newcommand{\DV}{\text{DV01}} % Mean reversion speed (interest rate models) \newcommand{\MeanReversion}{\varkappa} % Modified duration \newcommand{\ModDur}{D} % Macaulay duration \newcommand{\MacDur}{D_{\text{Mac}}} % Effective duration \newcommand{\EffDur}{D_{\text{eff}}} % DV01 vector \newcommand{\DVvec}{\bm{d}} % Long-run mean rate (Vasicek/CIR/Hull-White) \newcommand{\LongRunRate}{\theta} % Hull-White time-varying drift \newcommand{\HullWhiteDrift}{\varphi} % Cash flow (coupon/principal payment) \newcommand{\CashFlow}{c} % --- Extreme Value ------------------------------------------------------- % Tail index (EVT, Pareto-type tails) \newcommand{\TailIndex}{\iota} % Shape parameter (xi = 1/iota) \newcommand{\ShapeParam}{\varsigma} % Threshold value (EVT) \newcommand{\Threshold}{u} % Extremal index (Leadbetter 1983) \newcommand{\ExtremalIndex}{\theta} % Multivariate extremal coefficient, theta in [1,d]. theta=1 complete tail dependence; theta=d asymptotic independence. Glyph collides with ExtremalIndex and ParamSet; disambiguated at source by macro name. \newcommand{\ExtremalCoef}{\theta} % GEV distribution function \newcommand{\GEV}{\mathcal{H}} % GPD distribution function \newcommand{\GPD}{\mathcal{G}} % GPD scale parameter \newcommand{\GPDscale}{\beta} % GPD scale parameter at a threshold u, distinct from the generic GPD scale \newcommand{\GPDscaleAtThreshold}{\beta_u} % Number of threshold exceedances (EVT) \newcommand{\TailCount}{C} % Sample maximum (block maxima, EVT) \newcommand{\SampleMax}{M} % Kurtosis value (realized, e.g. sample kurtosis) \newcommand{\KurtosisVal}{\kappa} % Pareto scaling constant (EVT) \newcommand{\ParetoConst}{\mathcal{A}} % Generic constant (EVT asymptotics) \newcommand{\RemainderConst}{\mathcal{C}} % Little-o asymptotic notation \newcommand{\LittleO}{o} % Moment order (EVT) \newcommand{\MomentOrder}{\mathscr{m}} % Doubled sample size (EVT block maxima) \newcommand{\DoubleSample}{D} % GEV normalizing location constant \newcommand{\GEVloc}{a} % GEV normalizing scale constant \newcommand{\GEVscale}{b} % Negative return (loss, EVT context) \newcommand{\NegativeReturn}{L} % --- Copulas ------------------------------------------------------------- % Copula function \newcommand{\Copula}{\mathcal{C}} % Copula density \newcommand{\CopulaDensity}{c} % Generator function (Archimedean copulas) \newcommand{\generator}{\varphi} % Copula dependence parameter \newcommand{\CopulaParam}{\theta} % Copula marginal parameter set \newcommand{\MarginalParamSet}{\eta} % Lower tail dependence coefficient \newcommand{\LowerTailDep}{\lambda_L} % Upper tail dependence coefficient \newcommand{\UpperTailDep}{\lambda_U} % Marginal CDF (second variable) \newcommand{\MarginalCDF}{G} % Joint density function \newcommand{\JointDensity}{h} % Marginal density function \newcommand{\MarginalDensity}{g} % Joint distribution function \newcommand{\JointCDF}{H} % Gaussian copula correlation parameter \newcommand{\CopulaCorr}{\rho} % --- Multivariate -------------------------------------------------------- % Diagonal matrix of conditional volatilities \newcommand{\DiagVolD}{\bm{D}} % Idiosyncratic variance matrix (diagonal, factor models) \newcommand{\IdioVarMatrix}{\bm{\Psi}} % Covariance matrix entry (two indices) \newcommand{\CovElement}{\sigma} % Auxiliary matrix in DCC dynamics \newcommand{\DCCauxQ}{\bm{Q}} % BEKK constant matrix \newcommand{\BEKKconst}{\bm{\Omega}} % BEKK ARCH parameter matrix \newcommand{\BEKKarch}{\bm{A}} % BEKK GARCH parameter matrix \newcommand{\BEKKgarch}{\bm{B}} % DCC rescaling diagonal matrix \newcommand{\DCCrescaleZ}{\bm{Z}} % DCC auxiliary matrix element \newcommand{\DCCelement}{q} % Factor loadings matrix (loadings/eigenvectors of the covariance matrix) \newcommand{\FactorLoadings}{\bm{\Lambda}} % Factor subscript label \newcommand{\factor}{\text{factor}} % PCA eigenvalue \newcommand{\Eigenvalue}{\lambda} % Number of factors in factor model \newcommand{\NumberFactors}{m} % Factor return vector \newcommand{\FactorReturn}{\bm{g}} % PCA factor score (scalar; not an eigenvector) \newcommand{\PCAFactor}{F} % BEKK cross-covariance coefficient \newcommand{\BEKKcross}{\delta} % Factor loading element \newcommand{\FactorLoading}{\Lambda} % DCC correlation persistence parameter \newcommand{\DCCxi}{\xi} % DCC news coefficient (weight on recent shocks) \newcommand{\DCCzeta}{\zeta} % --- Simulation ---------------------------------------------------------- % Number of simulation paths \newcommand{\NumberSims}{B} % Cholesky factor of covariance matrix \newcommand{\Cholesky}{\bm{L}} % Uniform random number \newcommand{\UniformDraw}{\tilde{u}} % --- Backtesting --------------------------------------------------------- % Violation ratio (observed/expected violations) \DeclareMathOperator{\ViolRatio}{VR} % Stressed Value at Risk \DeclareMathOperator{\StressedVaR}{SVaR} % QLIKE loss function for variance forecasts \DeclareMathOperator{\QLIKE}{QLIKE} % Mean squared error (forecast loss function) \DeclareMathOperator{\MSE}{MSE} % Mean absolute error (forecast loss function) \DeclareMathOperator{\MAE}{MAE} % Markov transition probability \newcommand{\TransProb}{\pi} % Exception indicator: 1 if y_t < -VaR_t \newcommand{\ExceptionInd}{\eta} % Exception count \newcommand{\ExceptionCount}{\upsilon} % Probability Integral Transform \newcommand{\ProbIntegralTransform}{\hat{u}} % Quantile score (pinball loss function) \newcommand{\QuantileScore}{\mathcal{S}} % Kolmogorov-Smirnov test statistic \newcommand{\KSstat}{D} % Test statistic (generic base letter) \newcommand{\TestStat}{J} % --- General ------------------------------------------------------------- % Regression intercept \newcommand{\RegressionIntercept}{a} % Regression slope \newcommand{\RegressionSlope}{\beta} % Risk factor \newcommand{\RiskFactor}{x} % Forecast horizon \newcommand{\ForecastHorizon}{h} % Block length (bootstrap) \newcommand{\BlockLength}{\ell} % Capital tau variant \newcommand{\DeliveryTime}{\Upsilon} % Today's calendar time in years (option pricing input; distinct from trading-date index t) \newcommand{\CalendarTime}{t^*} % Probability measure \newcommand{\ProbMeasure}{\mathbb{P}} % Risk-neutral measure \newcommand{\RiskNeutral}{\mathbb{Q}} % Pricing function (Black-Scholes, bond pricing, etc.) \newcommand{\PricingFn}{V} % Indicator function \newcommand{\Indicator}{\bm{1}} % Vector of ones \newcommand{\OnesVector}{\bm{1}} % Risk factor sensitivity (first-order P&L) \newcommand{\Sensitivity}{\delta} % Second-order sensitivity (convexity/gamma) \newcommand{\ConvexitySens}{\gamma} % Output floor percentage (Basel) \newcommand{\OutputFloor}{\alpha} % --- Subscript Labels ---------------------------------------------------- % Subscript label: annualized \newcommand{\Annual}{a} % Subscript label: implied \newcommand{\Implied}{I} % Subscript label: portfolio \newcommand{\Portfolio}{\pi} % --- Text Abbreviations -------------------------------------------- % S&P 500 index \newcommand{\SP}{\text{S\&P-500}} % Student-t distribution (text) \newcommand{\St}{\text{Student-t}} % Geopolitical Risk index (Caldara and Iacoviello) \newcommand{\GPR}{\text{GPR}} \]

26  Running backtests

We are no longer estimating risk one day at a time. We are building a forecast engine that runs across thousands of trading days and produces the dataset the backtests need.

That means fixing one set of parameters for all four models, running them in parallel and saving the results in a format both Unix and Windows can read. The next chapter, Chapter 27, takes that dataset and tests it.

26.1 GARCH estimation speed

Backtesting requires fitting GARCH models thousands of times across rolling windows. The choice of estimation package determines total runtime. We benchmark GARCH(1,1) estimation across R, Python and Julia using a 2000-observation window from S&P 500 returns, the same window size used for the backtest estimation below.

Table 26.1: GARCH(1,1) estimation speed comparison
Platform Package Normal (ms) Student-t (ms)
R tseries 0.7
Julia ARCHModels 0.8 1.6
Julia Manual 1.5
Python arch 4.7 6.9
R fGarch 13 41
R rugarch 42 66
R tsgarch 180 296

These timings are illustrative and relative rather than a reproducible benchmark, since hardware and package versions were not recorded. R tseries and Julia ARCHModels are the fastest implementations for normal errors. The manual Julia implementation using Nelder-Mead is roughly twice as slow despite being compiled code.

For Student-t distributions, Julia ARCHModels was the quickest in this run at 1.6 ms. Python arch at 7 ms is next, followed by R fGarch at 41 ms. The R frameworks rugarch and tsgarch, which support a wider range of GARCH variants and distributions, are slower, reflecting their more extensive modelling infrastructure.

For rolling window backtests with thousands of re-estimations the differences above compound, and on this hardware ARCHModels was the quickest for both distributions. In R, tseries is fastest for normal errors but only supports that distribution. For Student-t, fGarch is the fastest R option.

26.2 Data and libraries

Backtesting adds parallel processing and serialisation to the libraries used so far.

library(ggplot2)
library(parallel)
source("common/functions.r", chdir = TRUE)

data = ProcessRawData()
sp500 = data$sp500
  • tsgarch provides GARCH estimation (loaded via functions.r);
  • parallel enables multicore processing via mclapply().
import numpy as np
import pandas as pd
import sys
sys.path.insert(0, 'common')
from functions import (ProcessRawData,
                       Risk_HS, Risk_EWMA, Risk_nGARCH, Risk_tGARCH, RunOneDay)
import multiprocessing

data = ProcessRawData()
sp500 = data['sp500']
y = sp500['y'].values
  • Risk functions (Risk_HS, Risk_EWMA, Risk_nGARCH, Risk_tGARCH) and RunOneDay are imported from common/functions.py;
  • multiprocessing is loaded here for core detection, and joblib is introduced later for the parallel run.
using Statistics, Distributions, DataFrames, Dates
using ARCHModels
using Serialization
include("common/functions.jl");

data = ProcessRawData();
sp500 = data["sp500"];
y = sp500[!, :y];
  • Risk functions (Risk_HS, Risk_EWMA, Risk_nGARCH, Risk_tGARCH) and RunOneDay are loaded from common/functions.jl;
  • ARCHModels provides fast GARCH model estimation (see Table 26.1);
  • Julia’s built-in threading via Threads.@threads provides parallel processing.

26.3 Backtesting framework setup

All four models use the same estimation window, probability level and portfolio value, so their forecasts can be compared directly.

par             = list()
par$asset       = "S&P 500"   # asset being tested
par$probability = 0.01        # probability level (1% VaR)
par$value       = 1000        # portfolio value in currency units
par$WE          = 2000        # estimation window size
par$T           = length(sp500$y)  # total sample size
par$WT          = par$T-par$WE     # testing window size
par$lambda      = 0.94         # decay parameter for EWMA
par$Methods     = c("HS","EWMA","nGARCH","tGARCH") # models we run
par = {
    'asset': 'S&P 500',
    'probability': 0.01,
    'value': 1000,
    'WE': 2000,
    'T': len(y),
    'lambda': 0.94,
    'Methods': ['HS', 'EWMA', 'nGARCH', 'tGARCH']
}
par['WT'] = par['T'] - par['WE']
par = Dict(
    "asset" => "S&P 500",
    "probability" => 0.01,
    "value" => 1000,
    "WE" => 2000,
    "T" => length(y),
    "lambda" => 0.94,
    "Methods" => ["HS", "EWMA", "nGARCH", "tGARCH"]
);
par["WT"] = par["T"] - par["WE"];

26.4 Backtesting functions

Systematic backtesting requires consistent methodology — the same estimation windows and the same set of models, applied to every trading day in the sample. The risk functions and RunOneDay() are defined in the common function files:

  • R: common/functions.rRisk_HS, Risk_EWMA, Risk_nGARCH, Risk_tGARCH, RunOneDay
  • Python: common/functions.pyRisk_HS, Risk_EWMA, Risk_nGARCH, Risk_tGARCH, RunOneDay
  • Julia: common/functions.jlRisk_HS, Risk_EWMA, Risk_nGARCH, Risk_tGARCH, RunOneDay

These functions implement Historical Simulation, EWMA, GARCH with normal errors and GARCH with Student-t errors. The RunOneDay() function calculates all risk measures for a single trading day, returning VaR and ES for each method.

26.5 Saving results

We argued in Section 16.3.1 that intermediate files should normally be avoided. Backtesting provides an exception. Re-estimating GARCH models across thousands of days takes time, and the subsequent statistical analysis in the next chapter is separate from the computation here. We save the backtest results to files so the validation chapter can load them directly without re-running the estimation.

We use language-specific binary formats (.RData, .pkl, .jls) for speed and precision, as discussed in Section 10.5. For workflows involving multiple languages, use Parquet. All three languages can read it.

26.5.1 Metadata for reproducibility

Save metadata with the results. It complements reproducible environments by recording the versions that generated the output. Knowing which package versions were used helps diagnose why results might differ when re-running code months or years later, or when results differ across machines. For regulatory purposes, being able to demonstrate exactly how and when results were generated matters, and sharing results with colleagues is easier when they can verify they have compatible software versions.

We save the backtest results along with the parameters used and metadata including the run date, machine name and package versions. This creates a self-documenting output that can be understood and reproduced later.

26.6 Parallel processing

Each day’s forecast is independent of others within the rolling window framework, making backtesting well-suited for multi-core implementation. Most modern computers have at least 4 cores, and some have 16 or more. Most languages use only one core by default. Parallel backtesting can therefore deliver large speedups.

26.6.1 Available cores

cores = detectCores()
cat("Available cores:", cores, "\n")
Available cores: 10
cores = multiprocessing.cpu_count()
print(f"Available cores: {cores}")
Available cores: 10
cores = Sys.CPU_THREADS;
println("Available cores: $cores");
Available cores: 4

26.6.2 Parallel implementation

Each language provides different mechanisms for parallel processing. The examples below show complete workflows that run the backtest in parallel and save the results with metadata.

26.6.2.1 R: mclapply (Mac/Linux)

The mclapply() function provides efficient parallel processing on Unix systems through forking. It distributes work across cores with minimal overhead.

# Run parallel backtest
backtest = mclapply(
  (par$WE+1):par$T,
  RunOneDay,
  y = sp500$y,
  par = par,
  Methods = par$Methods,
  mc.cores = cores,
  mc.preschedule = TRUE
)
backtest = data.frame(do.call(rbind, backtest))
names(backtest) = RunOneDay(par = par, Methods = par$Methods, HeaderOnly = TRUE)
backtest$date = sp500$date[backtest$index]
backtest$date.t = ymd(backtest$date)

# Create metadata
metadata = list(
  run_date = Sys.time(),
  machine = Sys.info()["nodename"],
  start_date = as.character(min(backtest$date)),
  end_date = as.character(max(backtest$date)),
  R_version = R.version.string,
  tsgarch_version = packageVersion("tsgarch")
)

# Save with metadata
results = list(backtest = backtest, par = par, metadata = metadata)
save(results, file = "Risk/backtest.RData")

26.6.2.2 R: foreach with doParallel (all platforms)

For Windows compatibility, or when forking is unavailable, the foreach package with doParallel provides a cluster-based alternative that works on all operating systems.

library(foreach)
library(doParallel)

cl = makeCluster(cores)
registerDoParallel(cl)

clusterEvalQ(cl, {
  library(reshape2)
  source("common/functions.r", chdir = TRUE)
})
clusterExport(cl, c("par", "sp500"))

backtest = foreach(i = (par$WE+1):par$T) %dopar% {
  RunOneDay(i, y = sp500$y, par = par, Methods = par$Methods)
}
stopCluster(cl)

backtest = data.frame(do.call(rbind, backtest))
names(backtest) = RunOneDay(par = par, Methods = par$Methods, HeaderOnly = TRUE)
backtest$date = sp500$date[backtest$index]
backtest$date.t = ymd(backtest$date)

# Create metadata (created in the parent process after the loop, once
# results have been collected back from the workers)
metadata = list(
    run_date = Sys.time(),
    machine = Sys.info()["nodename"],
    start_date = as.character(min(backtest$date)),
    end_date = as.character(max(backtest$date)),
    R_version = R.version.string,
    tsgarch_version = packageVersion("tsgarch")
)

results = list(backtest = backtest, par = par, metadata = metadata)
save(results, file = "Risk/backtest.RData")

The cluster approach requires explicitly loading libraries and exporting variables to worker processes, unlike mclapply() which inherits the parent environment through forking.

26.6.2.3 Python: joblib

Python’s joblib library provides straightforward parallel processing with Parallel and delayed. The n_jobs=-1 parameter uses all available cores.

from joblib import Parallel, delayed
import pickle
import platform
from datetime import datetime
import arch

# Run parallel backtest
results = Parallel(n_jobs=-1)(
    delayed(RunOneDay)(t, y, par, par['Methods'])
    for t in range(par['WE'], par['T'])
)
backtest = pd.DataFrame(results)
backtest['date'] = sp500['date'].values[par['WE']:par['T']]
backtest['date.t'] = pd.to_datetime(backtest['date'].astype(str), format='%Y%m%d')

# Create metadata
metadata = {
    'run_date': datetime.now().isoformat(),
    'machine': platform.node(),
    'start_date': str(backtest['date'].min()),
    'end_date': str(backtest['date'].max()),
    'python_version': platform.python_version(),
    'arch_version': arch.__version__,
    'numpy_version': np.__version__,
    'pandas_version': pd.__version__
}

# Save with metadata
with open("Risk/backtest.pkl", "wb") as f:
    pickle.dump({'backtest': backtest, 'par': par, 'metadata': metadata}, f)

26.6.2.4 Julia: Threads.@threads

Julia provides native multi-threading. The @threads macro distributes loop iterations across available threads. Julia threads share memory, avoiding the serialisation overhead of process-based parallelism.

using Pkg, Serialization

# Run parallel backtest
results = Vector{Dict{String, Any}}(undef, par["WT"])
Threads.@threads for i in 1:par["WT"]
    t = par["WE"] + i
    results[i] = RunOneDay(t, y, par; Methods=par["Methods"])
end
backtest = DataFrame(results)
backtest[!, :date] = [sp500[Int(idx), :date] for idx in backtest[!, :index]]

# Create metadata
metadata = Dict(
    "run_date" => string(Dates.now()),
    "machine" => gethostname(),
    "start_date" => string(minimum(backtest[!, :date])),
    "end_date" => string(maximum(backtest[!, :date])),
    "julia_version" => string(VERSION),
    "ARCHModels_version" => let
        deps = Pkg.dependencies()
        uuid = first(k for (k, v) in deps if v.name == "ARCHModels")
        string(deps[uuid].version)
    end
)

# Save with metadata
serialize("Risk/backtest.jls", Dict("backtest" => backtest, "par" => par, "metadata" => metadata))

26.7 Sanity check

A quick plot of VaR against actual returns serves as a sanity check. The VaR line should track volatility clusters, and returns should occasionally breach the threshold but not too often.

26.8 Next steps

We now have a complete backtest dataset with VaR and ES forecasts from four models (HS, EWMA, GARCH, GARCH-t) across thousands of trading days. The data is saved in native formats for each language:

  • R: Risk/backtest.RData
  • Python: Risk/backtest.pkl
  • Julia: Risk/backtest.jls

The next chapter, Chapter 27, loads these files and implements statistical tests for backtesting validation, including:

  • Violation frequency analysis to test if violations occur at the expected rate;
  • Independence testing to check if violations are clustered;
  • Comparative analysis across HS, EWMA, GARCH and GARCH-t models;
  • ES evaluation using the Acerbi-Székely approach.