Skip to content

GJR-GARCH process and paths

GJR-GARCH couples spot returns and variance with an asymmetric leverage effect. The shared Rust process is exposed through C, Go and Python. This is a stochastic process and simulation API, not the historical Garch11 estimator. Calibrated models and European engines are documented separately in GJR-GARCH pricing (#1165, unreleased).

Parameters and units

Input Meaning
spot Positive initial spot; the process retains a live quote
daily_variance Nonnegative daily initial variance, not volatility
omega Nonnegative daily variance intercept
alpha, beta, gamma Daily GJR coefficients
lambda_ / Go Lambda Finite risk-premium parameter
days_per_year Positive annualization factor, conventionally 252
horizon Nonnegative time in years

All numbers must be finite. This library requires alpha >= 0, beta >= 0 and alpha + gamma >= 0. These are explicit supported-domain restrictions: the upstream process itself does not validate parameters, and its calibration model has a different constraint contract. Zero initial variance and intercept are supported deterministic boundary cases.

State component zero is spot; component one is annualized variance. Initial variance is days_per_year * daily_variance; the drift intercept is days_per_year**2 * omega. Do not pass annualized variance as the daily input.

Persistence is beta + alpha * (1 + lambda_**2) + gamma * q3, where q3 = lambda_ * normal_pdf(lambda_) + (1 + lambda_**2) * normal_cdf(lambda_). With a positive intercept, a finite stationary mean variance requires persistence below one; finite-horizon simulation does not impose this condition. No stationary-moment claim is made for other parameters. Unrepresentable coefficients or simulated states raise errors.

Supported schemes

The implementation follows the executable pinned QuantLib process. It uses 2 + 4 * lambda_**2 in the diffusion, not the header's fourth-power typo.

Python scheme Go constant / C value Negative variance handling in evolution
PartialTruncation GJRPartialTruncation / 0 Raw variance in drift; zero diffusion
FullTruncation GJRFullTruncation / 1 Positive part in drift/diffusion; raw state retained
Reflection GJRReflection / 2 Absolute variance in drift/diffusion and state base

Python defaults to FullTruncation; select the Go scheme explicitly. Unknown schemes are rejected. Raw variance output can be negative under every scheme: none guarantees a positive next Euler state. Do not silently clip the returned state. Reflection's infinitesimal diffusion uses a signed square root for negative variance, whereas its evolution uses a positive square root. At zero elapsed time a negative Reflection state becomes its absolute value.

Seeded paths

import itofin

config = dict(
    spot=100.0, daily_variance=0.04 / 252.0,
    risk_free_rate=0.05, dividend_yield=0.02,
    omega=2e-6, alpha=0.03, beta=0.90, gamma=0.04, lambda_=0.1,
    days_per_year=252.0, horizon=1.0, steps=252, paths=8, seed=42,
    scheme="FullTruncation",
)
full = itofin.simulate_gjr(**config)
terminal = itofin.simulate_gjr(**config, terminal_only=True)
assert full.shape == (8, 253, 2)
assert terminal.shape == (8, 2)
assert (terminal == full[:, -1, :]).all()
config := itofin.GJRConfig{
    Spot: 100, DailyVariance: 0.04 / 252,
    RiskFreeRate: 0.05, DividendYield: 0.02,
    Omega: 2e-6, Alpha: 0.03, Beta: 0.90, Gamma: 0.04, Lambda: 0.1,
    DaysPerYear: 252, Horizon: 1, Steps: 252, Paths: 8, Seed: 42,
    Scheme: itofin.GJRFullTruncation,
}
full, err := itofin.SimulateGJR(config)
if err != nil { return err }
fmt.Println(full.Paths, full.Times, full.Assets)
use libitofin::errors::QlResult;
use libitofin::methods::montecarlo::gjr_paths::{GjrRequest, gjr_paths};
use libitofin::processes::GjrGarchDiscretization;

fn main() -> QlResult<()> {
    let request = GjrRequest {
        spot: 100.0,
        daily_variance: 0.04 / 252.0,
        risk_free_rate: 0.05,
        dividend_yield: 0.02,
        omega: 2e-6,
        alpha: 0.03,
        beta: 0.90,
        gamma: 0.04,
        lambda: 0.1,
        days_per_year: 252.0,
        horizon: 1.0,
        steps: 252,
        paths: 8,
        seed: 42,
        discretization: GjrGarchDiscretization::FullTruncation,
        terminal_only: false,
    };
    let full = gjr_paths(&request)?;
    let terminal = gjr_paths(&GjrRequest {
        terminal_only: true,
        ..request
    })?;
    for path in 0..request.paths {
        let offset = (path * (request.steps + 1) + request.steps) * 2;
        assert_eq!(&full[offset..offset + 2], &terminal[path * 2..path * 2 + 2]);
    }
    println!(
        "{} paths, {} times, 2 components",
        request.paths,
        request.steps + 1
    );
    Ok(())
}

The normal stream is MT19937 plus inverse-normal transformation, consumed in path, step, spot-factor, variance-factor order. Each step consumes two draws, including zero-horizon runs. Seed zero is rejected. Identical inputs use the same Rust kernel in all bindings; terminal values exactly match full-path tails. Increasing path count preserves the existing path prefix.

Full layout is [path, time, component], including time zero; terminal layout is [path, component]. Python returns owned C-contiguous NumPy float64 arrays. Go returns owned Simulation.Values, with Assets=2 and row-major indexing. Zero horizon repeats the initial positive-spot/nonnegative-variance state.

The default Python/Go limit is 16,777,216 output values (128 MiB). max_output_values=0 / Go MaxOutputValues=0 selects that limit; positive values override it. Negative limits, invalid dimensions, arithmetic overflow and native allocation failures are errors. Terminal mode reduces output memory, not the number of simulated steps.

The live process retains its market inputs; stateless paths instead use copied flat rates and parameters. Closing external Go input handles does not release the process's retained inputs. Calls through a closed process/session fail.

simulate_gjr

simulate_gjr(spot: float, daily_variance: float, risk_free_rate: float, dividend_yield: float, omega: float, alpha: float, beta: float, gamma: float, lambda_: float, horizon: float, steps: int, paths: int, seed: int, days_per_year: float = 252.0, scheme: str = 'FullTruncation', terminal_only: bool = False, max_output_values: int = 0) -> NDArray[float64]

Generate seeded GJR-GARCH diffusion-approximation spot/variance paths.

Daily daily_variance and omega parameters are converted internally. Output component zero is spot; component one is raw annual variance, which may be negative with truncation schemes. This is not the daily GJR recursion. Rates and horizon are annualized, continuously compounded.

scheme is PartialTruncation, FullTruncation or Reflection. Each step consumes two MT19937/inverse-normal draws in path/time/factor order, even for zero horizon. Equal seeds and inputs reproduce the same paths across Python and Go. Zero seed is rejected.

Returns:

Type Description
NDArray[float64]

numpy.ndarray: Owned contiguous float64 values shaped (paths, steps + 1, 2) including initial state, or (paths, 2) with terminal_only. Terminal rows match final full rows exactly.

Raises:

Type Description
ItofinError

If parameters, time grid, simulation or output size are invalid. max_output_values=0 selects 16,777,216 values (128 MiB).

See the process API reference.