Skip to content

Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ForwardFlowPrice paths & shocks

Price paths and shocks

Each run follows a monthly Bitcoin price path from entry to the market horizon. That path affects stop-sale proceeds, early completion and the Holder’s exposure. This chapter explains the core generators, shock and sensitivity overlays, interpolation and seeded draws.

Implementation: paths.rs and engine.rs::run. Additional diffusion, jump, regime and upload models are detailed in Surfaces and hedging.

Prices are f64 throughout. They are model inputs, not ledger amounts, and become cents only at the moment a posting is written (a strike at origination, a sale at a stop). That rule is stated at the head of paths.rs and in the repository’s critical rules (“Decimal for money, f64 for boundaries/display”).

The shape of a path

A path has \(H + 1\) entries, indexed by month: path[0] is the start price (input #1, $60,000 at the base terms) and path[H] is the last month simulated. The horizon is set by the book, not by the path (engine.rs::SimConfig::horizon):

\[ H = \max\big(H_{\text{market}},\ (\text{cohorts} - 1) + \text{term} + \lfloor \text{lag}/30.4375 \rfloor + 1\big) . \]

Cohorts here are those that actually originate, capped by the origination window (input #18, engine.rs::SimConfig::effective_cohorts). The tail is the settlement allowance for the last stop (engine.rs::settlement_tail_months): a payment missed at age term is sold lag days later (input #24) and priced between the two marks around the sale date, so the ledger needs the mark past it — one month at the program’s 18 days (v1.5’s extra month), three at 90 days. Before 2026-09-05 the tail was one month at every lag, and a 90-day sale on a two-month Agreement posted in month 3 at the last mark’s price instead of month 5 at the interpolated one (the audit’s finding 6; finding_6_a_90_day_lag_settles_inside_the_ledger). \(H_{\text{market}}\) is the market horizon, market_horizon_months, unset by default: when the cockpit sets it, a pacing change (fewer cohorts, an origination window) no longer shortens the path and moves the bridge’s endpoint date — with it set, cutting 24 cohorts to 3 leaves the path bit-identical, where before month-one spot moved from $60,623.59 to $60,720.54 (finding 2; finding_2_the_market_horizon_pins_the_path_under_pacing_changes). It must be at least the pacing’s own requirement. At the base terms a single vintage has \(H = 61\); the paced book of 24 monthly cohorts has \(H = 84\), which is the 84-month horizon the Model Card’s production runs use.

The order of operations in engine.rs::run is fixed:

  1. Generate the path from the chosen mode (paths::generate), consuming the run’s random stream if the mode needs it.
  2. Apply the shock designer, if set (paths::apply_shock).
  3. If a bump is set with hold_strikes, keep a copy of the path as it stands; that copy is the strike path.
  4. Apply the bump, if set (paths::apply_bump).
  5. At each cohort month, derive the intramonth dispersion spread from the twelve months before it on the strike path if one was kept, otherwise on the final path (the stated volatility when fewer than three returns precede the month).

Each cohort’s Agreements then strike at strike_path[m] (or path[m] when no strike path was kept), and every later reading of the coin, from the monthly moneyness checks to the stop sale, is against the final path. A change that only touches months from \(m\) on — a shock at \(m\), a bump from \(m\), the origination window closing at \(m\) with the market horizon set — leaves every strike struck and every posting dated before \(m\) unchanged (finding_2_prefix_invariance_nothing_dated_before_a_change_moves).

The Brownian bridge

PathMode::Bridge { end_price, vol_annual } is the default mode and the one the cockpit’s two price fields drive (inputs #1, #2 and #2b). Start and end are pinned; the volatility fills the middle. Monte Carlo under this mode is many bridges between the same two points, so “what if the coin ends at $X” is a two-field question and the distribution of outcomes comes entirely from the route taken.

The construction is sequential and lives in paths.rs::bridge. Work in log space with \(x_k = \ln(P_k / P_0)\), so \(x_0 = 0\), and let the target be

\[ T = \ln\frac{P_{\text{end}}}{P_0}, \qquad \sigma_m = \frac{\sigma}{\sqrt{12}} . \]

At step \(k\) (producing month \(k+1\)) there are \(n = H - k\) steps remaining. The next increment is drawn conditional on the remaining distance to the endpoint:

\[ x_{k+1} = x_k + \frac{T - x_k}{n} + \sigma_m \sqrt{\frac{n - 1}{n}}; Z_k, \qquad Z_k \sim N(0, 1). \]

The mean of the step is the fraction of the remaining distance that one step out of \(n\) should cover; the variance is \(\sigma_m^2 (n-1)/n\), which is the variance of one step of a Brownian bridge with \(n\) steps to go. At the last step \(n = 1\), the variance is zero and the mean is exactly the remaining gap, so the endpoint is hit by construction. The code then overwrites path[H] with end_price to remove floating-point residue (a debug_assert checks the residue was below \(10^{-9}\) relative). Prices are \(P_k = P_0 e^{x_k}\).

This is the sequential conditioning the note at the head of paths.rs::bridge insists on: the steps are never independent samples forced to the endpoint afterwards, which would give the wrong interior variance. The conditional recursion gives the bridge law without any rescaling.

Arithmetic at the default volatility of 43%: \(\sigma_m = 0.43 / \sqrt{12} = 0.12413\), so a free month would move the log price by 12.4% one standard deviation. On the 84-month horizon the first step’s standard deviation is \(0.12413 \sqrt{83/84} = 0.12339\); with two steps to go it is \(0.12413 \sqrt{1/2} = 0.08777\); the final step is deterministic. Pinning the endpoint takes only a little variance out of the early months and all of it out of the last one.

Vol 0 is the log-linear ramp. With \(\sigma = 0\) every increment is exactly \((T - x_k)/n\), which is constant, so the path is a straight line in log space from start to end. Start = end gives a flat path at $60,000 for every month, which is why the contract-math fixtures use Bridge { end_price: 60_000, vol_annual: 0.0 }: no price movement, nothing behavioral fires, and the ledger shows the pure schedule. The test zero_vol_bridge_is_deterministic_ramp checks that two different seeds produce the same flat path. A ramp from $60,000 to $90,000 over 84 months has a monthly log step of \(\ln 1.5 / 84 = 0.004827\), about 0.48% a month, 5.8% a year continuously compounded, with no noise at all.

A caution the cockpit states next to the volatility field: 43% is the recent coin, and the bootstrap’s default regime (2017 onward) ran near 70%. The two modes’ defaults embed different Bitcoins.

Historical replay

PathMode::HistoricalReplay { start_index } replays the embedded monthly closes from a chosen bar, rebased so the path starts at the configured start price (paths.rs::replay):

\[ P_k = P_0 \cdot \frac{C_{s+k}}{C_s}, \qquad k = 0, \dots, H, \]

where \(C\) is the chronological close series and \(s\) is the start index. The shape of the path is Bitcoin’s actual history from that month; only the level is rescaled. Replay includes whatever drift history had, so the Model Card calls its replay rows descriptive rather than probabilistic.

Feasibility. The run needs \(H + 1\) bars from index \(s\), so the condition is \(s + H < N\) with \(N\) the number of bars. Otherwise generate returns PathError::InsufficientHistory { start, needed, available }, which the API maps to HTTP 400 rather than a crash. With 174 bars and the paced book’s \(H = 84\), the feasible starts are indices 0 through 89, ninety of them, from February 2012 to July 2019; that is the “90 feasible historical start months” of the Model Card’s paced-replay construction, and why its starts end mid-2019. A single vintage at \(H = 61\) has 113 feasible starts, the Model Card’s single-vintage replay row. The GET /api/forwardflow/history endpoint returns the months and closes so the cockpit can disable start dates that would not fit.

The vintage backtest (POST /api/forwardflow/backtest, see The API) is this mode run once per feasible start with the rest of the configuration held fixed.

The embedded data

The series is backend/data/btc_historical_monthly.csv, compiled into the crate with include_str! and parsed once into a OnceLock (paths.rs::historical_closes, historical_months). There are no data files at runtime. The file follows the investing.com export convention: newest first, quoted fields, thousands separators inside the quotes, MM/DD/YYYY dates, and the columns Date, Price, Open, High, Low, Vol., Change %. The engine reads only Date and Price, reverses the rows into chronological order, and checks in debug builds that the months ascend. The first data line of the file is the bar for July 2026 at a close of $62,875.5; the last line is February 2012 at $4.9.

Parsed, that is 174 monthly bars, index 0 = February 2012 through index 173 = July 2026, hence 173 monthly log returns. Two indices recur in the examples and the cockpit: index_of_month(2017, 1) is 59, the “modern regime” default of the bootstrap picker, and index_of_month(2024, 7) is 149, the start of the trailing-24-month window the Model Card prices on. index_of_month returns the first bar at or after the requested month; a month before the series clamps to index 0, and a month after the series returns None so that a caller can never silently fall back to 2012 (the test index_of_month_rejects_future_dates).

The zero-drift block bootstrap

PathMode::ZeroDriftBootstrap { block_len, regime_start_index } is the memorandum’s pricing stance and the mode the Model Card’s production figures are run on (paths.rs::bootstrap).

Take the closes from the regime start onward, form their log returns, and remove the mean:

\[ r_i = \ln\frac{C_{i+1}}{C_i}, \qquad \bar r = \frac{1}{n}\sum_i r_i, \qquad \tilde r_i = r_i - \bar r . \]

Then build the path by concatenating blocks of consecutive de-meaned returns, drawn circularly (model audit 2026-09-06, M03): each block start \(s\) is drawn uniformly from all \(n\) positions \(0 \dots n-1\), the \(L\) returns \(\tilde r_{s \bmod n} \dots \tilde r_{(s+L-1) \bmod n}\) are appended in order — a block that starts near the window’s end wraps to its beginning — and the loop repeats until \(H\) returns have been used; the last block is cut where the horizon lands. Prices are \(P_k = P_0 \exp(\sum_{j<k} \tilde r_j)\). Under this rule every observation has weight exactly \(1/n\) at every position of every block, the cut block included, so the expected log return of every month is the de-meaned mean, zero. Zero drift means zero expected log return of the path under the sampler’s own weights — and nothing more: the median and the mean of the price factor are those of the empirical block distribution, not a lognormal’s, so no exact Gaussian mean/median relation holds. On the 24 de-meaned returns of the pricing window the median monthly log step is −0.46%, \(\log \mathbb{E}[e^{\text{step}}]\) is +0.69% and half the population log variance is +0.685%: the median price drifts slightly down and the mean up (model audit 2026-09-07, bootstrap-interpretation.json). (The earlier draw took starts from \(0 \dots n-L\) without wrapping, which weighted interior returns more than the edges while the de-meaning weighted them equally, and left an exact log drift of +1.24%, −1.73% and −3.67% a year on the three windows below; tests/model_audit_2026_09_06.rs enumerates the sampler’s weights and samples 4,000 five-year paths per window.) The regime must supply at least \(L\) returns; otherwise PathError::RegimeTooShort is returned, and the cockpit clamps the picker.

Why de-meaned. The bootstrap removes the selected window’s average log return so the Holder can test Buyer behavior without carrying that historical log-growth assumption forward. It retains the observed deviations, including large moves and the ordering within each block. This means zero expected log return, not zero expected price appreciation: converting log returns back to prices can still produce a rising arithmetic mean price. Volatility is not an input in this mode; it comes from the chosen historical window. The resulting paths are conditional scenarios, not forecasts.

The block length. The default is 6. A block of six consecutive months preserves half a year of the actual sequence, which is where drawdown streaks and the recoveries after them live; drawing single months would destroy that clustering. The bootstrap has no pinned endpoint, so unlike the bridge its terminal price is free, and the Monte Carlo distribution reflects both the route and the destination.

The regimes in use. The three windows that appear in the Model Card and the cockpit, with their realized annualized volatility computed from the population variance of the window’s log returns (the definition in exposure.rs::realized_vol, below), and in brackets the sample-variance figure the Model Card prints (examples/w0108_refresh.rs divides by \(n - 1\)):

RegimeStart indexLog returnsMean per monthRealized vol, population (sample)
Full history, Feb 20120173+5.5%86.5% (86.8%)
Modern regime, Jan 201759114+3.7%69.1% (69.4%)
Trailing 24 months, Jul 202414924−0.1%40.6% (41.4%)

The trailing window is the production pricing regime (Model Card §4): 24 returns, which asserts that current volatility persists. The full-history window is the printed stress. The “modern regime” from 2017 is the cockpit’s default when the bootstrap is selected, chosen to drop the early hundred-fold years without dropping the institutional-era volatility. The mean column is what de-meaning removes.

The custom path

PathMode::Custom { points } (v1.4, spec input #2c+) is a deterministic, seed-free path through anchor points. Each anchor is (month, ratio), the price at that month as a multiple of the start price. The origin \((0, 1.0)\) is implicit. Between anchors the path is linear in log space; after the last anchor it is flat (paths.rs::custom):

\[ \ln\frac{P_m}{P_0} = \ell_i + (\ell_{i+1} - \ell_i),\frac{m - m_i}{m_{i+1} - m_i}, \quad m_i \le m \le m_{i+1}, \qquad \ell_i = \ln \rho_i , \]

and \(P_m = P_0 \rho_{\text{last}}\) for \(m\) past the last anchor. Validation: at least one anchor, months strictly increasing and starting after month 0, ratios positive and finite; a violation panics with the input number, which the API turns into a 400.

The test custom_hits_anchors_interpolates_log_linear_and_extends_flat is the worked example. Anchors \((6, 0.40), (18, 0.80), (30, 0.40)\) on a $60,000 start give $24,000 at month 6, $48,000 at month 18, $24,000 at month 30, and $24,000 at every month after. Month 12, halfway between the first two anchors, is the geometric mean: \(60{,}000 \sqrt{0.40 \times 0.80} = $33{,}941.13\), not the arithmetic $36,000. Log-linear interpolation means a constant monthly rate of change between anchors, which is what a “fell 60% over six months” instruction means.

The mode exists so that mid-path reversals (double dips, whipsaws, a rally into a crash) can be written as data literals. It is exposed in the engine, the API and the TypeScript types; the cockpit has no picker for it.

The shock designer

The shock (spec input #17, paths::Shock) is a multiplicative overlay applied after generation, so it composes with any path mode. Its four fields are the start month \(X\), the drop \(Z \in (0, 1)\), the duration \(W \ge 1\), and an optional recovery level \(R\). Write \(F = 1 - Z\) for the floor and \(H\) for the last month. The factor multiplying path[m] is (paths.rs::apply_shock):

\[ f(m) = \begin{cases} 1 & m < X \\ F^{,(m - X + 1)/W} & X \le m \le X + W - 1 \\ F & m > X + W - 1, ; \text{permanent} \\ \exp!\Big(\ln F + (\ln R - \ln F),\dfrac{m - X - W + 1}{H - (X + W - 1)}\Big) & m > X + W - 1, ; \text{recovering} \end{cases} \]

The ramp is log-linear and reaches exactly \(F\) at the trough month \(X + W - 1\) (clamped to \(H\) if the ramp would run off the end). With no recovery the factor stays at \(F\) for good. With a recovery, the factor moves log-linearly from \(F\) at the trough to \(R\) at month \(H\); \(R = 1\) is a full round trip back onto the unshocked path, \(R = 0.8\) ends 20% below it. Validation (engine.rs::validate): \(0 < Z < 1\), \(W \ge 1\), \(R > 0\) and finite.

Worked example from the test shock_designer_overlays_any_path: a flat $60,000 path with \(X = 6\), \(Z = 0.5\), \(W = 3\), permanent. Months 0 to 5 are untouched. Month 6 is \(60{,}000 \times 0.5^{1/3} = $47{,}622\), month 7 is \(60{,}000 \times 0.5^{2/3} = $37{,}798\), month 8 is exactly $30,000, and every later month is $30,000. With \(R = 1\) on a single-vintage run (\(H = 61\)) the recovery spans \(61 - 8 = 53\) months; at month 35, 27 months into the recovery, the factor is \(0.5^{,1 - 27/53} = 0.712\), about $42,700, and month 61 is back at $60,000.

A larger shock at the base terms: \(X = 12\), \(Z = 0.70\), \(W = 6\) on the paced book. The floor is 0.30; the ramp reads 0.818, 0.669, 0.548, 0.448, 0.367, 0.300 at months 12 through 17, and the coin sits at 30% of its unshocked level from month 17 onward. The tornado’s “BTC −70%/3mo permanent, flow continues” row is the faster version, \(X = 1\), \(Z = 0.70\), \(W = 3\): the ramp reads 0.669, 0.448, 0.300 at months 1 through 3. The test crash_with_flow_continuing_beats_single_strike runs that shock on the default bridge (43% volatility, seed 9, rational mode on): a book that keeps originating through the crater, with later cohorts striking low, beats a single vintage struck at the top.

The shock is the cleaner crash instrument because it leaves the underlying path mode alone. Moving the bridge’s endpoint changes every month; the shock changes only the months from \(X\) on, and by a stated amount.

The bump overlay

The bump (v1.6, paths::PathBump) is the second overlay, applied after the shock and before the run. It has four fields: from_month, price_factor, vol_factor and hold_strikes. It exists for the risk desk’s Greeks, which are computed by bump-and-revalue, and the desk can also set it directly as a what-if. Validation: the price factor within \((0.1, 10)\), the vol factor within \([0, 5)\).

Let \(a = \max(m_{\text{from}}, 1)\) with \(m_{\text{from}}\) the from_month; month 0 is never bumped. Two operations, in this order (paths.rs::apply_bump):

Vol factor. Take the log returns of the future months, \(r_m = \ln(P_m / P_{m-1})\) for \(m = a \dots H\), and their mean \(\bar r\). Rebuild the future path from \(P_{a-1}\) with the deviations around the mean scaled by \(v\):

\[ r’m = \bar r + v,(r_m - \bar r), \qquad P’m = P{a-1} \exp!\Big(\sum{j=a}^{m} r’_j\Big). \]

The mean log return of the future segment is unchanged, so the drift is kept, and the realized volatility of the segment scales by exactly \(v\). At \(v = 0\) every future return equals \(\bar r\) and the path becomes a ramp (the test bump_overlay_scales_prices_and_vol checks the returns are constant). On a flat path every deviation is zero, so the vol factor is a no-op and the risk desk reports vega as exactly zero. The same continuation-scaling method applies to a bridge.

Price factor. Multiply every price from month \(a\) on by \(p\). A factor of 1.10 from month 1 leaves month 0 at $60,000 and lifts every later month by 10% (the same test).

Strikes held or floating. With hold_strikes = true the engine strikes every cohort off the path as it stood before the bump, so the existing book’s terms do not move and only the coin does (the test bump_overlay_scales_prices_and_vol checks the held strikes equal the unbumped run’s). With hold_strikes = false cohorts that originate in bumped months strike at the bumped prices. The intramonth dispersion spread (below) is derived from the strike path when strikes are held, so a bump does not change the spread either. The Greeks never set hold_strikes: exposure.rs::greeks runs every bump with hold_strikes: false and gets the existing book by stopping origination at the bump month (origination_stop_month), so no cohort strikes on a bumped price, which is the “strikes held” of spec v1.6 by another route. The commitment delta is the same bump on the full pacing, where later cohorts do strike at the bumped prices. On a book with no stops the existing book’s delta is zero: the schedule is fixed in dollars and only a stop sale reads the price.

How the desk uses it (exposure.rs::greeks): delta and gamma from price factors \(1 \pm 0.05\) starting the month after the ladder month; vega from a vol bump of ±5 points (the API defaults, price_bump and vol_bump 0.05), which on every path mode is translated into a vol factor \(v = (\sigma_{\text{real}} \pm 0.05)/\sigma_{\text{real}}\) against the base path’s realized volatility (labelled in the report as path-amplitude scaling). The Greeks themselves are the subject of The risk desk. The risk endpoint refuses to compute Greeks while a what-if bump is set in the config, so the two uses do not stack.

Reading prices between the marks

The path has one price per month, but two places in the engine need a price at a finer resolution.

The stop sale. A stop is recorded 18 days after the missed payment date by default (input #24, stop_sale_lag_days; day 16 is the Stop Date and the sale follows within two business days, Marc, 2026-09-03, spec v1.5). The sale price is the log-linear interpolation between the monthly marks (engine.rs::stop_sale):

\[ \text{pos} = m_{\text{missed}} + \frac{\text{lag}}{30.4375}, \qquad k = \lfloor \text{pos} \rfloor, \quad f = \text{pos} - k, \qquad P_{\text{sale}} = \exp\big((1 - f)\ln P_k + f \ln P_{k+1}\big). \]

At 18 days, \(f = 18 / 30.4375 = 0.5914\), so the sale price sits 59% of the way in log space from the missed month’s mark to the next one; at lag 0 it is the missed month’s mark, at about 30 days the next mark. The postings land at the first payment date at or after the sale, \(\lceil\text{pos}\rceil\), which at 18 days is the month after the missed one. The proceeds are the sale price less the haircut (input #15, default 0) and the 25 bp sale cost; what happens to them is the waterfall of The stop waterfall. The Model Card notes that at monthly resolution the difference between an 18-day and a 30-day sale is a fraction of one month’s move.

Intramonth strike dispersion (input #20, default off in the engine’s SimConfig::default; the cockpit’s base configuration turns it on, Marc, 2026-07-13, spec v1.4). Originations happen throughout a month, not at its close. With the toggle on, each Agreement in a cohort draws its own strike around the cohort month’s price. The spread is derived from the path’s own past so that it fits any mode, uses only what is known at entry, and is exactly zero on a flat path (engine.rs::entry_sigma_monthly): with \(\sigma_m\) the population standard deviation of the path’s monthly log returns over the twelve months before the cohort month (the mode’s stated volatility over \(\sqrt{12}\) when fewer than three returns precede it),

\[ \sigma_{\text{intra}} = \frac{\sigma_m}{\sqrt 2}, \qquad K = P_m \exp!\big(\sigma_{\text{intra}} Z - \tfrac12 \sigma_{\text{intra}}^2\big), \quad Z \sim N(0,1). \]

The \(1/\sqrt 2\) is the standard deviation of a price observed at a uniformly random time inside the month relative to its end, and the half-variance term makes the draw mean-preserving, so the expected strike equals the monthly mark. On a path whose realized volatility is 41.4%, \(\sigma_m = 0.1195\) and \(\sigma_{\text{intra}} = 0.0845\), so one standard deviation of entry price is about 8.5% around the month’s mark. The purchase price is the strike times input #6b (par at the base terms), so it disperses with it. The test intramonth_dispersion_smears_strikes_and_preserves_the_mean checks the spread and the mean; dispersion_is_inert_on_a_flat_path checks that toggling it on a flat path changes nothing to the cent. The purpose is to smear threshold behavior (moneyness, the conviction rule, coverage) across a cohort instead of firing it for ten identical twins at once (Marc, 2026-07-12, spec v1.4).

The seed, and what it drives

The engine is fully seeded (ChaCha20) and the same configuration gives byte-identical output; the test seeded_determinism runs one configuration twice and compares. Since v1.6 the seed feeds two kinds of stream, and the distinction is what makes the bump a sensitivity rather than noise.

The run stream is ChaCha20Rng::seed_from_u64(config.seed) (engine.rs::run). It is consumed by path generation only: the bridge’s normals and the bootstrap’s block starts. Replay and the custom path do not touch it, and the shock and the bump are deterministic overlays. So the seed drives the path, and only the path, at the book level.

The Agreement streams. Every Agreement gets its own ChaCha20Rng, keyed from (config.seed, agreement id) through a splitmix64 expansion (engine.rs::agreement_rng). The Agreement’s strike draw under dispersion, its stop draws at every payment date, its early-completion draws and, in the rational mode, its redirect pick all come from that stream, and the draws are made unconditionally at each date whether or not they are used. Consequently a bumped price, a shock, or a different exit elsewhere in the book changes decisions but never the random numbers behind them. Two runs on the same seed that differ only in the bump produce the same draws Agreement by Agreement, and the difference in the Holder’s outcome is attributable to the price alone. That is what bump-and-revalue needs (test greeks_are_sensitivities_not_draw_noise).

Monte Carlo and ensembles. Run \(i\) of a Monte Carlo of \(n\) runs uses seed \(\text{base} + i\) (outputs.rs::run_monte_carlo, seeds base_seed..base_seed+n); the API’s streaming endpoint runs chunks with a seed offset the same way (forwardflow_api.rs::run_chunk), and identical requests are memoized because the result is a pure function of the request. The Greeks average over greek_seeds seeds (default 16, at most 256) starting at the config’s seed. A change of seed therefore changes the path and every Agreement’s draws together, which is the intended meaning of “another world”; a change of anything else on the same seed holds the world fixed.

The Model Card’s figures are stamped at seed 42, and it records that the v1.6 move to per-Agreement streams changed what seed 42 draws relative to the v1.5 run of the same morning: the zero-drift medians moved by a tenth of a point, the single-vintage replay median by ten points. Same seed, different draw assignment; the seed is reproducibility, not a pinned outcome across engine versions.

Realized volatility of a path

The engine’s own definition, used for the intramonth spread and for translating a vega bump into a vol factor, is the population standard deviation of the monthly log returns, annualized by \(\sqrt{12}\) (exposure.rs::realized_vol):

\[ \sigma_{\text{real}} = \sqrt{12};\sqrt{\frac{1}{n}\sum_{i=1}^{n}(r_i - \bar r)^2}, \qquad r_i = \ln\frac{P_i}{P_{i-1}}, \]

and zero for a path shorter than three prices. Note the two conventions in the repository. The engine divides by \(n\); the examples that print the Model Card’s regime figures (examples/w0108_refresh.rs, examples/bridge_print_set.rs) divide by \(n - 1\). On the trailing 24-month window the two read 40.6% and 41.4%; on the full history 86.5% and 86.8%. The Model Card quotes the sample figures. Neither convention enters a ledger posting; the difference matters only when a reader tries to reproduce a printed number.

A bridge at 43% does not realize 43%: the pinning removes variance, most of it in the last months, so the realized figure on one bridge is a little below the input and varies by seed. The bootstrap realizes whatever its window’s de-meaned returns happen to give under the blocks drawn. A replay realizes history’s figure for that window. A custom path or a ramp realizes the volatility of its own kinks, which for a pure ramp is zero.

Choosing a mode

ModeRandomnessEndpointDriftWhat it answers
BridgeSeeded normalsPinnedSet by the endpoints“If the coin ends at $X, what do the routes there do to the paper?”
Historical replayNoneFreeHistory’s“What would this book have done from month M?”
Zero-drift bootstrapSeeded block startsFreeZero by constructionThe pricing stance: current volatility, no appreciation
CustomNoneFreeAs drawnA stated reversal shape as a data literal
Shock (overlay)NoneScaledAdds a crashCrash timing and depth on top of any of the above
Bump (overlay)NoneScaledKept (vol factor) or shifted (price factor)The Greeks; a what-if from a month on

GBM, jump diffusion, regime switching, uploaded monthly paths and the volatility surface are implemented and documented in Surfaces and hedging. Their modelling limits are in Limits and assumptions. The inputs that select and parameterize each mode are listed with their numbers in Every input.