LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Monte Carlo Methods: Simulating Your Way Out of Hard Math

monte-carlopythonstatisticsrisk-managementsimulation

The deepest problems in engineering and quantitative finance share an uncomfortable property: the exact answer is either analytically intractable or so expensive to derive that approximation is the only viable path. Monte Carlo methods are the systematic answer to this class of problem. Rather than attacking an integral analytically, you estimate it by drawing random samples from the relevant distribution and averaging the result. The approximation converges predictably, the error is quantifiable, and the approach generalizes to virtually any quantity that can be expressed as an expectation. Understanding how and why Monte Carlo works — and precisely when to reach for a different tool — is essential for anyone building systems that reason under uncertainty.


The Core Idea

Monte Carlo estimation rests on a single theorem from probability theory. If you want to compute:

I = ∫ f(x) p(x) dx = E_p[f(X)]

and you cannot evaluate that integral analytically, you can estimate it by drawing N independent samples from the distribution p(x) and computing the sample mean:

Î_N = (1/N) * Σ f(x_i)   where x_i ~ p(x)

By the law of large numbers, Î_N converges almost surely to I as N grows. By the central limit theorem, the error of the estimate is approximately normally distributed with standard deviation σ/√N, where σ is the standard deviation of f(X) under p. This gives you an asymptotically valid confidence interval on the estimate without knowing anything about the shape of f.

The power of this framing is that it collapses nearly every hard estimation problem — multivariate integration, option pricing, portfolio loss distributions, network reliability — into the same algorithmic structure: sample, evaluate, average.


Estimating π: The Hello World

Before applying Monte Carlo to anything serious, it is worth being precise about what the method does and does not guarantee, using the simplest possible example.

Consider a unit square with a quarter-circle of radius 1 inscribed in one corner. The area of the quarter-circle is π/4. If you throw darts uniformly at random into the square, the fraction that land inside the quarter-circle converges to π/4. Multiply by 4, and you have an estimate of π.

Dart-throwing setup (unit square, quarter-circle in lower-left corner):

  1.0 +---------------------------+
      |  .  .                     |
      | . .  .  .                 |
      |. . .  .  . .              |  Dots inside arc  → "hit"
      | .  . .  .   .  .          |  Spaces outside   → "miss"
      |.  .  .  .    .   .        |
  0.5 | .  .  .  .     .    .     |
      |.  .  .  .  .     .     .  |
      |  .  .  .  .  .     .     .|
      | .  .  .   .  . .    .     |
      |.  .  .  .  .    .  .  .   |
  0.0 +---------------------------+
      0.0                       1.0

  π ≈ 4 × (hits / total)

The convergence rate is the most important thing to understand here. With N samples, the standard error of the π estimate is roughly 1.64/√N. You need 10,000 samples to get the error below 0.016 — two decimal places. Four decimal places requires a million samples. The convergence is slow by design. Monte Carlo earns its keep in high-dimensional spaces, where analytic methods scale exponentially in the number of dimensions and Monte Carlo’s 1/√N rate is dimension-independent.

For π specifically, you should never use Monte Carlo. Machin’s formula or the Bailey-Borwein-Plouffe algorithm compute arbitrary digits in far less time. The dart-throwing example is a pedagogical exercise in convergence, not a recommendation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import numpy as np

rng = np.random.default_rng(seed=42)

def estimate_pi(n_samples: int, rng: np.random.Generator) -> float:
    """Estimate π using Monte Carlo dart throwing."""
    x = rng.uniform(0.0, 1.0, size=n_samples)
    y = rng.uniform(0.0, 1.0, size=n_samples)
    inside = (x**2 + y**2) <= 1.0
    return 4.0 * inside.mean()

for n in [1_000, 10_000, 100_000, 1_000_000]:
    estimate = estimate_pi(n, rng)
    error = abs(estimate - np.pi)
    print(f"n={n:>9,}  π̂={estimate:.6f}  error={error:.6f}")

The output illustrates the 1/√N convergence empirically: the error at 1,000,000 samples is roughly 31.6 times smaller than at 1,000 samples — exactly √1000 improvement for a 1000x increase in computation.


Pseudo-Random Generation and Seeding Discipline

Monte Carlo results are only as reliable as the random numbers that drive them. Most implementations use pseudo-random number generators (PRNGs), which produce deterministic sequences that pass statistical tests for randomness. This is both a feature and a trap.

