Skip to content

Fix the GBM model and the price simulation - #5

Open
shehio wants to merge 1 commit into
developfrom
fix/gbm-price-simulation
Open

Fix the GBM model and the price simulation#5
shehio wants to merge 1 commit into
developfrom
fix/gbm-price-simulation

Conversation

@shehio

@shehio shehio commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Summary

StockHelper.calculate_price() returned NaN, and the geometric Brownian motion behind it was wrong in three independent ways. All four defects sat in a module no test reached.

What Was Broken

1. Arity mismatch — calculate_price() returned NaN. run(sample_number, probability_distribution, estimator_function) calls estimator_function(probability_distribution()), i.e. with one argument, while geometric_brownian(estimators, time, random) takes three. So estimators was bound to the random draw, estimators.mean and estimators.std were undefined, and every term was NaN.

Reproduced on develop:

calculate_price(5) = NaN
miu = undefined, sigma = undefined, drift = NaN, shock = NaN

2. The shock did not scale with √Δt. It was sigma * random * time, so the variance of the log return grew as time² instead of linearly in time.

3. No Itô correction. The drift was mu * time, not (mu - sigma²/2) * time, so E[S_T] overshot S_0·e^{μT} by exp(σ²T/2).

4. Math.random() was used where a standard normal is required. It is uniform on [0, 1) and therefore never negative — the simulated price could only ever be pushed up. Over 400,000 draws, 0 shocks were negative and the minimum shock was exactly 0.0000.

The Fix

  • calculate_price(price, estimators, time, sample_size?) closes over the price, estimators and horizon, so the single-argument estimator run() expects receives only the random draw. It now returns S_0 · E[exp(log return)].
  • standard_normal() (Box-Muller) supplies the normal draws.
  • The log return over Δt is now (μ − σ²/2)Δt + σ√(Δt)·Z, so E[S_T] = S_0·e^{μT}.
  • Dropped the per-sample console.log inside geometric_brownian; it printed one line per Monte Carlo path.

Measured Before and After

S_0 = 100, μ = 0.08, σ = 0.25, T = 2, 400,000 paths. Analytical E[S_T] = S_0·e^{μT} = 117.3511.

E[S_T] Ratio to analytical
Before 152.2503 1.2974 (+29.7%)
After 117.2575 0.9992

The resulting distribution is lognormal, as it should be — the log returns are normal:

Moment Simulated Expected
Mean 0.0976 0.0975 = (μ − σ²/2)T
Variance 0.1249 0.1250 = σ²T
Skewness −0.0014 ~0

Tests

finance/test/models.test.ts is new and is the first test to reach finance/models.ts at all. It covers the standard normal's moments, that it actually produces negative draws, the Itô correction, the √Δt scaling, and the convergence of E[S_T] to S_0·e^{μT}.

Suite: 34 tests / 10 suites before → 40 tests / 11 suites after, all passing.

🤖 Generated with Claude Code

https://claude.ai/code/session_01THEv7BFJd31NjvBRehbZPL

calculate_price() returned NaN: run() invokes the estimator with a single
argument while geometric_brownian expects (estimators, time, random), so
estimators.mean and estimators.std were undefined. The price, estimators
and horizon are now closed over by the estimator passed to run().

The model itself had two further errors:

* The shock was sigma * random * time instead of sigma * sqrt(time) * random,
  so variance grew with the square of the horizon rather than linearly.
* The drift was missing the Ito correction, and the generator was
  Math.random, uniform on [0, 1), where a standard normal is required.
  With a non-negative shock the simulated price could only ever move up.

standard_normal (Box-Muller) supplies the normal draws, and the log return
over dt is now (mu - sigma^2 / 2)dt + sigma * sqrt(dt) * Z, so that
E[S_T] = S_0 * exp(mu * T).

Measured with S_0 = 100, mu = 0.08, sigma = 0.25, T = 2 over 400k paths:
the analytical mean is 117.3511; the old model gave 152.2503 (+29.7%, with
0 of 400000 shocks negative), the corrected one gives 117.2575.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant