Calibrate to the Market

Fit GARCH by Maximum Likelihood

Java simulates a seeded GARCH(1,1) series, fits its likelihood on the TeaVM CPU engine and estimates EWMA decay. The browser workload is capped by the observation count.

Data-generating GARCH(1,1)
Simulation
Java source
GarchCalibrationRiskStudio.java

This exact source runs in TeaVM. Form changes update its Java literals and reset manual edits.

import com.nablatensor.quant.estimate.Ewma;
import com.nablatensor.quant.estimate.Fit;
import com.nablatensor.quant.estimate.Garch11;
import java.util.Locale;
import java.util.Random;

/**
 * nablatensor.com/learn, chapter 7.6: the real {@code GarchMleShowcase}, at a
 * far smaller observation count. {@code Garch11.fit} already hardcodes {@code
 * .on("cpu")} internally -- the recorded negative log-likelihood unrolls one
 * five-op recursion step per observation, so even a few thousand observations
 * builds a tape past what {@code cpu-jit}'s straight-line bytecode kernel can
 * emit, the same JVM classfile ceiling this page's own text measures. The
 * scalar engine has no such ceiling, which is the entire reason this example
 * runs on it rather than the fast one.
 */
public final class GarchCalibrationRiskStudio {
  private GarchCalibrationRiskStudio() {}

  public static void main(String[] args) {
    int n = 400;
    long seed = 20260906L;
    double omega = 2e-6, alpha = 0.07, beta = 0.92;
    double[] returns = simulate(omega, alpha, beta, n, seed);
    long t0 = System.nanoTime();
    Fit fit = Garch11.fit(returns);
    double ms = (System.nanoTime() - t0) / 1e6;
    double[] se = fit.standardErrors();
    row("omega", omega, fit.params()
        .omega(), se[0]);
    row("alpha", alpha, fit.params()
        .alpha(), se[1]);
    row("beta", beta, fit.params()
        .beta(), se[2]);
    double lambda = Ewma.estimateByMaximumLikelihood(returns);
    double fittedOmega = fit.params()
        .omega(), fittedAlpha = fit.params()
        .alpha(), fittedBeta = fit.params()
        .beta();
    double annVol = Math.sqrt(fittedOmega / (1 - fittedAlpha - fittedBeta)) * Math.sqrt(252.0);
    double trueAnnVol = Math.sqrt(omega / (1 - alpha - beta)) * Math.sqrt(252.0);
    System.out.println("RESULT|" + fittedOmega + "|" + fittedAlpha + "|" + fittedBeta + "|" + fit.persistence()
        + "|" + annVol + "|" + trueAnnVol + "|" + lambda + "|" + fit.iterations() + "|" + ms);
  }

  private static void row(String name, double truth, double fitted, double stdError) {}

  private static double[] simulate(double w, double a, double b, int n, long seed) {
    Random rng = new Random(seed);
    double[] r = new double[n];
    double s2 = w / (1.0 - a - b);
    for (int t = 0; t < n; t++) {
      r[t] = Math.sqrt(s2) * rng.nextGaussian();
      s2 = w + a * r[t] * r[t] + b * s2;
    }
    return r;
  }
}
TeaVM compiles and runs the Java source above in this browser.
Implementation guide

Volatility clustering as a long recursion

GARCH fits conditional variance dynamics from returns, making its likelihood a sequential computation with potentially many thousands of observations.

Core mechanism

Each conditional variance depends on its predecessor and the prior shock; the log-likelihood accumulates over the time series. An adjoint score differentiates that entire recursion with respect to model parameters.

Practical workflow

Clean returns, choose a distributional assumption, fit with constraints, inspect persistence and residual diagnostics, then test stability over rolling samples.

Key details

*Keywords: garch java, garch maximum likelihood java, ewma volatility java, riskmetrics lambda java, volatility estimation java, pca yield curve java*

Feature F4. Estimating volatilities and correlations is a curriculum chapter in its own right. The point here is small and specific: the Gaussian negative log-likelihood of a return series is recorded once, and every optimiser iteration reads the exact score vector — d(logL)/d(theta) — from a single adjoint sweep, the same way a SABR or Heston calibration reads its gradient off an option surface.

In a GARCH(1,1) model, the next conditional variance is driven by a constant, the previous squared innovation and the previous conditional variance. The parameters therefore describe immediate shock response and persistence, while the likelihood evaluates how plausible the observed return sequence is under those conditional variances.

The usual stationarity and positivity constraints are not cosmetic optimiser bounds: they determine whether the variance recursion is well behaved. After fitting, residual diagnostics and out-of-sample behaviour are needed because a high in-sample likelihood does not establish a useful volatility forecast.

Scope and review point

For very long histories, recording and memory become engineering constraints. Statistical adequacy also requires diagnostics beyond optimiser convergence.