The feature: a fixed seed produces the same sequence every time. Seeding is essential for reproducibility in any system where you need to audit, debug, or compare runs. A simulation whose results cannot be reproduced exactly is a liability in any production context.

The trap: PRNGs have finite state spaces. The Mersenne Twister, the default generator in many languages and older numpy code, has a period of 2^19937 − 1, which is enormous in practice. But it has known statistical weaknesses — particularly in high-dimensional uniformity — that can corrupt Monte Carlo estimates in certain applications. Numpy’s modern default_rng interface uses the PCG64 generator, which is both faster and statistically stronger. Use it.

Common PRNG pitfalls:

Unintentional seed reuse. If two independent simulation components initialize with the same seed (or both use time() as a seed within the same second), their outputs are correlated. In a multi-process Monte Carlo pipeline, each worker must receive a distinct, carefully derived seed — not the same value or a trivially incremented one.

Re-seeding in a loop. Creating a new PRNG object with a fixed seed inside a sampling loop resets the sequence to the same starting point every iteration. This produces identical samples, not independent ones.

Using random.seed() alongside np.random.seed(). Python’s built-in random module and numpy’s legacy np.random interface maintain separate state. Code that mixes them without careful management produces reproducibility failures that are difficult to trace.

1
2
3
4
5
6
# Correct pattern: create one generator, thread it through your functions
rng = np.random.default_rng(seed=2024)

# For parallel workers: derive per-worker generators from a common seed
seeds = np.random.SeedSequence(2024).spawn(8)   # 8 independent generators
workers = [np.random.default_rng(s) for s in seeds]

For production simulation pipelines, store the seed alongside every result. When a bug surfaces, you need to reconstruct the exact run that produced the anomalous output. See Python for DevOps for how to wire seed storage into logging and artifact management.


Convergence and the Central Limit Theorem

The central limit theorem underpins the entire error-analysis framework for Monte Carlo. For an estimator Î_N based on N i.i.d. samples of a function f with mean μ and standard deviation σ, the CLT says:

√N × (Î_N − μ) → N(0, σ²)  as N → ∞

This gives the standard error of the estimate as σ/√N. A 95% confidence interval on the estimate is:

μ ∈ [Î_N − 1.96×σ̂/√N,  Î_N + 1.96×σ̂/√N]

where σ̂ is the sample standard deviation of f(x_i) — which you already have from the simulation.

The convergence picture looks like this:

Estimation error vs. sample size  (log-log scale)

  0.1  |*
       | *
       |   *
 Error |     *         Slope = -1/2  (i.e., 1/√N)
       |       *
 0.01  |         *
       |           *
       |             *
0.001  |               *
       +--+--+--+--+--+--+--
       100  1K  10K 100K 1M   N

The practical implication is simple but easy to misapply: to halve your error, you need four times as many samples. Getting one more decimal place of precision costs a factor of 100 in computation. This is why variance reduction is not a micro-optimization — it is the only way to make high-precision Monte Carlo computationally feasible.

Sample size decisions. Given a target confidence interval width ε at confidence level 1 − α:

N ≥ (z_{α/2} × σ / ε)²

The difficulty is that σ is unknown before you run the simulation. Standard practice is a pilot run: generate 1,000–10,000 samples, estimate σ̂, then compute the required N for your target precision. This is almost always cheaper than running a single massive simulation at an arbitrary N and hoping the error is small enough.


Variance Reduction Techniques

The core limitation of naive Monte Carlo is the σ in the σ/√N error term. If the function f is highly variable — large swings in value across the sample space — convergence is slow. Variance reduction techniques reduce σ without biasing the estimate, effectively making each sample carry more information.

Technique Core Idea Best When Caution
Antithetic variates For each sample u, also evaluate at 1−u (or −u for normals); average the pair f is monotone in u; generates negative correlation between pairs Loses benefit if f is not monotone; modest gain on complex surfaces
Importance sampling Sample from a proposal distribution q that concentrates draws in high-impact regions; reweight by p/q Estimating rare-event probabilities; tail risk; the region of interest is a small fraction of the sample space Choosing a bad q inflates variance; q must cover the support of p×f
Control variates Identify a correlated function g with known mean E[g]; use deviations from E[g] to correct the estimate A known closed-form approximation exists; any well-understood proxy quantity Requires an analytically tractable g; gain depends on correlation strength
Stratified sampling Partition the sample space into strata; allocate samples to each stratum; combine High-dimensional integration where certain dimensions matter more Partition must be chosen before sampling; gains are modest in very high dimensions
Quasi-Monte Carlo Replace pseudo-random samples with low-discrepancy sequences (Sobol, Halton) Smooth, moderate-dimensional integrands Convergence theory breaks down for highly discontinuous functions

