Market Risk Simulation for Multi‑Currency Revenue Linked to Brent (EUR Book)

This guide shows how to simulate monthly revenue risk when commodity prices are linked to Brent (in USD), the book currency is EUR, and you have revenue streams across EUR, USD, INR, and AUD markets. We model Brent with a mean‑reverting process and preserve correlation with FX using a Monte Carlo method.

Assumptions:

  • Prices are monthly and linked to Brent in USD.
  • Brent follows a mean‑reverting (Ornstein–Uhlenbeck) process in log‑space.
  • FX pairs are modeled in log‑space (e.g., GBM for simplicity) and jointly simulated with Brent via a correlation matrix.
  • Book currency is EUR; portfolio revenue is aggregated in EUR.

Why this setup works:

  • OU captures commodity mean reversion (to a long‑run mean) better than pure GBM.
  • A single correlated shock vector at each month preserves co‑movement between Brent and FX (EURUSD, USDINR, USDAUD).

Modeling

Notation (monthly step $\Delta t = 1/12$):

  • Brent price (USD): $P_t$. We simulate $X_t = \ln P_t$ via OU:

    $$ dX_t = \kappa(\theta - X_t) dt + \sigma dW_t. $$

    • Discrete:

    $$ X_{t+\Delta} = \theta + (X_t - \theta) e^{-\kappa\Delta} + \sigma_\Delta \varepsilon_t, $$

    where $\sigma_\Delta = \sigma \sqrt{\tfrac{1 - e^{-2\kappa\Delta}}{2\kappa}}$. Then $P_{t+\Delta} = e^{X_{t+\Delta}}$.

  • FX pairs in log space:

    • $Y^{\mathrm{EURUSD}}_t = \ln(\mathrm{EURUSD}_t)$, $Y^{\mathrm{USDINR}}_t$, $Y^{\mathrm{USDAUD}}_t$.
    • GBM discretization (log form): $Y_{t+\Delta} = Y_t + (\mu - \tfrac{1}{2}\sigma^2) \Delta + \sigma\sqrt{\Delta} \zeta_t$.
  • Correlation: Stack shocks $[\varepsilon_t, \zeta_t^{\mathrm{EURUSD}}, \zeta_t^{\mathrm{USDINR}}, \zeta_t^{\mathrm{USDAUD}}]$ as a 4‑vector $Z_t$ with $\operatorname{Corr}(Z_t) = C$. Use Cholesky: $Z_t = L u_t$, $u_t \sim \mathcal{N}(0, I)$.

Portfolio revenue in EUR at month $t$ (spot conversion):

  • For any USD‑denominated exposure amount $A_{\mathrm{USD}, t}$, EUR value is $A_{\mathrm{USD}, t} / \mathrm{EURUSD}_t.$
  • If revenue is invoiced/settled locally (INR/AUD) but priced in USD and converted at spot, the USD→LOC→EUR conversion collapses to the same USD→EUR conversion at time $t$. If local currency is held for $m$ months before converting to EUR, you’d add FX path risk for that holding period (see note below).

Algorithm (monthly Monte Carlo)

  1. Calibrate parameters from monthly history:
    • OU: κ, θ, σ for ln Brent.
    • FX: (μ, σ) for ln EURUSD, ln USDINR, ln USDAUD.
    • Correlation matrix C across [Brent OU shock, EURUSD, USDINR, USDAUD].
  2. Choose horizon T (months) and number of paths N.
  3. Precompute L = chol(C).
  4. For t = 1..T and each path i:
    • Draw u ~ N(0, I_4), set Z = L u.
    • Update ln Brent via OU using Z[0]. Update FX logs using Z[1..3].
    • Compute prices and FX levels for month t.
    • Compute EUR revenue for each stream and sum to portfolio.
  5. Aggregate distribution statistics (mean, P50/P90, VaR, ES) across paths.

Note on local currency holding periods:

  • If a portion of revenue is held in INR/AUD for m_t months before converting to EUR, multiply the USD‑priced amount by the FX leg to LOC at receipt and by the inverse EUR leg at conversion time; this introduces additional FX path dependency. The same correlated shock machinery applies; just reference the FX states at the later conversion month.

Minimal Python example (NumPy)

This example simulates 36 monthly steps with 50k paths. Adjust N for speed/accuracy.

import numpy as np

# --- Config ---
T = 36                 # months
N = 50_000             # paths
dt = 1.0/12.0

# Volumes (barrels or energy units) per stream
V_USD = 1.0
V_EUR = 0.5
V_INR = 0.7
V_AUD = 0.8

# Brent OU (on log price)
kappa = 1.0            # mean reversion speed (per year)
theta = np.log(75.0)   # long-run ln(USD/bbl)
sigma_ou = 0.35        # annual vol of OU innovation
X0 = np.log(80.0)      # initial ln price

