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

ForwardFlowInputs & defaults

Inputs and defaults

This chapter explains the core simulation inputs using the specification’s established input numbers. It gives the JSON field, default, validation bounds and effect. Workspace labels may be shorter than the historical labels in these tables. The API explorer supplies the deployed schema, and Surfaces and hedging covers additional price models and desk-specific parameters. The chapters that derive the math are linked from each group.

Where the inputs live

Every input is a field of engine.rs::SimConfig, the config member of every request body on the API (see The API) and the SimConfig type in lib.ts. Distinguish the engine defaults, the legacy desk configuration and a new workspace scenario.

  • engine.rs::SimConfig::default is the program base case as the engine understands it, used by the tests and the examples.
  • lib.ts::BASE_CONFIG supplies the legacy and advanced desk base configuration.
  • New Dollar and Coin workspace scenarios (scenario.ts::defaultWorkspaceScenario) start from that configuration but use illustrative GBM with continuous annual drift 0 and volatility 43%. Saved scenarios keep their own path mode and assumptions.

Two differences matter: intramonth strike dispersion (#20) is off in the engine default and on in the workspace; market_horizon_months is null in the engine default and explicitly 84 in BASE_CONFIG. The default 24-cohort, 60-month book needs 84 months either way, but the explicit horizon keeps the market endpoint fixed when pacing is shortened. MCP fills omitted fields from the engine default, not the workspace draft. The archived Model Card run of 3 September 2026 is a historical configuration, not a current default: drawdown multipliers (#23) on, 24 cohorts of 20, the zero-drift bootstrap from the trailing 24 months. Its §2 states it in full.

Input validation is defined by engine.rs::SimConfig::check, called before API work estimation and by the engine’s validate wrapper. The engine wrapper panics on failure with a message naming the input; forwardflow_api.rs::catch_engine unwinds the panic into HTTP 400 with that message as the body. A few checks live in the module that owns the input: the custom per-year curve in defaults.rs::custom_yearly_hazard, the custom path in paths.rs::custom, the boundary’s parameters in boundary.rs::BoundaryParams::validate, the risk desk’s bumps in exposure.rs::greeks. Path generation failures (paths.rs::PathError) are ordinary errors and reach the client the same way. The error texts below were read back from the running engine on 2026-09-03. Where a message says “default”, that is the engine’s identifier for a stop.

Numbering. The spec table runs #1 to #25 with three gaps that are not fields of SimConfig. #10 is the state of #9 when it is off. #14 (Monte Carlo runs) and #19 (the inverse price solver, target_effective_irr, accepted in \( -0.9 < r < 10 \) by outputs.rs::solve_purchase_price) are request fields of their own endpoints; the solver, the heatmap grids and the vintage backtest are described in The API. #21, a pooled-vehicle view, was removed (Marc, 2026-07-12; spec v1.4 change 11): recycling multiplies wealth, never the rate. The bump overlay and the seed carry no spec number.

Types on the wire. Money-exact fields are Decimal in Rust and travel as JSON strings ("1.475"); prices, rates and probabilities are f64 numbers; counts are unsigned integers; optional inputs are null when off; the enums (path, scenario) are externally tagged objects such as {"Bridge": {"end_price": 60000, "vol_annual": 0.43}}.

Price path

The path is generated once per run by paths.rs::generate from the seed; the shock and the bump overlays are then applied to it, in that order. It has horizon + 1 monthly marks, month 0 to month \( H \), where engine.rs::SimConfig::horizon sets

\[ H_{\min} = (C_{\text{eff}} - 1) + n + \left\lfloor\frac{\text{lag days}}{30.4375}\right\rfloor + 1, \]

with \( C_{\text{eff}} \) the cohorts that actually originate (#13 capped by #18) and \( n \) the term. horizon() uses market_horizon_months when supplied, which must be at least this minimum; otherwise it uses the minimum. At the base case that is \( 23 + 60 + 1 = 84 \), so 85 marks; the extra month lets a stop at the last stoppable payment date settle inside the ledger (spec v1.5 change 4). Derivations: Price paths, shocks and bumps.

#Cockpit labelFieldTypeDefaultRange and error textWhat it does, where it is read
1Start pricestart_pricef64, dollars60,000finite, \( 0 < p < 10^{12} \). start price (input #1) out of range: {p}Month-0 price: the first cohort’s strike, and the level every path mode is rebased to. Read in engine.rs::run for path generation and cohort strikes, and by the risk desk for the frontier’s terms.
2End pricepath.Bridge.end_pricef64, dollars= start (60,000)finite, \( 0 < p < 10^{12} \). end price (input #2) out of range: {p}The pinned final mark of the bridge. paths.rs::bridge.
2bVolatility %/yrpath.Bridge.vol_annualf64, fraction0.430 to 5. volatility (input #2b) must be 0–500%, got {v}Annual volatility of the bridge, \( \sigma_m = \sigma / \sqrt{12} \) per month; 0 gives the deterministic log-linear ramp. paths.rs::bridge.
2cModepathenumBridgesee the modes belowWhich generator runs. paths.rs::generate.

The eight modes of paths.rs::PathMode (the four additional stochastic/upload modes are derived in Surfaces and hedging):

ModeCockpitFieldsNotes
Bridge“Bridge (pinned)”end_price, vol_annualSequential Brownian bridge in log space, each step conditioned on the distance left to the endpoint.
HistoricalReplay“Historical replay”, field “Start month”start_indexActual monthly closes from that index, rescaled so month 0 equals #1. Fails with historical replay needs {H+1} months from index {i}, have {k} when the horizon runs past the series (paths.rs::PathError::InsufficientHistory). The cockpit’s mode switch lands on index 21, November 2013.
ZeroDriftBootstrap“Real returns, trend removed”, fields “Regime from” and “Block length (months)”regime_start_index, block_lenBlock bootstrap of de-meaned log returns from the regime start onward. Fails with bootstrap regime slice too short: {k} returns when fewer than block_len + 1 bars remain (paths.rs::PathError::RegimeTooShort). The cockpit’s switch uses block 6 from index 59, January 2017.
CustomCustom anchors / crash presetspoints: [[month, ratio], ...]Piecewise log-linear through anchors from (0, 1.0), flat after the last. paths.rs::custom requires at least one anchor (custom path (input #2c) needs at least one anchor point), strictly increasing months after 0 (custom path (input #2c): anchor months must be strictly increasing and start after month 0) and positive finite ratios (custom path (input #2c): price ratios must be positive and finite).
GbmGeometric Brownian motionmu_annual, vol_annualUnpinned monthly lognormal path; drift is the continuous rate of expected price growth. Current workspace starting cases use this mode.
JumpDiffusionJump diffusionmu_annual, vol_annual, jump_rate_annual, jumpGBM diffusion with compensated Poisson log jumps; Merton or Kou jump sizes.
RegimeSwitchingRegime switchingmu_annual, calm_vol, stressed_vol, p_calm_to_stressed, p_stressed_to_calm, start_stressedTwo monthly volatility states with a common continuous drift.
UploadUploaded monthly pricespricesAt least horizon + 1 positive finite prices; rebased to the configured start price.

The embedded series (paths.rs::historical_closes, compiled in from backend/data/btc_historical_monthly.csv) has 174 monthly bars, February 2012 to July 2026. Bar \( i \) is the month \( 12(y - 2012) + (m - 2) \), so January 2017 is 59 and July 2024, the start of the Model Card’s trailing-24-month pricing window, is 149. GET /api/forwardflow/history returns the month list.

Paper terms

The contract math is in contract.rs::ContractTerms; Contract math and the fee derives it. Every Agreement in a run shares these five inputs. Only the strike varies, by cohort month and, when #20 is on, within a cohort.

#Cockpit labelFieldTypeDefaultRange and error textWhat it does, where it is read
3First N payments → BTC Noworigination_paymentsu321\( N \le n \). N (input #3) cannot exceed the termPayments 1 to N route whole to BTC Now and are never delivered to the Holder, so they carry no servicing fee; 0 turns the toggle off. engine.rs::run step (1). A stop’s refund base still counts them (engine.rs::Agreement::paid_in).
4Servicing fee % of each deliveryservicing_fee_rateDecimal, fraction“0.05”\( 0 \le f < 1 \). servicing fee (input #4) must be 0–100% of each delivered dollar, got {f}Flat share of every dollar delivered to the Holder: payments after the first N, early-completion payoffs, stop-sale deliveries (Marc, 2026-08-31; spec v1.5 change 2). Not derived from the term. contract.rs::ContractTerms::fee_rate, split by fees.rs::FeeState::split with cumulative rounding, so the lifetime fee is exact to the cent.
5Price multiple ×multipleDecimal“1.475”\( 0 < m < 100 \). price multiple (input #5) must be positive and sane, got {m}The Purchase Price as a multiple of the coin’s cost. The implied financing rate is a display derived from it by contract.rs::ContractTerms::implied_monthly_rate (a bisection on the annuity identity); 1.475× at 60 months is 16.50% nominal. contract.rs::ContractTerms::terminal, schedule.
6Term (months)term_monthsu32601 to 480. term (input #6) must be 1–480 months, got {n}Payments per Agreement; the payment, the hazard shape and the horizon re-derive from it, and nothing hard-codes 60 (spec v1.1 change 1). A hazard draw after payment n−1 can miss payment n; no new hazard is drawn after completion at n. A positive baseline lifetime prior therefore needs \( n \ge 2 \).
6bPurchase price % of coin costpurchase_pct_of_strikeDecimal, fraction“1.00” (par)\( 0 < q < 100 \). purchase price (input #6b) must be a positive fraction of strike, got {q}What the Holder pays per Agreement, as a fraction of that Agreement’s strike; par since v1.4 (was 1.05). Posted at origination in engine.rs::run as PurchasePrice from Owner to BtcNow.

At the base terms a $60,000 coin at 1.475× is a Purchase Price of $88,500. contract.rs::ContractTerms::schedule divides it into 60 payments quantized to cents toward zero, the last absorbing any residual; here every payment is exactly $1,475.00. Payment 1 goes to BTC Now; each later payment splits $73.75 to BTC Now and $1,401.25 to the Holder, who paid $60,000 at par.

Behavior

Who stops, who completes early, and how the price bends both. The monthly order is fixed in engine.rs::run: (1) scheduled payments, with the rational boundary’s decision before each; (2) walks armed at an earlier date, then the conviction streaks; (3) early completions; (4) the hazard draws. Who stops paying derives the hazards and the modes. The bracket a Holder runs is #7 alone, then #23, #9 and #25 (spec §1, row 25).

#Cockpit labelFieldTypeDefaultRange and error textWhat it does, where it is read
7Lifetime default % (and the per-year bars in custom mode)scenarioenum{"BaselineCurve": {"lifetime": 0.4}}see the variants belowThe hazard \( h_t \) by payment age, built once per run by defaults.rs::DefaultScenario::monthly_hazard and drawn against in engine.rs::run step (4); \( h_n \equiv 0 \).
8the chips 700+ · 600 · 500 · 400(loads #7)presetnonenoneLifetime targets 15% · 35% · 55% · 70% (defaults.rs::DefaultScenario::fico_preset, lib.ts::FICO_PRESETS): the baseline curve at those targets, not separate fields.
9Rational defaultrational_defaultboolfalsenoneOn: a drawn stop sticks only if the Buyer’s coin is below the amortized obligation \( B_t \) (engine.rs::Agreement::underwater); an in-the-money draw is redirected uniformly to an underwater Agreement, or suppressed and counted when none exists (Marc, v1.1). engine.rs::run step (4).
10(the off state of #9)Stops land by the hazard alone, blind to price.
11Lost-conviction rule; X% below entry price; Y consecutive monthsconviction = {enabled, x_underwater, y_consecutive}bool, f64, u32off; 0.5 and 6 when onwhen on: \( 0 \le X \le 1 \), \( Y \ge 1 \). lost-conviction rule (input #11): X must be 0–100% and Y ≥ 1A Buyer whose coin has sat below \( (1 - X) \) times the strike for \( Y \) consecutive payment dates walks at the next one; the walk is the missed payment. X is against the strike, not the obligation (Marc, v1.1). engine.rs::run step (2b) arms, step (2a) executes; defaults.rs::ConvictionRule.
12Early completion %/mosettlement_propensityf64, per month0.0250 to 1. settlement propensity (input #12) must be 0–100%, got {p}Monthly chance of early completion, scaled by how far in the money the coin is: \( u_t = p \cdot \max(0, (S_t - R_t)/S_t) \). The Buyer pays the remaining schedule \( R_t \) in cash and takes the coin. engine.rs::run step (3), engine.rs::settle.
22Take-profit gate; Profit ≥ % of all-in costsettlement_min_returnOption<f64>null (off); the cockpit sets 0.10 when switched on0 to 10. settlement take-profit gate (input #22) must be 0–1000%, got {x}When set, early completion needs \( S_t \ge P (1 + x) \), the coin beating the whole Purchase Price \( P \) by \( x \); at 10% that is $97,350 on the base coin. The Agreement’s random number is drawn first regardless (v1.6, so the stream never depends on the price); the gate is then checked before that number is compared with \( u_t \). engine.rs::run step (3).
23Drawdown-scaled hazarddrawdown_hazard_multipliersboolfalse (engine and cockpit); on in the Model Card’s runnoneMultiplies \( h_t \) by a state read off the coin against the Buyer’s entry: in the money ×0.5, drawdown ≤30% ×1.0, 30–50% ×1.5, 50–70% ×2.0, deeper ×3.0, capped at 1. engine.rs::drawdown_multiplier, step (4).

The three variants of defaults.rs::DefaultScenario:

VariantJSONSemanticsValidation and error text
BaselineCurve{"BaselineCurve": {"lifetime": 0.40}}The program hump, normalized to term fraction (at 60 months: ages 1–3 ×1.0, 4–15 ×2.0, 16–24 ×1.2, 25–36 ×0.7, 37–60 ×0.25), scaled by bisection so the flat-path lifetime stop share equals the target exactly.\( 0 \le L < 1 \): lifetime default target (input #7/#8) must be in [0%, 100%), got {L}. And \( n \ge 2 \) unless \( L = 0 \): lifetime default target (input #7) needs a term (input #6) of at least 2 months — the final payment date cannot default.
FlatAnnual{"FlatAnnual": {"annual_rate": 0.10}}A constant annual stop rate converted to the monthly hazard \( 1 - (1 - r)^{1/12} \); rate semantics, not a lifetime target. API only, no cockpit picker.\( 0 \le r \le 1 \): flat annual default rate (input #7) must be 0–100%, got {r}.
CustomYearly{"CustomYearly": {"shares": [s1, ..., sY]}}shares[y] is the share of the original book that stops in year y+1, summing to the lifetime; each year’s mass is spread over its stoppable ages and divided by the survival so far, so a flat path realizes the shares exactly (v1.4).One share per year, \( \lceil n/12 \rceil \) of them: custom per-year defaults (input #7) need one share per year of the term: {Y} for {n} months, got {k}. Each finite and non-negative: custom per-year defaults (input #7) must be finite and non-negative. Sum below 100% with a \( 10^{-9} \) headroom: custom per-year defaults (input #7) must sum below 100%, got {pct}%. A year with no stoppable age must carry zero: custom per-year defaults (input #7): year {y} has no defaultable payment age at a {n}-month term (the final payment date cannot default) — its share must be 0%. All in defaults.rs::custom_yearly_hazard.

The validation messages above retain the phrase “the final payment date cannot default”. This refers to the absence of a new hazard draw after the final payment, not to immunity of that receipt: a draw after payment term−1 can miss the final payment, as can a walk decided before it.

Book and pacing

#Cockpit labelFieldTypeDefaultRange and error textWhat it does, where it is read
13Monthly cohortscohortsu32241 to 1,200. cohorts (input #13) must be at most 1,200, got {c}. Zero cohorts or Agreements per cohort are rejected with a named minimum-count messageMonths in a row in which a cohort originates, month 0 first. engine.rs::run, the origination block.
13Agreements per cohortagreements_per_cohortu3210cohorts × per-cohort ≤ 50,000. book size (inputs #13 × #18) must be at most 50,000 Agreements, got {b}Agreements signed in each originating month, at that month’s price unless #20 is on. The message’s “#18” names this count; the spec lists both counts under #13.
18Origination stops at month (0 = never)origination_stop_monthOption<u32>null\( \ge 1 \). origination window (input #18): stop month must be at least 1No cohort originates from this month on; the book runs off. engine.rs::SimConfig::effective_cohorts caps #13 with it and the horizon shortens unless the market horizon is set. Stopping origination caps the size of the position, never the rate on what is owned.
20Intramonth strike dispersionintramonth_strike_dispersionboolfalse in SimConfig::default; true in BASE_CONFIG (Marc, 2026-07-13)noneEach Agreement draws its own strike log-normally around its month’s price, mean-preserving, with \( \sigma_{\text{intra}} = \sigma_{\text{monthly}} / \sqrt{2} \) from the realized log returns of the twelve months before its cohort month — the path mode’s stated volatility with fewer than three behind it; zero on a flat path — from the Agreement’s own stream. engine.rs::run, the origination block; engine.rs::entry_sigma_monthly.
Market horizon (months)market_horizon_monthsOption<u32>null\( \ge \) the pacing’s requirement, \( \le 2{,}400 \). market horizon (market_horizon_months) must be at least the pacing's own requirement of {n} months (last cohort + term + the stop-sale settlement tail), got {h}The last month the price path is generated to, so a pacing change cannot move the bridge’s endpoint date (audit 2026-09-05, finding 2). null = \( (\text{cohorts} - 1) + \text{term} + \lfloor \text{lag}/30.4375 \rfloor + 1 \). engine.rs::SimConfig::horizon.

The base book is 24 × 10 = 240 Agreements.

The stop

After a missed payment, day 16 is the Stop Date, the coin is sold for dollars within two business days, and the proceeds pay by the ruled waterfall (Marc, 2026-08-22 and 2026-09-03; spec v1.5 change 1). Three inputs shape the sale; The stop waterfall derives the rest.

#Cockpit labelFieldTypeDefaultRange and error textWhat it does, where it is read
15Haircut % (sale below spot)haircutf64, log units0\( \ge 0 \), finite. stop-sale haircut (input #15) must be non-negative, got {h}The sale executes at spot \( \times e^{-h} \). The cockpit shows a percent and stores \( h = -\ln(1 - \text{pct}) \); the tornado’s “haircut 50%” is \( h = 0.693 \). Default 0 since v1.4 (Marc, 2026-07-12): a one-coin sale has no market impact, and the delay is priced by #24. engine.rs::stop_sale.
16Market-sale cost (bps)sale_cost_bpsf64, basis points250 to below 10,000. market-sale cost (input #16) must be 0–10000 bps, got {b}Execution cost of the sale: proceeds \( \times (1 - b/10{,}000) \). engine.rs::stop_sale; the same fraction goes to boundary.rs::rational_frontier.
24Sale lag (days after the missed payment)stop_sale_lag_daysf64, calendar days (the cockpit rounds to whole days)180 to 90. stop-sale lag (input #24) must be 0–90 days, got {d}Days from the missed payment date \( D \) to the recorded sale: day 16 is the Stop Date (R-1035) plus two business days (R-1037). The sale is priced by log-linear interpolation between the marks at \( D + d/30.4375 \); the cash posts at the first payment date at or after sale, \( \lceil D + d/30.4375 \rceil \); the horizon carries \( \lfloor d/30.4375 \rfloor + 1 \) months past the last term so the sale prices and lands inside the ledger at every lag (engine.rs::settlement_tail_months). engine.rs::stop_sale.

At the base lag the sale sits \( 18 / 30.4375 = 0.591 \) of the way from the missed date’s mark to the next, and the cash posts one month after the missed date. Worked at the base terms, with the coin at $60,000 when sold and 12 payments made: proceeds \( V = 60{,}000 \times (1 - 0.0025) = 59{,}850.00 \); paid in \( A = 12 \times 1{,}475 = 17{,}700 \); Purchase Price \( P = 88{,}500 \); refund \( \min(A, \max(0, V + A - P)) = \min(17{,}700, \max(0, -10{,}950)) = 0 \); the Holder is delivered $59,850.00 less 5% ($2,992.50), net $56,857.50; shortfall \( R - V = 70{,}800 - 59{,}850 = 10{,}950.00 \) (engine.rs::stop_sale).

The overlays

Multiplicative overlays applied to the generated path before the run starts, the shock first (engine.rs::run). Neither changes the seed’s draws.

#Cockpit labelFieldTypeDefaultRange and error textWhat it does, where it is read
17Shock designer; Drop %; Start month; Over months; Recover to % (blank = flat)shock = {start_month, drop_pct, duration_months, recover_to_pct}Option; u32, f64, u32, Option<f64>null; the cockpit toggle loads start 6, drop 0.5, 3 months, no recovery\( 0 < Z < 1 \): shock designer (input #17): drop must be 0–100% exclusive, got {Z}. \( W \ge 1 \): shock designer (input #17): duration must be at least 1 month. \( R > 0 \) when given: shock designer (input #17): recovery level must be positive, got {R}From month \( X \) the path is multiplied by a log-linear ramp reaching exactly \( 1 - Z \) at month \( X + W - 1 \), then held, or recovered log-linearly to \( R \) times the unshocked level at the final mark. Composes with any path mode. paths.rs::apply_shock.
none in the cockpit (the Risk desk uses it internally)bump = {from_month, price_factor, vol_factor, hold_strikes}Option; u32, f64, f64, boolnull\( 0.1 < \text{price} < 10 \): path bump: price factor must be within (0.1, 10), got {f}. \( 0 \le \text{vol} < 5 \): path bump: vol factor must be within [0, 5), got {f}. Must be null when asking for the Greeks: risk desk: clear the what-if bump (config.bump) before asking for the GreeksFrom from_month on, every price is multiplied by price_factor and the log returns’ deviations from their mean are scaled by vol_factor, so realized volatility moves while the drift is kept. hold_strikes true keeps strikes and purchase prices on the unbumped path (the existing book); false lets later cohorts strike at the bumped prices (the commitment). paths.rs::apply_bump; strikes in engine.rs::run. v1.6.

The rational boundary

Input #25 (spec v1.6 change 6) is the fifth behavior mode: a Buyer whose coin sits below the computed walk-away frontier for the payment date does not pay; the walk is the missed payment, settled like any stop with the exit tag RationalBoundary. The frontier is computed once per run by boundary.rs::rational_frontier from cohort-1 terms and is scale-invariant in the strike, so it serves every Agreement, dispersed strikes included. engine.rs::run reads frontier[t − 1] before payment \( t \) as a fraction of the entry price and compares spot with that fraction times the Agreement’s strike. The risk desk derives the lattice.

Cockpit label (Risk desk page)FieldTypeDefaultRange and error textWhat it does
Run the book with the rational boundary as the behavior moderational_boundaryOption<BoundaryParams>null (off)On when the object is present.
Lattice sigma %/yrrational_boundary.sigma_annualf640.414\( 0 < \sigma < 2.5 \). rational boundary (input #25): sigma must be 0–250%, got {s}The lattice’s annual volatility: the pricing stance’s trailing-24-month realized, 41.4%.
Boundary believed drift %/yrrational_boundary.mu_annualf640.25finite, \( \lvert \mu \rvert < 2 \). rational boundary (input #25): believed drift must be within ±200%/yr, got {m}The Buyer’s believed annual drift; zero is the pessimist who walks at or above par from the first dates.
Buyer discount rate %/yrrational_boundary.r_c_annualf640.15\( 0 \le r < 1 \). rational boundary (input #25): discount rate must be 0–100%, got {r}The Buyer’s discount rate on the continuation value.
Walk cost, % of coin costrational_boundary.walk_cost_of_strikef64, fraction0.025\( 0 \le c < 1 \). rational boundary (input #25): walk cost must be 0–100% of the coin's cost, got {c}What walking costs the Buyer as a lump (the six-month lockout, the re-strike at market, the lost access); 2.5% of a $60,000 coin is $1,500, about one payment.

One more check runs inside boundary.rs::rational_frontier: if \( \lvert \mu \rvert \sqrt{\Delta} \) exceeds \( \sigma \), with \( \Delta = 1/48 \) of a year (the lattice takes four price steps a month), the branch probability leaves \( [0, 1] \) and the run fails with rational boundary (input #25): |mu|·√Δ exceeds sigma — raise sigma or lower |mu|. The defaults are boundary.rs::BoundaryParams::default; the object is validated by boundary.rs::BoundaryParams::validate, called from engine.rs::validate and again by the frontier.

The seed

Cockpit labelFieldTypeDefaultRangeWhat it does, where it is read
Seedseedu6442anySeeds the ChaCha20 stream that draws the path (engine.rs::run) and, through engine.rs::agreement_rng, one independent stream per Agreement keyed on (seed, Agreement id) via splitmix64 (v1.6). Same seed and inputs give a byte-identical run; a bump or a changed exit elsewhere changes decisions, never the random numbers behind them. Monte Carlo run \( i \) uses seed \( + i \) (outputs.rs::run_monte_carlo).

The Risk desk request

POST /api/forwardflow/risk takes a config plus six options of forwardflow_api.rs::RiskRequest, shaping exposure.rs::exposure_report (the two lines, coverage, the exposure ladder, PD·LGD·EAD per vintage, the Greeks) and a frontier family. A config error surfaces as above; the option errors are listed here.

Cockpit label (Risk desk page)FieldTypeDefaultRange and error textWhat it does, where it is read
Ladder monthladder_monthOption<u32>null = the month of peak capital at riskclamped to the horizonThe calendar month at which the exposure ladder is cut, and the “as of” month of the Greeks, whose bumps start the month after. exposure.rs::exposure, exposure.rs::greeks.
Price bump %price_bumpf64, fraction0.05\( 0 < b < 0.5 \). risk desk: price bump must be a fraction in (0, 0.5)Delta and gamma by revaluing with every future price × \( (1 \pm b) \), strikes held for the existing book, released for the commitment delta. exposure.rs::greeks via paths.rs::PathBump.
Vol bump (points)vol_bumpf64, fraction (0.05 = 5 points)0.05\( 0 < v < 1 \). risk desk: vol bump must be a fraction in (0, 1)Vega: on every path mode, scale log-return deviations after the as-of month around their mean. The pre-as-of path is retained; a flat path is a no-op. The method is named in vega_method. exposure.rs::greeks.
Believed drift 1 … k %/yrfrontier_musVec<f64>, fractions[0, 0.10, 0.25, 0.50]1 to 8 entries. frontier_mus must hold 1–8 believed driftsOne frontier per believed drift, other lattice parameters shared; forwardflow_api.rs::ff_risk calls boundary.rs::rational_frontier once per drift, each bound by the #25 checks.
Lattice sigma %/yr · Buyer discount rate %/yr · Walk cost, % of coin costfrontier_paramsOption<BoundaryParams>nullthe #25 rangesThe lattice parameters other than drift. Resolution in ff_risk: this field, else the config’s rational_boundary, else BoundaryParams::default(); the response echoes the resolved set as frontier_params.
(not exposed; the cockpit omits it and the server’s default applies)greek_seedsu32161 to 256. risk desk: greek_seeds must be 1–256Seeds averaged for each Greek, starting at the config’s seed. exposure.rs::greeks.

forwardflow_api.rs::HEAVY admits three risk, Monte Carlo, heatmap or backtest requests at a time.

The Monte Carlo request

POST /api/forwardflow/montecarlo and the WebSocket GET /api/forwardflow/montecarlo/ws take forwardflow_api.rs::MonteCarloRequest: a config and runs (spec input #14, “Monte Carlo runs”). Run \( i \) is the config with seed \( + i \); on a bridge that is the same endpoints redrawn, endpoint-pinned by design (spec §9, question 4). Outputs covers the percentiles.

FieldTypeDefaultRange and error textNotes
runsu64none; the pages choose1 to 100,000. runs must be 1–100000 (input #14)forwardflow_api.rs::MAX_MC_RUNS.
runs × book≤ 24,000,000 Agreement-runs. runs × book size must stay ≤ 24000000 Agreement-runs — lower runs (input #14) or the book (inputs #13 × #18)forwardflow_api.rs::MAX_MC_AGREEMENT_RUNS: the full run ceiling at the 240-Agreement base book; bigger books get proportionally fewer runs. Checked by forwardflow_api.rs::validate_mc before any engine work.

Identical requests, keyed on (runs, the config’s JSON), are memoized in a 64-entry LRU (forwardflow_api.rs::MC_CACHE), sound because the engine is seeded. The WebSocket variant runs chunks of 1,000 (forwardflow_api.rs::MC_CHUNK) with a Progress frame per chunk.