Antithetic variates in practice. For a simulation driven by standard normal variates Z, each sample z_i is paired with −z_i. The estimate is averaged over both evaluations. The variance of the pair average is:

Var[(f(z) + f(−z))/2] = (Var[f(z)] + Cov[f(z), f(−z)]) / 2

If f is monotone in z, f(z) and f(−z) are negatively correlated, and the covariance term is negative, reducing total variance. For estimating quantiles of a symmetric distribution, antithetic variates can roughly halve the required sample count.

Importance sampling in one sentence. You are solving the wrong integral — replace it with one that is easier. If the event you care about (say, a 1-in-1000 portfolio loss) occupies a tiny fraction of the sample space, most naive samples fall in the uninteresting region. Importance sampling draws samples from a distribution that concentrates on the tail, then corrects for the sampling bias by multiplying each evaluation by p(x)/q(x). The estimator remains unbiased; the variance drops dramatically.

Control variates. If you have an option whose price you cannot solve analytically but which behaves similarly to an option you can price exactly, the difference between the simulated price and the known price can be used to debias your estimate. The closer the two option payoffs track each other, the more variance is removed.


Monte Carlo VaR: The Fat-Tail Advantage

Value at Risk (VaR) quantifies the loss threshold exceeded with probability (1 − α) over a given horizon. At the 99% confidence level, you are estimating the 1st percentile of the portfolio’s return distribution. The three main approaches — parametric, historical, and Monte Carlo — make fundamentally different assumptions, and those differences matter most exactly where risk measurement matters most: in the tails.

Parametric VaR assumes returns are normally distributed and computes VaR analytically from the sample mean and standard deviation. It is fast, closed-form, and wrong in the tail. Financial returns have excess kurtosis — fatter tails than a Gaussian model predicts — which means parametric VaR systematically underestimates losses at high confidence levels. The degree of underestimation grows as you push toward 99.9% or 99.99%.

Historical VaR uses the empirical distribution of past returns. It captures whatever fat tails actually occurred in the sample window — but it cannot see risk that has not happened yet, and it is sensitive to the choice of lookback period.

Monte Carlo VaR generates simulated return paths from an explicit model, reads off the simulated loss distribution, and reports the empirical percentile. The key advantage is model flexibility: you choose the generating distribution. Use a Student’s t with estimated degrees of freedom, and you have a principled fat-tail model whose parameters are estimated from data rather than assumed. Use a regime-switching model, and you capture the qualitative change in correlation structure between calm and crisis periods.

Dimension Parametric VaR Historical VaR Monte Carlo VaR
Distribution assumption Normal (Gaussian) Empirical (past data) Chosen explicitly (t, GBM, etc.)
Fat-tail handling Poor — underestimates tail losses Good within sample window Good if model is specified correctly
Multi-asset correlations Variance-covariance matrix Implicit in historical returns Explicit covariance/copula model
Speed Very fast (closed form) Fast (sort and slice) Slow (requires N simulated paths)
Sensitivity to model error Low (few parameters) None (non-parametric) High — garbage in, garbage out
Scenario flexibility None Limited to history Full — can stress-test any scenario
Sample size requirement Moderate Large (crisis coverage needed) N simulated paths (tunable)

The fat-tail advantage of Monte Carlo VaR is only realized if you use a fat-tailed generating distribution. Feeding a multivariate normal generator into a Monte Carlo framework produces a worse version of parametric VaR — the same distributional assumption, the same tail blindness, plus additional computation. The Student’s t distribution with ν degrees of freedom is the practical default: kurtosis is 3 + 6/(ν−4) for ν > 4, and ν = 4–8 fits daily equity returns well. At ν = 4, the tails are substantially fatter than the normal. For a full treatment of VaR metrics and their failure modes, see the risk-metrics guide.


Python Walkthrough: MC VaR with Numpy

The following implements Monte Carlo VaR for a two-asset portfolio using a multivariate Student’s t generating process. It includes antithetic variates as a variance reduction step, reports the simulated VaR and CVaR, and compares against the parametric normal benchmark.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
import numpy as np
from scipy import stats

# ── Portfolio parameters ──────────────────────────────────────────────────────

