How the engine works
This chapter is the map of the machine. It says what the pieces are, in what order one simulated month runs, which conventions fix the timing and the cents, and why the same request always returns the same bytes. The math of each piece has its own chapter; this one shows where each piece sits and names the function that owns it.
Three layers
The simulator is three programs stacked on one another.
| Layer | Where | Job |
|---|---|---|
| Engine | Rust crate forwardflow (backend/forwardflow/) | All model math. Config in, a ledger and a list of Agreements out. No I/O, no clock, no network. |
| API | Axum server (backend/src/main.rs, backend/src/api/forwardflow_api.rs) | Stateless HTTP over the crate. Runs the engine on a blocking thread, turns validation panics into HTTP 400, memoizes Monte Carlo, enforces the abuse caps. |
| Cockpit | Next.js app (web/app/forwardflow/) | Research, risk analysis, hedge strategies, historical vintages, monthly analysis and scenario reporting. The browser displays server results and derives presentation summaries and independent ledger checks. |
The source defines the implemented calculation. The specification records its intent. A disagreement requires investigation; it is not resolved merely by displaying a result. lib.rs lists the modules: contract (the schedule and the implied rate), defaults (hazards), paths (prices), ledger, fees, money, engine (the monthly loop), outputs (IRR, WAL, Monte Carlo), exposure and boundary (risk analysis), and surface, hedge, series and coin (pricing, hedge flows, monthly analysis and coin-denominated analysis). The server exposes the calculations through the REST API and the bounded MCP interface. The API reference lists the current route families.
There is no database. A request carries the whole configuration; the response carries the whole result. There is no saved-run database. The server does retain bounded computation caches and access-limit state; the browser can retain scenario drafts and completed run records locally.
Three entities and the Market boundary
Every dollar the engine moves belongs to one of three parties, or crosses the boundary to the market. They are the variants of ledger.rs::EntityId.
| Ledger entity | The word in this book | Who |
|---|---|---|
Owner | the Holder | The receiving side of the paper. Holder 0 is always BTC Now; a Holder n bought the paper by Sale. |
BtcNow | BTC Now | Originator and servicer. Takes the first N payments and the servicing fee, never a share of a stop sale. |
Obligor(id) | the Buyer of Agreement id | The paying side. Buyer 0 opened the Agreement; the paying side changes hands by Transfer. |
Market | the market | The external counterparty of the recorded stop sale. Dollars enter the system here when a coin is sold. |
The identifiers Owner and Obligor are older than the vocabulary lock (Marc, 2026-09-02) and stay in the code so that stored results keep deserializing. The words in prose are Holder and Buyer.
The Market entity is what makes a stop balance. The coin is never on the ledger; only dollars are. When a stop sells the coin, the proceeds are posted from Market to the Holder, to BTC Now and to the Buyer, and the Market’s balance goes negative by exactly the sale amount. Everything the three parties end up with was either paid by a Buyer, paid by the Holder, or sold to the market.
The ledger
ledger.rs::Ledger is an append-only list of postings. A posting is one transfer: month, from, to, amount, kind, Agreement id. There are no debits without credits, because Ledger::transfer writes both sides of one amount at once. That is what “double-entry” means here, and it gives conservation for free.
\[ \sum_{e \in \text{entities}} \text{balance}(e) \equiv 0 \]
ledger.rs::Ledger::conservation_sum computes that sum; engine.rs::run asserts it is zero at the end of every run in debug builds, ff_simulate reports it as conservation_ok on every response, and the invariant test conservation_to_the_cent_across_scenarios checks it across scenario families. Ledger::verify_balances recomputes every balance from the raw postings and compares it with the running map, which is the drill-down audit: any reported total can be rebuilt from the postings by hand.
Three rules in Ledger::transfer keep the ledger honest. An amount must already be quantized to the cent, or the call panics. An amount must be non-negative; a refund is a transfer from the market to the Buyer, not a negative transfer the other way. An amount of exactly zero is dropped, so a stop whose refund is zero posts no StopRefund line at all. The invariant test stop_sale_regime_one_exact_cents asserts that last point.
Money: Decimal and f64
The engine keeps two kinds of numbers apart on purpose.
Money is rust_decimal::Decimal, quantized to cents by money.rs::cents, which rounds half-to-even. Every ledger amount, every schedule line, every fee posting is Decimal. Sums of Decimals are exact, so the schedule sums to the terminal value to the cent and the lifetime fee is exact to the cent.
Boundaries and statistics are f64. Prices on the path, the sale price of a stop, the implied financing rate, moneyness, hazards, IRR, WAL and the percentiles of a Monte Carlo are all floats. A float becomes money in exactly one place: money.rs::cents_from_f64, which is called when a strike is fixed at origination, when a sale price is turned into proceeds, when the value of the coin at early completion is recorded as the Buyer’s equity, and for the frontier’s reference strike. Once a number has become money it never goes back to a float on the ledger side.
The rule in the repository’s instructions is the short form: Decimal for money, f64 for boundaries and display, never f64 in a ledger posting.
One month, in order
engine.rs::run is a loop over calendar months \(m = 0, 1, \dots, H\). Before the loop it validates the configuration, seeds the path generator, draws the price path, applies the shock overlay and the bump overlay in that order, and (when input #25 is on) computes the rational walk-away frontier once. Then, each month, five steps run in a fixed order. The order is fixed for reproducibility, and because it is part of the model: which step runs first decides who has paid before who leaves.
| Step | What happens | Code |
|---|---|---|
| 0. Originate | If \(m\) is still inside the origination window, a cohort of agreements_per_cohort Agreements opens. Each gets a strike (the month’s price, or a dispersed draw around it), a schedule, a fee state, and its own random stream. The Holder pays BTC Now for the paper, strike times input #6b: one PurchasePrice posting per Agreement. | engine.rs::run, origination block |
| 1. Payments | Every active Agreement at age \(1 \le t \le \text{term}\) makes payment \(t\), unless a walk is pending for it. With the rational boundary on, a Buyer whose coin is below the frontier for this payment date does not pay; the walk is armed instead. Payment \(t \le N\) goes whole to BTC Now (OriginationFee). Payment \(t > N\) splits into PaymentDelivery to the Holder and FlowFee to BTC Now. Payment \(\text{term}\) closes the Agreement as Completed. | engine.rs::run, step (1) |
| 2. Walks | (2a) Every pending walk executes: the missed payment date is this month, and stop_sale runs. (2b) With the conviction rule on, streaks are updated: a Buyer whose coin is below \((1-X)\) times the strike for \(Y\) consecutive payment dates has a walk armed for the next payment date. | engine.rs::run, step (2); engine.rs::stop_sale |
| 3. Early completions | When the propensity \(p\) (input #12) is above zero, every active Agreement with no walk pending at age \(1 \le t < \text{term}\) draws a uniform \(x\) from its own stream. If the coin beats the remaining schedule, the Buyer completes early with probability \(u_t = p \cdot \max(0, (S_t - R_t)/S_t)\), paying \(R_t\) in cash and taking the coin. With the take-profit gate on, the draw still happens but only counts once \(S_t \ge P(1+x)\). | engine.rs::run, step (3); engine.rs::settle |
| 4. Hazard draws | Every active Agreement with no walk pending at age \(1 \le t < \text{term}\) draws a uniform \(d\) from its own stream and stops if \(d < h_t\), where \(h_t\) is the scenario’s monthly hazard, scaled by the drawdown multiplier (capped at 1) when input #23 is on. In rational mode (input #9) the draw only sticks on a Buyer whose coin is below the amortized obligation; otherwise it is redirected to a random underwater Buyer, or suppressed and counted if there is none. | engine.rs::run, step (4); engine.rs::drawdown_multiplier; engine.rs::Agreement::underwater |
Two consequences of the order matter to a risk desk. First, an early completion or a hazard draw in month \(m\) follows that month’s payment. The draw records the next payment date as missed. Second, a successfully paid final date completes the Agreement before any further draw, but a hazard draw after payment \(\text{term}-1\) can miss the final payment. A pending walk or the boundary’s decision before payment can also consume it unpaid. defaults.rs calibrates the baseline lifetime target over draw ages 1 through term−1 and sets the hazard at the term to zero.
The rational-boundary decision sits inside step 1 rather than in a step of its own, because it is a decision about this payment. The Buyer looks at the coin, looks at the frontier for payment \(t\), and either pays or does not. A Buyer who does not pay has a walk pending, and (2a) executes it in the same month. The exit carries the tag RationalBoundary and is counted separately in the exit split. This mode was lifted from the parked Behavior Engine into the main engine as spec change v1.6 #6 (Marc, 2026-09-03); the frontier itself is derived in The risk desk.
The behavior models behind steps 2 to 4 are in Who stops paying. The conviction rule and the boundary are “walks” and the hazard is a “draw”: walks are decided at or before a payment date and consume it unpaid, draws happen after the payment date has passed.
Timing conventions
Every exit is stamped with a month, and the stop has three dates. The conventions are deterministic and written into engine.rs; reconciling a per-Agreement row against the postings needs all of them.
A draw at age \(t\) misses payment \(t+1\). Step 4 runs after step 1, so the Buyer made payment \(t\) this month. The missed payment date \(D\) is the next one, \(m+1\), and stop_sale is called with missed = m + 1. The Agreement’s exit_month is \(D\), and payments_made is \(t\).
A walk is the missed payment. A conviction walk or a boundary walk pending at month \(m\) consumes that payment date unpaid. The missed date is \(m\) itself; stop_sale is called with missed = m. So a walk at age \(t\) has payments_made \(= t-1\) and exit_month \(= m\).
Day 16 is the Stop Date; the sale is 18 days after the missed payment. A payment up to 15 days late carries nothing (R-1035). The coin is sold for dollars within two business days of day 16 (R-1037), which the engine models as stop_sale_lag_days = 18 calendar days after \(D\) (input #24). The sale is priced on the path by log-linear interpolation between the monthly marks:
\[ \text{pos} = D + \frac{\text{lag}}{30.4375}, \qquad k = \lfloor \text{pos} \rfloor, \qquad f = \text{pos} - k, \qquad S_{\text{sale}} = \exp\big((1-f)\ln S_k + f \ln S_{k+1}\big) \]
At 18 days, \(f = 0.5914\). The proceeds are \(V = S_{\text{sale}} \cdot e^{-\text{haircut}} \cdot (1 - \text{sale cost})\), turned into cents by cents_from_f64. Lag 0 sells at the missed date’s mark, lag 30.4375 at the next one; the invariant test stop_sale_regime_one_exact_cents checks all three.
Cash lands at the first monthly date at or after the sale. The three stop postings (StopSaleDelivery, FlowFee, StopRefund) are dated \(\lceil\text{pos}\rceil\), clamped to the horizon. At 18 days that is always \(D + 1\). The exit month stays \(D\).
Horizon. engine.rs::SimConfig::horizon uses the automatic run-off horizon below unless a supplied market_horizon_months extends it. A supplied shorter horizon is rejected. The automatic horizon is
\[ H = (\text{effective cohorts} - 1) + \text{term} + \left\lfloor\frac{\text{stop sale lag days}}{30.4375}\right\rfloor + 1 \]
The effective cohort count includes the origination cut-off. The last cohort originates at month \(\text{effective cohorts} - 1\); its Buyers can miss payment \(\text{term}\) at the latest. The settlement tail keeps that final stop’s sale and receipt inside the ledger. At the default 18-day lag it adds one month: 24 cohorts and 60 payments give \(H = 23 + 60 + 1 = 84\), with 85 path marks.
The whole stop is worked, with its three regimes, in The stop waterfall.
Determinism
The engine is fully seeded. Same configuration and same seed produce the same postings, byte for byte. Three mechanisms make that true, and a fourth makes it useful.
The path has its own stream. engine.rs::run seeds a ChaCha20 generator from config.seed (ChaCha20Rng::seed_from_u64) and uses it only for the price path: the bridge’s increments or the bootstrap’s block picks. Historical replay and custom paths draw nothing. Shocks and bumps are deterministic overlays applied after the draw.
Every Agreement has its own stream. engine.rs::agreement_rng(seed, id) builds a 256-bit ChaCha20 key from the configuration seed and the Agreement’s id. The mixing is splitmix64: the id is multiplied by the golden-ratio constant \(\text{0x9E3779B97F4A7C15}\), the product is rotated left by 23 bits and XORed with the seed, and four rounds of splitmix64 (add the constant, apply the finalizer) fill the four 64-bit words of the key. Agreement 17 under seed 42 gets the same stream in every run of every configuration that uses seed 42, whatever the other 239 Agreements did. Its dispersed strike (input #20), its early-completion draw, its hazard draw and, in rational mode, the redirect pick all come from this stream.
Draws are unconditional. In steps 3 and 4 the uniform is drawn before the engine looks at the price, every month the Agreement is eligible, whether or not the price makes the draw matter. Spec v1.6 introduced both this and the per-Agreement streams for one reason: bump-and-revalue. When the risk desk bumps every future price by 5% and reruns, the random numbers behind every decision are identical; only the decisions change. The difference between the two runs is a sensitivity, not draw noise. Under the earlier single shared stream, one Agreement leaving a month earlier shifted every later draw in the book, and the Greeks were unreadable.
Monte Carlo seeds are consecutive. outputs.rs::run_monte_carlo runs \(n\) simulations with seeds \(\text{base}, \text{base}+1, \dots, \text{base}+n-1\) (wrapping_add), in parallel on the rayon pool, collected in order. Run \(i\) of a 10,000-run Monte Carlo is exactly the single run you get by asking for seed \(\text{base}+i\). The WebSocket variant runs the same seeds in chunks of 1,000 (forwardflow_api.rs::run_chunk passes the chunk offset), so streamed and one-shot results are the same numbers. The reported base_seed on the summary is what a reader needs to reproduce any run of the distribution.
One consequence when comparing across versions: changing how the streams are derived changes the draws. The Model Card notes that the v1.6 per-Agreement streams draw differently from v1.5 at the same seed, which moved the zero-drift medians by a tenth of a point and the single-vintage replay median by ten points. Determinism is a property of one engine version.
The posting kinds
ledger.rs::TxKind has seven variants. Every dollar in a run is one of them.
| Kind | From → To | When | Meaning |
|---|---|---|---|
PurchasePrice | Holder → BTC Now | Origination month | The Holder buys the paper. Strike times input #6b (par at the base terms: $60,000). Paper acquisition, not BTC Now’s take. The identifier names the Holder’s purchase, not the Buyer’s Purchase Price \(P\). |
OriginationFee | Buyer → BTC Now | Payments 1 to N | One of the first N payments, routed whole to BTC Now (input #3, N = 1 at the base terms). Never delivered to the Holder, so it carries no servicing fee. |
PaymentDelivery | Buyer → Holder | Payments N+1 to term | A scheduled payment, net of the servicing fee. |
FlowFee | Buyer or Market → BTC Now | With every delivery | The flat servicing fee on the delivered dollar (5%, Marc 2026-08-31). Posted alongside a PaymentDelivery, a MakeWholeDelivery or a StopSaleDelivery. |
MakeWholeDelivery | Buyer → Holder | Early completion | The remaining schedule \(R_t\), paid in cash, net of the fee. The identifier predates the vocabulary; it means early completion (spec v1.5 change 3, cash only). |
StopSaleDelivery | Market → Holder | Payment date after the sale | Stop-sale proceeds less the Buyer’s refund, net of the fee. Can exceed the remaining schedule: the surplus above the Purchase Price is the Holder’s (Marc, 2026-09-03). |
StopRefund | Market → Buyer | Payment date after the sale | The Buyer’s dollar refund, \(\min(A, \max(0, V + A - P))\) (R-1033, Marc 2026-08-22). Dropped when zero. |
BTC Now’s take is OriginationFee plus FlowFee, and nothing else (outputs.rs::analyze computes total_take as exactly that sum). There is no posting kind for a share of a stop sale, and the repository’s rules forbid adding one.
The money conventions
Three conventions fix how rational numbers become cents. They were decided at milestone M1 (M0_FIXTURES.md, “Cent-quantization conventions”) and they are what makes every identity in Verification testable to the cent.
C1. The payment is floored to the cent; the final payment absorbs the residual. contract.rs::ContractTerms::schedule computes
\[ \text{PMT} = \Big\lfloor \frac{P}{\text{term}} \Big\rfloor_{0.01}, \qquad \text{payment}_{\text{term}} = P - (\text{term} - 1),\text{PMT} \]
where \(P = \text{strike} \times \text{multiple}\) is the Purchase Price of the coin to the Buyer (the terminal value), itself rounded half-even to the cent by ContractTerms::terminal before the division. Flooring rather than rounding was an amendment after adversarial review (2026-07-11): rounding half-even could push the accumulated schedule past \(P\) at a micro-strike and make the final payment negative. With the floor, the residual is always non-negative and the schedule sums to \(P\) exactly. At the base terms, \(88{,}500 / 60 = 1{,}475.00\) is a whole cent and all 60 payments are equal. At 36 months, \(88{,}500 / 36 = 2{,}458.3\overline{3}\): payments 1 to 35 are $2,458.33 and payment 36 is $2,458.45.
C2. The fee is rounded cumulatively. fees.rs::FeeState::split keeps, per Agreement, the cumulative dollars delivered and the cumulative fee posted, and computes each fee posting as a difference:
\[ \text{fee}_k = \text{round}(f \cdot \text{cum}k) - \text{round}(f \cdot \text{cum}{k-1}), \qquad \text{net}_k = \text{delivery}_k - \text{fee}_k \]
Each posting conserves by construction (net plus fee equals the delivery), and the lifetime fee is exactly \(\text{round}(f \times \text{total delivered})\) whatever the individual postings did. The price of that exactness is that a single posting may wobble by a cent against \(\text{round}(f \times \text{delivery})\). At the base terms there is no wobble: 5% of $1,475.00 is $73.75, and the Holder nets $1,401.25 on every delivered payment. At 36 months the wobble appears: 5% of $2,458.33 is $122.9165, so the first fee posting is $122.92, the second is $122.91 (because \(\text{round}(0.05 \times 4{,}916.66) = 245.83\)), and the cumulative total is exact at every step. The fee applies to every delivered dollar: scheduled payments after the first N, early-completion payoffs, and stop-sale deliveries (Marc, 2026-08-31, “we retain 5% of each send”). The first N payments are not deliveries and carry no fee.
C3. IRR is computed on the quantized flows. outputs.rs::analyze builds the Holder’s monthly net flow from the ledger (ledger.rs::Ledger::owner_monthly_gross, inflows and outflows kept separate and netted only for IRR and payback), converts each cent-exact Decimal to f64, and outputs.rs::irr solves for the monthly rate by a grid scan in steps of 0.01 for a sign change of NPV over \((-0.95, 5.0)\) followed by 200 bisection steps. Nominal is 12 times the monthly rate; effective is \((1+r)^{12} - 1\). The M0 fixtures were generated in exact rational arithmetic on unquantized flows, so the Rust engine is required to match them within 0.01 percentage points, not exactly. The metrics are derived in Outputs.
A worked example at the base terms
Take the engine’s own default configuration (engine.rs::SimConfig::default): a $60,000 coin, 1.475× so \(P = $88{,}500\), 60 payments of $1,475, the first to BTC Now, 5% servicing, the Holder buys at par, sale 18 days after a missed payment at 25 bp cost and no haircut, a bridge from $60,000 to $60,000 at 43% volatility, the baseline hazard curve at a 40% lifetime prior, 2.5% monthly early-completion propensity, 24 cohorts of 10, seed 42. Posting this as POST /api/forwardflow/simulate with include_postings: true gives 240 Agreements, an 85-mark path, 17,728 postings and conservation_ok: true.
Origination. In month 0, ten PurchasePrice postings of $60,000.00 move from the Holder to BTC Now.
A delivered payment. Agreement 2 (cohort 0) pays $1,475.00 at age 1, posted whole to BTC Now as OriginationFee. At age 2 the same $1,475.00 splits: PaymentDelivery $1,401.25 to the Holder, FlowFee $73.75 to BTC Now.
A stop. Agreement 2 draws below its hazard at age 22 (month 22). Payment 22 was made in step 1; the draw in step 4 misses payment 23, so exit_month is 23 and payments_made is 22. The sale sits at position \(23 + 18/30.4375 = 23.591\), between the month-23 mark of $33,375.25 and the month-24 mark of $36,038.10; log-linear interpolation gives $34,925.24, and after 25 bp the proceeds are \(V = $34{,}837.93\). The Buyer has paid \(A = 22 \times 1{,}475 = $32{,}450\); the remaining schedule is \(R = 88{,}500 - 32{,}450 = $56{,}050\). Then \(V + A - P = -21{,}212.07 < 0\), so the refund is zero and no StopRefund posts. The whole $34,837.93 is delivered. Under C2 the Agreement had delivered \(21 \times 1{,}475 = $30{,}975\) with a cumulative fee of $1,548.75; the new cumulative is $65,812.93 and \(\text{round}(0.05 \times 65{,}812.93) = 3{,}290.65\), so the fee posting is $1,741.90 and the Holder’s StopSaleDelivery is $33,096.03. Both post in month 24, the payment date after the sale. The row shows shortfall_usd \(= R - V = $21{,}212.07\), stop_surplus_usd 0, owner_net $62,522.28 and capital_pnl $2,522.28: the Holder got its $60,000 back and a little more, because 21 delivered payments came in before the stop.
The three regimes of the waterfall, by the same arithmetic, for a Buyer who stops after 12 payments (\(A = $17{,}700\), \(R = $70{,}800\)) with 11 prior deliveries:
| Proceeds \(V\) | Refund \(\min(A, \max(0, V+A-P))\) | Delivered \(V - \text{refund}\) | Fee | Holder net | Shortfall \(\max(0, R-V)\) | Surplus \(\max(0, V-P)\) |
|---|---|---|---|---|---|---|
| $50,000 | $0 | $50,000.00 | $2,500.00 | $47,500.00 | $20,800 | $0 |
| $80,000 | $9,200 | $70,800.00 | $3,540.00 | $67,260.00 | $0 | $0 |
| $100,000 | $17,700 | $82,300.00 | $4,115.00 | $78,185.00 | $0 | $11,500 |
Below the schedule line the Holder takes everything and books a shortfall. Between the schedule and the Purchase Price the Holder is made whole first and the Buyer gets the rest of what he paid. Above the Purchase Price the Buyer is refunded in full and the surplus stays with the Holder. The refund base \(A\) includes payment 1, which BTC Now kept; that is modelled as “the maximum he paid” and is flagged as a term-sheet question, not a ruling (spec v1.5 change 8).
Abuse caps and the memoized Monte Carlo
The API runs on a shared box, so the engine and the server both bound the work a request can ask for. The cohort, book-size and Monte Carlo-work caps and the semaphore were added in the hosting hardening of 2026-07-13 (spec v1.4 change 13). Every cap returns HTTP 400 naming the offending input; the semaphore queues rather than refuses.
| Cap | Value | Where |
|---|---|---|
| Cohorts (input #13) | at most 1,200 | engine.rs::validate |
| Book size (cohorts × Agreements per cohort) | at most 50,000 Agreements | engine.rs::validate |
| Term (input #6) | 1 to 480 months | engine.rs::validate |
| Monte Carlo runs (input #14) | 1 to 100,000 | forwardflow_api.rs::validate_mc |
| Monte Carlo work (runs × book) | at most 24,000,000 Agreement-runs | forwardflow_api.rs::validate_mc |
| Concurrent heavy requests (Monte Carlo, heatmap, backtest) | 3 permits; the rest queue | forwardflow_api.rs::HEAVY |
The 24 million figure is the 100,000-run ceiling at the default 240-Agreement book; a bigger book gets fewer runs. The engine’s validate panics on a bad input with a message that names it; forwardflow_api.rs::catch_engine catches the unwind and turns it into the 400. That is why the build profiles must never set panic = "abort": the whole error path depends on unwinding.
Identical Monte Carlo requests are memoized. forwardflow_api.rs::MC_CACHE is an LRU of 64 summaries keyed by the run count and the configuration’s JSON. A hit returns the stored summary without touching the engine; the WebSocket variant sends its Complete frame at once. This is sound only because the engine is seeded: the same configuration and run count always produce the same summary, so a cached copy is the answer, not an approximation of it. The cache is in memory and is empty after a restart. A single simulate is not cached; it is fast enough not to need it.
The server also leaves one core free for the async runtime (main.rs sizes the rayon pool at cores minus one), gzips responses, and caches CORS preflights for a day. None of that changes a number.
What byte-identical means
The repository’s rule reads “same config ⇒ byte-identical output”. Here is the precise claim.
Given the same engine build, the same SimConfig (every field, including seed) and the same request flags, POST /api/forwardflow/simulate returns the same response body, byte for byte: the same postings in the same order with the same amounts, the same per-Agreement rows, the same path, the same outputs. Two consecutive requests at the base configuration above return bodies with the same SHA-256, and that is the check anyone can repeat with curl and shasum. The invariant test seeded_determinism in backend/forwardflow/tests/invariants.rs asserts the same thing one level down: two runs of one configuration serialize to identical postings, and a different seed serializes to different ones.
What is covered: every random draw (the path, dispersed strikes, early-completion draws, hazard draws, rational-mode redirects), every posting, every derived output, and every run of a Monte Carlo individually by its seed. Parallelism does not enter: each Monte Carlo run is independent and collected in order.
What is not covered: a different engine version, which may derive the streams differently (v1.5 to v1.6 did); a different configuration that looks the same but differs in a defaulted field; and floating-point results across different hardware or compilers, which the repository does not test. On one build on one machine, the claim is exact.
The risk desk depends on this. A bump-and-revalue Greek is the difference between two runs that share every draw and differ only in the prices. A Model Card figure carries its seed and its configuration, and is the deterministic function of them. If a reader reproduces a run and gets a different number, the build or the configuration changed, and the engine’s job is to make that the only possible explanation.