# FX logs (GBM in log form)
mu_eurusd = 0.00;  sig_eurusd = 0.12
mu_usdinr = 0.02;  sig_usdinr = 0.10
mu_usdaud = -0.01; sig_usdaud = 0.13

Y0_eurusd = np.log(1.10)  # EURUSD ~ 1.10 USD per EUR
Y0_usdinr = np.log(83.0)
Y0_usdaud = np.log(1.55)

# Correlation matrix across [OU shock, EURUSD, USDINR, USDAUD]
Corr = np.array([
    [ 1.00, -0.35,  0.25, -0.15],
    [-0.35,  1.00, -0.10,  0.40],
    [ 0.25, -0.10,  1.00, -0.05],
    [-0.15,  0.40, -0.05,  1.00],
])

# Cholesky (ensure positive definiteness)
L = np.linalg.cholesky(Corr)

# Precompute OU discrete parameters
exp_kdt = np.exp(-kappa*dt)
sigma_dt_ou = sigma_ou * np.sqrt((1.0 - np.exp(-2.0*kappa*dt)) / (2.0*kappa))

def simulate():
    # State arrays: only need running values + optional storage
    X = np.full(N, X0)
    Yeu = np.full(N, Y0_eurusd)
    Yinr = np.full(N, Y0_usdinr)
    Yaud = np.full(N, Y0_usdaud)

    # Accumulate portfolio revenue in EUR across months (sum of monthly revenue)
    portfolio = np.zeros(N)

    for _ in range(T):
        u = np.random.normal(size=(4, N))
        Z = L @ u  # correlated shocks, shape (4, N)

        # Brent OU in log space
        X = theta + (X - theta) * exp_kdt + sigma_dt_ou * Z[0]
        P = np.exp(X)

        # FX in log space (Euler in logs)
        Yeu = Yeu + (mu_eurusd - 0.5*sig_eurusd**2)*dt + sig_eurusd*np.sqrt(dt)*Z[1]
        Yinr = Yinr + (mu_usdinr - 0.5*sig_usdinr**2)*dt + sig_usdinr*np.sqrt(dt)*Z[2]
        Yaud = Yaud + (mu_usdaud - 0.5*sig_usdaud**2)*dt + sig_usdaud*np.sqrt(dt)*Z[3]

        EURUSD = np.exp(Yeu)   # USD per EUR
        USDINR = np.exp(Yinr)  # INR per USD
        USDAUD = np.exp(Yaud)  # AUD per USD

        # Monthly revenue per stream in EUR (spot conversion at month-end)
        # USD stream: USD to EUR via divide by EURUSD
        rev_usd_eur = V_USD * P / EURUSD

        # EUR stream: priced in USD but settled directly in EUR at spot
        rev_eur_eur = V_EUR * P / EURUSD

        # INR stream: priced in USD then converted INR at spot, then to EUR at spot (collapses to USD->EUR)
        rev_inr_eur = V_INR * P / EURUSD

        # AUD stream: same logic
        rev_aud_eur = V_AUD * P / EURUSD

        portfolio += (rev_usd_eur + rev_eur_eur + rev_inr_eur + rev_aud_eur)

    return portfolio

portfolio = simulate()

mean = portfolio.mean()
p50 = np.quantile(portfolio, 0.50)
p90 = np.quantile(portfolio, 0.90)
var95 = np.quantile(portfolio - mean, 0.05)  # downside at 5%

print({
    'mean': float(mean),
    'p50': float(p50),
    'p90': float(p90),
    'VaR(95%)': float(var95)
})

Calibration tips:

  • OU parameters for ln Brent: estimate κ, θ, σ from monthly log prices via least squares (AR(1) mapping) or MLE.
  • FX: estimate drift/vol on monthly log changes; set drift to 0 if using risk‑neutral for pricing.
  • Correlation matrix: compute Pearson correlations on standardized monthly innovations; ensure positive definite (shrink towards identity if needed).

Extensions:

  • Basis differentials per stream: price_i = Brent + basis_i (USD) or multiplicative premium.
  • Local currency holding periods: convert at receipt (USD→LOC) and later to EUR after m months; reuse the simulated path at the later month.
  • Include inflation, freight, or discounting; compute present value in EUR.
  • Stress tests: shift κ or vol, twist the correlation matrix, or apply FX devaluation scenarios.

Validation:

  • Check sample correlations of simulated shocks/path returns match target C (within Monte Carlo error).
  • Ensure OU unconditional mean near exp(θ) and half‑life ≈ ln(2)/κ in years.

This approach gives a consistent, monthly Monte Carlo engine that preserves correlation between Brent and FX while keeping a EUR‑book view of portfolio revenue.


Historical reconstruction for knowledge retention & reference; not investment advice.