Pseudorandom Number Generation
Computers can't generate true randomness on demand, so every Monte Carlo simulation runs on numbers that only look random. Understanding how the illusion is built explains both why it usually works and the specific ways it can quietly fail.
Prerequisites: Floating-Point Arithmetic and Precision
Every Monte Carlo option price, every bootstrapped confidence interval, every randomized backtest split relies on a stream of "random" numbers — and yet a computer, a deterministic machine that does exactly what its instructions say every single time, cannot generate a genuinely random number from nothing. What it generates instead is a long, carefully engineered sequence that passes every statistical test for randomness while being, underneath, completely predictable if you know where it started. That gap between "looks random" and "is deterministic" is the entire subject.
The analogy: a very long, very good shuffle
Imagine a deck of a trillion cards, pre-shuffled once by an expert so thoroughly that no pattern is visible anywhere — consecutive cards show no correlation, runs of the same suit appear exactly as often as chance predicts, every statistical test you throw at it comes back clean. Now imagine that deck is fixed and public: anyone who deals from card number 4,502,187 forward will see the identical sequence you would. The deck isn't random in the sense of "could have come out differently" — it is one specific, deterministic sequence. It's random only in the sense that nothing about its structure lets you predict card 4,502,188 from the cards you've seen so far, unless you already know the deck's layout.
A pseudorandom number generator (PRNG) is exactly this: a long, fixed, deterministic sequence, generated by a formula, chosen because it looks statistically indistinguishable from true randomness. Where you start dealing from the deck is the seed.
Writing it down
A PRNG is a function that takes a state and produces two things: the next output number, and a new, updated state. Written as a recurrence,
where is the internal state at step , is the (deterministic) update rule, and turns the state into the output number you actually see (typically scaled to look like a uniform value between 0 and 1). In words: the generator's entire future is a function of where it is right now — feed it the same starting state (the seed) and it reproduces the exact same sequence every time, on any machine, forever.
A simple (and now largely obsolete, for reasons below) example is the linear congruential generator:
In words: multiply the current state by a constant , add a constant , and keep only the remainder after dividing by — the modulo operation is what forces the sequence to wrap around and stay bounded rather than growing forever, and the specific choice of , , determines how "random-looking" and how long the resulting cycle is before it repeats.
Every path drawn by the explorer above was generated from a stream of pseudorandom numbers exactly like this — deterministic under the hood, statistically indistinguishable from true noise on the surface. Redraw it a few times and notice: unless you fix the seed, each redraw uses a fresh, unpredictable starting point (usually seeded from the system clock or an OS entropy source), which is why the paths differ each time.
Turning uniform pseudorandom numbers into normally distributed ones (needed for almost all financial simulation) is itself a small algorithm — commonly the Box-Muller transform, which takes two independent uniform values and combines them, via a square root and a cosine, into one standard normal draw. The bell curve above is what the output of that transform should look like once you histogram enough of it; if your simulated returns don't converge to something like this shape, the generator or the transform has a bug.
Worked example 1: reproducing a backtest
You run a Monte Carlo backtest that simulates 10,000 price paths and reports a Sharpe ratio of 1.42. Your colleague reruns the exact same code and gets 1.38. Who's right, and why the discrepancy?
If the code seeds the generator explicitly — rng = np.random.default_rng(seed=42) — both of you get the identical 10,000 paths and identical Sharpe ratio, because the entire sequence is a deterministic function of the seed 42. If instead the code seeds from the system clock (the default in many libraries when no seed is given), each run draws a genuinely different sequence, and 1.42 versus 1.38 are simply two different samples from the same underlying random process — neither is "right," and the actual answer worth reporting is something like "1.40 ± 0.05" from running many seeds and looking at the spread. This is precisely why reproducible research always fixes and reports the seed: it converts "was there a bug, or just sampling noise" from a mystery into a one-line check.
Worked example 2: the birthday-paradox failure mode
A trading system generates order IDs as pseudorandom 32-bit integers, reseeding the generator from the current millisecond timestamp every time a new order arrives. Two orders sent in the same millisecond seed from the same clock value and — because the generator is deterministic — produce the identical first output number, causing an order-ID collision downstream.
Work the odds: if the system sends orders at a peak rate where roughly 1,000 orders can occur within any given millisecond during a burst (not far-fetched in a fast market), then by the same birthday-paradox reasoning used for hash collisions, the pairs of orders that could share a millisecond number in the thousands, while the number of distinct milliseconds in even one trading day is only about — collisions aren't a remote tail risk here, they're close to guaranteed during any burst. The fix isn't a "better" random number generator; it's using a single, continuously-running generator (never reseeded mid-session) or a proper source of entropy (like a cryptographic RNG or a monotonic counter) for anything where uniqueness, not just statistical randomness, is the actual requirement.
What this means in practice
- Always fix a seed for anything you need to reproduce or debug — backtests, unit tests, and reported research results should log the seed used, exactly the way you'd log a software version.
- Never reseed a generator mid-run inside a loop. Reseeding from a coarse or repeating source (like a timestamp) can silently correlate or duplicate outputs that were supposed to be independent — the failure in worked example 2.
- Not all PRNGs are equal. The old linear congruential generator has visible correlations between consecutive outputs when plotted in higher dimensions (points fall on hyperplanes) and should not be trusted for serious simulation; modern defaults like the Mersenne Twister or PCG have far longer periods and pass much stricter statistical test suites.
- Randomness for security is a different, stricter requirement. A PRNG good enough for Monte Carlo pricing (statistically random) is not automatically safe for generating cryptographic keys or trading credentials (must also be unpredictable even to an adversary who has seen prior outputs) — that needs a cryptographically secure generator, a distinct category.
A PRNG is a deterministic formula that produces a long sequence which looks statistically random but is fully determined by its starting seed. Same seed, same sequence, always — which is a feature for reproducibility and a hazard the moment two processes accidentally share one.
When debugging "random" behavior that seems inconsistent between runs, the first question is always: is the seed fixed? If not, you're not debugging a bug, you're debugging sampling variance — run it 50 times and look at the distribution of outcomes before concluding anything is wrong.
The classic and costly mistake is assuming pseudorandom numbers are independent across parallel processes without checking. Spinning up many worker processes that each seed their generator from the system clock can produce identical or highly correlated streams if two workers happen to start in the same clock tick — silently collapsing what was supposed to be, say, 10,000 independent Monte Carlo paths into far fewer effectively-distinct ones, which understates the true variance of your estimate and can make a backtest look far more confident than it deserves to be. The fix is explicit, non-overlapping seeding (e.g., seed each worker with its own index) or a generator library designed for parallel streams.
Related concepts
Practice in interviews
Further reading
- Knuth, The Art of Computer Programming, Vol 2 (ch. 3)
- Matsumoto & Nishimura, Mersenne Twister (1998)