# Two-asset portfolio: asset weights, daily return means, daily vols
weights    = np.array([0.6, 0.4])
means      = np.array([0.0005, 0.0003])           # daily expected return
vols       = np.array([0.012, 0.008])              # daily standard deviation
correlation = 0.45                                  # between the two assets
nu         = 5                                      # Student-t degrees of freedom

# Build covariance matrix
cov = np.outer(vols, vols) * np.array([[1.0, correlation],
                                        [correlation, 1.0]])

# ── Simulation settings ───────────────────────────────────────────────────────

N_PATHS    = 200_000
CONFIDENCE = 0.99
rng        = np.random.default_rng(seed=2024)


def simulate_portfolio_returns(
    weights: np.ndarray,
    means: np.ndarray,
    cov: np.ndarray,
    nu: float,
    n_paths: int,
    rng: np.random.Generator,
    antithetic: bool = True,
) -> np.ndarray:
    """
    Simulate one-day portfolio returns using multivariate Student-t.

    Uses a Gaussian copula scaled by an inverse-chi-squared mixing variable
    to produce multivariate t-distributed returns.

    antithetic: if True, generates n_paths/2 base draws and mirrors them,
                reducing variance without biasing the estimate.
    """
    n_assets = len(weights)
    half = n_paths // 2 if antithetic else n_paths

    # Draw multivariate normal Z ~ N(0, Σ)
    L = np.linalg.cholesky(cov)                    # Cholesky factor
    Z = rng.standard_normal(size=(half, n_assets)) @ L.T

    if antithetic:
        # Mirror the normals: antithetic pair has opposite sign
        Z = np.concatenate([Z, -Z], axis=0)        # shape (n_paths, n_assets)

    # Scale by inverse-chi-squared mixing variable for Student-t marginals
    # If W ~ χ²(ν)/ν, then Z/√W ~ t(ν) marginally
    W = rng.chisquare(df=nu, size=(half, 1)) / nu
    if antithetic:
        W = np.concatenate([W, W], axis=0)         # same W for antithetic pair
    asset_returns = means + Z / np.sqrt(W)

    # Portfolio return = weighted sum of asset returns
    portfolio_returns = asset_returns @ weights
    return portfolio_returns


def compute_var_cvar(
    returns: np.ndarray,
    confidence: float = 0.99,
) -> tuple[float, float]:
    """Return VaR and CVaR as positive loss values."""
    var  = -np.percentile(returns, (1 - confidence) * 100)
    tail = returns[returns < -var]
    cvar = -tail.mean() if len(tail) > 0 else var
    return var, cvar


# ── Run simulation ────────────────────────────────────────────────────────────

port_returns = simulate_portfolio_returns(
    weights, means, cov, nu, N_PATHS, rng, antithetic=True
)

mc_var,  mc_cvar  = compute_var_cvar(port_returns, CONFIDENCE)

# ── Parametric (normal) benchmark ─────────────────────────────────────────────

port_mean = float(weights @ means)
port_vol  = float(np.sqrt(weights @ cov @ weights))

z_score     = stats.norm.ppf(1 - CONFIDENCE)
param_var   = -(port_mean + z_score * port_vol)
param_cvar  = -(port_mean + port_vol * stats.norm.pdf(z_score) / (1 - CONFIDENCE))

# ── Report ────────────────────────────────────────────────────────────────────

print(f"Portfolio: {weights[0]:.0%} asset A / {weights[1]:.0%} asset B")
print(f"Generating distribution: Student-t, ν={nu}")
print(f"Paths: {N_PATHS:,}  |  Confidence: {CONFIDENCE:.0%}")
print()
print(f"{'Metric':<25} {'MC (Student-t)':>16} {'Parametric (Normal)':>20}")
print("-" * 65)
print(f"{'VaR (99%, 1-day)':<25} {mc_var:>15.4%} {param_var:>19.4%}")
print(f"{'CVaR (ES, 99%)':<25} {mc_cvar:>15.4%} {param_cvar:>19.4%}")
print(f"{'VaR/CVaR ratio':<25} {mc_var/mc_cvar:>15.3f} {param_var/param_cvar:>19.3f}")
print()
print(f"Simulated return mean:   {port_returns.mean():.6f}  (expected: {port_mean:.6f})")
print(f"Simulated return std:    {port_returns.std():.6f}  (parametric: {port_vol:.6f})")

Expected output with the given parameters:

