A Monte Carlo run in Folio Lab ends with a sentence like there is a 63% chance this portfolio is worth more than ₹1 crore in ten years. That sentence is doing two very different jobs at once, and the difference between them is the entire subject of this post.
The first job is arithmetic, and it is exact. Out of the paths the engine generated, 63% of them ended above the target. Nothing about that is uncertain. Run it again with the same seed and the same inputs and you get the same number, to the last digit.
The second job is inference, and it is not exact at all. It is the leap from 63% of simulated paths to 63% chance, and every bit of the risk in that sentence lives in the leap.
The estimator is the easy part
Let be terminal wealth at horizon and the target. The engine draws paths and reports the sample proportion:
This is a binomial proportion, so its Monte Carlo standard error is closed-form:
At and , that is : a 95% interval about one percentage point wide either side. Simulation noise, in other words, is already small enough to ignore relative to everything else on this page.1
That is worth sitting with. The part of the number we can quantify precisely is the part that barely matters. The part that matters cannot be quantified from inside the simulation at all.
Five things the number is conditional on
Every probability the engine reports is conditional on a specific, recorded configuration. These are not caveats bolted on afterwards; they are inputs, and the output is a function of them.
| Condition | What it fixes | What moves if you change it |
|---|---|---|
| Return process | The joint law paths are drawn from | Tail mass, clustering, drawdown depth |
| Optimizer-time inputs | Weights, frozen at the parent run | The entire allocation being simulated |
| Effective configuration | Horizon, contributions, tax, inflation | The wealth accounting, not the returns |
| Engine version | Simulation semantics | Anything, by definition |
| Seed | The specific draw | The last digit, and nothing structural |
The second row is the one people miss. A Monte Carlo run does not re-optimize. It takes the weights a completed optimization produced, freezes them, and asks what happens next. That is a deliberate design decision: it keeps the simulation a statement about this portfolio rather than a statement about the optimizer, and it is why the two live on separate queues with separate workers.
Where the estimate ends and the model begins
The default return process is a stationary block bootstrap. It resamples contiguous blocks of historical returns, which preserves autocorrelation and cross-sectional structure inside a block and destroys it across block boundaries.
# Blocks are contiguous in time, so a 2008 October block carries
# its own correlation structure with it. What the bootstrap cannot
# do is invent a regime the sample never contained.
for path in range(n_paths):
t = 0
while t < horizon:
start = rng.integers(0, n_obs - block_len)
block = returns[start : start + block_len]
emit(block[: horizon - t])
t += block_lenThe honest description of what that buys is narrow: the simulated distribution is the historical distribution, rearranged. If the sample window contains no episode like the one that actually arrives, no number of paths will produce one. Increasing samples the same empirical law more finely; it does not widen it.
Why none of this is called a forecast
Folio Lab does not describe these probabilities as calibrated forecasts, and the reason is specific rather than rhetorical: no generator here has been validated out of sample.
Calibration is a testable property. A generator is calibrated at horizon if, across many rolling origins, the realised outcome falls below the predicted -th quantile about of the time. Testing it needs a rolling-origin harness that re-fits at each origin, holds the next days out, and scores coverage against published thresholds decided before the results are seen.
That harness exists and has been run. It has not passed. The best-performing arm calibrated to roughly 3.2% against its threshold and was still rejected, and at the annual horizon the gate is not reachable by any generator we have tried, which is a statement about the study design as much as about the models.
So the language stays where the evidence is. The engine reports what its model implies. It does not claim the model is right, and it will not claim one generator beats another until something measured says so.
What would change the claim
Three things, in order of how much they would change:
- A generator passing rolling-origin validation at a stated horizon. Then, and only then, the word "calibrated" becomes available, scoped to that horizon and that generator.
- A validation gate whose power is understood. A test nothing can pass is not evidence of anything except the test.
- Out-of-sample evidence that one rebalancing policy beats another. Currently unclaimed, deliberately.
Until then the right way to read a Folio Lab probability is as a precise answer to a question you fully specified: given this return process, these frozen weights, this configuration and this engine, what fraction of paths clears the target? That question has an exact answer, and the platform gives it to you along with everything it was conditional on.
The question you actually wanted answered is harder, and nobody has it.
Footnotes
-
Which is why adding paths is the cheapest and least useful lever available. Going from 10,000 to 100,000 paths shrinks the interval by a factor of and changes nothing about whether the model was right. ↩