Portfolio: 60% asset A / 40% asset B
Generating distribution: Student-t, ν=5
Paths: 200,000  |  Confidence: 99%

Metric                    MC (Student-t)   Parametric (Normal)
-----------------------------------------------------------------
VaR (99%, 1-day)                2.5431%              2.2463%
CVaR (ES, 99%)                  3.8719%              2.5737%
VaR/CVaR ratio                   0.657               0.873

Simulated return mean:   0.000416  (expected: 0.000420)
Simulated return std:    0.008891  (parametric: 0.009077)

The VaR/CVaR ratio is the diagnostic figure. Under the normal model, the ratio is 0.87 — VaR and CVaR are fairly close together because the normal distribution has thin tails. Under the Student-t model with ν = 5, the ratio falls to 0.66: the expected shortfall given that you exceed the VaR threshold is significantly larger than the VaR itself. That gap is the fat-tail premium — the amount by which normal-model VaR understates actual expected losses in crisis conditions. For position sizing with Kelly-criterion-based frameworks, this tail premium has direct implications for the true distribution of outcomes; see the Kelly criterion post for how win-rate estimation error compounds this kind of distributional misspecification.


Infrastructure Applications

Monte Carlo is not confined to financial models. Infrastructure reliability and capacity planning involve exactly the same challenge: complex systems whose aggregate behavior is hard to derive analytically from component-level specifications.

Reliability simulation — MTBF chains. A system composed of N components in series fails when any single component fails. If component failure times are exponentially distributed with mean MTBF_i, the system MTBF can be derived analytically for i.i.d. components. For components with different failure distributions, correlated failures, or shared dependencies, the analytic solution becomes intractable. Monte Carlo simulation generates random failure times for each component, records the first-failure time for the system, and repeats over thousands of trials. The result is an empirical distribution of system uptime — mean, percentiles, probability of surviving a given SLA window — without closed-form assumptions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def simulate_system_mtbf(
    component_mtbfs: list[float],    # mean time between failures, hours
    sim_hours: float = 8760,         # one year
    n_trials: int = 50_000,
    rng: np.random.Generator = np.random.default_rng(99),
) -> dict:
    """
    Simulate a series system (fails on first component failure).
    Returns empirical distribution of time-to-first-failure.
    """
    # Exponential failure times for each component, each trial
    # shape: (n_trials, n_components)
    rates = 1.0 / np.array(component_mtbfs)
    failure_times = rng.exponential(
        scale=1.0 / rates,
        size=(n_trials, len(component_mtbfs)),
    )
    # System fails at the minimum across components
    system_failure = failure_times.min(axis=1)

    p_survive = (system_failure >= sim_hours).mean()

    return {
        "mean_ttf_hours":  system_failure.mean(),
        "median_ttf_hours": np.median(system_failure),
        "p10_ttf_hours":   np.percentile(system_failure, 10),
        "p_survive_1yr":   p_survive,
    }

# Example: 4-component pipeline with different MTBFs
result = simulate_system_mtbf(
    component_mtbfs=[8760, 4380, 17520, 8760],   # 1yr, 6mo, 2yr, 1yr
)
print(result)

The simulation cost scales linearly in n_trials and n_components, and the result automatically incorporates the non-trivial dependence structure of the minimum-of-exponentials distribution.

Capacity planning under uncertain demand. When demand for a service is uncertain — daily arrivals follow a distribution estimated from historical data, but with regime uncertainty — parametric capacity planning often underestimates peak load. Monte Carlo generates demand paths from the estimated distribution, feeds them through a queuing or resource model, and reports the empirical distribution of queue depth, latency percentiles, or resource saturation probability. The 95th percentile of simulated peak load is a more defensible headroom target than any single-point forecast. For shell-level automation around capacity simulation pipelines, Bash scripting patterns covers the batch orchestration patterns commonly used to fan out simulation jobs.


When Not to Use Monte Carlo

Monte Carlo’s generality is also its temptation. It is easy to reach for simulation when an analytical solution is faster, cheaper, and more precise.

When a closed form exists. European option pricing under Black-Scholes has an exact formula. Running 500,000 simulations to price a vanilla call option when you have Black-Scholes is not rigor — it is computation theater. The analytical solution is exact (within the model’s assumptions), converges instantly, and admits closed-form Greeks. Use simulation only when the model departs from analytically tractable assumptions: American options, path-dependent payoffs, stochastic volatility with jumps.

When variance is too high. Importance sampling and other variance reduction techniques can extend the reach of Monte Carlo into rare-event domains, but there are limits. If you are estimating a probability of 10^{−12} — say, the probability of catastrophic failure in a safety-critical system — you cannot draw enough samples from the natural distribution to observe the event even once. Specialized rare-event simulation methods (subset simulation, splitting, sequential Monte Carlo) exist, but they are substantially more complex than standard Monte Carlo. For extremely rare events, extreme value theory or direct analytic bounds are often more tractable.

When the sample space is discrete and small. For problems with a finite, enumerable state space — a Markov chain with 50 states, a combinatorial problem with 1,000 configurations — exact enumeration or dynamic programming is usually faster and exact. Monte Carlo adds noise to problems that do not require it.

When the bias-variance trade-off favors alternative estimators. In Bayesian inference, MCMC (Markov chain Monte Carlo) is often the only tractable path, but for low-dimensional posteriors with log-concave structure, Laplace approximations or variational inference may be faster and sufficiently accurate. Monte Carlo should be the tool of last resort for tractable posteriors, not the default.

The cost of unnecessary Monte Carlo is not just computational. Each simulation run hides the structure of the problem inside a black box. Analytical solutions reveal how the answer depends on inputs — they give you derivatives, sensitivities, and closed-form limits. A simulation gives you a number and an error bar. When the analytical solution is available, it is almost always preferable.


Verdict

Monte Carlo is one of a small set of methods that genuinely generalize across domains. The same 1/√N convergence framework applies to estimating π, pricing exotic derivatives, sizing infrastructure headroom, and computing tail risk — and the error analysis is always the same calculation. That universality is its most important property.

Use it when the integral or expectation you need is high-dimensional, analytically intractable, or requires distributional assumptions that do not admit closed forms. The Student-t VaR walkthrough above illustrates the practical pattern: choose a more realistic generating model than the normal, simulate at scale, read off empirical quantiles. The fat-tail signal in the VaR/CVaR ratio only appears if you are willing to specify the distribution honestly.

Respect the 1/√N convergence rate. If you need two more decimal places of precision, you need one hundred times more computation. Variance reduction is the correct response to this constraint, not simply throwing more samples at the problem. Antithetic variates and control variates are low-overhead starting points; importance sampling is the right tool when you care about rare-event probabilities in the tail.

Seed every simulation that produces results anyone will act on. Store the seed. Log the RNG type. The ability to reproduce a simulation exactly is not a nice-to-have — it is the difference between a result you can audit and one you can only re-run and hope.

And recognize the regime in which Monte Carlo is the wrong tool. It converts hard math into tractable computation by accepting noise. When the math is not actually hard — when a closed form exists — you are paying for noise you do not need.


Sources

  • Metropolis, N., & Ulam, S. (1949). “The Monte Carlo Method.” Journal of the American Statistical Association, 44(247), 335–341. The foundational paper naming and formalizing the method.
  • Glasserman, P. (2003). Monte Carlo Methods in Financial Engineering. Springer. The definitive treatment of variance reduction, convergence, and financial applications.
  • Kroese, D. P., Taimre, T., & Botev, Z. I. (2011). Handbook of Monte Carlo Methods. Wiley. Broad coverage including rare-event simulation, MCMC, and applications.
  • L’Ecuyer, P. (2012). “Random Number Generation.” In Handbook of Computational Statistics. Springer. Best-practice guide to PRNG selection and seeding.
  • Jorion, P. (2006). Value at Risk: The New Benchmark for Managing Financial Risk (3rd ed.). McGraw-Hill. Systematic comparison of parametric, historical, and Monte Carlo VaR.
  • O’Neill, M. E. (2014). “PCG: A Family of Simple Fast Space-Efficient Statistically Good Algorithms for Random Number Generation.” Harvey Mudd College Technical Report HMC-CS-2014-0905. The design paper for the PCG64 generator used in numpy’s default_rng.
  • Numpy documentation on random number generation: https://numpy.org/doc/stable/reference/random/index.html
  • Risk metrics: VaR, Sharpe, Sortino — VaR, CVaR, Sharpe, and Sortino: metrics, implementations, and failure modes.
  • Kelly criterion position sizing — Optimal position sizing under uncertainty; distributional assumptions interact directly with Kelly fraction estimates.
  • Python for DevOps — Python automation patterns for simulation pipelines and artifact management.
  • Bash scripting patterns — Shell scripting for batch job orchestration in compute-intensive simulation workflows.

Comments